import * as core from '@actions/core'
import * as github from '@actions/github'

import {Env} from './env'
import {Inputs} from './inputs'
import {
  canDiffCommits,
  cleanShaInput,
  getCurrentBranchName,
  getHeadSha,
  getParentSha,
  getPreviousGitTag,
  getRemoteBranchHeadSha,
  gitFetch,
  gitFetchSubmodules,
  gitLog,
  verifyCommitSha
} from './utils'

const getCurrentSHA = async ({
  inputs,
  workingDirectory
}: {
  inputs: Inputs
  workingDirectory: string
}): Promise<string> => {
  let currentSha = await cleanShaInput({
    sha: inputs.sha,
    cwd: workingDirectory,
    token: inputs.token
  })
  core.debug('Getting current SHA...')

  if (inputs.until) {
    core.debug(`Getting base SHA for '${inputs.until}'...`)
    try {
      currentSha = await gitLog({
        cwd: workingDirectory,
        args: [
          '--format=%H',
          '-n',
          '1',
          '--date',
          'local',
          '--until',
          inputs.until
        ]
      })
    } catch (error) {
      core.error(
        `Invalid until date: ${inputs.until}. ${(error as Error).message}`
      )
      throw error
    }
  } else {
    if (!currentSha) {
      if (
        github.context.payload.pull_request?.head?.sha &&
        (await verifyCommitSha({
          sha: github.context.payload.pull_request?.head?.sha,
          cwd: workingDirectory,
          showAsErrorMessage: false
        })) === 0
      ) {
        currentSha = github.context.payload.pull_request?.head?.sha
      } else if (github.context.eventName === 'merge_group') {
        currentSha = github.context.payload.merge_group?.head_sha
      } else {
        currentSha = await getHeadSha({cwd: workingDirectory})
      }
    }
  }

  await verifyCommitSha({sha: currentSha, cwd: workingDirectory})
  core.debug(`Current SHA: ${currentSha}`)

  return currentSha
}

export interface DiffResult {
  previousSha: string
  currentSha: string
  currentBranch: string
  targetBranch: string
  diff: string
  initialCommit?: boolean
}

