import console from "node:console";
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from "node:fs";
import { dirname, extname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import process from "node:process";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const frameworkVersion = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version;
const issues = [];
const warnings = [];
const checks = [];

function addCheck(name, passed, detail) {
  checks.push({ name, passed, detail });
  if (!passed) issues.push({ severity: "error", code: name, detail });
}

function addWarning(name, detail) {
  warnings.push({ severity: "warning", code: name, detail });
}

function walk(dir, output = []) {
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
    if (["node_modules", ".git", ".publish", "dist", ".wrnexus"].includes(entry.name)) continue;
    const path = join(dir, entry.name);
    if (entry.isDirectory()) walk(path, output);
    else output.push(path);
  }
  return output;
}

function listGitTrackedFiles() {
  try {
    const output = execFileSync("git", ["ls-files", "-z"], {
      cwd: root,
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"],
    });
    return output
      .split("\0")
      .filter(Boolean)
      .map((path) => join(root, path))
      .filter((path) => existsSync(path) && statSync(path).isFile());
  } catch {
    // Source archives do not contain .git metadata. In that environment the
    // archive itself is the release boundary, so scanning every included file
    // is the correct fallback.
    return walk(root);
  }
}

const allFiles = walk(root);
const trackedFiles = listGitTrackedFiles();
const trackedSet = new Set(trackedFiles.map((path) => resolve(path)));

const manifests = trackedFiles
  .filter((path) => path.startsWith(join(root, "packages")) && path.endsWith("package.json"))
  .map((path) => ({ path, value: JSON.parse(readFileSync(path, "utf8")) }))
  .filter(({ value }) => value.name?.startsWith("@wrnexus/"));
addCheck(
  "SEC-PACKAGE-VERSION",
  manifests.every(({ value }) => /^0\.8\.\d+$/.test(value.version)),
  "Every @wrnexus package must use a valid independent 0.8 version.",
);
addCheck(
  "SEC-PRIVATE-PUBLISH",
  manifests.every(({ value }) => value.publishConfig?.access !== "public"),
  "No framework package may opt into public access in a private release.",
);

const secretNames = /(?:^|\/)(?:\.env(?:\..+)?|id_rsa|id_ed25519|.*\.(?:pem|p12|pfx|key))$/i;
const safeSecretTemplates = /(?:^|\/)\.env(?:\.[^/]+)*\.(?:example|sample|template)$/i;
function isSecretLike(path) {
  const normalized = relative(root, path).replace(/\\/g, "/");
  return secretNames.test(normalized) && !safeSecretTemplates.test(normalized);
}

const trackedSecretFiles = trackedFiles.filter(isSecretLike);
addCheck(
  "SEC-NO-TRACKED-SECRET-FILES",
  trackedSecretFiles.length === 0,
  trackedSecretFiles.length > 0
    ? `${trackedSecretFiles.map((path) => relative(root, path)).join(", ")}; run: bun run repair:workspace`
    : "No tracked secret-like files found.",
);

const temporaryTypecheckPattern = /(?:^|\/)(?:focus-shims\.d\.ts|tsconfig\.focus\.json)$/i;
const trackedTypecheckHelpers = trackedFiles.filter((path) =>
  temporaryTypecheckPattern.test(relative(root, path).replace(/\\/g, "/")),
);
addCheck(
  "SEC-NO-TRACKED-TYPECHECK-SHIMS",
  trackedTypecheckHelpers.length === 0,
  trackedTypecheckHelpers.length > 0
    ? `${trackedTypecheckHelpers.map((path) => relative(root, path)).join(", ")}; run: bun run repair:workspace`
    : "No temporary typecheck helpers are tracked.",
);

const localTypecheckHelpers = allFiles.filter(
  (path) =>
    temporaryTypecheckPattern.test(relative(root, path).replace(/\\/g, "/")) &&
    !trackedSet.has(resolve(path)),
);
if (localTypecheckHelpers.length > 0) {
  addWarning(
    "SEC-LOCAL-TYPECHECK-SHIMS",
    `Temporary local typecheck helpers are ignored by the root TypeScript program and can be removed with bun run repair:workspace: ${localTypecheckHelpers
      .map((path) => relative(root, path))
      .join(", ")}`,
  );
}

const localSecretFiles = allFiles.filter(
  (path) => isSecretLike(path) && !trackedSet.has(resolve(path)),
);
if (localSecretFiles.length > 0) {
  addWarning(
    "SEC-LOCAL-SECRET-FILES",
    `Ignored or untracked local secret files were not included in the release audit: ${localSecretFiles
      .map((path) => relative(root, path))
      .join(", ")}`,
  );
}

