/**
 * One-shot: fill every south-facing manifest enemy entry missing pixellabObjectId (PixelLab API v2).
 * Keys: legacy `enemy.<slug>.south` or `enemy.<slug>` with rotation/file `*.south.png`.
 * Requires PIXELLAB_API_TOKEN.
 *
 *   node scripts/pixellab-fill-missing-south-one-shot.mjs
 *   node scripts/pixellab-fill-missing-south-one-shot.mjs --concurrency=4
 *
 * Wave mode (stagger creates, then shared wait, then parallel poll/download):
 *   node scripts/pixellab-fill-missing-south-one-shot.mjs --delay-between-creates-sec=10 --post-queue-wait-sec=110 --concurrency=10
 */

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.join(__dirname, '..');
const SQL = path.join(ROOT, 'backend/migrations/000006b_enemy_data.sql');
const MANIFEST = path.join(ROOT, 'frontend/public/assets/game/manifest.json');
const BASE = 'https://api.pixellab.ai/v2';

const ARCHETYPE_REF = {
  wolf: 'enemy.wolf',
  boar: 'enemy.boar',
  zombie: 'enemy.zombie',
  spider: 'enemy.spider',
  orc: 'enemy.orc',
  skeleton: 'enemy.skeleton_archer',
  battle_lizard: 'enemy.battle_lizard',
  element: 'enemy.water_element',
  demon: 'enemy.fire_demon',
  skeleton_king: 'enemy.skeleton_king',
  forest_warden: 'enemy.forest_warden',
  titan: 'enemy.lightning_titan',
  bandit: 'enemy.orc',
  cultist: 'enemy.skeleton_archer',
  golem: 'enemy.ice_guardian',
  wraith: 'enemy.zombie',
  treant: 'enemy.forest_warden',
  basilisk: 'enemy.battle_lizard',
  wyvern: 'enemy.battle_lizard',
  harpy: 'enemy.spider',
  manticore: 'enemy.battle_lizard',
  shade: 'enemy.zombie',
};

const BIOME_HINT = {
  meadow: 'sunlit meadow greens and wildflowers',
  forest: 'deep forest shadows, moss and bark tones',
  ruins: 'grey stone dust, cracked masonry palette',
  canyon: 'red rock, dust, harsh sun',
  swamp: 'murky teal, bog mist, rotting wood',
  volcanic: 'ember glow, black rock, lava highlights',
  astral: 'cosmic violet-blue, star specks, ethereal glow',
};

function parseArgs() {
  let concurrency = 6;
  let delayBetweenCreatesSec = 0;
  let postQueueWaitSec = 0;
  for (const x of process.argv.slice(2)) {
    if (x.startsWith('--delay-between-creates-sec=')) {
      delayBetweenCreatesSec = Math.max(0, parseInt(x.slice(28), 10) || 0);
    }
    if (x.startsWith('--post-queue-wait-sec=')) {
      postQueueWaitSec = Math.max(0, parseInt(x.slice(22), 10) || 0);
    }
  }
  return { concurrency, delayBetweenCreatesSec, postQueueWaitSec };
}

function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

