/**
 * Build-time art generation via OpenAI gpt-image-2.
 *
 * THE ONE RULE: every scene prompt carries the SAME projection phrase and the
 * SAME light direction, verbatim. That is what keeps the diner, the guests and
 * the dishes in one coherent scene. (The griddle card is a deliberate flat
 * "UI card" break.)
 *
 * - Writes PNGs into public/art/ and refreshes public/art/manifest.json
 * - CACHED: skips any file that already exists (no re-billing)
 * - Props are generated on SOLID MAGENTA (#FF00FF) and keyed out at runtime
 *
 * Run: node scripts/gen-art.ts     (Node 22+ strips types natively)
 * Cost: $0.04 per image, 11 images = $0.44 max for a cold run.
 */
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const OUT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'public', 'art');
const MODEL = 'gpt-image-2';
const COST_PER_IMAGE = 0.04;

const PROJ =
  'strict straight-on side view at eye level, flat frontal elevation like a theater stage set, ' +
  'zero perspective convergence, no vanishing point';

const LIGHT =
  'lit by soft warm golden afternoon light from the upper left, gentle soft shadows falling to the lower right';

const STYLE =
  'cozy hand-painted cartoon illustration for a casual mobile cooking game, clean dark-brown outlines, ' +
  'warm saturated 1950s retro American diner palette of cream, cherry red, teal and chrome, smooth painterly shading';

const CHROMA =
  'isolated on a completely flat, uniform, solid pure magenta background (#FF00FF). ' +
  'The background must be one single flat magenta color with no gradients, no vignette, ' +
  'no cast shadows on the background, no outlines or glow bleeding into the background.';

const GUEST_BASE =
  `A single diner guest character for a casual mobile cooking game, shown from the waist up, ` +
  `facing the viewer straight on, ${PROJ}, ${LIGHT}, ${STYLE}. `;

const GUEST_END =
  ' Friendly neutral expression, hands resting at waist level as if resting on a counter. ' +
  `One character only, centered, head fully inside the frame. ${CHROMA}`;

const DISH_BASE = (food: string): string =>
  `${food}, served on a round white ceramic diner plate, ${PROJ}, ${LIGHT}, ${STYLE}. ` +
  'The plate is tipped slightly toward the camera in classic vintage diner-advertisement style so the ' +
  'food on it is clearly visible, while the camera stays at eye level. No steam, no smoke (the game ' +
  `engine adds steam). One serving only, centered. ${CHROMA}`;

interface ArtSpec {
  file: string;
  size: '1024x1024' | '1024x1536' | '1536x1024';
  prompt: string;
}

