#!/usr/bin/env node
/**
 * Build modular characters from recipes.
 *
 *   pnpm cast                         # all presets → assets/characters/
 *   pnpm creator -- --list            # list parts + presets
 *   pnpm creator -- --id survivor     # one recipe
 *   pnpm creator -- --id survivor --texture   # UV + Grok Imagine albedo
 *   pnpm creator -- --id survivor --texture --force  # repaint, replacing the cache
 *   pnpm creator -- --id survivor --texture --notes "charcoal business suit"
 *   pnpm creator -- --id survivor --albedo painted.png       # import, no Grok
 *   pnpm creator -- --id survivor --uv-guide  # UV guide only (no Grok)
 */
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { textureCharacter } from "../texture/textureCharacter.js";
import { assembleCharacter, formatAssemblyReport } from "./assemble.js";
import { listParts, PART_DEFINITIONS } from "./parts.js";
import { CHARACTER_PRESETS, presetById } from "./presets.js";

const ROOT = resolve(fileURLToPath(new URL("../../..", import.meta.url)));
const OUT_DIR = join(ROOT, "assets", "characters");

function parseArgs(argv: string[]) {
  const args = {
    list: false,
    id: undefined as string | undefined,
    help: false,
    texture: false,
    uvGuide: false,
    force: false,
    albedo: undefined as string | undefined,
    notes: undefined as string | undefined,
  };
  for (let i = 0; i < argv.length; i++) {
    const token = argv[i]!;
    if (token === "--") continue;
    if (token === "--list" || token === "-l") args.list = true;
    else if (token === "--id" || token === "-i") args.id = argv[++i];
    else if (token === "--texture" || token === "-t") args.texture = true;
    else if (token === "--uv-guide") args.uvGuide = true;
    else if (token === "--force" || token === "-f") args.force = true;
    else if (token === "--albedo") args.albedo = argv[++i];
    else if (token === "--notes" || token === "-n") args.notes = argv[++i];
    else if (token === "--help" || token === "-h") args.help = true;
  }
  return args;
}

function printList(): void {
  console.log("Bodies:  male | female\n");
  console.log("Slots & parts:");
  for (const slot of ["hair", "upper", "lower", "feet", "accessory"] as const) {
    const parts = listParts(slot);
    console.log(`  ${slot}`);
    for (const part of parts) {
      const body = part.body ? ` [${part.body}]` : "";
      console.log(`    ${part.id.padEnd(22)} ${part.name}${body}`);
    }
  }
  console.log("\nPresets:");
  for (const preset of CHARACTER_PRESETS) {
    console.log(`  ${preset.id.padEnd(16)} ${preset.name} (${preset.body})`);
  }
}

async function buildOne(
  id: string,
  opts: {
    texture?: boolean;
    uvGuide?: boolean;
    force?: boolean;
    albedo?: string;
    notes?: string;
  } = {},
): Promise<void> {
  const preset = presetById(id);
  if (!preset) {
    console.error(`Unknown preset "${id}". Use --list to see options.`);
    process.exit(1);
  }
  // --notes overrides the preset's wardrobe direction for this run, so prompt
  // wording can be iterated on without editing presets.ts
  const recipe = opts.notes ? { ...preset, styleNotes: opts.notes } : preset;

  await mkdir(OUT_DIR, { recursive: true });

  if (opts.texture || opts.uvGuide || opts.albedo) {
    // Relative to the shell's cwd, which under `pnpm creator` is tools/, not
    // the repo root — so say where we looked rather than leaking a bare ENOENT.
    const albedoPath = opts.albedo ? resolve(opts.albedo) : undefined;
    const importAlbedo = albedoPath
      ? new Uint8Array(
          await readFile(albedoPath).catch(() => {
            throw new Error(`Cannot read --albedo image at ${albedoPath}`);
          }),
        )
      : undefined;
    const label = importAlbedo
      ? "texture, imported"
      : opts.uvGuide
        ? "uv-guide"
        : opts.force
          ? "texture, forced"
          : "texture";
    console.log(`\n→ ${recipe.name} (${recipe.id})  [${label}]`);
    const tex = await textureCharacter(recipe, {
      outDir: OUT_DIR,
      guideOnly: Boolean(opts.uvGuide && !opts.texture && !importAlbedo),
      force: Boolean(opts.force),
      ...(importAlbedo ? { importAlbedo } : {}),
    });
    const outPath = join(OUT_DIR, `${recipe.id}.glb`);
    await writeFile(outPath, tex.glb);
    await writeFile(
      join(OUT_DIR, `${recipe.id}.recipe.json`),
      JSON.stringify({ ...recipe, stats: tex.assembled.stats, textured: tex.textured }, null, 2),
    );
    console.log(formatAssemblyReport(tex.assembled));
    console.log(`  uv-guide   ${tex.guidePath}`);
    if (tex.albedoPath) {
      const how = tex.imported ? " (imported)" : tex.cached ? " (cached)" : opts.force ? " (regenerated)" : "";
      console.log(`  albedo     ${tex.albedoPath}${how}`);
    }
    if (tex.costUsd > 0) console.log(`  cost       $${tex.costUsd.toFixed(3)}`);
    console.log(`  output     ${outPath}  (${(tex.glb.byteLength / 1024).toFixed(1)} KB)${tex.textured ? " textured" : ""}`);
    return;
  }

  const result = assembleCharacter(recipe);
  const outPath = join(OUT_DIR, `${recipe.id}.glb`);
  await writeFile(outPath, result.glb);

  await writeFile(
    join(OUT_DIR, `${recipe.id}.recipe.json`),
    JSON.stringify(
      {
        ...recipe,
        stats: result.stats,
      },
      null,
      2,
    ),
  );

  console.log(`\n→ ${recipe.name} (${recipe.id})`);
  console.log(formatAssemblyReport(result));
  console.log(`  output     ${outPath}  (${(result.glb.byteLength / 1024).toFixed(1)} KB)`);
}

async function main(): Promise<void> {
  const args = parseArgs(process.argv.slice(2));

  if (args.help) {
    console.log(`Usage:
  pnpm creator -- --list
  pnpm creator -- --id survivor
  pnpm creator -- --id survivor --uv-guide   # bake UV template PNG only
  pnpm creator -- --id survivor --texture    # UV + Grok Imagine albedo → textured GLB
  pnpm creator -- --id survivor --texture --force   # ignore the cached albedo and repaint it
  pnpm creator -- --id survivor --texture --notes "charcoal three-piece business suit"
  pnpm creator -- --id survivor --albedo painted.png  # import an albedo, no Grok involved
  pnpm cast                                  # build every preset (vertex colour)
`);
    return;
  }

  if (args.list) {
    printList();
    return;
  }

  if (args.id) {
    await buildOne(args.id, {
      texture: args.texture,
      uvGuide: args.uvGuide,
      force: args.force,
      ...(args.albedo ? { albedo: args.albedo } : {}),
      ...(args.notes ? { notes: args.notes } : {}),
    });
    return;
  }

  // Default: all presets (vertex colour — texturing is opt-in per id).
  await mkdir(OUT_DIR, { recursive: true });
  console.log(`Building ${CHARACTER_PRESETS.length} characters → ${OUT_DIR}\n`);
  for (const recipe of CHARACTER_PRESETS) {
    await buildOne(recipe.id);
  }
  console.log(`\nDone. ${PART_DEFINITIONS.length} parts available in the catalog.`);
}

main().catch((error: unknown) => {
  console.error(error instanceof Error ? error.message : String(error));
  process.exit(1);
});
