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

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const releaseVersion = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version;
const require = createRequire(import.meta.url);
const componentPackages = new Set([
  "auth",
  "captcha",
  "i18n",
  "image",
  "realtime",
  "ui",
  "uploader",
  "validation",
]);
const requestedHelperFiles = {
  auth: "src/helpers.ts",
  captcha: "src/helpers.ts",
  db: "src/helpers.ts",
  encryption: "src/http.ts",
  i18n: "src/index.ts",
  image: "src/index.ts",
  jwt: "src/helpers.ts",
  realtime: "src/messages.ts",
  uploader: "src/helpers.ts",
  validation: "src/helpers.ts",
};

function writeIfChanged(path, source) {
  if (existsSync(path) && readFileSync(path, "utf8") === source) return false;
  writeFileSync(path, source, "utf8");
  return true;
}

function walkFiles(directory, pattern) {
  const files = [];
  if (!existsSync(directory)) return files;
  const visit = (current) => {
    for (const entry of readdirSync(current, { withFileTypes: true })) {
      const path = join(current, entry.name);
      if (entry.isDirectory()) visit(path);
      else if (entry.isFile() && pattern.test(entry.name)) files.push(path);
    }
  };
  visit(directory);
  return files.sort();
}

function helperIsPublic(entrySource, helperFile, main) {
  if (!helperFile || helperFile === main) return true;
  const relativeHelper = `./${relative(dirname(main), helperFile).replace(/\\/g, "/").replace(/\.ts$/, ".ts")}`;
  const withoutExtension = relativeHelper.replace(/\.ts$/, "");
  return entrySource.includes(relativeHelper) || entrySource.includes(withoutExtension);
}

const compiler = require(join(root, "editors", "vscode", "src", "compiler.cjs"));
const uiCatalog = JSON.parse(
  readFileSync(join(root, "packages", "ui", "component-catalog.json"), "utf8"),
);
const uiComponentNames = new Set(uiCatalog.components.map((entry) => entry.name));
const packageDirectories = readdirSync(join(root, "packages"), { withFileTypes: true })
  .filter(
    (entry) =>
      entry.isDirectory() && existsSync(join(root, "packages", entry.name, "package.json")),
  )
  .map((entry) => entry.name)
  .sort();

const packages = [];
const failures = [];
let componentCount = 0;
let helperPackageCount = 0;

