#!/usr/bin/env node
// Enforces a hard-zero policy for Knip's unused exports.
import { fileURLToPath } from "node:url";
import { isLikelyRepoFilePath, runKnip, uniqueSorted } from "./deadcode-knip-runner.mjs";

const KNIP_ISSUES = "exports,nsExports,types,nsTypes,enumMembers,namespaceMembers";

const KNIP_SCANS = [
  {
    name: "production unused-export scan",
    args: ["--config", "config/knip.config.ts", "--production"],
  },
  {
    name: "full-tree unused-export scan",
    args: ["--config", "config/knip.all-exports.config.ts"],
  },
  {
    name: "script unused-export scan",
    args: ["--config", "config/knip.scripts-exports.config.ts", "--include-entry-exports"],
  },
];

const KNIP_COMMON_ARGS = [
  "--no-progress",
  "--reporter",
  "compact",
  "--include",
  KNIP_ISSUES,
  "--no-config-hints",
];

/** Parses compact Knip export sections into one path-and-symbol entry per finding. */
export function parseKnipCompactUnusedExportsResult(output) {
  const entries = [];
  let inExportSection = false;
  let sawExportSection = false;

  for (const line of output.split(/\r?\n/u)) {
    const sectionMatch =
      /^(Unused exports|Exports in used namespace|Unused exported types|Exported types in used namespace|Unused exported enum members|Unused exported namespace members) \(\d+\)$/u.exec(
        line,
      );
    if (sectionMatch) {
      inExportSection = true;
      sawExportSection = true;
      continue;
    }
    if (/^Unused .+ \(\d+\)$/u.test(line)) {
      inExportSection = false;
      continue;
    }
    if (!inExportSection) {
      continue;
    }

    const separatorIndex = line.indexOf(": ");
    if (separatorIndex === -1) {
      continue;
    }
    const file = line.slice(0, separatorIndex).trim();
    if (!isLikelyRepoFilePath(file)) {
      continue;
    }
    const symbols = line.slice(separatorIndex + 2).split(", ");
    for (const symbol of symbols) {
      const trimmedSymbol = symbol.trim();
      if (trimmedSymbol) {
        entries.push(`${file}: ${trimmedSymbol}`);
      }
    }
  }

  return { entries: uniqueSorted(entries), sawExportSection };
}

/** Parses compact Knip export sections into one path-and-symbol entry per finding. */
export function parseKnipCompactUnusedExports(output) {
  return parseKnipCompactUnusedExportsResult(output).entries;
}

/** Rejects every unused export reported by Knip. */
export function checkUnusedExports(output) {
  const entries = parseKnipCompactUnusedExports(output);
  return {
    ok: entries.length === 0,
    entries,
    message:
      entries.length === 0
        ? ""
        : [
            "Unused exports are not allowed:",
            ...entries.map((entry) => `  ${entry}`),
            "Delete the exports or model their real production consumers in Knip.",
          ].join("\n"),
  };
}

/** Classifies Knip export output, rejecting findings after resolution failures. */
export function checkExportScan(scanName, output) {
  const resolutionErrors = output
    .split(/\r?\n/u)
    .filter((line) => line.startsWith("ERROR: Error loading "));
  if (resolutionErrors.length > 0) {
    return {
      ok: false,
      entries: [],
      message: [
        `deadcode ${scanName} could not resolve workspace modules; export findings would be unreliable and are discarded.`,
        ...resolutionErrors,
        "Install workspace dependencies in-tree (pnpm install) — on delegated boxes the crabbox wrapper links the hydrated modules dir into the workdir — and rerun.",
      ].join("\n"),
    };
  }

  const check = checkUnusedExports(output);
  return {
    ...check,
    message: check.ok ? "" : `${scanName}:\n${check.message}`,
  };
}

async function main() {
  // The scans are independent Knip child processes over separate configs;
  // running them concurrently cuts the lane's serial wall clock roughly 2x.
  const results = await Promise.all(
    KNIP_SCANS.map(async (scan) => ({
      scan,
      result: await runKnip([...scan.args, ...KNIP_COMMON_ARGS], { scanName: scan.name }),
    })),
  );
  for (const { scan, result } of results) {
    if (!reportUnusedExportScan(scan, result)) {
      process.exitCode = 1;
      return;
    }
  }
  console.log(
    "[deadcode] Knip production and full-tree unused-export checks passed with 0 entries.",
  );
}

function reportUnusedExportScan(scan, result) {
  if (result.errorCode || result.status === null) {
    console.error(
      `deadcode ${scan.name} failed: ${result.errorCode ?? result.signal ?? "unknown"}${
        result.errorMessage ? `: ${result.errorMessage}` : ""
      }`,
    );
    if (result.output) {
      console.error(result.output);
    }
    return false;
  }

  const check = checkExportScan(scan.name, result.output);
  if (!check.ok) {
    console.error(check.message);
    return false;
  }

  const parsed = parseKnipCompactUnusedExportsResult(result.output);
  // Knip's compact reporter omits empty sections, so a clean scan (exit 0)
  // legitimately prints no export sections; sectionless output is only a
  // failure signal when Knip also exited nonzero (crash/config error).
  if (!parsed.sawExportSection && result.status !== 0) {
    console.error(`deadcode ${scan.name} produced no export sections.`);
    if (result.output) {
      console.error(result.output);
    }
    return false;
  }

  if (result.status !== 0) {
    console.error(`deadcode ${scan.name} exited with status ${result.status}.`);
    if (result.output) {
      console.error(result.output);
    }
    return false;
  }
  console.log(`[deadcode] Knip ${scan.name} passed with 0 entries.`);
  return true;
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  await main();
}
