#!/usr/bin/env node
/**
 * Distribution provenance gate.
 *
 * Every tracked media/model/font artifact must be listed exactly once in
 * PROVENANCE.json and match its recorded SHA-256. Original generated artifacts
 * must name a tracked generator and lineage note. A copied item additionally
 * needs an immutable upstream revision, exact upstream path, and intake record.
 */

import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { dirname, extname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const manifestPath = resolve(root, "PROVENANCE.json");
const ARTIFACT_EXTENSIONS = new Set([
  ".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".ico",
  ".glb", ".gltf", ".fbx", ".obj", ".hdr", ".exr",
  ".ttf", ".otf", ".woff", ".woff2",
  ".mp3", ".wav", ".ogg", ".mp4", ".webm",
]);
const FONT_EXTENSIONS = new Set([".ttf", ".otf", ".woff", ".woff2"]);
const ALLOWED_ARTIFACT_LICENSES = new Set([
  "Apache-2.0", "BSD-3-Clause", "CC0-1.0", "CC-BY-4.0", "ISC", "MIT",
]);
const problems = [];

function trackedFiles() {
  try {
    return execFileSync("git", ["ls-files", "-z"], {
      cwd: root,
      encoding: "utf8",
      maxBuffer: 64 * 1024 * 1024,
    }).split("\0").filter(Boolean);
  } catch {
    console.error("check-provenance: FAIL — run this gate inside a git checkout.");
    process.exit(1);
  }
}

function nonEmpty(value) {
  return typeof value === "string" && value.trim().length > 0;
}

function validDate(value) {
  return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value);
}

function hash(path) {
  return createHash("sha256").update(readFileSync(resolve(root, path))).digest("hex");
}

function pathIsCovered(path, tracked) {
  if (path.endsWith("/")) return [...tracked].some((candidate) => candidate.startsWith(path));
  return tracked.has(path);
}

let manifest;
try {
  manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
} catch (error) {
  console.error(`check-provenance: FAIL — cannot parse PROVENANCE.json (${String(error)}).`);
  process.exit(1);
}

const tracked = new Set(trackedFiles());
const artifacts = [...tracked].filter((path) => ARTIFACT_EXTENSIONS.has(extname(path).toLowerCase()));
const records = Array.isArray(manifest.distributedArtifacts) ? manifest.distributedArtifacts : [];
const recordCounts = new Map();

if (manifest.schemaVersion !== 1) problems.push("schemaVersion must be 1");
if (!nonEmpty(manifest.projectLicense)) problems.push("projectLicense is required");

for (const [index, record] of records.entries()) {
  const at = `distributedArtifacts[${index}]`;
  if (!nonEmpty(record?.path)) {
    problems.push(`${at}: path is required`);
    continue;
  }
  recordCounts.set(record.path, (recordCounts.get(record.path) ?? 0) + 1);
  if (!tracked.has(record.path)) problems.push(`${at}: ${record.path} is not a tracked file`);
  if (!ARTIFACT_EXTENSIONS.has(extname(record.path).toLowerCase())) {
    problems.push(`${at}: ${record.path} is not a recognized distribution artifact`);
  }
  if (!nonEmpty(record.kind)) problems.push(`${at}: kind is required`);
  if (!ALLOWED_ARTIFACT_LICENSES.has(record.license)) {
    problems.push(`${at}: license ${String(record.license)} is not in the reviewed allowlist`);
  }
  if (!/^[a-f0-9]{64}$/.test(record.sha256 ?? "")) problems.push(`${at}: sha256 must be 64 lowercase hex characters`);
  else if (tracked.has(record.path) && hash(record.path) !== record.sha256) problems.push(`${at}: SHA-256 mismatch for ${record.path}`);
  if (!validDate(record.intakeDate)) problems.push(`${at}: intakeDate must be YYYY-MM-DD`);
  if (!nonEmpty(record.intakeNote)) problems.push(`${at}: intakeNote is required`);

  if (record.origin === "repository-generated") {
    if (!nonEmpty(record.generator) || !tracked.has(record.generator)) {
      problems.push(`${at}: repository-generated artifact requires a tracked generator`);
    }
    if (!Array.isArray(record.inputs) || record.inputs.length === 0 ||
        record.inputs.some((input) => !nonEmpty(input) || !tracked.has(input))) {
      problems.push(`${at}: repository-generated artifact requires non-empty tracked inputs`);
    }
    if (record.source !== undefined) problems.push(`${at}: repository-generated artifact must not claim an upstream source`);
  } else if (record.origin === "copied") {
    const source = record.source;
    if (!nonEmpty(source?.url) || !/^https:\/\//.test(source.url)) problems.push(`${at}: copied item requires an HTTPS source URL`);
    if (!nonEmpty(source?.upstreamPath)) problems.push(`${at}: copied item requires its exact upstreamPath`);
    if (!nonEmpty(source?.revision) || !/^[a-f0-9]{40,64}$/i.test(source.revision)) {
      problems.push(`${at}: copied item requires an immutable 40-64 hex upstream revision`);
    } else if (nonEmpty(source?.url) && !source.url.toLowerCase().includes(source.revision.toLowerCase())) {
      problems.push(`${at}: copied item source URL must be pinned to its recorded revision`);
    }
  } else {
    problems.push(`${at}: origin must be repository-generated or copied`);
  }
}

for (const artifact of artifacts) {
  const count = recordCounts.get(artifact) ?? 0;
  if (count !== 1) problems.push(`${artifact}: expected exactly one manifest record, found ${count}`);
}
for (const [path, count] of recordCounts) {
  if (count !== 1) problems.push(`${path}: duplicate manifest records (${count})`);
}

const lineage = Array.isArray(manifest.proceduralLineage) ? manifest.proceduralLineage : [];
if (lineage.length === 0) problems.push("proceduralLineage must contain at least one original-source record");
for (const [index, record] of lineage.entries()) {
  const at = `proceduralLineage[${index}]`;
  if (!nonEmpty(record?.path) || !pathIsCovered(record.path, tracked)) problems.push(`${at}: path must identify tracked source`);
  if (record?.origin !== "original") problems.push(`${at}: origin must be original`);
  if (!nonEmpty(record?.kind)) problems.push(`${at}: kind is required`);
  if (record?.sourceLicense !== "Apache-2.0") problems.push(`${at}: sourceLicense must be Apache-2.0`);
  if (!nonEmpty(record?.lineage)) problems.push(`${at}: lineage is required`);
}

const trackedFonts = artifacts.filter((path) => FONT_EXTENSIONS.has(extname(path).toLowerCase()));
const codeFiles = [...tracked].filter((path) =>
  path !== "scripts/check-provenance.mjs" && /\.(?:css|html|js|mjs|ts|tsx)$/.test(path)
);
for (const path of codeFiles) {
  const text = readFileSync(resolve(root, path), "utf8");
  if (/@font-face\b/i.test(text) || /fonts\.(?:googleapis|gstatic)\.com/i.test(text)) {
    problems.push(`${path}: bundled/remote web fonts are forbidden by the system-font policy`);
  }
}

if (problems.length > 0) {
  console.error("\ncheck-provenance: FAIL\n");
  for (const problem of problems) console.error(`  ${problem}`);
  console.error("");
  process.exit(1);
}

console.log(`check-provenance: ok — ${artifacts.length} tracked distribution artifacts are hash-manifested.`);
console.log(`  ${records.filter((record) => record.origin === "copied").length} copied items; ${lineage.length} original procedural/data lineage records.`);
console.log(`  ${trackedFonts.length} bundled fonts; system-font/no-remote-font policy verified.`);