for (const directory of packageDirectories) {
  const packageRoot = join(root, "packages", directory);
  const manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
  const main = String(manifest.main ?? manifest.exports?.["."] ?? "src/index.ts").replace(
    /^\.\//,
    "",
  );
  const entry = join(packageRoot, main);
  const entrySource = existsSync(entry) ? readFileSync(entry, "utf8") : "";
  const componentsDir = join(packageRoot, "components");
  const components = existsSync(componentsDir)
    ? readdirSync(componentsDir)
        .filter((name) => name.endsWith(".wrn"))
        .sort()
    : [];
  const tests = walkFiles(join(packageRoot, "test"), /\.(?:test|spec)\.[cm]?[jt]s$/).map((path) =>
    relative(join(packageRoot, "test"), path).replace(/\\/g, "/"),
  );
  const sourceFiles = walkFiles(join(packageRoot, "src"), /\.[cm]?ts$/);
  const allSource = sourceFiles.map((path) => readFileSync(path, "utf8")).join("\n");
  const exported = [
    ...allSource.matchAll(
      /\bexport\s+(?:declare\s+)?(?:async\s+)?(?:function|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/g,
    ),
  ].map((match) => match[1]);
  const helperFile = requestedHelperFiles[directory];
  const hasHelperKit = helperFile
    ? existsSync(join(packageRoot, helperFile))
    : exported.length > 0 ||
      /export\s+\{/.test(allSource) ||
      /export\s+\*/.test(allSource) ||
      directory === "cli";
  const publicHelper = helperIsPublic(entrySource, helperFile, main);
  if (hasHelperKit) helperPackageCount++;
  componentCount += components.length;

  if (!/^0\.8\.\d+$/.test(manifest.version))
    failures.push(`${manifest.name}: invalid independent version ${manifest.version}`);
  if (!existsSync(entry)) failures.push(`${manifest.name}: missing public entry ${main}`);
  if (!existsSync(join(packageRoot, "README.md")))
    failures.push(`${manifest.name}: missing README.md`);
  if (!tests.length) failures.push(`${manifest.name}: missing package test`);
  if (!hasHelperKit) failures.push(`${manifest.name}: no public helper/API surface found`);
  if (!publicHelper) failures.push(`${manifest.name}: ${helperFile} is not exported by ${main}`);

  const usedUiComponents = new Set();
  const componentFailures = [];
  for (const component of components) {
    const file = join(componentsDir, component);
    const source = readFileSync(file, "utf8");
    for (const match of source.matchAll(/<([A-Z][A-Za-z0-9]*)\b/g)) {
      if (uiComponentNames.has(match[1])) usedUiComponents.add(match[1]);
    }
    try {
      compiler.compileWireFile(source);
    } catch (error) {
      componentFailures.push(
        `${component}: ${error instanceof Error ? error.message : String(error)}`,
      );
    }
  }

  if (componentPackages.has(directory)) {
    if (!components.length)
      failures.push(`${manifest.name}: component package has no .wrn components`);
    if (directory !== "ui" && manifest.dependencies?.["@wrnexus/ui"] !== "workspace:*") {
      failures.push(`${manifest.name}: component package must depend on @wrnexus/ui`);
    }
    if (directory !== "ui" && usedUiComponents.size === 0) {
      failures.push(`${manifest.name}: package blocks do not compose any @wrnexus/ui component`);
    }
    if (directory !== "ui" && !manifest.exports?.["./plugin"]) {
      failures.push(`${manifest.name}: component package must export ./plugin`);
    }
  }

  for (const issue of componentFailures) failures.push(`${manifest.name}: ${issue}`);

  packages.push({
    name: manifest.name,
    directory,
    version: manifest.version,
    kind: componentPackages.has(directory) ? "component-kit" : "helper-kit",
    entry: relative(root, entry).replace(/\\/g, "/"),
    helperFile: helperFile ?? null,
    helperIsPublic: publicHelper,
    publicExportCount: exported.length,
    components,
    uiComponents: [...usedUiComponents].sort(),
    tests,
    readme: existsSync(join(packageRoot, "README.md")),
    usesUi: directory === "ui" || manifest.dependencies?.["@wrnexus/ui"] === "workspace:*",
  });
}

const authSource = readdirSync(join(root, "packages", "auth", "components"))
  .filter((name) => name.endsWith(".wrn"))
  .map((name) => readFileSync(join(root, "packages", "auth", "components", name), "utf8"))
  .join("\n");
if (/<(?:button|select|textarea)\b/.test(authSource)) {
  failures.push("@wrnexus/auth: visible native controls must use @wrnexus/ui components");
}
for (const match of authSource.matchAll(/<input\b[\s\S]*?>/g)) {
  if (!/\btype=(?:"hidden"|'hidden')/i.test(match[0])) {
    failures.push(
      "@wrnexus/auth: visible native input must use @wrnexus/ui Input/PinInput components",
    );
    break;
  }
}

const report = {
  version: releaseVersion,
  summary: {
    packageCount: packages.length,
    helperPackageCount,
    componentPackageCount: packages.filter((entry) => entry.kind === "component-kit").length,
    componentCount,
    failures: failures.length,
  },
  packages,
  failures,
};

const markdown = [
  `# WRNexusJS ${releaseVersion} Package Kits`,
  "",
  "Every framework package has a public API/helper surface, package tests, and documentation. Packages that directly render developer-facing UI additionally provide package-owned `.wrn` blocks composed from `@wrnexus/ui`.",
  "",
  `- Packages: ${report.summary.packageCount}`,
  `- Helper/API kits: ${report.summary.helperPackageCount}`,
  `- Component kits: ${report.summary.componentPackageCount}`,
  `- Package-owned components: ${report.summary.componentCount}`,
  "",
  "| Package | Kit | Helper/API entry | Components | UI composition | Tests |",
  "|---|---|---|---:|---|---:|",
  ...packages.map(
    (entry) =>
      `| \`${entry.name}\` | ${entry.kind} | ${entry.helperFile ? `\`${entry.helperFile}\`` : `\`${entry.entry}\``} | ${entry.components.length} | ${entry.uiComponents.length ? entry.uiComponents.map((name) => `\`${name}\``).join(", ") : "—"} | ${entry.tests.length} |`,
  ),
  "",
  "## Component-kit rule",
  "",
  "Infrastructure-only packages remain helper/API kits and do not pull browser UI into server code. UI-facing packages own complete blocks and register them through their package plugin.",
  "",
  "## Security boundary for encrypted HTTP bodies",
  "",
  "`@wrnexus/encryption` encrypted HTTP envelopes add application-layer confidentiality, replay checks, and method/path/request binding. They do not replace HTTPS and cannot hide data from a browser user who receives the decryption key. Use them for service-to-service, native/mobile, controlled agents, or field-level protection with server-managed keys.",
  "",
  failures.length
    ? "## Failures\n\n" + failures.map((failure) => `- ${failure}`).join("\n") + "\n"
    : "## Audit result\n\nAll package-kit requirements pass.\n",
].join("\n");

if (process.argv.includes("--write")) {
  const reportDir = join(root, ".wrnexus", "reports");
  mkdirSync(reportDir, { recursive: true });
  writeIfChanged(
    join(reportDir, "PACKAGE-KITS-0.8.0.json"),
    `${JSON.stringify(report, null, 2)}\n`,
  );
  writeIfChanged(join(reportDir, "PACKAGE-KITS-0.8.0.md"), `${markdown}\n`);
}
console.log(JSON.stringify(report.summary, null, 2));
for (const failure of failures) console.error(`FAIL ${failure}`);
if (failures.length) process.exitCode = 1;
