import { git, npm, readPackageJSON } from './utils.ts';

let args: ParsedArgs;
try {
  args = parseArgs();
  validateBranchState(args.releaseBranch);
} catch (error) {
  console.error(error instanceof Error ? error.message : String(error));
  process.exit(1);
}

console.log('Installing dependencies...');
npm().ci('--ignore-scripts');

console.log('Bumping package version without creating a tag...');
npm().version(...args.npmVersionArgs, '--no-git-tag-version');

console.log('Updating src/version.ts...');
npm().exec(
  '--',
  'node',
  '--experimental-strip-types',
  'resources/gen-version.ts',
);

console.log('Running test suite...');
npm().run('test');

const { version } = readPackageJSON();
console.log(`Generating changelog for v${version}...`);
const changelogArgs = ['--silent', 'changelog'];
if (args.fromRev != null) {
  changelogArgs.push('--', args.fromRev);
}
const releaseChangelog = npm().runOutput(...changelogArgs);
const releaseCommitTitle = `chore(release): v${version}`;

console.log('Creating release commit...');
git().add('package.json', 'package-lock.json', 'src/version.ts');
git().commit('-m', releaseCommitTitle, '-m', releaseChangelog);

const currentBranch = git({ quiet: true }).revParse('--abbrev-ref', 'HEAD');

console.log('');
console.log(`Release commit created for v${version}.`);
console.log(
  `Next steps: push "${currentBranch}", open a PR to "${args.releaseBranch}", wait for CI, then merge.`,
);

interface ParsedArgs {
  fromRev: string | null;
  releaseBranch: string;
  npmVersionArgs: Array<string>;
}

function parseArgs(): ParsedArgs {
  const rawArgs = process.argv.slice(2);
  const fromRevArgName = '--fromRev';
  let fromRev: string | null = null;
  let releasePrepareArgs = rawArgs;
  if (rawArgs[0] === fromRevArgName) {
    fromRev = rawArgs[1] ?? null;
    releasePrepareArgs = rawArgs.slice(2);
  } else if (rawArgs.includes(fromRevArgName)) {
    throwUsage(`${fromRevArgName} must be the first argument when provided.`);
  }

  const releaseBranch = releasePrepareArgs[0];
  if (releaseBranch == null || releaseBranch.trim() === '') {
    throwUsage('Missing required release branch as the first argument.');
  }
  if (releaseBranch.startsWith('-')) {
    throwUsage(
      'Missing required release branch as the first argument (before options).',
    );
  }

  const npmVersionArgs = releasePrepareArgs.slice(1);

  if (npmVersionArgs.length === 0) {
    throwUsage(
      'Missing npm version arguments (e.g. patch, major, prerelease --preid beta).',
    );
  }

  return {
    fromRev,
    releaseBranch,
    npmVersionArgs,
  };
}

function validateBranchState(releaseBranch: string): void {
  const checkedBranch = git({ quiet: true }).revParse('--abbrev-ref', 'HEAD');
  if (checkedBranch === 'HEAD') {
    throw new Error(
      'Git is in detached HEAD state (not on a local branch). ' +
        'Switch to a local branch based on the release branch first, for example:\n' +
        `  git switch -c release-${releaseBranch.replace(
          /[^a-zA-Z0-9._-]/g,
          '-',
        )} ${releaseBranch}`,
    );
  }
  if (checkedBranch === releaseBranch) {
    throw new Error(
      `Release prepare must not run on "${releaseBranch}". Create a local release branch first.`,
    );
  }

  const status = git().status('--porcelain').trim();
  if (status !== '') {
    throw new Error(
      'Working directory must be clean before running release:prepare.',
    );
  }
  const branchStatus = git().status('--porcelain', '--branch');
  const branchSummary = branchStatus.split('\n')[0] ?? '';
  if (/\[[^\]]+\]/.test(branchSummary)) {
    throw new Error(
      `Current branch "${checkedBranch}" is not up to date with its upstream.`,
    );
  }

  let releaseBranchHead: string;
  try {
    releaseBranchHead = git({ quiet: true }).revParse(releaseBranch);
  } catch (error) {
    throw new Error(
      `Release branch "${releaseBranch}" does not exist locally.`,
      {
        cause: error,
      },
    );
  }

  let releaseBranchUpstream: string;
  try {
    releaseBranchUpstream = git({ quiet: true }).revParse(
      '--abbrev-ref',
      `${releaseBranch}@{upstream}`,
    );
  } catch (error) {
    throw new Error(
      `Release branch "${releaseBranch}" does not track a remote branch. ` +
        'Set one first (for example: git branch --set-upstream-to ' +
        `<remote>/${releaseBranch} ${releaseBranch}).`,
      { cause: error },
    );
  }

  const upstreamRemote = releaseBranchUpstream.split('/')[0];
  try {
    git().fetch('--quiet', '--tags', upstreamRemote, releaseBranch);
  } catch (error) {
    throw new Error(
      `Failed to fetch "${releaseBranchUpstream}" and tags from "${upstreamRemote}". ` +
        'Check remote access, authentication, git remote configuration, ' +
        'and local/remote tag state.',
      { cause: error },
    );
  }

  const upstreamReleaseBranchHead = git({ quiet: true }).revParse(
    `${releaseBranch}@{upstream}`,
  );
  const localOnlyCommits = git().revList(
    `${upstreamReleaseBranchHead}..${releaseBranchHead}`,
  );
  const upstreamOnlyCommits = git().revList(
    `${releaseBranchHead}..${upstreamReleaseBranchHead}`,
  );
  if (localOnlyCommits.length > 0 && upstreamOnlyCommits.length > 0) {
    throw new Error(
      `Local "${releaseBranch}" has diverged from "${releaseBranchUpstream}". ` +
        'Resolve conflicts and synchronize first (for example: ' +
        `git switch ${releaseBranch} && git pull --rebase).`,
    );
  }
  if (upstreamOnlyCommits.length > 0) {
    throw new Error(
      `Local "${releaseBranch}" is behind "${releaseBranchUpstream}". ` +
        `Update it first (for example: git switch ${releaseBranch} && git pull --ff-only).`,
    );
  }
  if (localOnlyCommits.length > 0) {
    throw new Error(
      `Local "${releaseBranch}" is ahead of "${releaseBranchUpstream}". ` +
        `Push or reset it before release prepare (for example: git switch ${releaseBranch} && git push).`,
    );
  }

  const currentHead = git({ quiet: true }).revParse('HEAD');
  if (currentHead !== releaseBranchHead) {
    throw new Error(
      `Current branch "${checkedBranch}" must match "${releaseBranch}" before preparing a release.`,
    );
  }
}

function throwUsage(message: string): never {
  throw new Error(
    `${message}\n` +
      'Usage: npm run release:prepare -- [--fromRev <fromRev>] <release-branch> <npm version args>\n' +
      'Examples:\n' +
      '  npm run release:prepare -- 17.x.x patch\n' +
      '  npm run release:prepare -- 17.x.x prerelease --preid alpha\n' +
      '  npm run release:prepare -- 17.x.x prerelease --preid beta\n' +
      '  npm run release:prepare -- --fromRev <fromRev> 17.x.x prerelease --preid rc',
  );
}