import { createHash } from 'node:crypto';
import { lstat, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { unzipSync } from 'fflate';

const repositoryRoot = path.resolve(
  path.dirname(fileURLToPath(import.meta.url)),
  '..'
);
const lockPath = path.join(
  repositoryRoot,
  'scripts',
  'reviewed-ffmpeg-core-lock.json'
);
const maximumArchiveBytes = 64 * 1024 * 1024;
const maximumAssetBytes = 64 * 1024 * 1024;
const requiredAssetPaths = new Set([
  'st/ffmpeg-core.js',
  'st/ffmpeg-core.wasm',
  'mt/ffmpeg-core.js',
  'mt/ffmpeg-core.wasm',
  'mt/ffmpeg-core.worker.js',
]);

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

function assertSafeRelativePath(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 reviewed-core path: ${String(value)}`);
  }
  return value;
}

async function requireRealDirectory(directory, create = false) {
  let details = await lstat(directory).catch((error) => {
    if (error && typeof error === 'object' && error.code === 'ENOENT') {
      return undefined;
    }
    throw error;
  });
  if (!details && create) {
    await mkdir(directory);
    details = await lstat(directory);
  }
  if (!details || details.isSymbolicLink() || !details.isDirectory()) {
    throw new Error(`Required directory is unsafe: ${directory}`);
  }
}

const lock = JSON.parse(await readFile(lockPath, 'utf8'));
const lockedAssetPaths = new Set(
  Array.isArray(lock.assets) ? lock.assets.map((asset) => asset.path) : []
);
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' ||
  !/^[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.patch?.upstreamCommit !== 'b409e36475bc21f0451b5b1e1d126fa82871439a' ||
  lock.patch?.stackSizeBytes !== 5 * 1024 * 1024 ||
  typeof lock.opusRegression?.singleThread !== 'boolean' ||
  typeof lock.opusRegression?.multiThread !== 'boolean' ||
  !Array.isArray(lock.assets) ||
  lock.assets.length !== 5 ||
  lock.assets.some(
    (asset) =>
      !requiredAssetPaths.has(asset.path) ||
      !Number.isSafeInteger(asset.bytes) ||
      asset.bytes < 1 ||
      asset.bytes > maximumAssetBytes ||
      !/^[a-f0-9]{64}$/.test(asset.sha256 ?? '')
  ) ||
  lockedAssetPaths.size !== requiredAssetPaths.size ||
  [...requiredAssetPaths].some((assetPath) => !lockedAssetPaths.has(assetPath))
) {
  throw new Error(`Invalid reviewed FFmpeg core lock: ${lockPath}`);
}

const artifactRelativePath = assertSafeRelativePath(lock.artifact?.path);
if (artifactRelativePath !== `vendor/ffmpeg-core-${lock.buildId}.zip`) {
  throw new Error('Reviewed-core artifact path and build identity disagree.');
}
const artifact = await readFile(
  path.join(repositoryRoot, ...artifactRelativePath.split('/'))
);
if (
  artifact.byteLength < 1 ||
  artifact.byteLength > maximumArchiveBytes ||
  artifact.byteLength !== lock.artifact.bytes ||
  sha256(artifact) !== lock.artifact.sha256
) {
  throw new Error('Reviewed FFmpeg core archive failed locked verification.');
}

const lockedArchiveEntries = new Map(
  lock.assets.map((asset) => [asset.path, asset])
);
const archiveEntries = new Set();
const uncompressed = unzipSync(artifact, {
  filter: (entry) => {
    const relative = assertSafeRelativePath(entry.name);
    const locked = lockedArchiveEntries.get(relative);
    if (
      !locked ||
      archiveEntries.has(relative) ||
      entry.originalSize !== locked.bytes ||
      entry.originalSize < 1 ||
      entry.originalSize > maximumAssetBytes ||
      entry.size < 1 ||
      entry.size > maximumArchiveBytes
    ) {
      throw new Error(
        `Reviewed FFmpeg core archive has an unexpected entry: ${entry.name}`
      );
    }
    archiveEntries.add(relative);
    return true;
  },
});
if (archiveEntries.size !== lockedArchiveEntries.size) {
  throw new Error('Reviewed FFmpeg core archive is missing an expected entry.');
}

const vendorRoot = path.join(repositoryRoot, 'public', 'vendor', 'ffmpeg');
const outputRoot = path.join(vendorRoot, lock.buildId);
await requireRealDirectory(repositoryRoot);
await requireRealDirectory(path.join(repositoryRoot, 'public'));
await requireRealDirectory(path.join(repositoryRoot, 'public', 'vendor'), true);
const existingVendorRoot = await lstat(vendorRoot).catch((error) => {
  if (error && typeof error === 'object' && error.code === 'ENOENT') {
    return undefined;
  }
  throw error;
});
if (
  existingVendorRoot &&
  (existingVendorRoot.isSymbolicLink() || !existingVendorRoot.isDirectory())
) {
  throw new Error(`Generated FFmpeg vendor root is unsafe: ${vendorRoot}`);
}
await rm(vendorRoot, { force: true, recursive: true });

for (const asset of lock.assets) {
  const relativeAssetPath = assertSafeRelativePath(asset.path);
  const bytes = uncompressed[relativeAssetPath];
  if (
    !bytes ||
    bytes.byteLength < 1 ||
    bytes.byteLength > maximumAssetBytes ||
    bytes.byteLength !== asset.bytes ||
    sha256(bytes) !== asset.sha256
  ) {
    throw new Error(
      `Reviewed FFmpeg core entry failed locked verification: ${asset.path}`
    );
  }
  const destination = path.join(outputRoot, ...relativeAssetPath.split('/'));
  await mkdir(path.dirname(destination), { recursive: true });
  await writeFile(destination, bytes, { flag: 'wx', mode: 0o644 });
}

const versionManifest = {
  schemaVersion: 2,
  buildId: lock.buildId,
  profile: lock.profile,
  coreVersion: lock.coreVersion,
  ffmpegVersion: lock.ffmpegVersion,
  sourceArchive: lock.sourceArchive,
  sourceLock: lock.sourceLock,
  buildDriver: lock.buildDriver,
  toolchain: lock.toolchain,
  patch: lock.patch,
  opusRegression: lock.opusRegression,
  assets: lock.assets,
};
await writeFile(
  path.join(outputRoot, 'version.json'),
  `${JSON.stringify(versionManifest, null, 2)}\n`,
  { flag: 'wx', mode: 0o644 }
);

console.log(
  `Verified and unpacked ${lock.assets.length} reviewed FFmpeg assets to ${path.relative(repositoryRoot, outputRoot)}`
);