const wrnFiles = trackedFiles.filter((path) => extname(path) === ".wrn");
const dangerousUrls = [];
for (const path of wrnFiles) {
  const source = readFileSync(path, "utf8");
  if (
    /\b(?:href|src|action|formaction)\s*=\s*["']\s*(?:javascript:|vbscript:|file:)/i.test(source)
  ) {
    dangerousUrls.push(relative(root, path));
  }
}
addCheck(
  "SEC-NO-DANGEROUS-STATIC-URLS",
  dangerousUrls.length === 0,
  dangerousUrls.join(", ") || "No dangerous static URLs found.",
);

const productionFiles = trackedFiles.filter((path) =>
  /packages\/(?:security|cache|image|observability|ssr|core)\/src\/.+\.ts$/.test(
    path.replace(/\\/g, "/"),
  ),
);
const dynamicCode = [];
for (const path of productionFiles) {
  const source = readFileSync(path, "utf8");
  if (/\beval\s*\(|\bnew\s+Function\s*\(/.test(source)) dynamicCode.push(relative(root, path));
}
addCheck(
  "SEC-FOUNDATION-NO-DYNAMIC-CODE",
  dynamicCode.length === 0,
  dynamicCode.join(", ") || "No dynamic code execution in security foundation packages.",
);

/*
 * Runtime budgets are measured on the code that actually ships.
 *
 * These used to measure the raw source, which counts comments -- and comments
 * are stripped by the production minifier, so they cost a user nothing. The
 * old metric therefore paid people to delete explanatory comments in exchange
 * for no real saving, while a genuine feature and a wall of prose looked
 * identical to it.
 *
 * The runtime is served from /__wrnexus/reactive.js as its own file, minified
 * and sent with an immutable year-long cache, so what a visitor pays is the
 * minified transfer once. That is the number worth defending.
 */
const runtimeBudgets = {
  "reactive-runtime.ts": 49_000,
  "component-controllers.ts": 24_100,
  "nav-runtime.ts": 12_000,
  "realtime-runtime.ts": 8_000,
};
const minifiedSizes = JSON.parse(
  execFileSync(
    process.platform === "win32" ? "bun.exe" : "bun",
    [join(root, "scripts", "lib", "measure-runtime-size.ts")],
    { cwd: root, encoding: "utf8" },
  ),
);
for (const [file, budget] of Object.entries(runtimeBudgets)) {
  const bytes = minifiedSizes[file];
  const sourceFile = file === "component-controllers.ts" ? "reactive-runtime.ts" : file;
  const raw = statSync(join(root, "packages", "csr", "src", sourceFile)).size;
  addCheck(
    `PERF-RUNTIME-SIZE-${file.replace(/-runtime\.ts$/, "").toUpperCase()}`,
    typeof bytes === "number" && bytes <= budget,
    `${file} minifies to ${bytes} bytes (budget ${budget}, ${raw} raw).`,
  );
}
addCheck(
  "PERF-ZERO-JS-ANALYSIS",
  existsSync(join(root, "packages", "compiler", "src", "analysis.ts")),
  "Static/interactive route analysis must exist.",
);
addCheck(
  "PERF-BROTLI",
  readFileSync(join(root, "packages", "dev-server", "src", "runtime.ts"), "utf8").includes(
    "brotliCompressSync",
  ),
  "Production runtime should prefer Brotli.",
);
addCheck(
  "OBS-METRICS",
  existsSync(join(root, "packages", "observability", "src", "metrics.ts")),
  "Metrics registry must exist.",
);
addCheck(
  "SUPPLY-SBOM",
  existsSync(join(root, "scripts", "generate-sbom.mjs")),
  "SBOM generator must exist.",
);

const report = {
  schemaVersion: 2,
  frameworkVersion,
  generatedAt: new Date().toISOString(),
  passed: issues.length === 0,
  checks,
  warnings,
  issues,
};
if (process.argv.includes("--write")) {
  const reportDir = join(root, ".wrnexus", "reports");
  mkdirSync(reportDir, { recursive: true });
  writeFileSync(
    join(reportDir, `security-performance-${frameworkVersion}.json`),
    JSON.stringify(report, null, 2) + "\n",
  );
}
for (const check of checks)
  console.log(`${check.passed ? "ok" : "FAIL"} ${check.name} — ${check.detail}`);
for (const warning of warnings) console.warn(`warn ${warning.code} — ${warning.detail}`);
if (issues.length) process.exitCode = 1;
