import { createHash } from "node:crypto";
import { createRequire } from "node:module";
import { constants as fsConstants } from "node:fs";
import {
  lstat,
  open as openFile,
  readFile,
  realpath
} from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { TextDecoder } from "node:util";
import { LocalAuditRunnerError } from "./veyact-local-audit-runner-core.mjs";

const require = createRequire(import.meta.url);
const fatalUtf8Decoder = new TextDecoder("utf-8", { fatal: true });
const parserPath = fileURLToPath(
  new URL("../vendor/typescript/5.9.3/typescript.cjs", import.meta.url)
);
const licensePath = fileURLToPath(
  new URL("../vendor/typescript/5.9.3/LICENSE.txt", import.meta.url)
);
const manifestPath = fileURLToPath(
  new URL(
    "../vendor/typescript/5.9.3/RUNRAIL_TYPESCRIPT_VENDOR.json",
    import.meta.url
  )
);
const supportedExtensions = new Map([
  [".jsx", "JSX"],
  [".ts", "TS"],
  [".tsx", "TSX"]
]);
let verifiedParserPromise = null;

export const RUNRAIL_TYPESCRIPT_PARSER = Object.freeze({
  profile: "runrail-parse-only-typescript-v1",
  package: "typescript",
  version: "5.9.3",
  parserSha256:
    "3ae902c92cc44dace175c0e69e13a4b0899f6983c6121d76b9ab8dd5795e7675",
  licenseSha256:
    "a7d00bfd54525bc694b6e32f64c7ebcf5e6b7ae3657be5cc12767bce74654a47",
  maximumFileCount: 200,
  maximumFileBytes: 1024 * 1024,
  maximumTotalBytes: 8 * 1024 * 1024,
  supportedExtensions: Object.freeze([...supportedExtensions.keys()]),
  projectCodeExecuted: false,
  tsconfigLoaded: false,
  moduleResolutionUsed: false,
  pluginLoadingUsed: false,
  dependencyLoadingUsed: false,
  emitUsed: false,
  authority: "none"
});

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

function parserError(code, message) {
  throw new LocalAuditRunnerError(code, message);
}

function exactKeys(value, expected) {
  if (
    !value ||
    typeof value !== "object" ||
    Array.isArray(value) ||
    Object.getPrototypeOf(value) !== Object.prototype
  ) {
    parserError(
      "typescript-parser-vendor-invalid",
      "The vendored TypeScript parser manifest is invalid."
    );
  }
  const actual = Object.keys(value).sort();
  const required = [...expected].sort();
  if (
    actual.length !== required.length ||
    actual.some((key, index) => key !== required[index])
  ) {
    parserError(
      "typescript-parser-vendor-invalid",
      "The vendored TypeScript parser manifest fields are invalid."
    );
  }
}

async function ordinaryFile(value, maximumBytes) {
  const absolute = path.resolve(value);
  const entry = await lstat(absolute).catch(() => null);
  if (
    !entry ||
    !entry.isFile() ||
    entry.isSymbolicLink() ||
    entry.size < 1 ||
    entry.size > maximumBytes ||
    await realpath(absolute) !== absolute
  ) {
    parserError(
      "typescript-parser-vendor-invalid",
      "The vendored TypeScript parser file is missing or unsafe."
    );
  }
  const handle = await openFile(
    absolute,
    fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0)
  );
  try {
    const opened = await handle.stat();
    if (!opened.isFile() || opened.size !== entry.size) {
      parserError(
        "typescript-parser-vendor-invalid",
        "The vendored TypeScript parser changed during verification."
      );
    }
    return await handle.readFile();
  } finally {
    await handle.close();
  }
}

