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

const repositoryRoot = path.resolve(
  path.dirname(fileURLToPath(import.meta.url)),
  '..'
);
const lockPath = path.join(
  repositoryRoot,
  'scripts',
  'reviewed-ffmpeg-core-lock.json'
);
const maximumAssetBytes = 64 * 1024 * 1024;
const fixedTimestamp = new Date(1980, 0, 1, 0, 0, 0, 0);
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}`);
  }
}

async function readLock() {
  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' ||
    !Number.isSafeInteger(lock.sourceArchive?.bytes) ||
    !/^[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 ?? '') ||
    !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)
    ) ||
    typeof lock.artifact?.path !== 'string' ||
    !Number.isSafeInteger(lock.artifact?.bytes) ||
    lock.artifact.bytes < 1 ||
    lock.artifact.bytes > 64 * 1024 * 1024 ||
    !/^[a-f0-9]{64}$/.test(lock.artifact?.sha256 ?? '')
  ) {
    throw new Error(`Invalid reviewed FFmpeg core lock: ${lockPath}`);
  }
  return lock;
}

async function readBuildAsset(asset) {
  const relativeAssetPath = assertSafeRelativePath(asset.path);
  const [mode, fileName, ...extra] = relativeAssetPath.split('/');
  if (
    extra.length > 0 ||
    !['st', 'mt'].includes(mode) ||
    !['ffmpeg-core.js', 'ffmpeg-core.wasm', 'ffmpeg-core.worker.js'].includes(
      fileName
    )
  ) {
    throw new Error(`Unexpected reviewed-core asset: ${relativeAssetPath}`);
  }
  const outputDirectory = path.join(
    repositoryRoot,
    '.core-build',
    'reviewed-stack5m',
    mode,
    'output'
  );
  const source = path.join(outputDirectory, fileName);
  const details = await lstat(source);
  if (details.isSymbolicLink() || !details.isFile()) {
    throw new Error(`Reviewed-core input must be a regular file: ${source}`);
  }
  const canonicalOutput = await realpath(outputDirectory);
  const canonicalSource = await realpath(source);
  if (!canonicalSource.startsWith(`${canonicalOutput}${path.sep}`)) {
    throw new Error(`Reviewed-core input resolves outside output: ${source}`);
  }
  const bytes = await readFile(canonicalSource);
  if (
    bytes.byteLength < 1 ||
    bytes.byteLength > maximumAssetBytes ||
    bytes.byteLength !== asset.bytes ||
    sha256(bytes) !== asset.sha256
  ) {
    throw new Error(`Reviewed-core build does not match lock: ${asset.path}`);
  }
  return bytes;
}

function verifyArchiveEntries(archive, lock) {
  const lockedEntries = new Map(
    lock.assets.map((asset) => [asset.path, asset])
  );
  const seen = new Set();
  const uncompressed = unzipSync(archive, {
    filter: (entry) => {
      const relative = assertSafeRelativePath(entry.name);
      const locked = lockedEntries.get(relative);
      if (
        !locked ||
        seen.has(relative) ||
        entry.originalSize !== locked.bytes ||
        entry.originalSize < 1 ||
        entry.originalSize > maximumAssetBytes ||
        entry.size < 1 ||
        entry.size > 64 * 1024 * 1024
      ) {
        throw new Error(
          `Reviewed-core archive contains an unexpected entry: ${entry.name}`
        );
      }
      seen.add(relative);
      return true;
    },
  });
  if (seen.size !== lockedEntries.size) {
    throw new Error('Reviewed-core archive is missing an expected entry.');
  }
  for (const asset of lock.assets) {
    const bytes = uncompressed[asset.path];
    if (
      !bytes ||
      bytes.byteLength !== asset.bytes ||
      sha256(bytes) !== asset.sha256
    ) {
      throw new Error(
        `Reviewed-core archive entry does not match lock: ${asset.path}`
      );
    }
  }
}

const lock = await readLock();
const buildDriver = await readFile(
  path.join(repositoryRoot, ...lock.buildDriver.path.split('/'))
);
if (sha256(buildDriver) !== lock.buildDriver.sha256) {
  throw new Error(
    'Reviewed-core build driver does not match its locked identity.'
  );
}
const sourceLock = await readFile(
  path.join(repositoryRoot, ...lock.sourceLock.path.split('/'))
);
if (sha256(sourceLock) !== lock.sourceLock.sha256) {
  throw new Error('FFmpeg source lock does not match its locked identity.');
}
const sourceArchive = await readFile(
  path.join(repositoryRoot, ...lock.sourceArchive.path.split('/'))
);
if (
  sourceArchive.byteLength !== lock.sourceArchive.bytes ||
  sha256(sourceArchive) !== lock.sourceArchive.sha256
) {
  throw new Error(
    'Reviewed-core Corresponding Source archive does not match its locked identity.'
  );
}
const artifactRelativePath = assertSafeRelativePath(lock.artifact.path);
const expectedArtifactRelativePath = `vendor/ffmpeg-core-${lock.buildId}.zip`;
if (artifactRelativePath !== expectedArtifactRelativePath) {
  throw new Error(
    `Reviewed-core artifact path must be ${expectedArtifactRelativePath}.`
  );
}
const artifactPath = path.join(
  repositoryRoot,
  ...artifactRelativePath.split('/')
);
const entries = {};
for (const asset of [...lock.assets].sort((left, right) =>
  left.path.localeCompare(right.path, 'en')
)) {
  entries[asset.path] = [
    await readBuildAsset(asset),
    { level: 9, mtime: fixedTimestamp },
  ];
}
const archive = Buffer.from(zipSync(entries, { level: 9 }));
verifyArchiveEntries(archive, lock);
const archiveDigest = sha256(archive);
if (
  archive.byteLength !== lock.artifact.bytes ||
  archiveDigest !== lock.artifact.sha256
) {
  throw new Error(
    `Deterministic reviewed-core archive does not match its locked identity: received ${String(archive.byteLength)} bytes/${archiveDigest}.`
  );
}

const artifactDirectory = path.dirname(artifactPath);
await requireRealDirectory(repositoryRoot);
await requireRealDirectory(artifactDirectory, true);
const stagingRoot = await mkdtemp(
  path.join(artifactDirectory, '.reviewed-core-package-')
);
if (
  !stagingRoot.startsWith(
    `${path.join(artifactDirectory, '.reviewed-core-package-')}`
  )
) {
  throw new Error(`Unexpected reviewed-core staging path: ${stagingRoot}`);
}
try {
  const stagedArtifact = path.join(stagingRoot, path.basename(artifactPath));
  await writeFile(stagedArtifact, archive, { flag: 'wx', mode: 0o644 });
  await rename(stagedArtifact, artifactPath);
} finally {
  await rm(stagingRoot, { force: true, recursive: true });
}

console.log(
  `Packaged ${lock.assets.length} reviewed FFmpeg assets as ${artifactRelativePath} (${archive.byteLength} bytes, ${archiveDigest})`
);
