import { createHash } from "node:crypto";
import {
  constants as fsConstants,
  copyFile,
  lstat,
  mkdir,
  mkdtemp,
  readFile,
  readdir,
  realpath,
  rename,
  rm,
  writeFile,
} from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import {
  createPythonSourceManifest,
  PYTHON_LEGAL_RESOURCES,
  transformPythonLegalResource,
} from "./python-engine-provenance.mjs";

export const PYTHON_ENGINE_LOCK = Object.freeze({
  bridgeAbi: 1,
  pythonVersion: "3.14.2",
  pyodideVersion: "314.0.3",
  maximumPatternUtf16: 65_536,
  maximumSubjectBytes: 16_777_216,
  maximumReplacementUtf16: 65_536,
  maximumOutputBytes: 67_108_864,
  maximumMatches: 10_000,
  maximumCaptureRows: 100_000,
  maximumCaptureGroups: 1_000,
  maximumNativeExpansionCodePoints: 16_777_216,
  npm: Object.freeze({
    package: "pyodide@314.0.3",
    resolved: "https://registry.npmjs.org/pyodide/-/pyodide-314.0.3.tgz",
    integrity:
      "sha512-sK40My6m8tmBUYtYH9au9rXUeh9x0wfahtHdOlGmJxZDsKBGKtP6KznyFB2+u/klbQTdDionR0uaVd176zVQzQ==",
    packageJsonSha256:
      "0393eb01a0707cddc251f1a66090998773fe1aa3cfde66eedc96e83e43861845",
  }),
  pyodide: Object.freeze({
    repository: "https://github.com/pyodide/pyodide.git",
    tag: "314.0.3",
    commit: "ac57031be7564f864d061cb37c5c152e59f83ad4",
    license: "MPL-2.0",
    licenseUrl:
      "https://raw.githubusercontent.com/pyodide/pyodide/314.0.3/LICENSE",
    licenseSha256:
      "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5",
  }),
  cpython: Object.freeze({
    repository: "https://github.com/python/cpython.git",
    tag: "v3.14.2",
    tagObject: "a1d0069daf8e85b25a0c3f96abc43182be6d429e",
    commit: "df793163d5821791d4e7caf88885a2c11a107986",
    license: "Python-2.0",
    licenseUrl:
      "https://raw.githubusercontent.com/python/cpython/v3.14.2/LICENSE",
    licenseSha256:
      "b0e25a78cffb43f4d92de8b61ccfa1f1f98ecbc22330b54b5251e7b6ba010231",
  }),
  emscriptenVersion: "5.0.3",
  abiVersion: "2026_0",
  lockPythonVersion: "3.14.0",
  assets: Object.freeze({
    "pyodide-lock.json":
      "c963d22858f6bcb8f41586a2142f03905ab370c88ea22a86a2736e95fac2a8f3",
    "pyodide.asm.mjs":
      "1a9775427ef6e8abaa7db88ece0515422d1886915ae5c9093776410c865dfd8d",
    "pyodide.asm.wasm":
      "e7f8fac36f8bf11085309cbc5c829b3ec3057c18bf1d73b05a6741612d63cdbf",
    "pyodide.mjs":
      "5cfc46f5dcbaf2a16f26e2363f441873eb424762609cc03db00d6a2ace4d00e5",
    "python_stdlib.zip":
      "444c770dfd75a32097fc0a7d5c1413fd3140601f49c3a1f2e9af0376fcd124b4",
  }),
  strippedLoaderSha256:
    "640ac750bf7cf42ee58b524faa09f803732d2237753cd630d6878edda8a107bf",
});

const PACK_FILES = Object.freeze(
  [
    ...PYTHON_LEGAL_RESOURCES.map(({ output }) => output),
    "SHA256SUMS",
    "SOURCE-MANIFEST.json",
    "engine-metadata.json",
    ...Object.keys(PYTHON_ENGINE_LOCK.assets),
  ].sort(),
);
const CHECKSUM_FILES = Object.freeze(
  PACK_FILES.filter((name) => name !== "SHA256SUMS"),
);

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

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

async function assertRealDirectory(candidate, label) {
  const details = await lstat(candidate).catch(() => null);
  if (!details?.isDirectory() || details.isSymbolicLink()) {
    throw new Error(`${label} must be a real directory: ${candidate}`);
  }
  return realpath(candidate);
}

async function assertRegularFile(candidate, label) {
  const details = await lstat(candidate).catch(() => null);
  if (!details?.isFile() || details.isSymbolicLink()) {
    throw new Error(
      `${label} must be a regular, non-symlink file: ${candidate}`,
    );
  }
  return realpath(candidate);
}

