import is from '@sindresorhus/is';
import type { RenovateConfig } from '../../../config/types';
import { addMeta, logger, removeMeta } from '../../../logger';
import { hashMap } from '../../../modules/manager';
import { scm } from '../../../modules/platform/scm';
import { getCache } from '../../../util/cache/repository';
import type { BranchCache } from '../../../util/cache/repository/types';
import { fingerprint } from '../../../util/fingerprint';
import { setBranchNewCommit } from '../../../util/git/set-branch-commit';
import { incCountValue, setCount } from '../../global/limits';
import type {
  BranchConfig,
  CacheFingerprintMatchResult,
  UpgradeFingerprintConfig,
} from '../../types';
import { processBranch } from '../update/branch';
import { upgradeFingerprintFields } from './fingerprint-fields';
import {
  getConcurrentBranchesCount,
  getConcurrentPrsCount,
  getPrHourlyCount,
} from './limits';

export type WriteUpdateResult = 'done' | 'automerged';

export function generateCommitFingerprintConfig(
  branch: BranchConfig,
): UpgradeFingerprintConfig[] {
  const res = branch.upgrades.map((upgrade) => {
    const filteredUpgrade = {} as UpgradeFingerprintConfig;
    for (const field of upgradeFingerprintFields) {
      filteredUpgrade[field] = upgrade[field];
    }
    return filteredUpgrade;
  });

  return res;
}

export function compareCacheFingerprint(
  branchState: BranchCache,
  commitFingerprint: string,
): CacheFingerprintMatchResult {
  if (!branchState.commitFingerprint) {
    logger.trace('branch.isUpToDate(): no fingerprint');
    return 'no-fingerprint';
  }

  if (commitFingerprint !== branchState.commitFingerprint) {
    logger.debug('branch.isUpToDate(): needs recalculation');
    return 'no-match';
  }

  logger.debug('branch.isUpToDate(): using cached result "true"');
  return 'matched';
}

export async function syncBranchState(
  branchName: string,
  baseBranch: string,
): Promise<BranchCache> {
  logger.debug('syncBranchState()');
  const branchSha = await scm.getBranchCommit(branchName);
  const baseBranchSha = await scm.getBranchCommit(baseBranch);

  const cache = getCache();
  cache.branches ??= [];
  const { branches: cachedBranches } = cache;
  let branchState = cachedBranches.find((br) => br.branchName === branchName);
  if (!branchState) {
    logger.debug(
      'syncBranchState(): Branch cache not found, creating minimal branchState',
    );
    // create a minimal branch state
    branchState = {
      branchName,
      sha: branchSha,
      baseBranch,
      baseBranchSha,
    } as BranchCache;
    cachedBranches.push(branchState);
  }

  // if base branch name has changed invalidate cached isModified state
  if (baseBranch !== branchState.baseBranch) {
    logger.debug('syncBranchState(): update baseBranch name');
    branchState.baseBranch = baseBranch;
    delete branchState.isModified;
    branchState.pristine = false;
  }

  // if base branch sha has changed invalidate cached isBehindBase state
  if (baseBranchSha !== branchState.baseBranchSha) {
    logger.debug('syncBranchState(): update baseBranchSha');
    delete branchState.isBehindBase;
    delete branchState.isConflicted;

    // update cached branchSha
    branchState.baseBranchSha = baseBranchSha;
    branchState.pristine = false;
  }

  // if branch sha has changed invalidate all cached states
  if (branchSha !== branchState.sha) {
    logger.debug('syncBranchState(): update branchSha');
    delete branchState.isBehindBase;
    delete branchState.isConflicted;
    delete branchState.isModified;
    delete branchState.commitFingerprint;

    // update cached branchSha
    branchState.sha = branchSha;
    branchState.pristine = false;
  }

  return branchState;
}

export async function writeUpdates(
  config: RenovateConfig,
  allBranches: BranchConfig[],
): Promise<WriteUpdateResult> {
  const branches = allBranches;
  logger.debug(
    `Processing ${branches.length} branch${
      branches.length === 1 ? '' : 'es'
    }: ${branches
      .map((b) => b.branchName)
      .sort()
      .join(', ')}`,
  );

  const concurrentPrsCount = await getConcurrentPrsCount(config, branches);
  setCount('ConcurrentPRs', concurrentPrsCount);

  const concurrentBranchesCount = await getConcurrentBranchesCount(branches);
  setCount('Branches', concurrentBranchesCount);

  const prsThisHourCount = await getPrHourlyCount(config);
  setCount('HourlyPRs', prsThisHourCount);

  for (const branch of branches) {
    const { baseBranch, branchName } = branch;
    const meta: Record<string, string> = { branch: branchName };
    if (config.baseBranchPatterns?.length && baseBranch) {
      meta.baseBranch = baseBranch;
    }
    addMeta(meta);
    const branchExisted = await scm.branchExists(branchName);
    const branchState = await syncBranchState(branchName, baseBranch);

    const managers = [
      ...new Set(
        branch.upgrades
          .map((upgrade) => hashMap.get(upgrade.manager) ?? upgrade.manager)
          .filter(is.string),
      ),
    ].sort();
    const commitFingerprint = fingerprint({
      commitFingerprintConfig: generateCommitFingerprintConfig(branch),
      managers,
    });
    branch.cacheFingerprintMatch = compareCacheFingerprint(
      branchState,
      commitFingerprint,
    );

    const res = await processBranch(branch);
    branch.prBlockedBy = res?.prBlockedBy;
    branch.prNo = res?.prNo;
    branch.result = res?.result;
    branch.commitFingerprint = res?.updatesVerified
      ? commitFingerprint
      : branchState.commitFingerprint;

    if (res?.commitSha) {
      setBranchNewCommit(branchName, baseBranch, res.commitSha);
    }
    if (
      branch.result === 'automerged' &&
      branch.automergeType !== 'pr-comment'
    ) {
      // Stop processing other branches because base branch has been changed
      return 'automerged';
    }
    if (!branchExisted && (await scm.branchExists(branch.branchName))) {
      incCountValue('Branches');
    }
  }
  removeMeta(['branch', 'baseBranch']);
  return 'done';
}
