import { execFile } from "node:child_process";
import { constants as fsConstants } from "node:fs";
import { access, mkdir, readFile, realpath } from "node:fs/promises";
import path from "node:path";
import { promisify, TextDecoder } from "node:util";
import { LocalAuditRunnerError } from "./veyact-local-audit-runner-core.mjs";
import {
  runRunrailTypeScriptParseCheck
} from "./runrail-typescript-parser-core.mjs";

const execFileAsync = promisify(execFile);
const fatalUtf8Decoder = new TextDecoder("utf-8", { fatal: true });
const supportedProfiles = new Set(["safe-static", "python-compile"]);
const javaScriptExtensions = new Set([".cjs", ".js", ".mjs"]);
const typeScriptParserExtensions = new Set([".jsx", ".ts", ".tsx"]);
const sourceExtensions = new Set([
  ".c",
  ".cpp",
  ".go",
  ".java",
  ".js",
  ".jsx",
  ".mjs",
  ".cjs",
  ".mts",
  ".cts",
  ".py",
  ".rb",
  ".rs",
  ".sh",
  ".ts",
  ".tsx"
]);

function extension(relativePath) {
  const name = path.posix.basename(String(relativePath || "")).toLowerCase();
  const dot = name.lastIndexOf(".");
  return dot > 0 ? name.slice(dot) : "";
}

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

async function resolvePythonExecutable() {
  const configured = String(process.env.RUNRAIL_PYTHON || "python3");
  const candidates = path.isAbsolute(configured)
    ? [configured]
    : String(process.env.PATH || "")
        .split(path.delimiter)
        .filter(Boolean)
        .map((directory) => path.join(directory, configured));
  for (const candidate of candidates) {
    try {
      const resolved = await realpath(candidate);
      await access(resolved, fsConstants.X_OK);
      return {
        resolved,
        source: process.env.RUNRAIL_PYTHON ? "runrail-pinned-environment" : "resolved-system-python"
      };
    } catch {
      // Continue through the bounded PATH candidates.
    }
  }
  throw new LocalAuditRunnerError("python-runtime-not-found", "Runrail could not resolve an executable Python runtime.");
}

function runtimeEnvironment(runRoot, executableRoot) {
  const verifierHome = path.join(runRoot, "verifier-home");
  const verifierTemp = path.join(runRoot, "verifier-temp");
  const windowsRuntimeEnvironment = process.platform === "win32"
    ? Object.fromEntries(
        ["ComSpec", "PATHEXT", "SystemDrive", "SystemRoot", "WINDIR"]
          .filter((key) => typeof process.env[key] === "string")
          .map((key) => [key, process.env[key]])
      )
    : {};
  return {
    paths: { verifierHome, verifierTemp },
    env: {
      ...windowsRuntimeEnvironment,
      PATH: executableRoot,
      HOME: verifierHome,
      TMPDIR: verifierTemp,
      TEMP: verifierTemp,
      TMP: verifierTemp,
      LANG: String(process.env.LANG || "C.UTF-8"),
      LC_ALL: String(process.env.LC_ALL || "C.UTF-8"),
      PYTHONHASHSEED: "0",
      PYTHONNOUSERSITE: "1",
      PYTEST_DISABLE_PLUGIN_AUTOLOAD: "1",
      NODE_OPTIONS: "--no-addons --no-deprecation"
    }
  };
}

async function prepareRuntimePaths(runtime) {
  await mkdir(runtime.paths.verifierHome, { recursive: true });
  await mkdir(runtime.paths.verifierTemp, { recursive: true });
}

async function runPythonCheck(candidateRoot, runRoot, pythonFileCount) {
  if (pythonFileCount === 0) {
    return {
      language: "python",
      parser: "python-compileall-isolated",
      fileCount: 0,
      status: "not-applicable",
      returnCode: 0,
      runtimeSource: "not-required"
    };
  }
  const python = await resolvePythonExecutable();
  const pycacheRoot = path.join(runRoot, "verifier-cache");
  const runtime = runtimeEnvironment(runRoot, path.dirname(python.resolved));
  await mkdir(pycacheRoot, { recursive: true });
  await prepareRuntimePaths(runtime);
  const completed = await execFileAsync(
    python.resolved,
    ["-I", "-S", "-X", `pycache_prefix=${pycacheRoot}`, "-m", "compileall", "-q", "."],
    {
      cwd: candidateRoot,
      env: {
        ...runtime.env,
        PYTHONPYCACHEPREFIX: pycacheRoot
      },
      timeout: 60_000,
      maxBuffer: 1024 * 1024,
      windowsHide: true
    }
  ).catch((error) => ({ code: Number.isInteger(error?.code) ? error.code : 1 }));
  const returnCode = Number(completed.code || 0);
  return {
    language: "python",
    parser: "python-compileall-isolated",
    fileCount: pythonFileCount,
    status: returnCode === 0 ? "passed" : "failed",
    returnCode,
    runtimeSource: python.source
  };
}