async function assertHash(file, expected, label) {
  const actual = await sha256File(file);
  if (actual !== expected) {
    throw new Error(
      `${label} SHA-256 mismatch: expected ${expected}, got ${actual}.`,
    );
  }
}

function withoutSourceMapReference(source) {
  const result = source.replace(
    /\n\/\/# sourceMappingURL=pyodide\.mjs\.map\s*$/u,
    "\n",
  );
  if (result === source) {
    throw new Error(
      "The pinned Pyodide loader no longer has the expected source-map trailer.",
    );
  }
  return result;
}

async function verifyPyodideSource(sourceDirectory) {
  const source = await assertRealDirectory(
    sourceDirectory,
    "Pyodide npm package",
  );
  for (const [name, expected] of Object.entries(PYTHON_ENGINE_LOCK.assets)) {
    await assertRegularFile(path.join(source, name), `Pyodide ${name}`);
    await assertHash(path.join(source, name), expected, `Pyodide ${name}`);
  }
  const packageFile = path.join(source, "package.json");
  await assertHash(
    packageFile,
    PYTHON_ENGINE_LOCK.npm.packageJsonSha256,
    "Pyodide package.json",
  );
  const packageJson = JSON.parse(await readFile(packageFile, "utf8"));
  if (
    packageJson.name !== "pyodide" ||
    packageJson.version !== PYTHON_ENGINE_LOCK.pyodideVersion ||
    packageJson.license !== PYTHON_ENGINE_LOCK.pyodide.license ||
    packageJson.repository?.url !== "git+https://github.com/pyodide/pyodide.git"
  ) {
    throw new Error("The Pyodide npm package identity is not pinned.");
  }
  const lock = JSON.parse(
    await readFile(path.join(source, "pyodide-lock.json"), "utf8"),
  );
  if (
    lock.info?.abi_version !== PYTHON_ENGINE_LOCK.abiVersion ||
    lock.info?.arch !== "wasm32" ||
    lock.info?.python !== PYTHON_ENGINE_LOCK.lockPythonVersion ||
    lock.info?.platform !==
      `emscripten_${PYTHON_ENGINE_LOCK.emscriptenVersion.replaceAll(".", "_")}`
  ) {
    throw new Error("The Pyodide lock-file platform identity is not pinned.");
  }
  return source;
}

async function fetchPinned(url, expectedHash, label) {
  const response = await fetch(url, {
    redirect: "follow",
    signal: AbortSignal.timeout(30_000),
  });
  if (!response.ok) {
    throw new Error(`${label} download failed with HTTP ${response.status}.`);
  }
  const value = Buffer.from(await response.arrayBuffer());
  const actual = sha256(value);
  if (actual !== expectedHash) {
    throw new Error(
      `${label} SHA-256 mismatch: expected ${expectedHash}, got ${actual}.`,
    );
  }
  return value;
}

async function fileRecord(directory, name) {
  const file = path.join(directory, name);
  const value = await readFile(file);
  return {
    path: name,
    sha256: sha256(value),
    bytes: value.byteLength,
  };
}

async function expectedSourceManifest(pack) {
  const legalFiles = [];
  for (const { output } of PYTHON_LEGAL_RESOURCES) {
    legalFiles.push(await fileRecord(pack, output));
  }
  return createPythonSourceManifest(legalFiles);
}