export const getSHAForNonPullRequestEvent = async (
  inputs: Inputs,
  env: Env,
  workingDirectory: string,
  isShallow: boolean,
  hasSubmodule: boolean,
  gitFetchExtraArgs: string[],
  isTag: boolean
): Promise<DiffResult> => {
  let targetBranch = env.GITHUB_REF_NAME
  let currentBranch = targetBranch
  let initialCommit = false

  if (!inputs.skipInitialFetch) {
    if (isShallow) {
      core.info('Repository is shallow, fetching more history...')

      if (isTag) {
        let sourceBranch = ''

        if (github.context.payload.base_ref) {
          sourceBranch = github.context.payload.base_ref.replace(
            'refs/heads/',
            ''
          )
        } else if (github.context.payload.release?.target_commitish) {
          sourceBranch = github.context.payload.release?.target_commitish
        }

        await gitFetch({
          cwd: workingDirectory,
          args: [
            ...gitFetchExtraArgs,
            '-u',
            '--progress',
            `--deepen=${inputs.fetchDepth}`,
            'origin',
            `+refs/heads/${sourceBranch}:refs/remotes/origin/${sourceBranch}`
          ]
        })
      } else {
        await gitFetch({
          cwd: workingDirectory,
          args: [
            ...gitFetchExtraArgs,
            '-u',
            '--progress',
            `--deepen=${inputs.fetchDepth}`,
            'origin',
            `+refs/heads/${targetBranch}:refs/remotes/origin/${targetBranch}`
          ]
        })
      }

      if (hasSubmodule) {
        await gitFetchSubmodules({
          cwd: workingDirectory,
          args: [
            ...gitFetchExtraArgs,
            '-u',
            '--progress',
            `--deepen=${inputs.fetchDepth}`
          ]
        })
      }
    } else {
      if (hasSubmodule && inputs.fetchSubmoduleHistory) {
        await gitFetchSubmodules({
          cwd: workingDirectory,
          args: [
            ...gitFetchExtraArgs,
            '-u',
            '--progress',
            `--deepen=${inputs.fetchDepth}`
          ]
        })
      }
    }
  }

  const currentSha = await getCurrentSHA({inputs, workingDirectory})
  let previousSha = await cleanShaInput({
    sha: inputs.baseSha,
    cwd: workingDirectory,
    token: inputs.token
  })
  const diff = '..'
  const currentBranchName = await getCurrentBranchName({cwd: workingDirectory})

  if (
    currentBranchName &&
    currentBranchName !== 'HEAD' &&
    (currentBranchName !== targetBranch || currentBranchName !== currentBranch)
  ) {
    targetBranch = currentBranchName
    currentBranch = currentBranchName
  }

  if (inputs.baseSha && inputs.sha && currentBranch && targetBranch) {
    if (previousSha === currentSha) {
      core.error(
        `Similar commit hashes detected: previous sha: ${previousSha} is equivalent to the current sha: ${currentSha}.`
      )
      core.error(
        `Please verify that both commits are valid, and increase the fetch_depth to a number higher than ${inputs.fetchDepth}.`
      )
      throw new Error('Similar commit hashes detected.')
    }

    core.debug(`Previous SHA: ${previousSha}`)

    return {
      previousSha,
      currentSha,
      currentBranch,
      targetBranch,
      diff
    }
  }

  if (!previousSha || previousSha === currentSha) {
    core.debug('Getting previous SHA...')
    if (inputs.since) {
      core.debug(`Getting base SHA for '${inputs.since}'...`)
      try {
        const allCommitsFrom = await gitLog({
          cwd: workingDirectory,
          args: ['--format=%H', '--date', 'local', '--since', inputs.since]
        })

        if (allCommitsFrom) {
          const allCommitsFromArray = allCommitsFrom.split('\n')
          previousSha = allCommitsFromArray[allCommitsFromArray.length - 1]
        }
      } catch (error) {
        core.error(
          `Invalid since date: ${inputs.since}. ${(error as Error).message}`
        )
        throw error
      }
    } else if (isTag) {
      core.debug('Getting previous SHA for tag...')
      const {sha, tag} = await getPreviousGitTag({cwd: workingDirectory})
      previousSha = sha
      targetBranch = tag
    } else {
      if (github.context.eventName === 'merge_group') {
        core.debug('Getting previous SHA for merge group...')
        previousSha = github.context.payload.merge_group?.base_sha
      } else {
        core.debug('Getting previous SHA for last remote commit...')
        if (
          github.context.payload.forced === 'false' ||
          !github.context.payload.forced
        ) {
          previousSha = github.context.payload.before
        }
      }

      if (
        !previousSha ||
        previousSha === '0000000000000000000000000000000000000000'
      ) {
        previousSha = await getParentSha({
          cwd: workingDirectory
        })
      } else if (
        (await verifyCommitSha({
          sha: previousSha,
          cwd: workingDirectory,
          showAsErrorMessage: false
        })) !== 0
      ) {
        core.warning(
          `Previous commit ${previousSha} is not valid. Using parent commit.`
        )
        previousSha = await getParentSha({
          cwd: workingDirectory
        })
      }

      if (!previousSha || previousSha === currentSha) {
        previousSha = await getParentSha({
          cwd: workingDirectory
        })

        if (!previousSha) {
          core.warning('Initial commit detected no previous commit found.')
          initialCommit = true
          previousSha = currentSha
        }
      }
    }
  }

  await verifyCommitSha({sha: previousSha, cwd: workingDirectory})
  core.debug(`Previous SHA: ${previousSha}`)

  core.debug(`Target branch: ${targetBranch}`)
  core.debug(`Current branch: ${currentBranch}`)

  if (!initialCommit && previousSha === currentSha) {
    core.error(
      `Similar commit hashes detected: previous sha: ${previousSha} is equivalent to the current sha: ${currentSha}.`
    )
    core.error(
      `Please verify that both commits are valid, and increase the fetch_depth to a number higher than ${inputs.fetchDepth}.`
    )
    throw new Error('Similar commit hashes detected.')
  }

  return {
    previousSha,
    currentSha,
    currentBranch,
    targetBranch,
    diff,
    initialCommit
  }
}