function parseEnemyRows(sqlText) {
  const re =
    /VALUES\s*\(\d+,\s*'([^']+)',\s*'([^']+)',\s*'([^']+)',\s*'([^']*)'/g;
  const map = new Map();
  let m;
  while ((m = re.exec(sqlText))) {
    map.set(m[1], { type: m[1], archetype: m[2], biome: m[3], name: m[4] });
  }
  return map;
}

function buildPrompt(row) {
  const b = BIOME_HINT[row.biome] ?? row.biome;
  return (
    `Pixel RPG combat sprite: ${row.name}. ` +
    `${row.archetype.replace(/_/g, ' ')} monster, ${b}. ` +
    `Low top-down view, facing south toward camera, full body, transparent background, ` +
    `game-ready, single black outline, medium shading — match init image proportions and style.`
  );
}

async function pollObject(token, objectId) {
  const url = `${BASE}/objects/${objectId}`;
  for (let i = 0; i < 120; i++) {
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${token}` },
    });
    const j = await res.json().catch(() => ({}));
    const data = j.data ?? j;
    const status = data?.status ?? data?.generation_status;
    if (status === 'completed' || data?.image_url || data?.download_url) {
      return data;
    }
    if (status === 'failed' || data?.error) {
      throw new Error(JSON.stringify(data?.error ?? data));
    }
    await sleep(3000);
  }
  throw new Error('Timeout waiting for object');
}

async function downloadMcpObject(objectId, destPath) {
  const url = `https://api.pixellab.ai/mcp/map-objects/${objectId}/download`;
  const res = await fetch(url);
  if (!res.ok) throw new Error(`download ${res.status}`);
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
  fs.writeFileSync(destPath, Buffer.from(await res.arrayBuffer()));
}

async function createMapObject(token, body, retries = 4) {
  for (let a = 0; a < retries; a++) {
    const cr = await fetch(`${BASE}/map-objects`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });
    const cj = await cr.json().catch(() => ({}));
    if (cr.status === 429 && a < retries - 1) {
      await sleep(15000 * (a + 1));
      continue;
    }
    if (!cr.ok) {
      throw new Error(`create ${cr.status} ${JSON.stringify(cj)}`);
    }
    const objectId = cj.data?.object_id ?? cj.object_id ?? cj.data?.id;
    if (!objectId) throw new Error(`no object_id ${JSON.stringify(cj)}`);
    return objectId;
  }
}

async function runPool(concurrency, items, fn) {
  let ix = 0;
  const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
    while (true) {
      const j = ix++;
      if (j >= items.length) break;
      await fn(items[j], j);
    }
  });
  await Promise.all(workers);
}

/** `enemy.<type_slug>` or legacy `enemy.<type_slug>.south` → DB `enemies.type` slug */
function textureKeyToEnemyTypeSlug(texKey) {
  let rest = texKey.startsWith('enemy.') ? texKey.slice('enemy.'.length) : texKey;
  if (rest.endsWith('.south')) rest = rest.slice(0, -'.south'.length);
  return rest;
}

/** @returns {{ slug: string, body: object } | { error: string }} */
function prepareCreateBody(texKey, textures, byType) {
  const slug = textureKeyToEnemyTypeSlug(texKey);
  const row = byType.get(slug);
  if (!row) return { error: `No SQL row ${slug}` };
  const refKey = ARCHETYPE_REF[row.archetype];
  if (!refKey || !textures[refKey]?.file) {
    return { error: `No archetype ref ${slug} ${row.archetype} ${refKey}` };
  }
  const refPath = path.join(ROOT, 'frontend/assets', textures[refKey].file);
  if (!fs.existsSync(refPath)) {
    return { error: `Missing ref file ${refPath}` };
  }
  const b64 = fs.readFileSync(refPath).toString('base64');
  const body = {
    description: buildPrompt(row),
    image_size: { width: 96, height: 96 },
    view: 'low top-down',
    outline: 'single color outline',
    shading: 'medium shading',
    detail: 'medium detail',
    init_image: { type: 'base64', base64: b64 },
    init_image_strength: 420,
    text_guidance_scale: 9,
  };
  return { slug, body };
}

async function finalizeSouthEntry(token, textures, texKey, slug, objectId) {
  await pollObject(token, objectId);
  const southFile = `enemies/enemy.${slug}.south.png`;
  const southAbs = path.join(ROOT, 'frontend/assets', southFile);
  await downloadMcpObject(objectId, southAbs);
  const entry = textures[texKey];
  entry.file = southFile;
  entry.kind = 'map_object';
  entry.rotation = 'south';
  entry.pixellabObjectId = objectId;
}