async function expectedMetadata(root, pack) {
  const bridgePath = path.join(root, "engines", "python", "bridge.py");
  const bridge = await readFile(bridgePath);
  const sourceManifest = await fileRecord(pack, "SOURCE-MANIFEST.json");
  const files = [];
  for (const name of CHECKSUM_FILES) {
    if (name !== "engine-metadata.json") {
      files.push(await fileRecord(pack, name));
    }
  }
  return {
    schemaVersion: 1,
    engine: "python",
    engineVersion: `CPython ${PYTHON_ENGINE_LOCK.pythonVersion} re`,
    runtime: `Pyodide ${PYTHON_ENGINE_LOCK.pyodideVersion}`,
    status: "production-runtime",
    bridge: {
      abiVersion: PYTHON_ENGINE_LOCK.bridgeAbi,
      sourceFile: {
        path: "engines/python/bridge.py",
        sha256: sha256(bridge),
        bytes: bridge.byteLength,
      },
      userValuesAreFunctionArguments: true,
      regexModule: "re",
      supportModules: {
        serialization: "json",
        replacementOutputEncoding: "canonical RFC 4648 base64",
        runtimeIdentity: "sys",
      },
      maximumPatternUtf16: PYTHON_ENGINE_LOCK.maximumPatternUtf16,
      maximumSubjectBytes: PYTHON_ENGINE_LOCK.maximumSubjectBytes,
      maximumReplacementUtf16: PYTHON_ENGINE_LOCK.maximumReplacementUtf16,
      maximumOutputBytes: PYTHON_ENGINE_LOCK.maximumOutputBytes,
      maximumMatches: PYTHON_ENGINE_LOCK.maximumMatches,
      maximumCaptureRows: PYTHON_ENGINE_LOCK.maximumCaptureRows,
      maximumCaptureGroups: PYTHON_ENGINE_LOCK.maximumCaptureGroups,
      maximumNativeExpansionCodePoints:
        PYTHON_ENGINE_LOCK.maximumNativeExpansionCodePoints,
    },
    source: {
      manifest: sourceManifest,
      pyodide: {
        repository: PYTHON_ENGINE_LOCK.pyodide.repository,
        tag: PYTHON_ENGINE_LOCK.pyodide.tag,
        commitSha1: PYTHON_ENGINE_LOCK.pyodide.commit,
        license: PYTHON_ENGINE_LOCK.pyodide.license,
        npmPackage: PYTHON_ENGINE_LOCK.npm.package,
        npmResolved: PYTHON_ENGINE_LOCK.npm.resolved,
        npmIntegrity: PYTHON_ENGINE_LOCK.npm.integrity,
      },
      cpython: {
        repository: PYTHON_ENGINE_LOCK.cpython.repository,
        tag: PYTHON_ENGINE_LOCK.cpython.tag,
        tagObjectSha1: PYTHON_ENGINE_LOCK.cpython.tagObject,
        commitSha1: PYTHON_ENGINE_LOCK.cpython.commit,
        license: PYTHON_ENGINE_LOCK.cpython.license,
      },
    },
    toolchain: {
      abiVersion: PYTHON_ENGINE_LOCK.abiVersion,
      architecture: "wasm32",
      emscriptenVersion: PYTHON_ENGINE_LOCK.emscriptenVersion,
      lockPythonVersion: PYTHON_ENGINE_LOCK.lockPythonVersion,
      executablePythonVersion: PYTHON_ENGINE_LOCK.pythonVersion,
      threads: false,
    },
    packaging: {
      loaderTransformation:
        "Removed only the upstream sourceMappingURL trailer; executable JavaScript is unchanged.",
      upstreamLoaderSha256: PYTHON_ENGINE_LOCK.assets["pyodide.mjs"],
      packedLoaderSha256: PYTHON_ENGINE_LOCK.strippedLoaderSha256,
      externalPackagesIncluded: false,
      legalClosure:
        "SOURCE-MANIFEST.json inventories every loader and native base-runtime component, exact preferred-form input and patch, plus independently anchored legal text.",
      lockPythonVersionNote:
        "The upstream lock's 3.14.0 field identifies its CPython 3.14 ABI baseline; the executable runtime self-identifies as 3.14.2.",
    },
    files,
  };
}

function equalJson(left, right) {
  return JSON.stringify(left) === JSON.stringify(right);
}

async function expectedChecksums(pack) {
  const lines = [];
  for (const name of CHECKSUM_FILES) {
    lines.push(`${await sha256File(path.join(pack, name))}  ${name}`);
  }
  return `${lines.join("\n")}\n`;
}

