import { createHash } from "node:crypto";
import { lstat, mkdir, readFile, readdir, readlink, writeFile } from "node:fs/promises";
import path from "node:path";

const runtimeManifestName = "RUNRAIL_PYTHON_RUNTIME.json";
const enginePathFileName = "runrail-engine.pth";

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

function stableValue(value) {
  if (Array.isArray(value)) return value.map(stableValue);
  if (value && typeof value === "object") {
    return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
  }
  return value;
}

function stableJson(value) {
  return `${JSON.stringify(stableValue(value), null, 2)}\n`;
}

function compareUtf8Names(left, right) {
  return Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8"));
}

function safeRelativePath(value) {
  const relative = String(value || "").replaceAll("\\", "/");
  return (
    relative.length > 0 &&
    relative.length <= 240 &&
    !path.posix.isAbsolute(relative) &&
    !relative.split("/").includes("") &&
    !relative.split("/").includes("..")
  );
}

function parsePythonVersion(value) {
  const match = String(value || "").match(/Python\s+(\d+)\.(\d+)(?:\.(\d+))?/);
  if (!match) throw new Error("runrail-python-runtime-version-invalid");
  return {
    version: `${match[1]}.${match[2]}${match[3] ? `.${match[3]}` : ""}`,
    libraryDirectory: `python${match[1]}.${match[2]}`
  };
}

function pythonRuntimePaths(releaseRoot, pythonVersion) {
  const pythonRoot = path.join(releaseRoot, "python-env");
  const executableRelative = process.platform === "win32"
    ? "python-env/Scripts/python.exe"
    : "python-env/bin/python";
  const sitePackagesRelative = process.platform === "win32"
    ? "python-env/Lib/site-packages"
    : `python-env/lib/${pythonVersion.libraryDirectory}/site-packages`;
  return {
    pythonRoot,
    manifestPath: path.join(pythonRoot, runtimeManifestName),
    executableRelative,
    executablePath: path.join(releaseRoot, ...executableRelative.split("/")),
    pyvenvConfigRelative: "python-env/pyvenv.cfg",
    pyvenvConfigPath: path.join(pythonRoot, "pyvenv.cfg"),
    sitePackagesRelative,
    sitePackagesPath: path.join(releaseRoot, ...sitePackagesRelative.split("/")),
    enginePathFileRelative: `${sitePackagesRelative}/${enginePathFileName}`,
    enginePathFilePath: path.join(releaseRoot, ...sitePackagesRelative.split("/"), enginePathFileName)
  };
}

async function runtimeTreeEntries(root, directory = root) {
  const entries = [];
  for (const name of (await readdir(directory)).sort(compareUtf8Names)) {
    if (directory === root && name === runtimeManifestName) continue;
    const absolute = path.join(directory, name);
    const relative = path.relative(root, absolute).split(path.sep).join("/");
    const info = await lstat(absolute);
    if (info.isSymbolicLink()) {
      entries.push({ path: relative, kind: "symlink", target: await readlink(absolute) });
    } else if (info.isDirectory()) {
      entries.push({ path: relative, kind: "directory" });
      entries.push(...(await runtimeTreeEntries(root, absolute)));
    } else if (info.isFile()) {
      entries.push({ path: relative, kind: "file", sha256: sha256(await readFile(absolute)) });
    } else {
      throw new Error("runrail-python-runtime-entry-type-invalid");
    }
  }
  return entries;
}

function runtimeIntegrityError(code, detail = "") {
  const error = new Error(detail ? `${code}:${detail}` : code);
  error.code = code;
  return error;
}

export async function createRunrailPythonRuntimeManifest(
  releaseRoot,
  { distributionVersion, pythonVersion: pythonVersionText }
) {
  const root = path.resolve(releaseRoot);
  const pythonVersion = parsePythonVersion(pythonVersionText);
  const paths = pythonRuntimePaths(root, pythonVersion);
  await mkdir(paths.sitePackagesPath, { recursive: true });
  const engineSource = path.join(root, "engine", "src");
  await writeFile(paths.enginePathFilePath, `${engineSource}\n`, "utf8");
  const entries = await runtimeTreeEntries(paths.pythonRoot);
  const body = {
    schemaVersion: 1,
    artifactType: "runrail-python-runtime",
    distributionVersion,
    platform: process.platform,
    pythonVersion: pythonVersion.version,
    executableRelative: paths.executableRelative,
    pyvenvConfigRelative: paths.pyvenvConfigRelative,
    sitePackagesRelative: paths.sitePackagesRelative,
    enginePathFileRelative: paths.enginePathFileRelative,
    enginePathFileSha256: sha256(await readFile(paths.enginePathFilePath)),
    entries,
    treeSha256: sha256(stableJson(entries))
  };
  const manifest = { ...body, checksumSha256: sha256(stableJson(body)) };
  await writeFile(paths.manifestPath, stableJson(manifest), "utf8");
  return verifyRunrailPythonRuntime(root, { expectedDistributionVersion: distributionVersion });
}