const ART: ArtSpec[] = [
  {
    file: 'bg.png',
    size: '1024x1536',
    prompt:
      `Full-bleed vertical background painting of the inside of a retro 1950s American diner, ` +
      `${PROJ}, ${LIGHT}, ${STYLE}. Composition from top to bottom: warm cream wall with two big ` +
      `chrome-framed windows showing a soft pastel morning sky, a hanging wooden menu board with NO ` +
      `readable text (just blurred pale chalk shapes), then a long back-counter shelf with a chrome ` +
      `espresso machine on the right end and a glass pie case on the left end, and below it a clean ` +
      `checkered linoleum floor in cream and butterscotch filling the lower third. NO people, NO ` +
      `foreground counter, NO text anywhere. Edge-to-edge painting.`,
  },
  {
    file: 'counter.png',
    size: '1536x1024',
    prompt:
      `A long empty diner service counter spanning the entire width of the frame, ${PROJ}, ${LIGHT}, ` +
      `${STYLE}. The butterscotch wooden countertop's top edge is a single perfectly horizontal line at ` +
      `about 35% down from the top of the image; below it a cherry-red counter front panel with a ` +
      `polished chrome trim strip, and three empty cherry-red vinyl bar stools with chrome legs evenly ` +
      `spaced in front of the counter. Nothing sits on the counter. Everything ABOVE the countertop ` +
      `edge — the top 35% of the image — is a completely flat, uniform, solid pure magenta background ` +
      `(#FF00FF) with no gradients and no objects. No text.`,
  },
  {
    file: 'guest0.png',
    size: '1024x1024',
    prompt:
      GUEST_BASE +
      'A burly friendly trucker with a chestnut beard, a navy trucker cap, and a red-and-black plaid flannel shirt.' +
      GUEST_END,
  },
  {
    file: 'guest1.png',
    size: '1024x1024',
    prompt:
      GUEST_BASE +
      'A sweet elderly grandmother with round gold glasses, a silver bun hairstyle, pearl earrings, and a lavender knit cardigan.' +
      GUEST_END,
  },
  {
    file: 'guest2.png',
    size: '1024x1024',
    prompt:
      GUEST_BASE +
      'A cheerful teenage student with freckles, headphones around the neck, and a mustard-yellow letterman jacket.' +
      GUEST_END,
  },
  {
    file: 'guest3.png',
    size: '1024x1024',
    prompt:
      GUEST_BASE +
      'A dapper businessman with a neat mustache, a navy pinstripe suit, a loosened red necktie, and a brown trilby hat.' +
      GUEST_END,
  },
  {
    file: 'dish_pancakes.png',
    size: '1024x1024',
    prompt: DISH_BASE(
      'A tall stack of three fluffy golden pancakes with a melting square of butter on top and maple syrup dripping down the sides',
    ),
  },
  {
    file: 'dish_eggs.png',
    size: '1024x1024',
    prompt: DISH_BASE(
      'Two sunny-side-up eggs with bright orange yolks and three crispy wavy bacon strips',
    ),
  },
  {
    file: 'dish_burger.png',
    size: '1024x1024',
    prompt: DISH_BASE(
      'A juicy cheeseburger with a glossy sesame bun, melted cheddar, lettuce and tomato, next to a small pile of crinkle-cut fries',
    ),
  },
  {
    file: 'dish_pie.png',
    size: '1024x1024',
    prompt: DISH_BASE(
      'A slice of cherry pie with a golden lattice crust and glossy red filling, beside a white ceramic mug of black coffee',
    ),
  },
  {
    file: 'griddle.png',
    size: '1024x1024',
    prompt:
      `A game-UI background card: a dark seasoned cast-iron griddle surface viewed from directly above ` +
      `(flat lay — this single image is intentionally top-down, framed as a flat UI card), subtle oil ` +
      `sheen and faint grill marks, ${LIGHT}, ${STYLE}. No food, no utensils, no hands, no text, ` +
      `edge-to-edge surface.`,
  },
];

async function generate(spec: ArtSpec): Promise<boolean> {
  const outPath = join(OUT_DIR, spec.file);
  if (existsSync(outPath)) {
    console.log(`  [cache] ${spec.file} already exists, skipping ($0.00)`);
    return false;
  }
  console.log(`  [gen]   ${spec.file} (${spec.size}) ...`);
  const res = await fetch('https://api.openai.com/v1/images/generations', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
    },
    body: JSON.stringify({ model: MODEL, prompt: spec.prompt, size: spec.size, n: 1 }),
  });
  if (!res.ok) {
    const body = await res.text();
    throw new Error(`API ${res.status} for ${spec.file}: ${body.slice(0, 400)}`);
  }
  const json = (await res.json()) as { data: Array<{ b64_json?: string; url?: string }> };
  const item = json.data[0];
  let buf: Buffer;
  if (item.b64_json) {
    buf = Buffer.from(item.b64_json, 'base64');
  } else if (item.url) {
    const imgRes = await fetch(item.url);
    buf = Buffer.from(await imgRes.arrayBuffer());
  } else {
    throw new Error(`No image payload for ${spec.file}`);
  }
  writeFileSync(outPath, buf);
  console.log(`  [done]  ${spec.file} (${(buf.length / 1024).toFixed(0)} KB)`);
  return true;
}

mkdirSync(OUT_DIR, { recursive: true });
if (!process.env.OPENAI_API_KEY) {
  console.error('OPENAI_API_KEY is not set');
  process.exit(1);
}

let generated = 0;
let failed = 0;
const queue = [...ART];
const workers = Array.from({ length: 4 }, async () => {
  for (;;) {
    const spec = queue.shift();
    if (!spec) return;
    try {
      if (await generate(spec)) generated++;
    } catch (err) {
      failed++;
      console.error(`  [FAIL]  ${spec.file}: ${err instanceof Error ? err.message : err}`);
    }
  }
});
await Promise.all(workers);

const manifest: Record<string, boolean> = {};
for (const f of readdirSync(OUT_DIR)) {
  if (f.endsWith('.png')) manifest[f] = true;
}
writeFileSync(join(OUT_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');

console.log(`\nGenerated ${generated} image(s), ${failed} failure(s), cost ≈ $${(generated * COST_PER_IMAGE).toFixed(2)}`);
console.log(`Manifest: ${Object.keys(manifest).length} file(s)`);
if (failed > 0) process.exit(1);