async function smokePythonPack(pack) {
  await WebAssembly.compile(
    await readFile(path.join(pack, "pyodide.asm.wasm")),
  );
  const moduleUrl = pathToFileURL(path.join(pack, "pyodide.mjs"));
  moduleUrl.searchParams.set("regex-tools-verification", String(Date.now()));
  const imported = await import(moduleUrl.href);
  if (
    imported.version !== PYTHON_ENGINE_LOCK.pyodideVersion ||
    typeof imported.loadPyodide !== "function"
  ) {
    throw new Error("The staged Pyodide module identity is invalid.");
  }
  const runtime = await imported.loadPyodide({
    indexURL: `${pack}${path.sep}`,
    stdout: () => undefined,
    stderr: () => undefined,
  });
  const result = JSON.parse(
    runtime.runPython(`
import _zstd, decimal, json, pyexpat, re, sqlite3, sys, zlib
expression = re.compile(r"(?P<emoji>😀)|(?P<word>[a-z]+)", re.ASCII)
matches = [
    [match.span(0), match.lastgroup]
    for match in expression.finditer("😀 abc")
]
replacement_match = re.compile(r"(?P<word>[a-z]+)").search("abc")
json.dumps({
    "python": ".".join(str(part) for part in sys.version_info[:3]),
    "implementation": sys.implementation.name,
    "libmpdec": decimal.__libmpdec_version__,
    "expat": pyexpat.EXPAT_VERSION,
    "sqlite": sqlite3.sqlite_version,
    "zlib": zlib.ZLIB_RUNTIME_VERSION,
    "zstd": _zstd.zstd_version,
    "matches": matches,
    "replacement": replacement_match.expand(r"[\\g<word>]"),
})
`),
  );
  if (
    result.python !== PYTHON_ENGINE_LOCK.pythonVersion ||
    result.implementation !== "cpython" ||
    result.libmpdec !== "2.5.1" ||
    result.expat !== "expat_2.7.3" ||
    result.sqlite !== "3.39.0" ||
    result.zlib !== "1.3.1" ||
    result.zstd !== "1.5.7" ||
    JSON.stringify(result.matches) !==
      JSON.stringify([
        [[0, 1], "emoji"],
        [[2, 5], "word"],
      ]) ||
    result.replacement !== "[abc]"
  ) {
    throw new Error("The staged CPython re runtime smoke test failed.");
  }
}

export async function verifyPythonPack(packDirectory, root) {
  const repositoryRoot = await assertRealDirectory(
    root,
    "Regex Tools repository",
  );
  const pack = await assertRealDirectory(packDirectory, "Python engine pack");
  const entries = (await readdir(pack, { withFileTypes: true })).sort(
    (left, right) =>
      left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
  );
  if (
    entries.length !== PACK_FILES.length ||
    entries.some(
      (entry, index) =>
        entry.name !== PACK_FILES[index] ||
        !entry.isFile() ||
        entry.isSymbolicLink(),
    )
  ) {
    throw new Error(
      `The staged Python pack must contain only: ${PACK_FILES.join(", ")}.`,
    );
  }

  for (const resource of PYTHON_LEGAL_RESOURCES) {
    await assertHash(
      path.join(pack, resource.output),
      resource.outputSha256,
      `staged ${resource.component} legal material`,
    );
  }
  await assertHash(
    path.join(pack, "pyodide.mjs"),
    PYTHON_ENGINE_LOCK.strippedLoaderSha256,
    "staged Pyodide loader",
  );
  for (const [name, expected] of Object.entries(PYTHON_ENGINE_LOCK.assets)) {
    if (name !== "pyodide.mjs") {
      await assertHash(path.join(pack, name), expected, `staged ${name}`);
    }
  }
  const loader = await readFile(path.join(pack, "pyodide.mjs"), "utf8");
  if (
    loader.includes("sourceMappingURL") ||
    loader.includes("/mnt/DATA/") ||
    loader.includes("/home/zemion/")
  ) {
    throw new Error(
      "The staged Pyodide loader leaks a source-map or local build path.",
    );
  }

  let metadata;
  try {
    metadata = JSON.parse(
      await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
    );
  } catch (error) {
    throw new Error("engine-metadata.json is not valid JSON.", {
      cause: error,
    });
  }
  let sourceManifest;
  try {
    sourceManifest = JSON.parse(
      await readFile(path.join(pack, "SOURCE-MANIFEST.json"), "utf8"),
    );
  } catch (error) {
    throw new Error("SOURCE-MANIFEST.json is not valid JSON.", {
      cause: error,
    });
  }
  const expectedManifest = await expectedSourceManifest(pack);
  if (!equalJson(sourceManifest, expectedManifest)) {
    throw new Error(
      "The staged Python source manifest does not match the pinned preferred-form and legal inventory.",
    );
  }
  const expected = await expectedMetadata(repositoryRoot, pack);
  if (!equalJson(metadata, expected)) {
    throw new Error(
      "The staged Python metadata does not match the pinned pack contract.",
    );
  }
  if (
    (await readFile(path.join(pack, "SHA256SUMS"), "utf8")) !==
    (await expectedChecksums(pack))
  ) {
    throw new Error("The staged Python SHA256SUMS file is incorrect.");
  }
  await smokePythonPack(pack);
  return metadata;
}

