import { createHash } from 'node:crypto';
import { access, lstat, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

const repositoryRoot = path.resolve(
  path.dirname(fileURLToPath(import.meta.url)),
  '..'
);

export function sha256(data) {
  return createHash('sha256').update(data).digest('hex');
}

export async function sha256File(file) {
  return sha256(await readFile(file));
}

async function requireRegularFile(file, label) {
  const details = await lstat(file);
  if (details.isSymbolicLink() || !details.isFile()) {
    throw new Error(`${label} must be a regular file: ${file}`);
  }
}

export function checksumLine(digest, archiveName) {
  if (!/^[a-f0-9]{64}$/.test(digest)) {
    throw new Error('SHA-256 digest must be 64 lowercase hexadecimal digits.');
  }
  if (
    typeof archiveName !== 'string' ||
    archiveName.length === 0 ||
    archiveName.includes('/') ||
    archiveName.includes('\\') ||
    archiveName.includes('\n') ||
    archiveName.includes('\r')
  ) {
    throw new Error('Checksum archive name must be one safe basename.');
  }
  return `${digest}  ${archiveName}\n`;
}

export function parseChecksumLine(contents) {
  const match = /^([a-f0-9]{64})  ([^/\\\r\n]+)\n?$/.exec(contents);
  if (!match) {
    throw new Error(
      'Checksum sidecar must contain `<lowercase-sha256>  <archive-basename>`.'
    );
  }
  return { digest: match[1], archiveName: match[2] };
}

export async function verifyChecksum(archivePath, sidecarPath) {
  await requireRegularFile(archivePath, 'Release archive');
  await requireRegularFile(sidecarPath, 'Checksum sidecar');
  const expected = parseChecksumLine(await readFile(sidecarPath, 'utf8'));
  if (expected.archiveName !== path.basename(archivePath)) {
    throw new Error(
      `Checksum names ${expected.archiveName}, not ${path.basename(archivePath)}.`
    );
  }
  const actual = await sha256File(archivePath);
  if (actual !== expected.digest) {
    throw new Error(
      `Checksum mismatch for ${path.basename(archivePath)}: expected ${expected.digest}, received ${actual}.`
    );
  }
  return actual;
}

export async function writeChecksum(archivePath, options = {}) {
  const sidecarPath = options.sidecarPath ?? `${archivePath}.sha256`;
  await requireRegularFile(archivePath, 'Release archive');
  if (options.force && (await exists(sidecarPath))) {
    await requireRegularFile(sidecarPath, 'Existing checksum sidecar');
  }
  const digest = await sha256File(archivePath);
  await writeFile(
    sidecarPath,
    checksumLine(digest, path.basename(archivePath)),
    {
      encoding: 'utf8',
      flag: options.force ? 'w' : 'wx',
    }
  );
  return { digest, sidecarPath };
}

async function exists(file) {
  try {
    await access(file);
    return true;
  } catch (error) {
    if (error && typeof error === 'object' && error.code === 'ENOENT') {
      return false;
    }
    throw error;
  }
}

async function main() {
  const packageJson = JSON.parse(
    await readFile(path.join(repositoryRoot, 'package.json'), 'utf8')
  );
  const defaultArchive = path.join(
    repositoryRoot,
    'release',
    `av-tools-${packageJson.version}.zip`
  );
  const argumentsList = process.argv.slice(2);
  const write = argumentsList.includes('--write');
  const force = argumentsList.includes('--force');
  const positional = argumentsList.filter(
    (argument) => argument !== '--write' && argument !== '--force'
  );
  if (positional.length > 1) {
    throw new Error(
      'Usage: node scripts/checksum-release.mjs [archive.zip] [--write] [--force]'
    );
  }

  const archivePath = path.resolve(
    repositoryRoot,
    positional[0] ?? defaultArchive
  );
  const sidecarPath = `${archivePath}.sha256`;
  if (write) {
    const result = await writeChecksum(archivePath, { force, sidecarPath });
    console.log(
      `Wrote ${path.relative(repositoryRoot, result.sidecarPath)} (${result.digest})`
    );
    return;
  }
  if (!(await exists(sidecarPath))) {
    throw new Error(
      `${path.relative(repositoryRoot, sidecarPath)} is missing; pass --write to create it.`
    );
  }
  const digest = await verifyChecksum(archivePath, sidecarPath);
  console.log(
    `Verified ${path.relative(repositoryRoot, archivePath)} (${digest})`
  );
}

if (
  process.argv[1] &&
  import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
) {
  await main();
}