async function loadVerifiedParser() {
  const [parserBytes, licenseBytes, manifestBytes] = await Promise.all([
    ordinaryFile(parserPath, 12 * 1024 * 1024),
    ordinaryFile(licensePath, 64 * 1024),
    ordinaryFile(manifestPath, 16 * 1024)
  ]);
  let manifest;
  try {
    manifest = JSON.parse(fatalUtf8Decoder.decode(manifestBytes));
  } catch {
    parserError(
      "typescript-parser-vendor-invalid",
      "The vendored TypeScript parser manifest cannot be parsed."
    );
  }
  exactKeys(manifest, [
    "schemaVersion",
    "artifactType",
    "package",
    "version",
    "license",
    "source",
    "parserPath",
    "parserSha256",
    "licensePath",
    "licenseSha256",
    "runtimeDependencyInstallRequired",
    "tsconfigLoaded",
    "moduleResolutionUsed",
    "pluginLoadingUsed",
    "projectDependencyLoadingUsed",
    "codeEmitUsed",
    "projectCodeExecuted"
  ]);
  if (
    manifest.schemaVersion !== 1 ||
    manifest.artifactType !== "runrail-vendored-typescript-parser" ||
    manifest.package !== RUNRAIL_TYPESCRIPT_PARSER.package ||
    manifest.version !== RUNRAIL_TYPESCRIPT_PARSER.version ||
    manifest.license !== "Apache-2.0" ||
    manifest.source !==
      "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" ||
    manifest.parserPath !==
      "vendor/typescript/5.9.3/typescript.cjs" ||
    manifest.parserSha256 !== RUNRAIL_TYPESCRIPT_PARSER.parserSha256 ||
    sha256(parserBytes) !== RUNRAIL_TYPESCRIPT_PARSER.parserSha256 ||
    manifest.licensePath !== "vendor/typescript/5.9.3/LICENSE.txt" ||
    manifest.licenseSha256 !== RUNRAIL_TYPESCRIPT_PARSER.licenseSha256 ||
    sha256(licenseBytes) !== RUNRAIL_TYPESCRIPT_PARSER.licenseSha256 ||
    manifest.runtimeDependencyInstallRequired !== false ||
    manifest.tsconfigLoaded !== false ||
    manifest.moduleResolutionUsed !== false ||
    manifest.pluginLoadingUsed !== false ||
    manifest.projectDependencyLoadingUsed !== false ||
    manifest.codeEmitUsed !== false ||
    manifest.projectCodeExecuted !== false
  ) {
    parserError(
      "typescript-parser-vendor-invalid",
      "The vendored TypeScript parser integrity or boundary declaration is invalid."
    );
  }
  const typescript = require(parserPath);
  if (
    typescript?.version !== RUNRAIL_TYPESCRIPT_PARSER.version ||
    typeof typescript.createSourceFile !== "function" ||
    typeof typescript.ScriptTarget?.Latest !== "number" ||
    typeof typescript.ScriptKind?.TS !== "number" ||
    typeof typescript.ScriptKind?.TSX !== "number" ||
    typeof typescript.ScriptKind?.JSX !== "number"
  ) {
    parserError(
      "typescript-parser-vendor-invalid",
      "The vendored TypeScript parser API is invalid."
    );
  }
  return { typescript, manifest };
}

export async function verifyRunrailTypeScriptVendor() {
  if (!verifiedParserPromise) {
    verifiedParserPromise = loadVerifiedParser().catch((error) => {
      verifiedParserPromise = null;
      throw error;
    });
  }
  const { manifest } = await verifiedParserPromise;
  return {
    profile: RUNRAIL_TYPESCRIPT_PARSER.profile,
    package: manifest.package,
    version: manifest.version,
    parserSha256: manifest.parserSha256,
    license: manifest.license,
    runtimeDependencyInstallRequired:
      manifest.runtimeDependencyInstallRequired,
    tsconfigLoaded: manifest.tsconfigLoaded,
    moduleResolutionUsed: manifest.moduleResolutionUsed,
    pluginLoadingUsed: manifest.pluginLoadingUsed,
    dependencyLoadingUsed: manifest.projectDependencyLoadingUsed,
    emitUsed: manifest.codeEmitUsed,
    projectCodeExecuted: manifest.projectCodeExecuted,
    authority: "none"
  };
}

function suffix(value) {
  return path.posix.extname(String(value || "").toLowerCase());
}

function safeRelativePath(value) {
  const relative = String(value || "").replaceAll("\\", "/");
  if (
    !relative ||
    relative.length > 512 ||
    path.posix.isAbsolute(relative) ||
    /^[A-Za-z]:\//.test(relative) ||
    relative.startsWith("//") ||
    relative.split("/").includes("..") ||
    relative.includes("\0")
  ) {
    parserError(
      "candidate-path-escape",
      "TypeScript parser path escaped the candidate root."
    );
  }
  return relative;
}

