/**
 * 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). The background
 *   plate is full-bleed and is NOT keyed.
 *
 * Run: node scripts/gen-sprites.ts   (Node 22+ strips types natively)
 * Cost: $0.04 per image, 7 images = $0.28 max for a cold run.
 */
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, moody dark-fantasy cavern palette of deep teal, ' +
  'slate blue and warm amber lantern light, 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' | '1024x768';
  prompt: string;
}

const SPRITES: SpriteSpec[] = [
  {
    file: 'player-idle.png',
    size: '1024x1024',
    prompt:
      `${STYLE}. A single small heroic explorer knight character, full body, standing idle pose facing right, ` +
      `wearing a rounded teal helmet with a soft amber-glowing lantern visor, short cape, sturdy 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 explorer knight character (rounded teal helmet with amber-glowing ` +
      `lantern visor, short cape, sturdy boots), full body, mid-stride running pose facing right, cape ` +
      `trailing, one leg forward one back, leaning into the run. One character only, centered. ${CHROMA}`,
  },
  {
    file: 'enemy.png',
    size: '1024x1024',
    prompt:
      `${STYLE}. A single small grumpy patrol creature: a round spiky cave beetle-slime with a cracked ` +
      `stone shell, two stubby legs, glowing hostile amber eyes, facing left, mid-walk. ` +
      `One creature only, centered. ${CHROMA}`,
  },
  {
    file: 'pickup-wings.png',
    size: '1024x1024',
    prompt:
      `${STYLE}. A single floating magical relic pickup item: a pair of small luminous aether wings made ` +
      `of teal-white light wrapped around a glowing amber crystal core, radiating gentle magic. ` +
      `Game item icon, one object only, centered. ${CHROMA}`,
  },
  {
    file: 'platform.png',
    size: '1024x1024',
    prompt:
      `${STYLE}. A single wide moss-covered ancient stone slab platform, viewed perfectly from the side, ` +
      `flat walkable top edge, carved runes faintly glowing amber, drips of moss and small ferns hanging ` +
      `off the edges, wider than tall (roughly 4:1 width to height), centered. ${CHROMA}`,
  },
  {
    file: 'ground.png',
    size: '1024x1024',
    prompt:
      `${STYLE}. A single large rectangular chunk of ancient cavern ground terrain, viewed perfectly from ` +
      `the side: dark stone block with a flat mossy walkable top surface, embedded glowing teal crystals ` +
      `and amber rune cracks in the rock face, roughly square block shape filling most of the frame, centered. ${CHROMA}`,
  },
  {
    file: 'background.png',
    size: '1024x1536',
    prompt:
      `${STYLE}. Full-bleed atmospheric background painting for a 2D metroidvania: the interior of a vast ` +
      `forgotten underground shrine cavern, towering mossy stone pillars and broken arches receding into ` +
      `misty darkness, faint god rays of pale teal light falling from cracks above, distant amber lantern ` +
      `glows, hanging vines, floating dust motes. Moody, mysterious, lots of negative space in the middle. ` +
      `NO characters, NO creatures, NO text. Edge-to-edge painting.`,
  },
  // ----- v2: three-room expansion (4 new images, $0.16) ---------------------
  {
    file: 'pickup-dash.png',
    size: '1024x1024',
    prompt:
      `${STYLE}. A single floating magical relic pickup item: an ember dash sigil — a small comet-shaped ` +
      `amber crystal shard streaking sideways, wrapped in trailing ribbons of warm orange-gold light and ` +
      `tiny sparks, radiating speed and momentum. Game item icon, one object only, centered. ${CHROMA}`,
  },
  {
    file: 'enemy-spore.png',
    size: '1024x1024',
    prompt:
      `${STYLE}. A single small hostile floating creature: a round luminous cave spore-wisp with a soft ` +
      `translucent pale-teal puffball body, a single large glowing amber eye, and a few short wispy ` +
      `tendrils drifting below it, gently hovering mid-air. One creature only, centered. ${CHROMA}`,
  },
  {
    file: 'background-grove.png',
    size: '1024x1536',
    prompt:
      `${STYLE}. Full-bleed atmospheric background painting for a 2D metroidvania: the interior of a vast ` +
      `underground crystal grove cavern, giant luminous teal and cyan crystals growing from the floor and ` +
      `ceiling like ancient trees, clusters of smaller glowing shards, pools of soft cyan light, drifting ` +
      `mist and spores. Moody, wondrous, lots of negative space in the middle. ` +
      `NO characters, NO creatures, NO text. Edge-to-edge painting.`,
  },
  {
    file: 'background-shrine.png',
    size: '1024x1536',
    prompt:
      `${STYLE}. Full-bleed atmospheric background painting for a 2D metroidvania: the interior of a ` +
      `derelict sunken shrine deep underground, a colossal ruined temple facade with cracked statues and ` +
      `toppled columns, warm amber braziers still burning among the rubble, shallow dark water with faint ` +
      `reflections, hanging roots and dust in pale shafts of light. Moody, solemn, 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.`);
    process.exit(1);
  }
}

main();