export const getSHAForPullRequestEvent = async (
  inputs: Inputs,
  env: Env,
  workingDirectory: string,
  isShallow: boolean,
  hasSubmodule: boolean,
  gitFetchExtraArgs: string[]
): Promise<DiffResult> => {
  let targetBranch = github.context.payload.pull_request?.base?.ref
  const currentBranch = github.context.payload.pull_request?.head?.ref
  if (inputs.sinceLastRemoteCommit) {
    targetBranch = currentBranch
  }

  if (!inputs.skipInitialFetch) {
    core.info('Repository is shallow, fetching more history...')
    if (isShallow) {
      let prFetchExitCode = await gitFetch({
        cwd: workingDirectory,
        args: [
          ...gitFetchExtraArgs,
          '-u',
          '--progress',
          'origin',
          `pull/${github.context.payload.pull_request?.number}/head:${currentBranch}`
        ]
      })

      if (prFetchExitCode !== 0) {
        prFetchExitCode = await gitFetch({
          cwd: workingDirectory,
          args: [
            ...gitFetchExtraArgs,
            '-u',
            '--progress',
            `--deepen=${inputs.fetchDepth}`,
            'origin',
            `+refs/heads/${currentBranch}*:refs/remotes/origin/${currentBranch}*`
          ]
        })
      }

      if (prFetchExitCode !== 0) {
        throw new Error(
          'Failed to fetch pull request branch. Please ensure "persist-credentials" is set to "true" when checking out the repository. See: https://github.com/actions/checkout#usage'
        )
      }

      if (!inputs.sinceLastRemoteCommit) {
        core.debug('Fetching target branch...')
        await gitFetch({
          cwd: workingDirectory,
          args: [
            ...gitFetchExtraArgs,
            '-u',
            '--progress',
            `--deepen=${inputs.fetchDepth}`,
            'origin',
            `+refs/heads/${targetBranch}:refs/remotes/origin/${targetBranch}`
          ]
        })

        if (hasSubmodule) {
          await gitFetchSubmodules({
            cwd: workingDirectory,
            args: [
              ...gitFetchExtraArgs,
              '-u',
              '--progress',
              `--deepen=${inputs.fetchDepth}`
            ]
          })
        }
      }
    } else {
      if (hasSubmodule && inputs.fetchSubmoduleHistory) {
        await gitFetchSubmodules({
          cwd: workingDirectory,
          args: [
            ...gitFetchExtraArgs,
            '-u',
            '--progress',
            `--deepen=${inputs.fetchDepth}`
          ]
        })
      }
    }
    core.info('Completed fetching more history.')
  }

  const currentSha = await getCurrentSHA({inputs, workingDirectory})
  let previousSha = await cleanShaInput({
    sha: inputs.baseSha,
    cwd: workingDirectory,
    token: inputs.token
  })
  let diff = '...'

  if (inputs.baseSha && inputs.sha && currentBranch && targetBranch) {
    if (previousSha === currentSha) {
      core.error(
        `Similar commit hashes detected: previous sha: ${previousSha} is equivalent to the current sha: ${currentSha}.`
      )
      core.error(
        `Please verify that both commits are valid, and increase the fetch_depth to a number higher than ${inputs.fetchDepth}.`
      )
      throw new Error('Similar commit hashes detected.')
    }

    core.debug(`Previous SHA: ${previousSha}`)

    return {
      previousSha,
      currentSha,
      currentBranch,
      targetBranch,
      diff
    }
  }

  if (
    !github.context.payload.pull_request?.base?.ref ||
    github.context.payload.head?.repo?.fork === 'true'
  ) {
    diff = '..'
  }

  if (!previousSha || previousSha === currentSha) {
    if (inputs.sinceLastRemoteCommit) {
      previousSha = github.context.payload.before

      if (
        !previousSha ||
        (previousSha &&
          (await verifyCommitSha({
            sha: previousSha,
            cwd: workingDirectory,
            showAsErrorMessage: false
          })) !== 0)
      ) {
        core.info(
          `Unable to locate the previous commit in the local history for ${github.context.eventName} (${github.context.payload.action}) event. Falling back to the previous commit in the local history.`
        )

        previousSha = await getParentSha({
          cwd: workingDirectory
        })

        if (
          github.context.payload.action &&
          github.context.payload.action === 'synchronize' &&
          previousSha &&
          (!previousSha ||
            (previousSha &&
              (await verifyCommitSha({
                sha: previousSha,
                cwd: workingDirectory,
                showAsErrorMessage: false
              })) !== 0))
        ) {
          throw new Error(
            'Unable to locate the previous commit in the local history. Please ensure to checkout pull request HEAD commit instead of the merge commit. See: https://github.com/actions/checkout/blob/main/README.md#checkout-pull-request-head-commit-instead-of-merge-commit'
          )
        }

        if (
          !previousSha ||
          (previousSha &&
            (await verifyCommitSha({
              sha: previousSha,
              cwd: workingDirectory,
              showAsErrorMessage: false
            })) !== 0)
        ) {
          throw new Error(
            'Unable to locate the previous commit in the local history. Please ensure to checkout pull request HEAD commit instead of the merge commit. See: https://github.com/actions/checkout/blob/main/README.md#checkout-pull-request-head-commit-instead-of-merge-commit'
          )
        }
      }
    } else {
      if (github.context.payload.action === 'closed') {
        previousSha = github.context.payload.pull_request?.base?.sha
      } else {
        previousSha = await getRemoteBranchHeadSha({
          cwd: workingDirectory,
          branch: targetBranch
        })

        if (!previousSha) {
          previousSha = github.context.payload.pull_request?.base?.sha
        }
      }

      if (isShallow) {
        if (
          !(await canDiffCommits({
            cwd: workingDirectory,
            sha1: previousSha,
            sha2: currentSha,
            diff
          }))
        ) {
          core.info(
            'Merge base is not in the local history, fetching remote target branch...'
          )

          for (let i = 1; i <= 10; i++) {
            await gitFetch({
              cwd: workingDirectory,
              args: [
                ...gitFetchExtraArgs,
                '-u',
                '--progress',
                `--deepen=${inputs.fetchDepth}`,
                'origin',
                `+refs/heads/${targetBranch}:refs/remotes/origin/${targetBranch}`
              ]
            })

            if (
              await canDiffCommits({
                cwd: workingDirectory,
                sha1: previousSha,
                sha2: currentSha,
                diff
              })
            ) {
              break
            }

            core.info(
              'Merge base is not in the local history, fetching remote target branch again...'
            )
            core.info(`Attempt ${i}/10`)
          }
        }
      }
    }

    if (!previousSha || previousSha === currentSha) {
      previousSha = github.context.payload.pull_request?.base?.sha
    }
  }

  if (
    !(await canDiffCommits({
      cwd: workingDirectory,
      sha1: previousSha,
      sha2: currentSha,
      diff
    }))
  ) {
    diff = '..'
  }

  await verifyCommitSha({sha: previousSha, cwd: workingDirectory})
  core.debug(`Previous SHA: ${previousSha}`)

  if (
    !(await canDiffCommits({
      cwd: workingDirectory,
      sha1: previousSha,
      sha2: currentSha,
      diff
    }))
  ) {
    core.warning(
      'If this pull request is from a forked repository, please set the checkout action `repository` input to the same repository as the pull request.'
    )
    core.warning(
      'This can be done by setting actions/checkout `repository` to ${{ github.event.pull_request.head.repo.full_name }}'
    )
    throw new Error(
      `Unable to determine a difference between ${previousSha}${diff}${currentSha}`
    )
  }

  if (previousSha === currentSha) {
    core.error(
      `Similar commit hashes detected: previous sha: ${previousSha} is equivalent to the current sha: ${currentSha}.`
    )
    // This occurs if a PR is created from a forked repository and the event is pull_request_target.
    //  - name: Checkout to branch
    //    uses: actions/checkout@v3
    // Without setting the repository to use the same repository as the pull request will cause the previousSha
    // to be the same as the currentSha since the currentSha cannot be found in the local history.
    // The solution is to use:
    //   - name: Checkout to branch
    //     uses: actions/checkout@v3
    //     with:
    //       repository: ${{ github.event.pull_request.head.repo.full_name }}
    if (github.context.eventName === 'pull_request_target') {
      core.warning(
        'If this pull request is from a forked repository, please set the checkout action `repository` input to the same repository as the pull request.'
      )
      core.warning(
        'This can be done by setting actions/checkout `repository` to ${{ github.event.pull_request.head.repo.full_name }}'
      )
    } else {
      core.error(
        `Please verify that both commits are valid, and increase the fetch_depth to a number higher than ${inputs.fetchDepth}.`
      )
    }
    throw new Error('Similar commit hashes detected.')
  }

  return {
    previousSha,
    currentSha,
    currentBranch,
    targetBranch,
    diff
  }
}
