import { createHash } from 'node:crypto';
import { lstat, readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { isDeepStrictEqual } from 'node:util';

const repositoryRoot = path.resolve(
  path.dirname(fileURLToPath(import.meta.url)),
  '..'
);
const argumentsList = process.argv.slice(2);
const runtimeOnly = argumentsList.includes('--runtime-only');
if (
  argumentsList.some((argument) => argument !== '--runtime-only') ||
  (runtimeOnly && argumentsList.length !== 1)
) {
  throw new Error(
    'Usage: node scripts/verify-ffmpeg-assets.mjs [--runtime-only]'
  );
}
const lock = JSON.parse(
  await readFile(
    path.join(repositoryRoot, 'scripts', 'reviewed-ffmpeg-core-lock.json'),
    'utf8'
  )
);
if (
  lock.schemaVersion !== 1 ||
  typeof lock.buildId !== 'string' ||
  !/^[a-zA-Z0-9._-]+$/.test(lock.buildId) ||
  lock.coreVersion !== '0.12.10' ||
  lock.profile !== 'reviewed-stack5m' ||
  lock.sourceArchive?.path !==
    'release/ffmpeg-core-0.12.10-reviewed-stack5m.1-corresponding-source.tar.xz' ||
  !Number.isSafeInteger(lock.sourceArchive?.bytes) ||
  lock.sourceArchive.bytes < 1 ||
  lock.sourceArchive.bytes > 512 * 1024 * 1024 ||
  !/^[a-f0-9]{64}$/.test(lock.sourceArchive?.sha256 ?? '') ||
  lock.sourceLock?.path !== 'scripts/ffmpeg-source-lock.json' ||
  !/^[a-f0-9]{64}$/.test(lock.sourceLock?.sha256 ?? '') ||
  lock.buildDriver?.path !== 'scripts/build-reviewed-ffmpeg-core.sh' ||
  !/^[a-f0-9]{64}$/.test(lock.buildDriver?.sha256 ?? '') ||
  lock.artifact?.path !== `vendor/ffmpeg-core-${lock.buildId}.zip` ||
  !Number.isSafeInteger(lock.artifact?.bytes) ||
  lock.artifact.bytes < 1 ||
  lock.artifact.bytes > 64 * 1024 * 1024 ||
  !/^[a-f0-9]{64}$/.test(lock.artifact?.sha256 ?? '') ||
  !Array.isArray(lock.assets) ||
  lock.assets.length !== 5
) {
  throw new Error('Invalid reviewed FFmpeg core lock.');
}
const outputRoot = path.join(
  repositoryRoot,
  'public',
  'vendor',
  'ffmpeg',
  lock.buildId
);
const manifestPath = path.join(outputRoot, 'version.json');
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex');
const safeAssetPath = (value) => {
  if (
    typeof value !== 'string' ||
    value.length === 0 ||
    value.includes('\\') ||
    value.includes('\0') ||
    value.startsWith('/') ||
    path.posix.normalize(value) !== value ||
    value.split('/').some((part) => !part || part === '.' || part === '..')
  ) {
    throw new Error(`Unsafe FFmpeg asset path: ${String(value)}`);
  }
  return value;
};

const verifyLockedFile = async (record, label) => {
  const relative = safeAssetPath(record.path);
  const absolute = path.join(repositoryRoot, ...relative.split('/'));
  const details = await lstat(absolute).catch((error) => {
    throw new Error(`${label} is missing: ${relative}`, { cause: error });
  });
  if (details.isSymbolicLink() || !details.isFile()) {
    throw new Error(`${label} must be a regular file: ${relative}`);
  }
  const bytes = await readFile(absolute);
  if (
    (record.bytes !== undefined && bytes.byteLength !== record.bytes) ||
    sha256(bytes) !== record.sha256
  ) {
    throw new Error(`${label} does not match its locked identity: ${relative}`);
  }
};

if (
  manifest.schemaVersion !== 2 ||
  manifest.buildId !== lock.buildId ||
  manifest.profile !== lock.profile ||
  manifest.coreVersion !== lock.coreVersion ||
  manifest.ffmpegVersion !== lock.ffmpegVersion ||
  !isDeepStrictEqual(manifest.sourceArchive, lock.sourceArchive) ||
  !isDeepStrictEqual(manifest.sourceLock, lock.sourceLock) ||
  !isDeepStrictEqual(manifest.buildDriver, lock.buildDriver) ||
  !isDeepStrictEqual(manifest.toolchain, lock.toolchain) ||
  !isDeepStrictEqual(manifest.patch, lock.patch) ||
  !isDeepStrictEqual(manifest.opusRegression, lock.opusRegression) ||
  !Array.isArray(manifest.assets) ||
  manifest.assets.length !== lock.assets.length
) {
  throw new Error(`Invalid reviewed FFmpeg asset manifest: ${manifestPath}`);
}

if (!runtimeOnly) {
  await verifyLockedFile(lock.sourceArchive, 'FFmpeg Corresponding Source');
}
await verifyLockedFile(lock.sourceLock, 'FFmpeg source lock');
await verifyLockedFile(lock.buildDriver, 'Reviewed-core build driver');
await verifyLockedFile(lock.artifact, 'Reviewed-core deterministic artifact');

const lockedAssets = new Map(lock.assets.map((asset) => [asset.path, asset]));
const seen = new Set();
for (const asset of manifest.assets) {
  safeAssetPath(asset.path);
  const locked = lockedAssets.get(asset.path);
  if (
    !locked ||
    seen.has(asset.path) ||
    asset.bytes !== locked.bytes ||
    asset.sha256 !== locked.sha256
  ) {
    throw new Error(`Unexpected reviewed FFmpeg asset: ${asset.path}`);
  }
  seen.add(asset.path);
  const bytes = await readFile(path.join(outputRoot, ...asset.path.split('/')));
  if (bytes.byteLength !== locked.bytes || sha256(bytes) !== locked.sha256) {
    throw new Error(`FFmpeg asset verification failed: ${asset.path}`);
  }
}
if (seen.size !== lockedAssets.size) {
  throw new Error('One or more reviewed FFmpeg assets are missing.');
}

console.log(
  `Verified ${seen.size} FFmpeg ${lock.buildId} assets in ${path.relative(repositoryRoot, outputRoot)}${runtimeOnly ? ' (runtime inputs only)' : ' with Corresponding Source'}`
);
