import { readdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

interface PackageManifest {
  readonly name?: string;
  readonly version?: string;
  readonly private?: boolean;
  readonly description?: string;
  readonly exports?: unknown;
}

interface ExportEntry {
  readonly subpath: string;
  readonly types?: string;
  readonly import?: string;
}

const workspaceRoot = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
const packagesRoot = path.join(workspaceRoot, "packages");
const outPath = path.join(workspaceRoot, "docs", "api-surface.md");

const packageDirs = await readdir(packagesRoot, { withFileTypes: true });
const packages = [];

for (const entry of packageDirs) {
  if (!entry.isDirectory()) {
    continue;
  }
  const packagePath = path.join(packagesRoot, entry.name, "package.json");
  const packageJson = await readOptionalFile(packagePath);
  if (!packageJson) {
    continue;
  }
  const manifest = JSON.parse(packageJson) as PackageManifest;
  if (!manifest.name?.startsWith("@runxhq/") || manifest.private === true) {
    continue;
  }
  packages.push({
    manifest,
    exports: normalizeExports(manifest.exports),
  });
}

packages.sort((left, right) => String(left.manifest.name).localeCompare(String(right.manifest.name)));

const lines = [
  "# API Surface",
  "",
  "<!-- Generated by scripts/gen-api-index.ts. Do not edit by hand. -->",
  "",
  "This page lists the public package entry points from each `@runxhq/*` package `exports` map.",
  "The package manifests are authoritative; regenerate this page with `pnpm docs:api`.",
  "",
];

for (const entry of packages) {
  lines.push(`## ${entry.manifest.name}`);
  lines.push("");
  if (entry.manifest.description) {
    lines.push(asAscii(entry.manifest.description));
    lines.push("");
  }
  lines.push(`Version: \`${entry.manifest.version ?? "unknown"}\``);
  lines.push("");
  lines.push("| Import | Types | Runtime |");
  lines.push("| --- | --- | --- |");
  for (const exported of entry.exports) {
    const importPath = exported.subpath === "." ? entry.manifest.name : `${entry.manifest.name}${exported.subpath.slice(1)}`;
    lines.push(`| \`${importPath}\` | \`${exported.types ?? "-"}\` | \`${exported.import ?? "-"}\` |`);
  }
  lines.push("");
}

function asAscii(value: string): string {
  return value.replace(/\u2014/g, "-").replace(/\u2013/g, "-").replace(/\u2019/g, "'");
}

await writeFile(outPath, `${lines.join("\n").trimEnd()}\n`);

function normalizeExports(exportsMap: unknown): readonly ExportEntry[] {
  if (!exportsMap || typeof exportsMap !== "object" || Array.isArray(exportsMap)) {
    return [];
  }
  return Object.entries(exportsMap as Record<string, unknown>)
    .map(([subpath, value]) => ({
      subpath,
      types: readExportTarget(value, "types"),
      import: readExportTarget(value, "import"),
    }))
    .sort((left, right) => left.subpath.localeCompare(right.subpath));
}

function readExportTarget(value: unknown, key: "types" | "import"): string | undefined {
  if (typeof value === "string") {
    return key === "import" ? value : undefined;
  }
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return undefined;
  }
  const target = (value as Record<string, unknown>)[key];
  return typeof target === "string" ? target : undefined;
}

async function readOptionalFile(filePath: string): Promise<string | undefined> {
  try {
    return await readFile(filePath, "utf8");
  } catch (error) {
    if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
      return undefined;
    }
    throw error;
  }
}