async function replaceDirectory(stage, target, label) {
  const targetDetails = await lstat(target).catch(() => null);
  if (
    targetDetails &&
    (!targetDetails.isDirectory() || targetDetails.isSymbolicLink())
  ) {
    throw new Error(`${label} must be a real directory.`);
  }
  if (!targetDetails) {
    await rename(stage, target);
    return;
  }
  const backup = `${target}.replaced-${process.pid}-${Date.now()}`;
  await rename(target, backup);
  try {
    await rename(stage, target);
  } catch (error) {
    await rename(backup, target);
    throw error;
  }
  await rm(backup, { recursive: true });
}

export async function installPythonPack(packDirectory, root) {
  const repositoryRoot = await assertRealDirectory(
    root,
    "Regex Tools repository",
  );
  const sourcePack = await assertRealDirectory(
    packDirectory,
    "verified Python engine pack",
  );
  const metadata = await verifyPythonPack(sourcePack, repositoryRoot);
  const engineRoot = await assertRealDirectory(
    path.join(repositoryRoot, "public", "engines"),
    "public engine directory",
  );
  const target = path.join(engineRoot, "python");
  const stage = await mkdtemp(path.join(engineRoot, ".python-install-"));
  try {
    for (const name of PACK_FILES) {
      await copyFile(
        path.join(sourcePack, name),
        path.join(stage, name),
        fsConstants.COPYFILE_EXCL,
      );
    }
    await verifyPythonPack(stage, repositoryRoot);
    await replaceDirectory(stage, target, "public/engines/python");
    return { output: target, metadata };
  } finally {
    await rm(stage, { recursive: true, force: true });
  }
}

export async function buildPythonPack(options, root) {
  const repositoryRoot = await assertRealDirectory(
    root,
    "Regex Tools repository",
  );
  const source = await verifyPyodideSource(options.sourceDirectory);
  const output = path.resolve(options.outputDirectory);
  await mkdir(path.dirname(output), { recursive: true });
  const stage = await mkdtemp(
    path.join(path.dirname(output), ".python-pack-build-"),
  );
  try {
    const legalFiles = await Promise.all(
      PYTHON_LEGAL_RESOURCES.map(async (resource) => {
        const sourceValue = await fetchPinned(
          resource.url,
          resource.sourceSha256,
          `${resource.component} legal source`,
        );
        return [resource, transformPythonLegalResource(resource, sourceValue)];
      }),
    );
    for (const [resource, value] of legalFiles) {
      await writeFile(path.join(stage, resource.output), value, { flag: "wx" });
    }
    for (const name of Object.keys(PYTHON_ENGINE_LOCK.assets)) {
      if (name === "pyodide.mjs") {
        const upstream = await readFile(path.join(source, name), "utf8");
        const packed = withoutSourceMapReference(upstream);
        if (sha256(packed) !== PYTHON_ENGINE_LOCK.strippedLoaderSha256) {
          throw new Error("The deterministic Pyodide loader output changed.");
        }
        await writeFile(path.join(stage, name), packed, { flag: "wx" });
      } else {
        await copyFile(
          path.join(source, name),
          path.join(stage, name),
          fsConstants.COPYFILE_EXCL,
        );
      }
    }
    await writeFile(
      path.join(stage, "SOURCE-MANIFEST.json"),
      `${JSON.stringify(await expectedSourceManifest(stage), null, 2)}\n`,
      { flag: "wx" },
    );
    const metadata = await expectedMetadata(repositoryRoot, stage);
    await writeFile(
      path.join(stage, "engine-metadata.json"),
      `${JSON.stringify(metadata, null, 2)}\n`,
      { flag: "wx" },
    );
    await writeFile(
      path.join(stage, "SHA256SUMS"),
      await expectedChecksums(stage),
      { flag: "wx" },
    );
    await verifyPythonPack(stage, repositoryRoot);
    await replaceDirectory(stage, output, "Python pack output");
    return { output, metadata };
  } finally {
    await rm(stage, { recursive: true, force: true });
  }
}

const invokedFile = process.argv[1]
  ? pathToFileURL(path.resolve(process.argv[1])).href
  : "";
if (import.meta.url === invokedFile) {
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
  const arguments_ = process.argv.slice(2);
  if (arguments_.length > 2) {
    throw new Error(
      "Usage: node scripts/python-engine-pack.mjs [node_modules/pyodide] [.engine-build/python]",
    );
  }
  const result = await buildPythonPack(
    {
      sourceDirectory: path.resolve(
        root,
        arguments_[0] ?? "node_modules/pyodide",
      ),
      outputDirectory: path.resolve(
        root,
        arguments_[1] ?? ".engine-build/python",
      ),
    },
    root,
  );
  console.log(
    `Built and verified ${result.metadata.runtime} / ${result.metadata.engineVersion} at ${result.output}.`,
  );
}
