const is = require('@sindresorhus/is');
const addrs = require('email-addresses');
const showdown = require('showdown');

const get = require('./gl-got-wrapper');
const endpoints = require('../../util/endpoints');

const converter = new showdown.Converter();
converter.setFlavor('github');

let config = {};

module.exports = {
  getRepos,
  cleanRepo: () => undefined,
  initRepo,
  getRepoForceRebase,
  setBaseBranch,
  // Search
  getFileList,
  // Branch
  branchExists,
  getAllRenovateBranches,
  isBranchStale,
  getBranchPr,
  getBranchStatus,
  getBranchStatusCheck,
  setBranchStatus,
  deleteBranch,
  mergeBranch,
  getBranchLastCommitTime,
  // issue
  ensureIssue,
  ensureIssueClosing,
  addAssignees,
  addReviewers,
  // Comments
  ensureComment,
  ensureCommentRemoval,
  // PR
  getPrList,
  findPr,
  createPr,
  getPr,
  getPrFiles,
  updatePr,
  mergePr,
  getPrBody,
  // file
  commitFilesToBranch,
  getFile,
  // commits
  getCommitMessages,
  // vulnerability alerts
  getVulnerabilityAlerts,
};

// Get all repositories that the user has access to
async function getRepos(token, endpoint) {
  logger.info('Autodiscovering GitLab repositories');
  logger.debug('getRepos(token, endpoint)');
  const opts = endpoints.find({ platform: 'gitlab' }, { token, endpoint });
  if (!opts.token) {
    throw new Error('No token found for getRepos');
  }
  endpoints.update({ ...opts, platform: 'gitlab', default: true });
  try {
    const url = `projects?membership=true&per_page=100`;
    const res = await get(url, { paginate: true });
    logger.info(`Discovered ${res.body.length} project(s)`);
    return res.body.map(repo => repo.path_with_namespace);
  } catch (err) {
    logger.error({ err }, `GitLab getRepos error`);
    throw err;
  }
}

