/**
 * Build-time sprite generation via OpenAI gpt-image-2.
 *
 * - Writes PNGs into public/sprites/
 * - CACHED: skips any sprite whose file already exists on disk (no re-billing).
 * - CHROMA KEY: sprites are generated on SOLID MAGENTA (#FF00FF) and keyed
 *   out at runtime by the game's image loader (src/assets.ts). Background
 *   plates are full-bleed and are NOT keyed.
 *
 * Run: node scripts/gen-sprites.ts   (Node 22+ strips types natively)
 * Cost: $0.04 per image, 12 images = $0.48 max for a cold run. ONE SHOT each.
 */
import { writeFileSync, existsSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

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

const STYLE =
  'hand-painted 2D indie platformer game art, frozen mountaintop observatory palette of deep ' +
  'midnight indigo, glacial ice cyan, pale starlight silver and warm polished brass, ' +
  'chunky readable silhouette, soft 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 shadows cast on the background, no outlines or glow bleeding into the background.';

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

const SPRITES: SpriteSpec[] = [
  {
    file: 'player-idle.png',
    size: '1024x1024',
    prompt:
      `${STYLE}. A single small heroic astronomer character, full body, standing idle pose facing right: ` +
      `a hooded deep-indigo parka with brass clasps and a pale fur trim, round brass goggles pushed up on ` +
      `the hood, thick mittens, a small glowing star-lantern hanging from the belt, sturdy snow boots. ` +
      `Cute but determined, big head small body proportions, one character only, centered. ${CHROMA}`,
  },
  {
    file: 'player-run.png',
    size: '1024x1024',
    prompt:
      `${STYLE}. The same small heroic astronomer character (hooded deep-indigo parka with brass clasps ` +
      `and pale fur trim, round brass goggles on the hood, thick mittens, glowing star-lantern on the belt, ` +
      `snow boots), full body, mid-stride running pose facing right, leaning into the run, scarf trailing, ` +
      `one leg forward one back. One character only, centered. ${CHROMA}`,
  },
  {
    file: 'enemy-wisp.png',
    size: '1024x1024',
    prompt:
      `${STYLE}. A single small hostile floating creature: a frost wisp — a round orb of pale ice-cyan ` +
      `light with a jagged crown of tiny orbiting ice crystals, one large cold staring pale-blue eye, ` +
      `wisps of freezing vapor drifting beneath it. One creature only, centered. ${CHROMA}`,
  },
  {
    file: 'enemy-scuttler.png',
    size: '1024x1024',
    prompt:
      `${STYLE}. A single small grumpy patrol creature: a clockwork brass scuttler beetle with a rounded ` +
      `frost-dusted brass shell, visible little gears and a ticking key on its back, six stubby legs, ` +
      `two cold glowing ice-blue eyes, facing left, mid-walk. One creature only, centered. ${CHROMA}`,
  },
  {
    file: 'boss-orrery.png',
    size: '1024x1536',
    prompt:
      `${STYLE}. A single huge imposing boss creature: THE ORRERY WARDEN — a corrupted clockwork orrery, ` +
      `a frozen miniature star as its core with one vast baleful pale eye, surrounded by two or three ` +
      `tilted spinning brass rings carrying tiny frozen planets, frost creeping over its gears, shards of ` +
      `ice hanging beneath it, radiating cold menace. Dramatic, vertical composition, one creature only, ` +
      `centered, full creature visible. ${CHROMA}`,
  },
  {
    file: 'pickup-stargrip.png',
    size: '1024x1024',
    prompt:
      `${STYLE}. A single floating magical relic pickup item: the STARGRIP — a pair of small brass ` +
      `climbing gauntlets with glowing ice-cyan crystal claws at the fingertips, hovering side by side, ` +
      `wrapped in gentle swirling ribbons of starlight and tiny snowflakes. Game item icon, one object ` +
      `only, centered. ${CHROMA}`,
  },
  {
    file: 'ground.png',
    size: '1024x1024',
    prompt:
      `${STYLE}. A single large rectangular chunk of observatory masonry terrain, viewed perfectly from ` +
      `the side: dark indigo dressed-stone block with a flat snow-dusted walkable top surface, thin brass ` +
      `inlay lines and faint engraved star-chart constellation marks on the rock face, icicles on the ` +
      `edges, roughly square block shape filling most of the frame, centered. ${CHROMA}`,
  },
  {
    file: 'platform.png',
    size: '1024x1024',
    prompt:
      `${STYLE}. A single wide observatory walkway platform, viewed perfectly from the side: a slab of ` +
      `dark indigo stone with a polished brass rail edge and a flat snow-dusted walkable top, small ` +
      `icicles hanging from the underside, wider than tall (roughly 4:1 width to height), centered. ${CHROMA}`,
  },
  {
    file: 'bg-vestibule.png',
    size: '1024x1536',
    prompt:
      `${STYLE}. Full-bleed atmospheric background painting for a 2D metroidvania, tall portrait ` +
      `composition: the towering entrance hall of a frozen mountaintop observatory at night — colossal ` +
      `indigo stone pillars rising out of frame, a huge cracked brass telescope barrel crossing high ` +
      `overhead, frost and snowdrifts, pale starlight falling through a shattered dome far above, faint ` +
      `warm brass lamp glows low on the walls. Moody, wondrous, lots of negative space in the middle. ` +
      `NO characters, NO creatures, NO text. Edge-to-edge painting.`,
  },
  {
    file: 'bg-archive.png',
    size: '1024x1536',
    prompt:
      `${STYLE}. Full-bleed atmospheric background painting for a 2D metroidvania, tall portrait ` +
      `composition: the frozen star-chart archive of an ancient observatory — impossibly tall shelves of ` +
      `scrolls and leather tomes sheathed in blue ice, hanging brass armillary spheres and astrolabes ` +
      `furred with frost, drifting snow motes, a cold cyan glow from deep between the stacks, faint warm ` +
      `candle embers long frozen. Moody, mysterious, lots of negative space in the middle. ` +
      `NO characters, NO creatures, NO text. Edge-to-edge painting.`,
  },
  {
    file: 'bg-shaft.png',
    size: '1024x1536',
    prompt:
      `${STYLE}. Full-bleed atmospheric background painting for a 2D metroidvania, tall portrait ` +
      `composition: looking up the great vertical telescope shaft of a frozen observatory — smooth ` +
      `ice-glazed indigo stone walls rising forever, ringed brass maintenance walkways and ladders ` +
      `receding upward into darkness, hanging chains with frozen lanterns, a brilliant circle of starlight ` +
      `at the very top. Vertiginous, moody, lots of negative space in the middle. ` +
      `NO characters, NO creatures, NO text. Edge-to-edge painting.`,
  },
  {
    file: 'bg-orrery.png',
    size: '1024x1536',
    prompt:
      `${STYLE}. Full-bleed atmospheric background painting for a 2D metroidvania, tall portrait ` +
      `composition: the dome chamber of a frozen observatory — a colossal ruined orrery mechanism, giant ` +
      `interlocking brass gears and planet-rings frozen mid-turn, all sheathed in ice, a green-violet ` +
      `aurora blazing through the shattered dome above, warm brass braziers still glowing low at the ` +
      `floor. Grand, ominous, lots of negative space in the middle. ` +
      `NO characters, NO creatures, NO text. Edge-to-edge painting.`,
  },
];

async function generate(spec: SpriteSpec): 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;
}

async function main() {
  if (!process.env.OPENAI_API_KEY) {
    console.error('OPENAI_API_KEY is not set — cannot generate sprites.');
    process.exit(2);
  }
  mkdirSync(OUT_DIR, { recursive: true });
  console.log(`Generating ${SPRITES.length} sprites with ${MODEL} -> ${OUT_DIR}`);

  let generated = 0;
  const failures: string[] = [];
  // modest concurrency: 2 at a time
  const queue = [...SPRITES];
  async function worker() {
    for (let spec = queue.shift(); spec; spec = queue.shift()) {
      try {
        if (await generate(spec)) generated++;
      } catch (err) {
        failures.push(spec.file);
        console.error(`  [FAIL]  ${spec.file}: ${(err as Error).message}`);
      }
    }
  }
  await Promise.all([worker(), worker()]);

  console.log('---');
  console.log(`Generated ${generated} new image(s), cost this run: $${(generated * COST_PER_IMAGE).toFixed(2)}`);
  if (failures.length) {
    console.error(`FAILED: ${failures.join(', ')} — game will fall back to procgen for these (NO automatic regen; budget cap).`);
    process.exit(1);
  }
}

main();
