/**
 * Content Types Generator (Issue #199)
 *
 * Emits `.mandu/generated/content-types.d.ts` with typed aliases for
 * every collection declared in `content.config.ts`. This gives users
 * full autocomplete on `docs.all()`, `docs.get(slug)`, etc. without
 * having to thread generics through every call site:
 *
 * ```ts
 * // user writes
 * import type { CollectionMap } from '.mandu/generated/content-types';
 * const entry = (await collections.docs.all())[0];
 * entry.data.title; // typed as string when schema has title: z.string()
 * ```
 *
 * # Contract
 *
 *   - Input: a map of collection-name → Collection instance (already
 *     returned from `defineCollection({ path, schema })`). We DO NOT
 *     re-execute `content.config.ts` here — the caller (the CLI
 *     generator in #196) is responsible for running the config in
 *     a scratch context and passing the live map to this function.
 *
 *   - Output: a single `.d.ts` file with:
 *       1. Re-exports of `Collection`, `CollectionEntry` types
 *       2. A `CollectionMap` interface keyed by collection name
 *       3. A `CollectionName` union type
 *       4. Per-collection `Entry{Name}` aliases
 *
 *   - Determinism: output order is stable (sorted by collection name)
 *     so regenerating without content changes produces a no-op diff.
 *
 * Schema-level type extraction is intentionally shallow for the MVP —
 * we emit `CollectionEntry<Record<string, unknown>>` for collections
 * without a schema, and `CollectionEntry<z.infer<typeof X>>` only
 * when the user exports the schema alongside the collection. A future
 * pass (tracked in a follow-up) will parse the Zod schema at build
 * time to emit concrete interfaces.
 */

import * as fs from "fs";
import * as path from "path";
import type { Collection } from "./collection";

/**
 * Structural view of a Collection accepted by the type emitter. The
 * generator only reads the collection's name — it never calls methods
 * — so the empty interface lets callers pass typed
 * `Collection<{title: string}>` without running into TypeScript's
 * invariance on generic classes.
 */
// biome-ignore lint/suspicious/noEmptyInterface: marker type for variance
interface CollectionLike {}

/** Input map: `{ docs: docsCollection, blog: blogCollection }`. */
export type CollectionRegistry = Record<string, CollectionLike>;

// Re-export the Collection type so users of @mandujs/core/content still
// see it alongside the registry.
export type { Collection };

/** Options for the type-emitter. */
export interface GenerateTypesOptions {
  /**
   * Destination path for the generated `.d.ts` file. Defaults to
   * `.mandu/generated/content-types.d.ts` relative to `root`.
   */
  outFile?: string;
  /**
   * Project root. Used to resolve `outFile` when that option is
   * relative. Defaults to `process.cwd()`.
   */
  root?: string;
  /**
   * Banner comment prepended to the file. Defaults to an
   * auto-generated warning so authors know not to hand-edit.
   */
  banner?: string;
}

const DEFAULT_BANNER = `/**
 * AUTO-GENERATED by @mandujs/core/content — do not edit by hand.
 * Regenerated on every \`mandu build\` / \`mandu dev\` bootstrap.
 */`;

/**
 * Generate the `content-types.d.ts` source string. Exposed separately
 * from the filesystem writer so tests can assert on the output without
 * touching disk.
 */
export function renderContentTypes(
  collections: CollectionRegistry
): string {
  const names = Object.keys(collections).sort();
  const lines: string[] = [];
  lines.push(DEFAULT_BANNER);
  lines.push("");
  lines.push(`import type { Collection, CollectionEntry } from "@mandujs/core/content";`);
  lines.push("");
  lines.push("// Per-collection entry aliases");
  for (const name of names) {
    // We can't introspect Zod at runtime without an ICU-sized schema
    // crawler, so we emit `Record<string, unknown>` here. Users who
    // want full field typing can import their schema directly:
    //   type DocEntry = CollectionEntry<z.infer<typeof docsSchema>>;
    lines.push(
      `export type Entry${capitalize(name)} = CollectionEntry<Record<string, unknown>>;`
    );
  }
  lines.push("");
  lines.push("export interface CollectionMap {");
  for (const name of names) {
    lines.push(`  ${JSON.stringify(name)}: Collection<Record<string, unknown>>;`);
  }
  lines.push("}");
  lines.push("");
  lines.push(
    `export type CollectionName = ${names.length > 0 ? names.map((n) => JSON.stringify(n)).join(" | ") : "never"};`
  );
  lines.push("");
  return lines.join("\n");
}

/**
 * Write the generated types file to disk. Creates the directory
 * tree if necessary and is a no-op if the file already contains the
 * exact same contents (prevents unnecessary filewatcher churn during
 * `mandu dev`).
 */
export function generateContentTypes(
  collections: CollectionRegistry,
  options: GenerateTypesOptions = {}
): { outFile: string; wrote: boolean } {
  const root = options.root ?? process.cwd();
  const outFile = path.isAbsolute(options.outFile ?? "")
    ? (options.outFile as string)
    : path.resolve(
        root,
        options.outFile ?? ".mandu/generated/content-types.d.ts"
      );
  const banner = options.banner ?? DEFAULT_BANNER;
  const body = renderContentTypes(collections);
  const content = banner === DEFAULT_BANNER ? body : banner + "\n\n" + body.slice(body.indexOf("\n\n") + 2);

  let existing: string | null = null;
  try {
    existing = fs.readFileSync(outFile, "utf8");
  } catch {
    existing = null;
  }
  if (existing === content) {
    return { outFile, wrote: false };
  }
  fs.mkdirSync(path.dirname(outFile), { recursive: true });
  fs.writeFileSync(outFile, content, "utf8");
  return { outFile, wrote: true };
}

function capitalize(value: string): string {
  // Collection names might contain dashes/underscores (`my-docs`),
  // which are not valid identifiers. Strip non-word chars and
  // PascalCase the remaining segments so `my-docs` becomes `MyDocs`.
  return value
    .split(/[^A-Za-z0-9]+/)
    .filter((part) => part.length > 0)
    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
    .join("");
}