function pathIsContained(root, candidate) {
  const relative = path.relative(root, candidate);
  return (
    relative !== "" &&
    relative !== ".." &&
    !relative.startsWith(`..${path.sep}`) &&
    !path.isAbsolute(relative)
  );
}

async function readCandidateSource(candidateRoot, relativePath) {
  const safePath = safeRelativePath(relativePath);
  const target = path.resolve(candidateRoot, ...safePath.split("/"));
  if (!pathIsContained(candidateRoot, target)) {
    parserError(
      "candidate-path-escape",
      "TypeScript parser path escaped the candidate root."
    );
  }
  const entry = await lstat(target).catch(() => null);
  if (
    !entry ||
    !entry.isFile() ||
    entry.isSymbolicLink() ||
    entry.size < 1 ||
    entry.size > RUNRAIL_TYPESCRIPT_PARSER.maximumFileBytes ||
    await realpath(target) !== target
  ) {
    parserError(
      "typescript-source-invalid",
      "TypeScript source is missing, oversized, or not an ordinary file."
    );
  }
  const handle = await openFile(
    target,
    fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0)
  );
  let bytes;
  try {
    const opened = await handle.stat();
    if (!opened.isFile() || opened.size !== entry.size) {
      parserError(
        "typescript-source-invalid",
        "TypeScript source changed while it was being read."
      );
    }
    bytes = await handle.readFile();
  } finally {
    await handle.close();
  }
  let text;
  try {
    text = fatalUtf8Decoder.decode(bytes);
  } catch {
    parserError(
      "typescript-source-invalid",
      "TypeScript source must be valid UTF-8."
    );
  }
  if (text.includes("\0")) {
    parserError(
      "typescript-source-invalid",
      "TypeScript source contains a NUL byte."
    );
  }
  return { safePath, bytes, text };
}

export async function runRunrailTypeScriptParseCheck({
  candidateRoot,
  changedFiles
}) {
  if (!Array.isArray(changedFiles)) {
    parserError(
      "typescript-parser-input-invalid",
      "TypeScript parser changed-file input is invalid."
    );
  }
  const relativePaths = changedFiles
    .filter((row) => row?.status !== "D")
    .map((row) => safeRelativePath(row?.path))
    .filter((relativePath) => supportedExtensions.has(suffix(relativePath)));
  if (relativePaths.length === 0) {
    return {
      language: "typescript",
      parser: "typescript-create-source-file-parse-only",
      fileCount: 0,
      status: "not-applicable",
      returnCode: 0,
      runtimeSource: "runrail-vendored-typescript-5.9.3"
    };
  }
  if (
    relativePaths.length > RUNRAIL_TYPESCRIPT_PARSER.maximumFileCount ||
    new Set(relativePaths).size !== relativePaths.length
  ) {
    parserError(
      "typescript-parser-input-invalid",
      "TypeScript parser file count is duplicated or exceeds its boundary."
    );
  }
  const { typescript } = await (verifiedParserPromise ||
    (verifiedParserPromise = loadVerifiedParser().catch((error) => {
      verifiedParserPromise = null;
      throw error;
    })));
  let totalBytes = 0;
  let diagnosticCount = 0;
  for (const relativePath of relativePaths) {
    const source = await readCandidateSource(
      path.resolve(candidateRoot),
      relativePath
    );
    totalBytes += source.bytes.length;
    if (totalBytes > RUNRAIL_TYPESCRIPT_PARSER.maximumTotalBytes) {
      parserError(
        "typescript-source-invalid",
        "TypeScript source total exceeds the parse-only boundary."
      );
    }
    const kindName = supportedExtensions.get(suffix(relativePath));
    const sourceFile = typescript.createSourceFile(
      source.safePath,
      source.text,
      typescript.ScriptTarget.Latest,
      false,
      typescript.ScriptKind[kindName]
    );
    diagnosticCount += Array.isArray(sourceFile.parseDiagnostics)
      ? sourceFile.parseDiagnostics.length
      : 0;
  }
  const passed = diagnosticCount === 0;
  return {
    language: "typescript",
    parser: "typescript-create-source-file-parse-only",
    fileCount: relativePaths.length,
    status: passed ? "passed" : "failed",
    returnCode: passed ? 0 : 1,
    runtimeSource: "runrail-vendored-typescript-5.9.3"
  };
}
