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

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const baselinePath = join(root, "docs", "public-api-0.8.json");

function flattenTargets(value) {
  if (typeof value === "string") return [value];
  if (Array.isArray(value)) return value.flatMap(flattenTargets);
  if (value && typeof value === "object") return Object.values(value).flatMap(flattenTargets);
  return [];
}

const entries = [];
for (const directory of readdirSync(join(root, "packages"), { withFileTypes: true })) {
  if (!directory.isDirectory()) continue;
  const packageRoot = join(root, "packages", directory.name);
  const manifestPath = join(packageRoot, "package.json");
  if (!existsSync(manifestPath)) continue;
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
  const exportsMap =
    manifest.exports && typeof manifest.exports === "object"
      ? manifest.exports
      : { ".": manifest.main ?? "./src/index.ts" };
  for (const [subpath, value] of Object.entries(exportsMap)) {
    const target = flattenTargets(value).find((candidate) => /\.[cm]?[jt]sx?$/.test(candidate));
    if (!target) continue;
    const absolute = resolve(packageRoot, target);
    if (existsSync(absolute)) entries.push({ package: manifest.name, subpath, absolute });
  }
}

const config = ts.parseJsonConfigFileContent(
  ts.readConfigFile(join(root, "tsconfig.json"), ts.sys.readFile).config,
  ts.sys,
  root,
);
const program = ts.createProgram(
  [...new Set(entries.map(({ absolute }) => absolute))],
  config.options,
);
const checker = program.getTypeChecker();
const packages = {};
for (const entry of entries.sort(
  (left, right) =>
    left.package.localeCompare(right.package) || left.subpath.localeCompare(right.subpath),
)) {
  const source = program.getSourceFile(entry.absolute);
  if (!source)
    throw new Error(`Public API entry was not loaded: ${relative(root, entry.absolute)}`);
  const symbol = checker.getSymbolAtLocation(source);
  const names = symbol
    ? checker
        .getExportsOfModule(symbol)
        .map((item) => item.getName())
        .filter((name) => name !== "__esModule")
        .sort()
    : [];
  (packages[entry.package] ??= {})[entry.subpath] = names;
}

const report = { schemaVersion: 1, releaseLine: "0.8", packages };
const output = `${JSON.stringify(report, null, 2)}\n`;
if (process.argv.includes("--write")) {
  writeFileSync(baselinePath, output, "utf8");
  console.log(`Wrote ${relative(root, baselinePath)}`);
} else {
  if (!existsSync(baselinePath)) {
    throw new Error("Public API baseline is missing. Run `bun run generate:public-api`.");
  }
  const expected = readFileSync(baselinePath, "utf8");
  if (expected !== output) {
    throw new Error(
      "Public API changed. Review compatibility, then run `bun run generate:public-api` intentionally.",
    );
  }
  console.log(`Public API baseline matches ${Object.keys(packages).length} packages.`);
}
