const Bluebird = require('bluebird')
const childProcess = require('child_process')
const _ = require('lodash')
const { Octokit } = require('@octokit/core')
const debugLib = require('debug')

const { getCurrentReleaseData } = require('./get-current-release-data')
const { getNextVersionForBinary } = require('../get-next-version')
const { getLinkedIssues } = require('./get-linked-issues')
const debug = debugLib('scripts:semantic-commits:get-binary-release-data')

const ensureAuth = () => {
  if (!process.env.GH_TOKEN) {
    throw new Error('The GH_TOKEN env is not set.')
  }
}

/**
 * Get the list of file names that have been added, deleted or changed since the git
 * sha associated with the latest tag published on npm.
 *
 * @param {object} latestReleaseInfo - data of the latest tag published on npm
 * @param {string} latestReleaseInfo.version - version of Cypress
 * @param {string} latestReleaseInfo.commitDate - date of release
 * @param {string} latestReleaseInfo.buildSha - git commit associated with published content
 */
const getChangedFilesSinceLastRelease = (latestReleaseInfo) => {
  const stdout = childProcess.execSync(`git diff ${latestReleaseInfo.buildSha}.. --name-only`, { encoding: 'utf8' })

  if (!stdout) {
    console.log('no file changes since last release')

    return []
  }

  return stdout.split('\n')
}

// returns number of milliseconds to wait for retry
const getRetryAfter = (headers) => {
  // retry-after header is number of seconds to wait
  if (headers['retry-after'] && headers['retry-after'] !== '0') {
    return parseInt(headers['retry-after'], 10) * 1000
  }

  // x-ratelimit-reset header is the time in seconds since epoch after
  // which to retry
  if (headers['x-ratelimit-reset']) {
    const epochSeconds = parseInt(headers['x-ratelimit-reset'], 10)

    // turn it into milliseconds to wait and pad it by a second for good measure
    return (epochSeconds - (Date.now() / 1000)) * 1000 + 1000
  }

  // otherwise, just wait a minute
  return 6000
}

// https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#secondary-rate-limits
const parseRateLimit = (err) => {
  if (err.status === 403 && err.response?.data?.message.includes('secondary rate limit')) {
    const retryAfter = getRetryAfter(err.response?.headers)

    return {
      rateLimitHit: true,
      retryAfter,
    }
  }

  return {
    rateLimitHit: false,
  }
}

const fetchPullRequest = async (octokit, pullNumber) => {
  try {
    const { data: pullRequest } = await octokit.request('GET /repos/{owner}/{repo}/pulls/{pull_number}', {
      owner: 'cypress-io',
      repo: 'cypress',
      pull_number: pullNumber,
    })

    return pullRequest
  } catch (err) {
    const { rateLimitHit, retryAfter } = parseRateLimit(err)

    if (rateLimitHit) {
      console.log(`Rate limit hit - Retry fetching PR #${pullNumber} after ${retryAfter}ms`)

      return Bluebird.delay(retryAfter).then(() => fetchPullRequest(octokit, pullNumber))
    }

    throw err
  }
}

/**
 * Get the next release version given the semantic commits in the git history. Then using the commit history,
 * determine which files have changed, list of PRs merged and issues resolved since the latest tag was
 * published on npm. It also collects the list of commit data including the semantic type, PR and associated
 * issues.
 *
 * @param {object} latestReleaseInfo - data of the latest tag published on npm
 * @param {string} latestReleaseInfo.version - version of Cypress
 * @param {string} latestReleaseInfo.commitDate - date of release
 * @param {string} latestReleaseInfo.buildSha - git commit associated with published content
 */
const getReleaseData = async (latestReleaseInfo) => {
  ensureAuth()
  const octokit = new Octokit({ auth: process.env.GH_TOKEN })

  let {
    nextVersion,
    commits: semanticCommits,
  } = await getNextVersionForBinary()

  semanticCommits = _.uniqBy(semanticCommits, (commit) => commit.header)

  const changedFiles = await getChangedFilesSinceLastRelease(latestReleaseInfo)

  const issuesInRelease = []
  const prsInRelease = []
  const commits = []

  await Bluebird.each(semanticCommits, (async (semanticResult) => {
    if (!semanticResult) return

    const { type: semanticType, references } = semanticResult

    if (!references.length) {
      console.log('Commit does not have an associated pull request number...')

      return
    }

    const refs = references.filter((r) => !r.raw.includes('revert #'))

    if (!refs.length) {
      console.log('Commit does not have an associated pull request number...')

      return
    }

    for (const [idx, ref] of refs.entries()) {
      let pullRequest

      try {
        pullRequest = await fetchPullRequest(octokit, ref.issue)
        debug(`Pull request #${ref.issue} found!`)
      } catch (err) {
        debug(`Error while fetching PR #${ ref.issue}:`, err)
        // If we have tried to fetch all other references and all of them failed,
        // print the failure of the last reference and exit
        if (idx === refs.length) {
          debug(`all references exhausted and no PR could be found!`)
          console.log(`Error while fetching PR #${ ref.issue}:`, err)
          throw err
        } else {
          // otherwise, we still might be able to link a PR to the commit
          debug(`Other references need to be tried. Continuing to see if we can find a corresponding pull request.`)
        }

        continue
      }

      const associatedIssues = pullRequest.body ? getLinkedIssues(pullRequest.body) : []

      commits.push({
        commitMessage: semanticResult.header,
        semanticType,
        prNumber: ref.issue,
        associatedIssues,
      })

      prsInRelease.push(`https://github.com/cypress-io/cypress/pull/${ref.issue}`)

      associatedIssues.forEach((issueNumber) => {
        issuesInRelease.push(`https://github.com/cypress-io/cypress/issues/${issueNumber}`)
      })

      // since we found our pull request, we don't need to check the rest of the references
      break
    }
  }))

  console.log('Next release version is', nextVersion)

  console.log(`${prsInRelease.length} pull requests have merged since ${latestReleaseInfo.version} was released.`)

  prsInRelease.forEach((link) => {
    console.log('  -', link)
  })

  console.log(`${issuesInRelease.length} issues addressed since ${latestReleaseInfo.version} was released.`)

  issuesInRelease.forEach((link) => {
    console.log('  -', link)
  })

  return {
    nextVersion,
    changedFiles,
    commits,
    issuesInRelease,
    prsInRelease,
  }
}

if (require.main !== module) {
  module.exports = {
    getReleaseData,
  }

  return
}

(async () => {
  const latestReleaseInfo = await getCurrentReleaseData()

  await getReleaseData(latestReleaseInfo)
})()
