const get = require('./gl-got-wrapper');
const addrs = require('email-addresses');

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,
  // file
  commitFilesToBranch,
  getFile,
  // commits
  getCommitMessages,
};

// Get all repositories that the user has access to
async function getRepos(token, endpoint) {
  logger.info('Autodiscovering GitLab repositories');
  logger.debug('getRepos(token, endpoint)');
  if (token) {
    process.env.GITLAB_TOKEN = token;
  } else if (!process.env.GITLAB_TOKEN) {
    throw new Error('No token found for getRepos');
  }
  if (endpoint) {
    process.env.GITLAB_ENDPOINT = endpoint;
  }
  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 }) {
  if (token) {
    process.env.GITLAB_TOKEN = token;
  } else if (!process.env.GITLAB_TOKEN) {
    throw new Error(`No token found for GitLab repository ${repository}`);
  }
  if (token) {
    process.env.GITLAB_TOKEN = token;
  }
  if (endpoint) {
    process.env.GITLAB_ENDPOINT = endpoint;
  }
  config = {};
  get.reset();
  config.repository = urlEscape(repository);
  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 {
    const res = await get(
      `projects/${
        config.repository
      }/repository/tree?ref=${branchName}&recursive=true&per_page=100`,
      { 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)}`
  );
}

function mergeBranch() {
  logger.warn('Unimplemented in GitLab: mergeBranch');
}

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

function ensureIssue() {
  // istanbul ignore next
  logger.warn(`ensureIssue() is not implemented`);
}

function ensureIssueClosing() {}

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 ensureComment() {
  // Todo: implement. See GitHub API for example
}

async function ensureCommentRemoval() {
  // Todo: implement. See GitHub API for example
}

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: Array.isArray(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;
  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;
}

function getPrFiles() {
  // TODO
  return [];
}

// 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;
}

// 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,
  gitAuthor
) {
  logger.debug(
    `commitFilesToBranch('${branchName}', files, message, '${parentBranch})'`
  );
  const opts = {
    body: {
      branch: branchName,
      commit_message: message,
      start_branch: parentBranch,
      actions: [],
    },
  };

  try {
    if (gitAuthor) {
      logger.debug({ gitAuthor }, 'Found gitAuthor');
      const { name, address } = addrs.parseOneAddress(gitAuthor);
      if (name && address) {
        opts.body.author_name = name;
        opts.body.author_email = address;
      }
    }
  } catch (err) {
    logger.warn({ gitAuthor }, 'Error parsing gitAuthor');
  }

  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);
  }
  if (await branchExists(branchName)) {
    logger.debug('Deleting existing branch');
    await deleteBranch(branchName);
  }
  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');
  try {
    const res = await get(`projects/${config.repository}/repository/commits`);
    return res.body.map(commit => commit.title);
  } catch (err) {
    logger.error({ err }, `getCommitMessages error`);
    return [];
  }
}

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;
}