export async function verifyRunrailPythonRuntime(
  releaseRoot,
  { expectedDistributionVersion = "2.4.14" } = {}
) {
  const root = path.resolve(releaseRoot);
  const manifestPath = path.join(root, "python-env", runtimeManifestName);
  let info;
  let manifest;
  try {
    info = await lstat(manifestPath);
    manifest = JSON.parse(await readFile(manifestPath, "utf8"));
  } catch {
    throw new Error("runrail-python-runtime-manifest-invalid");
  }
  if (!info.isFile() || info.isSymbolicLink()) {
    throw new Error("runrail-python-runtime-manifest-invalid");
  }
  const { checksumSha256, ...body } = manifest;
  const pythonVersion = parsePythonVersion(`Python ${manifest.pythonVersion}`);
  const expected = pythonRuntimePaths(root, pythonVersion);
  if (
    manifest.schemaVersion !== 1 ||
    manifest.artifactType !== "runrail-python-runtime" ||
    manifest.distributionVersion !== expectedDistributionVersion ||
    manifest.platform !== process.platform ||
    !safeRelativePath(manifest.executableRelative) ||
    manifest.executableRelative !== expected.executableRelative ||
    manifest.pyvenvConfigRelative !== expected.pyvenvConfigRelative ||
    manifest.sitePackagesRelative !== expected.sitePackagesRelative ||
    manifest.enginePathFileRelative !== expected.enginePathFileRelative ||
    !/^[a-f0-9]{64}$/.test(String(manifest.enginePathFileSha256 || "")) ||
    !Array.isArray(manifest.entries) ||
    !/^[a-f0-9]{64}$/.test(String(manifest.treeSha256 || "")) ||
    manifest.treeSha256 !== sha256(stableJson(manifest.entries)) ||
    checksumSha256 !== sha256(stableJson(body))
  ) {
    throw new Error("runrail-python-runtime-manifest-invalid");
  }
  for (const target of [expected.executablePath, expected.pyvenvConfigPath, expected.enginePathFilePath]) {
    const targetInfo = await lstat(target);
    if ((!targetInfo.isFile() && !targetInfo.isSymbolicLink()) || targetInfo.isDirectory()) {
      throw new Error("runrail-python-runtime-surface-invalid");
    }
  }
  if (sha256(await readFile(expected.enginePathFilePath)) !== manifest.enginePathFileSha256) {
    throw new Error("runrail-python-runtime-engine-path-tampered");
  }
  const actualEntries = await runtimeTreeEntries(expected.pythonRoot);
  const actualTreeSha256 = sha256(stableJson(actualEntries));
  if (actualTreeSha256 !== manifest.treeSha256) {
    const expectedByPath = new Map(manifest.entries.map((entry) => [entry.path, stableJson(entry)]));
    const actualByPath = new Map(actualEntries.map((entry) => [entry.path, stableJson(entry)]));
    const changedPath = [...new Set([...expectedByPath.keys(), ...actualByPath.keys()])]
      .sort(compareUtf8Names)
      .find((relative) => expectedByPath.get(relative) !== actualByPath.get(relative)) || "unknown";
    throw runtimeIntegrityError("runrail-python-runtime-tree-tampered", changedPath);
  }
  return {
    mode: "verified-pip-free-isolated-runtime",
    version: manifest.pythonVersion,
    manifestPath,
    manifestSha256: sha256(await readFile(manifestPath)),
    executablePath: expected.executablePath,
    sitePackagesPath: expected.sitePackagesPath,
    enginePathFilePath: expected.enginePathFilePath,
    treeSha256: manifest.treeSha256
  };
}
