import {
  type ReposConfiguration,
  type GitConfig,
} from './configTypes.ts'
import util from 'util'
import childProcess from 'child_process'
import {minimatch} from 'minimatch'

import { type Repository} from './dataTypes.ts'
import cloneUrl from './vcses/git/helpers.ts'
import { addBranchToCommitsMap } from './vcses/git/operations.ts'
import { getLocation} from './helpers.ts'
import { getFileList } from './vcses/git/operations.ts'

const exec = util.promisify(childProcess.exec)

const branchesForReposMap: Map<string, Array<{name: string, description?: string, compareTo?: string}>> = new Map()

const getBranches = async (repoConfig: GitConfig, repoName: string): Promise<Array<{name: string, description?: string, compareTo?: string}>> => {
  const cachedBranchNames = branchesForReposMap.get(repoName)
  if (cachedBranchNames !== undefined) {
    return cachedBranchNames
  }

  // Get all branches available in the repository
  const allBranches = (await exec(`git -C ${repoConfig.location} branch --format="%(refname:short)"`)).stdout.split("\n").filter(branch => branch !== '')

  // Sort the list of branch descriptions from branchesToPull by the length
  // of their patterns.
  // Then, for each branch from the repository, see if it matches a pattern.
  // If that pattern has rules associated with it (like a description or a max),
  // apply those.
  type RulesObject = {max?: number, description?: string, compareTo?: string}
  let branchRules: Array<{pattern: string, matches: Array<string>, rules: RulesObject}> = repoConfig.branchesToPull.map((branchDescription) => {
    const rules: RulesObject = {}

    if (typeof branchDescription !== 'string') {
      if (branchDescription.max) { rules.max = branchDescription.max }
      if (branchDescription.description) { rules.description = branchDescription.description }
      if (branchDescription.compareTo) { rules.compareTo = branchDescription.compareTo }
    }

    return {
      pattern: (typeof branchDescription === 'string' ? branchDescription : branchDescription.pattern),
      matches: [],
      rules,
    }
  })

  branchRules.sort((a, b) => b.pattern.length - a.pattern.length)

  allBranches.forEach((branchName) => {
    const matchingPatternIndex = branchRules.findIndex((branchRule) => {
      return minimatch(branchName, branchRule.pattern)
    })

    if (matchingPatternIndex === -1) { return }

    const matchedRule = branchRules[matchingPatternIndex]
    matchedRule.matches.push(branchName)
    if (
      matchedRule.rules?.max
      && matchedRule.rules?.max < matchedRule.matches.length
    ) {
      matchedRule.matches.pop()
    }
  })

  const branches: Array<{
    name: string,
    description?: string,
    compareTo?: string,
  }> = branchRules.map((branchRule) => {
    return branchRule.matches.map((match) => {
      const result = {name: match}
      if (branchRule.rules.description) { result['description'] = branchRule.rules.description }
      if (branchRule.rules.compareTo) { result['compareTo'] = branchRule.rules.compareTo }
      return result
    }).flat()
  }).flat()

  if (branchesForReposMap.get(repoName) === undefined) {
    branchesForReposMap.set(repoName, branches)
  }

  return branches
}

const getBranchNames = async (repoConfig, repoName) => {
  return (await getBranches(repoConfig, repoName)).map(branch => branch.name)
}

let cachedRepos: Array<Repository> | null = null

const repos: (reposConfig: ReposConfiguration, outputDir: string) => Promise<Array<Repository>> = async (reposConfig, outputDir) => {
  if (cachedRepos !== null) { return cachedRepos }

  const repoNames = Object.keys(reposConfig.repos)
  cachedRepos = []

  for (const repoName of repoNames) {
    const repoLocation = getLocation(reposConfig, outputDir, repoName)
    const branchesToAdd = await getBranches(reposConfig.repos[repoName], repoName)
    const commits: Repository['commits'] = new Map()
    for (const branchName of await getBranchNames(reposConfig.repos[repoName], repoName)) {
      await addBranchToCommitsMap(branchName, repoLocation, commits)
    }

    const branches = await Promise.all(branchesToAdd.map(async (branch) => {
      const repoLocation = getLocation(reposConfig, outputDir, repoName)
      const branchHeadRes = await exec(`git -C ${repoLocation} show-ref refs/heads/${branch.name}`)
      const branchHead = branchHeadRes.stdout.split(" ")[0]
      const result = {
        name: branch.name,
        head: branchHead,
        fileList: await getFileList(branch.name, repoLocation)
      }
      if (branch.description) { result['description'] = branch.description }
      if (branch.compareTo) { result['compareTo'] = branch.compareTo }

      return result
    }))

    const branchesWithCompareToInfo: Repository['branches'] = branches.map((branch) => {
      const branchDescription = branchesToAdd.find(branchToAdd => branchToAdd.name === branch.name)
      const compareTo = branchDescription.compareTo || reposConfig.repos[repoName].defaultBranch
      const compareToBranch = branches.find((test) => test.name === compareTo)

      const compareToBranchCommits = new Set<string>()
      let currentCommit = commits.get(compareToBranch.head)
      while (currentCommit !== undefined) {
        compareToBranchCommits.add(currentCommit.hash)
        currentCommit = commits.get(currentCommit.parent)
      }

      const thisBranchCommits = new Set<string>()
      currentCommit = commits.get(branch.head)
      while (currentCommit !== undefined) {
        thisBranchCommits.add(currentCommit.hash)
        currentCommit = commits.get(currentCommit.parent)
      }

      // At this point, we have all commits in the compareTo branch in one set, and
      // all commits from this branch in another set.
      const onlyInThisBranch = Array.from(thisBranchCommits).filter((thisBranchCommit) => {
        return !commits.get(thisBranchCommit).isMerge && !compareToBranchCommits.has(thisBranchCommit)
      }).length
      const onlyInCompareToBranch = Array.from(compareToBranchCommits).filter((compareToBranchCommit) => {
        return !commits.get(compareToBranchCommit).isMerge && !thisBranchCommits.has(compareToBranchCommit)
      }).length

      const compareToInfo = {
        ahead: onlyInThisBranch,
        behind: onlyInCompareToBranch,
        compareTo: compareTo,
      }

      return {...branch, ...compareToInfo}
    })

    cachedRepos.push({
      name: repoName,
      description: reposConfig.repos[repoName].description,
      branches: branchesWithCompareToInfo,
      cloneUrl: cloneUrl.cloneUrl(reposConfig.baseUrl, repoName),
      defaultBranch: reposConfig.repos[repoName].defaultBranch,
      tags: [], // todo
      commits,
    })
  }

  return cachedRepos
}

export {repos, getBranchNames}