async function runSingleJavaScriptCheck(candidateRoot, runRoot, relativePath) {
  const target = path.resolve(candidateRoot, ...relativePath.split("/"));
  if (!pathIsContained(candidateRoot, target)) {
    throw new LocalAuditRunnerError("candidate-path-escape", "JavaScript verifier path escaped the candidate root.");
  }
  const runtime = runtimeEnvironment(runRoot, path.dirname(process.execPath));
  await prepareRuntimePaths(runtime);
  const completed = await execFileAsync(process.execPath, ["--check", target], {
    cwd: candidateRoot,
    env: runtime.env,
    timeout: 10_000,
    maxBuffer: 256 * 1024,
    windowsHide: true
  }).catch((error) => ({ code: Number.isInteger(error?.code) ? error.code : 1 }));
  return Number(completed.code || 0);
}

async function runJavaScriptCheck(candidateRoot, runRoot, relativePaths) {
  if (relativePaths.length === 0) {
    return {
      language: "javascript",
      parser: "node-check-no-execution",
      fileCount: 0,
      status: "not-applicable",
      returnCode: 0,
      runtimeSource: "runrail-process-runtime"
    };
  }
  const returnCodes = [];
  for (let offset = 0; offset < relativePaths.length; offset += 4) {
    returnCodes.push(
      ...(await Promise.all(
        relativePaths
          .slice(offset, offset + 4)
          .map((relativePath) => runSingleJavaScriptCheck(candidateRoot, runRoot, relativePath))
      ))
    );
  }
  const returnCode = returnCodes.every((code) => code === 0) ? 0 : 1;
  return {
    language: "javascript",
    parser: "node-check-no-execution",
    fileCount: relativePaths.length,
    status: returnCode === 0 ? "passed" : "failed",
    returnCode,
    runtimeSource: "runrail-process-runtime"
  };
}

async function runJsonCheck(candidateRoot, relativePaths) {
  if (relativePaths.length === 0) {
    return {
      language: "json",
      parser: "json-parse-no-eval",
      fileCount: 0,
      status: "not-applicable",
      returnCode: 0,
      runtimeSource: "runrail-node-json-parser"
    };
  }
  let failed = false;
  for (const relativePath of relativePaths) {
    const target = path.resolve(candidateRoot, ...relativePath.split("/"));
    if (!pathIsContained(candidateRoot, target)) {
      throw new LocalAuditRunnerError("candidate-path-escape", "JSON verifier path escaped the candidate root.");
    }
    try {
      JSON.parse(fatalUtf8Decoder.decode(await readFile(target)));
    } catch {
      failed = true;
    }
  }
  return {
    language: "json",
    parser: "json-parse-no-eval",
    fileCount: relativePaths.length,
    status: failed ? "failed" : "passed",
    returnCode: failed ? 1 : 0,
    runtimeSource: "runrail-node-json-parser"
  };
}

export async function runRunrailStaticVerifier({
  profile,
  candidateRoot,
  runRoot,
  changedFiles,
  materialized
}) {
  if (!supportedProfiles.has(profile)) {
    throw new LocalAuditRunnerError(
      "verifier-profile-not-allowed",
      "Runrail accepts only the safe-static or python-compile verifier profiles."
    );
  }
  if (!Array.isArray(changedFiles) || !materialized || !Number.isSafeInteger(materialized.pythonFileCount)) {
    throw new LocalAuditRunnerError("verifier-input-invalid", "Runrail static verifier input is invalid.");
  }
  if (profile === "python-compile" && materialized.pythonFileCount < 1) {
    throw new LocalAuditRunnerError(
      "verifier-profile-not-applicable",
      "python-compile requires at least one tracked Python file."
    );
  }
  const changedMaterializedPaths = changedFiles
    .filter((row) => row.status !== "D")
    .map((row) => row.path);
  const javaScriptPaths = profile === "safe-static"
    ? changedMaterializedPaths.filter((relativePath) => javaScriptExtensions.has(extension(relativePath)))
    : [];
  const jsonPaths = profile === "safe-static"
    ? changedMaterializedPaths.filter((relativePath) => extension(relativePath) === ".json")
    : [];
  const unsupportedChangedSourceFileCount = changedMaterializedPaths.filter((relativePath) => {
    const suffix = extension(relativePath);
    return (
      sourceExtensions.has(suffix) &&
      suffix !== ".py" &&
      !javaScriptExtensions.has(suffix) &&
      !typeScriptParserExtensions.has(suffix)
    );
  }).length;
  const checks = [
    await runPythonCheck(candidateRoot, runRoot, materialized.pythonFileCount)
  ];
  if (profile === "safe-static") {
    checks.push(
      await runJavaScriptCheck(candidateRoot, runRoot, javaScriptPaths),
      await runJsonCheck(candidateRoot, jsonPaths),
      await runRunrailTypeScriptParseCheck({
        candidateRoot,
        changedFiles
      })
    );
  }
  const passed =
    unsupportedChangedSourceFileCount === 0 &&
    checks.every((check) => check.status !== "failed");
  return {
    profile,
    commandClass: profile === "safe-static"
      ? "bounded-native-parser-static-validation"
      : "python-isolated-mode-stdlib-compile",
    checks,
    checkedFileCount: checks.reduce((count, check) => count + check.fileCount, 0),
    unsupportedChangedSourceFileCount,
    passed,
    returnCode: passed ? 0 : 1,
    arbitraryCommandAccepted: false,
    repositoryCodeImported: false,
    projectCodeExecuted: false,
    externalPluginsEnabled: false,
    osSandboxEnabled: false,
    rawOutputStored: false
  };
}