function loadPixellabTokenFromEnvFiles() {
  if (process.env.PIXELLAB_API_TOKEN) return;
  const candidates = [
    path.join(ROOT, '.env'),
    path.join(ROOT, 'frontend', '.env.local'),
  ];
  for (const p of candidates) {
    if (!fs.existsSync(p)) continue;
    const text = fs.readFileSync(p, 'utf8');
    for (const line of text.split(/\r?\n/)) {
      const t = line.trim();
      if (!t || t.startsWith('#')) continue;
      const m = t.match(/^PIXELLAB_API_TOKEN\s*=\s*(.*)$/);
      if (!m) continue;
      let v = m[1].trim();
      if (
        (v.startsWith('"') && v.endsWith('"')) ||
        (v.startsWith("'") && v.endsWith("'"))
      ) {
        v = v.slice(1, -1);
      }
      process.env.PIXELLAB_API_TOKEN = v;
      return;
    }
  }
}

async function main() {
  loadPixellabTokenFromEnvFiles();
  const token = process.env.PIXELLAB_API_TOKEN;
  if (!token) {
    console.error(
      'Set PIXELLAB_API_TOKEN in the environment or in repo root .env / frontend/.env.local',
    );
    process.exit(1);
  }

  const { concurrency, delayBetweenCreatesSec, postQueueWaitSec } = parseArgs();
  const sql = fs.readFileSync(SQL, 'utf8');
  const byType = parseEnemyRows(sql);

  const manifest = JSON.parse(fs.readFileSync(MANIFEST, 'utf8'));
  const { textures } = manifest;

  const missing = Object.keys(textures)
    .filter((k) => {
      if (!k.startsWith('enemy.') || textures[k].pixellabObjectId) return false;
      if (k.endsWith('.south')) return true;
      const e = textures[k];
      if (e.rotation === 'south') return true;
      const f = e.file || '';
      return f.endsWith('.south.png');
    })
    .sort();

  if (missing.length === 0) {
    console.log('Nothing missing.');
    return;
  }

  console.log(
    'Missing count:',
    missing.length,
    'concurrency:',
    concurrency,
    delayBetweenCreatesSec > 0
      ? `wave: delayBetweenCreatesSec=${delayBetweenCreatesSec} postQueueWaitSec=${postQueueWaitSec}`
      : '',
  );

  let ok = 0;
  let fail = 0;

  if (delayBetweenCreatesSec > 0) {
    const queued = [];
    for (let i = 0; i < missing.length; i++) {
      const texKey = missing[i];
      const prep = prepareCreateBody(texKey, textures, byType);
      if ('error' in prep) {
        console.error('SKIP', texKey, prep.error);
        fail++;
        continue;
      }
      try {
        const objectId = await createMapObject(token, prep.body);
        queued.push({ texKey, slug: prep.slug, objectId });
        console.log('Queued', prep.slug, objectId, `(${queued.length}/${missing.length})`);
      } catch (e) {
        console.error('FAIL create', prep.slug, e.message);
        fail++;
      }
      if (i < missing.length - 1 && delayBetweenCreatesSec > 0) {
        await sleep(delayBetweenCreatesSec * 1000);
      }
    }
    if (postQueueWaitSec > 0) {
      console.log(`Post-queue wait ${postQueueWaitSec}s…`);
      await sleep(postQueueWaitSec * 1000);
    }
    await runPool(concurrency, queued, async (job) => {
      try {
        await finalizeSouthEntry(token, textures, job.texKey, job.slug, job.objectId);
        ok++;
        console.log('OK', job.slug, job.objectId);
      } catch (e) {
        console.error('FAIL', job.slug, e.message);
        fail++;
      }
    });
  } else {
    await runPool(concurrency, missing, async (texKey) => {
      const prep = prepareCreateBody(texKey, textures, byType);
      if ('error' in prep) {
        console.error('SKIP', texKey, prep.error);
        fail++;
        return;
      }
      try {
        const objectId = await createMapObject(token, prep.body);
        await finalizeSouthEntry(token, textures, texKey, prep.slug, objectId);
        ok++;
        console.log('OK', prep.slug, objectId);
      } catch (e) {
        console.error('FAIL', prep.slug, e.message);
        fail++;
      }
    });
  }

  if (ok > 0) {
    manifest.version = (manifest.version ?? 0) + 1;
    fs.writeFileSync(MANIFEST, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
  }

  console.log('Done. OK', ok, 'FAIL', fail, 'manifest.version', manifest.version);
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});