function urlEscape(str) {
  return str ? str.replace(/\//g, '%2F') : str;
}

// Initialize GitLab by getting base branch
async function initRepo({ repository, token, endpoint, gitAuthor }) {
  const opts = endpoints.find({ platform: 'gitlab' }, { token, endpoint });
  if (!opts.token) {
    throw new Error(`No token found for GitLab repository ${repository}`);
  }
  endpoints.update({ ...opts, platform: 'gitlab', default: true });
  config = {};
  get.reset();
  config.repository = urlEscape(repository);
  if (gitAuthor) {
    try {
      config.gitAuthor = addrs.parseOneAddress(gitAuthor);
    } catch (err) /* istanbul ignore next */ {
      logger.error(
        { gitAuthor, err, message: err.message },
        'Invalid gitAuthor'
      );
      throw new Error('Invalid gitAuthor');
    }
  }
  try {
    const res = await get(`projects/${config.repository}`);
    config.defaultBranch = res.body.default_branch;
    config.baseBranch = config.defaultBranch;
    logger.debug(`${repository} default branch = ${config.baseBranch}`);
    // Discover our user email
    config.email = (await get(`user`)).body.email;
    delete config.prList;
    delete config.fileList;
    await Promise.all([getPrList(), getFileList()]);
  } catch (err) {
    logger.error({ err }, `GitLab init error`);
    throw err;
  }
  return {};
}

function getRepoForceRebase() {
  return false;
}

async function setBaseBranch(branchName) {
  if (branchName) {
    logger.debug(`Setting baseBranch to ${branchName}`);
    config.baseBranch = branchName;
    delete config.fileList;
    await getFileList(branchName);
  }
}

// Search

// Get full file list
async function getFileList(branchName = config.baseBranch) {
  if (config.fileList) {
    return config.fileList;
  }
  try {
    let url = `projects/${
      config.repository
    }/repository/tree?ref=${branchName}&per_page=100`;
    if (!(process.env.RENOVATE_DISABLE_FILE_RECURSION === 'true')) {
      url += '&recursive=true';
    }
    const res = await get(url, { paginate: true });
    config.fileList = res.body
      .filter(item => item.type === 'blob' && item.mode !== '120000')
      .map(item => item.path)
      .sort();
    logger.debug(`Retrieved fileList with length ${config.fileList.length}`);
  } catch (err) {
    logger.info('Error retrieving git tree - no files detected');
    config.fileList = [];
  }
  return config.fileList;
}

// Branch

// Returns true if branch exists, otherwise false
async function branchExists(branchName) {
  logger.debug(`Checking if branch exists: ${branchName}`);
  try {
    const url = `projects/${config.repository}/repository/branches/${urlEscape(
      branchName
    )}`;
    const res = await get(url);
    if (res.statusCode === 200) {
      logger.debug('Branch exists');
      return true;
    }
    // This probably shouldn't happen
    logger.debug("Branch doesn't exist");
    return false;
  } catch (error) {
    if (error.statusCode === 404) {
      // If file not found, then return false
      logger.debug("Branch doesn't exist");
      return false;
    }
    // Propagate if it's any other error
    throw error;
  }
}

async function getAllRenovateBranches(branchPrefix) {
  logger.debug(`getAllRenovateBranches(${branchPrefix})`);
  const allBranches = await get(
    `projects/${config.repository}/repository/branches`
  );
  return allBranches.body.reduce((arr, branch) => {
    if (branch.name.startsWith(branchPrefix)) {
      arr.push(branch.name);
    }
    return arr;
  }, []);
}

async function isBranchStale(branchName) {
  logger.debug(`isBranchStale(${branchName})`);
  const branchDetails = await getBranchDetails(branchName);
  logger.trace({ branchDetails }, 'branchDetails');
  const parentSha = branchDetails.body.commit.parent_ids[0];
  logger.debug(`parentSha=${parentSha}`);
  const baseCommitSHA = await getBaseCommitSHA();
  logger.debug(`baseCommitSHA=${baseCommitSHA}`);
  // Return true if the SHAs don't match
  return parentSha !== baseCommitSHA;
}

// Returns the Pull Request for a branch. Null if not exists.
async function getBranchPr(branchName) {
  logger.debug(`getBranchPr(${branchName})`);
  if (!(await branchExists(branchName))) {
    return null;
  }
  const urlString = `projects/${
    config.repository
  }/merge_requests?state=opened&per_page=100`;
  const res = await get(urlString, { paginate: true });
  logger.debug(`Got res with ${res.body.length} results`);
  let pr = null;
  res.body.forEach(result => {
    if (result.source_branch === branchName) {
      pr = result;
    }
  });
  if (!pr) {
    return null;
  }
  return getPr(pr.iid);
}

// Returns the combined status for a branch.
async function getBranchStatus(branchName, requiredStatusChecks) {
  logger.debug(`getBranchStatus(${branchName})`);
  if (!requiredStatusChecks) {
    // null means disable status checks, so it always succeeds
    return 'success';
  }
  if (requiredStatusChecks.length) {
    // This is Unsupported
    logger.warn({ requiredStatusChecks }, `Unsupported requiredStatusChecks`);
    return 'failed';
  }
  // First, get the branch to find the commit SHA
  let url = `projects/${config.repository}/repository/branches/${urlEscape(
    branchName
  )}`;
  let res = await get(url);
  const branchSha = res.body.commit.id;
  // Now, check the statuses for that commit
  url = `projects/${
    config.repository
  }/repository/commits/${branchSha}/statuses`;
  res = await get(url);
  logger.debug(`Got res with ${res.body.length} results`);
  if (res.body.length === 0) {
    // Return 'pending' if we have no status checks
    return 'pending';
  }
  let status = 'success';
  // Return 'success' if all are success
  res.body.forEach(check => {
    // If one is failed then don't overwrite that
    if (status !== 'failure') {
      if (!check.allow_failure) {
        if (check.status === 'failed') {
          status = 'failure';
        } else if (check.status !== 'success') {
          ({ status } = check);
        }
      }
    }
  });
  return status;
}

async function getBranchStatusCheck(branchName, context) {
  // First, get the branch to find the commit SHA
  let url = `projects/${config.repository}/repository/branches/${urlEscape(
    branchName
  )}`;
  let res = await get(url);
  const branchSha = res.body.commit.id;
  // Now, check the statuses for that commit
  url = `projects/${
    config.repository
  }/repository/commits/${branchSha}/statuses`;
  res = await get(url);
  logger.debug(`Got res with ${res.body.length} results`);
  for (const check of res.body) {
    if (check.name === context) {
      return check.state;
    }
  }
  return null;
}

async function setBranchStatus(
  branchName,
  context,
  description,
  state,
  targetUrl
) {
  // First, get the branch to find the commit SHA
  let url = `projects/${config.repository}/repository/branches/${urlEscape(
    branchName
  )}`;
  const res = await get(url);
  const branchSha = res.body.commit.id;
  // Now, check the statuses for that commit
  url = `projects/${config.repository}/statuses/${branchSha}`;
  const options = {
    state,
    description,
    context,
  };
  if (targetUrl) {
    options.target_url = targetUrl;
  }
  await get.post(url, { body: options });
}

async function deleteBranch(branchName, closePr = false) {
  if (closePr) {
    logger.debug('Closing PR');
    const pr = await getBranchPr(branchName);
    // istanbul ignore if
    if (pr) {
      await get.put(
        `projects/${config.repository}/merge_requests/${pr.number}`,
        {
          body: {
            state_event: 'close',
          },
        }
      );
    }
  }
  await get.delete(
    `projects/${config.repository}/repository/branches/${urlEscape(branchName)}`
  );
}

async function mergeBranch(branchName) {
  logger.debug(`mergeBranch(${branchName}`);
  const branchURI = encodeURIComponent(branchName);
  try {
    await get.post(
      `projects/${
        config.repository
      }/repository/commits/${branchURI}/cherry_pick?branch=${config.baseBranch}`
    );
  } catch (err) {
    logger.info(
      expandError(err),
      `Error pushing branch merge for ${branchName}`
    );
    throw new Error('Branch automerge failed');
  }
  // Update base commit
  config.baseCommitSHA = null;
  // Delete branch
  await deleteBranch(branchName);
}

async function getBranchLastCommitTime(branchName) {
  try {
    const res = await get(
      `projects/${config.repository}/repository/commits?ref_name=${urlEscape(
        branchName
      )}`
    );
    return new Date(res.body[0].committed_date);
  } catch (err) {
    logger.error({ err }, `getBranchLastCommitTime error`);
    return new Date();
  }
}

// Issue

async function getIssueList() {
  if (!config.issueList) {
    const res = await get(`projects/${config.repository}/issues?state=opened`, {
      useCache: false,
    });
    // istanbul ignore if
    if (!is.array(res.body)) {
      logger.warn({ responseBody: res.body }, 'Could not retrieve issue list');
      return [];
    }
    config.issueList = res.body.map(i => ({
      iid: i.iid,
      title: i.title,
    }));
  }
  return config.issueList;
}

async function ensureIssue(title, body) {
  logger.debug(`ensureIssue()`);
  try {
    const issueList = await getIssueList();
    const issue = issueList.find(i => i.title === title);
    if (issue) {
      const issueBody = (await get(
        `projects/${config.repository}/issues/${issue.iid}`
      )).body.body;
      if (issueBody !== body) {
        logger.debug('Updating issue body');
        await get.put(`projects/${config.repository}/issues/${issue.iid}`, {
          body: { description: body },
        });
        return 'updated';
      }
    } else {
      await get.post(`projects/${config.repository}/issues`, {
        body: {
          title,
          description: body,
        },
      });
      // delete issueList so that it will be refetched as necessary
      delete config.issueList;
      return 'created';
    }
  } catch (err) /* istanbul ignore next */ {
    if (err.message.startsWith('Issues are disabled for this repo')) {
      logger.info(`Could not create issue: ${err.message}`);
    } else {
      logger.warn(expandError(err), 'Could not ensure issue');
    }
  }
  return null;
}

async function ensureIssueClosing(title) {
  logger.debug(`ensureIssueClosing()`);
  const issueList = await getIssueList();
  for (const issue of issueList) {
    if (issue.title === title) {
      logger.info({ issue }, 'Closing issue');
      await get.delete(`projects/${config.repository}/issues/${issue.iid}`, {
        body: { state: 'closed' },
      });
    }
  }
}

async function addAssignees(iid, assignees) {
  logger.debug(`Adding assignees ${assignees} to #${iid}`);
  if (assignees.length > 1) {
    logger.warn('Cannot assign more than one assignee to Merge Requests');
  }
  try {
    const assigneeId = (await get(`users?username=${assignees[0]}`)).body[0].id;
    let url = `projects/${config.repository}/merge_requests/${iid}`;
    url += `?assignee_id=${assigneeId}`;
    await get.put(url);
  } catch (err) {
    logger.error({ iid, assignees }, 'Failed to add assignees');
  }
}

function addReviewers(iid, reviewers) {
  logger.debug(`addReviewers('${iid}, '${reviewers})`);
  logger.warn('Unimplemented in GitLab: approvals');
}

async function getComments(issueNo) {
  // GET projects/:owner/:repo/merge_requests/:number/notes
  logger.debug(`Getting comments for #${issueNo}`);
  const url = `projects/${config.repository}/merge_requests/${issueNo}/notes`;
  const comments = (await get(url, { paginate: true })).body;
  logger.debug(`Found ${comments.length} comments`);
  return comments;
}

async function addComment(issueNo, body) {
  // POST projects/:owner/:repo/merge_requests/:number/notes
  await get.post(
    `projects/${config.repository}/merge_requests/${issueNo}/notes`,
    {
      body: { body },
    }
  );
}

async function editComment(issueNo, commentId, body) {
  // PATCH projects/:owner/:repo/merge_requests/:number/notes/:id
  await get.patch(
    `projects/${
      config.repository
    }/merge_requests/${issueNo}/notes/${commentId}`,
    {
      body: { body },
    }
  );
}

async function deleteComment(issueNo, commentId) {
  // DELETE projects/:owner/:repo/merge_requests/:number/notes/:id
  await get.delete(
    `projects/${config.repository}/merge_requests/${issueNo}/notes/${commentId}`
  );
}

async function ensureComment(issueNo, topic, content) {
  const comments = await getComments(issueNo);
  let body;
  let commentId;
  let commentNeedsUpdating;
  if (topic) {
    logger.debug(`Ensuring comment "${topic}" in #${issueNo}`);
    body = `### ${topic}\n\n${content}`;
    comments.forEach(comment => {
      if (comment.body.startsWith(`### ${topic}\n\n`)) {
        commentId = comment.id;
        commentNeedsUpdating = comment.body !== body;
      }
    });
  } else {
    logger.debug(`Ensuring content-only comment in #${issueNo}`);
    body = `${content}`;
    comments.forEach(comment => {
      if (comment.body === body) {
        commentId = comment.id;
        commentNeedsUpdating = false;
      }
    });
  }
  if (!commentId) {
    await addComment(issueNo, body);
    logger.info({ repository: config.repository, issueNo }, 'Added comment');
  } else if (commentNeedsUpdating) {
    await editComment(issueNo, commentId, body);
    logger.info({ repository: config.repository, issueNo }, 'Updated comment');
  } else {
    logger.debug('Comment is already update-to-date');
  }
}

async function ensureCommentRemoval(issueNo, topic) {
  logger.debug(`Ensuring comment "${topic}" in #${issueNo} is removed`);
  const comments = await getComments(issueNo);
  let commentId;
  comments.forEach(comment => {
    if (comment.body.startsWith(`### ${topic}\n\n`)) {
      commentId = comment.id;
    }
  });
  if (commentId) {
    await deleteComment(issueNo, commentId);
  }
}

async function getPrList() {
  if (!config.prList) {
    const urlString = `projects/${
      config.repository
    }/merge_requests?per_page=100`;
    const res = await get(urlString, { paginate: true });
    config.prList = res.body.map(pr => ({
      number: pr.iid,
      branchName: pr.source_branch,
      title: pr.title,
      state: pr.state === 'opened' ? 'open' : pr.state,
      createdAt: pr.created_at,
    }));
  }
  return config.prList;
}

function matchesState(state, desiredState) {
  if (desiredState === 'all') {
    return true;
  }
  if (desiredState[0] === '!') {
    return state !== desiredState.substring(1);
  }
  return state === desiredState;
}

async function findPr(branchName, prTitle, state = 'all') {
  logger.debug(`findPr(${branchName}, ${prTitle}, ${state})`);
  const prList = await getPrList();
  return prList.find(
    p =>
      p.branchName === branchName &&
      (!prTitle || p.title === prTitle) &&
      matchesState(p.state, state)
  );
}

// Pull Request

async function createPr(
  branchName,
  title,
  description,
  labels,
  useDefaultBranch
) {
  const targetBranch = useDefaultBranch
    ? config.defaultBranch
    : config.baseBranch;
  logger.debug(`Creating Merge Request: ${title}`);
  const res = await get.post(`projects/${config.repository}/merge_requests`, {
    body: {
      source_branch: branchName,
      target_branch: targetBranch,
      remove_source_branch: true,
      title,
      description,
      labels: is.array(labels) ? labels.join(',') : null,
    },
  });
  const pr = res.body;
  pr.number = pr.iid;
  pr.branchName = branchName;
  pr.displayNumber = `Merge Request #${pr.iid}`;
  return pr;
}

async function getPr(iid) {
  logger.debug(`getPr(${iid})`);
  const url = `projects/${config.repository}/merge_requests/${iid}`;
  const pr = (await get(url)).body;
  // Harmonize fields with GitHub
  pr.branchName = pr.source_branch;
  pr.number = pr.iid;
  pr.displayNumber = `Merge Request #${pr.iid}`;
  pr.body = pr.description;
  pr.state = pr.state === 'opened' ? 'open' : pr.state;
  if (pr.merge_status === 'cannot_be_merged') {
    logger.debug('pr cannot be merged');
    pr.canMerge = false;
    pr.isUnmergeable = true;
  } else {
    // Actually.. we can't be sure
    pr.canMerge = true;
  }
  // Check if the most recent branch commit is by us
  // If not then we don't allow it to be rebased, in case someone's changes would be lost
  const branchUrl = `projects/${
    config.repository
  }/repository/branches/${urlEscape(pr.source_branch)}`;
  try {
    const branch = (await get(branchUrl)).body;
    if (
      branch &&
      branch.commit &&
      branch.commit.author_email === config.email
    ) {
      pr.canRebase = true;
    }
  } catch (err) {
    logger.warn({ err }, 'Error getting PR branch');
    pr.isUnmergeable = true;
  }
  return pr;
}

// Return a list of all modified files in a PR
async function getPrFiles(mrNo) {
  logger.debug({ mrNo }, 'getPrFiles');
  if (!mrNo) {
    return [];
  }
  const files = (await get(
    `projects/${config.repository}/merge_requests/${mrNo}/changes`
  )).body;
  return files.map(f => f.filename);
}

// istanbul ignore next
async function reopenPr(iid) {
  await get.put(`projects/${config.repository}/merge_requests/${iid}`, {
    body: {
      state_event: 'reopen',
    },
  });
}

async function updatePr(iid, title, description) {
  await get.put(`projects/${config.repository}/merge_requests/${iid}`, {
    body: {
      title,
      description,
    },
  });
}

async function mergePr(iid) {
  await get.put(`projects/${config.repository}/merge_requests/${iid}/merge`, {
    body: {
      should_remove_source_branch: true,
    },
  });
  return true;
}

function getPrBody(input) {
  // Convert to HTML using GitHub-flavoured markdown as it is more feature-rich than GitLab's flavour
  return converter
    .makeHtml(input)
    .replace(/Pull Request/g, 'Merge Request')
    .replace(/PR/g, 'MR')
    .replace(
      `<p><details><br />\n<summary>Release Notes</summary></p>`,
      '\n<details>\n\n<summary>Release Notes</summary>\n\n'
    )
    .replace('<p></details></p>', '\n</details>\n');
  // TODO: set maximum length
}

// Generic File operations

async function getFile(filePath, branchName) {
  logger.debug(`getFile(filePath=${filePath}, branchName=${branchName})`);
  if (!branchName || branchName === config.baseBranch) {
    if (config.fileList && !config.fileList.includes(filePath)) {
      return null;
    }
  }
  try {
    const url = `projects/${config.repository}/repository/files/${urlEscape(
      filePath
    )}?ref=${branchName || config.baseBranch}`;
    const res = await get(url);
    return Buffer.from(res.body.content, 'base64').toString();
  } catch (error) {
    if (error.statusCode === 404) {
      // If file not found, then return null JSON
      return null;
    }
    // Propagate if it's any other error
    throw error;
  }
}

// Add a new commit, create branch if not existing
async function commitFilesToBranch(
  branchName,
  files,
  message,
  parentBranch = config.baseBranch
) {
  logger.debug(
    `commitFilesToBranch('${branchName}', files, message, '${parentBranch})'`
  );
  const opts = {
    body: {
      branch: branchName,
      commit_message: message,
      start_branch: parentBranch,
      actions: [],
    },
  };
  // istanbul ignore if
  if (config.gitAuthor) {
    opts.body.author_name = config.gitAuthor.name;
    opts.body.author_email = config.gitAuthor.address;
  }
  for (const file of files) {
    const action = {
      file_path: file.name,
      content: Buffer.from(file.contents).toString('base64'),
      encoding: 'base64',
    };
    action.action = (await getFile(file.name)) ? 'update' : 'create';
    opts.body.actions.push(action);
  }
  try {
    if (await branchExists(branchName)) {
      logger.debug('Deleting existing branch');
      await deleteBranch(branchName);
    }
  } catch (err) {
    // istanbul ignore next
    logger.info(`Ignoring branch deletion failure`);
  }
  logger.debug('Adding commits');
  await get.post(`projects/${config.repository}/repository/commits`, opts);
  // Reopen PR if it previousluy existed and was closed by GitLab when we deleted branch
  const pr = await getBranchPr(branchName);
  // istanbul ignore if
  if (pr) {
    logger.debug('Reopening PR');
    await reopenPr(pr.number);
  }
}

// GET /projects/:id/repository/commits
async function getCommitMessages() {
  logger.debug('getCommitMessages');
  const res = await get(`projects/${config.repository}/repository/commits`);
  return res.body.map(commit => commit.title);
}

function getBranchDetails(branchName) {
  const url = `/projects/${config.repository}/repository/branches/${urlEscape(
    branchName
  )}`;
  return get(url);
}

async function getBaseCommitSHA() {
  if (!config.baseCommitSHA) {
    const branchDetails = await getBranchDetails(config.baseBranch);
    config.baseCommitSHA = branchDetails.body.commit.id;
  }
  return config.baseCommitSHA;
}

// istanbul ignore next
function expandError(err) {
  return {
    err,
    message: err.message,
    body: err.response ? err.response.body : undefined,
  };
}

function getVulnerabilityAlerts() {
  return [];
}