/* game.jsx — Folkdale: LLM-driven NES RPG with inventory, equipment & realtime combat */
const { useState, useEffect, useRef, useCallback } = React;
const G = window.GAME;
const InvIcon = window.ItemIcon;       // from inventory-screen.jsx
const InvScreen = window.InventoryScreen;
const SAVE_KEY = "aldoria_save_v2";

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "Hyrule",
  "scanlines": true,
  "npcBob": true
}/*EDITMODE-END*/;

function clean(s) {
  if (!s) return "";
  return s.replace(/[*_`#>]/g, "").replace(/^["']|["']$/g, "").trim();
}
function loadSave() {
  let s = null;
  try { s = JSON.parse(localStorage.getItem(SAVE_KEY)); } catch (e) {}
  const base = { gold: G.START.gold, items: { ...G.START.items }, weapon: G.START.weapon, armor: G.START.armor, hp: G.START.hp, maxHp: G.START.maxHp };
  if (s && s.items) return { ...base, ...s, items: { ...s.items } };
  return base;
}
function buildEnemies() {
  return G.ENEMY_SPAWNS.map((sp, i) => {
    const d = G.ENEMIES[sp.kind];
    return { id: i, kind: sp.kind, x: sp.x, y: sp.y, spawnX: sp.x, spawnY: sp.y,
      hp: d.hp, maxHp: d.hp, dmg: d.dmg, speed: d.speed, leash: d.leash,
      sprite: d.sprite, palette: d.palette, erratic: d.erratic,
      nextAt: 0, hurtUntil: 0, dead: false, reviveAt: 0 };
  });
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const theme = G.THEMES[t.theme] || G.THEMES.Hyrule;
  const themeRef = useRef(theme); themeRef.current = theme;

  const canvasRef = useRef(null);
  const player = useRef({ x: 9, y: 9, dir: "down" });
  const cmb = useRef({ cdUntil: 0, swing: { tiles: [], until: 0 }, hurtUntil: 0 });
  const frame = useRef(0);
  const enemiesRef = useRef(null);
  if (enemiesRef.current === null) enemiesRef.current = buildEnemies();

  const [inv, setInv] = useState(loadSave);
  const invRef = useRef(inv); invRef.current = inv;

  const [dlg, setDlg] = useState(null);
  const dlgRef = useRef(null); dlgRef.current = dlg;
  const [invOpen, setInvOpen] = useState(false);
  const invOpenRef = useRef(false); invOpenRef.current = invOpen;
  const [input, setInput] = useState("");
  const [scale, setScale] = useState(1);
  const [toasts, setToasts] = useState([]);
  const cdBarRef = useRef(null);

  const W = G.COLS * G.TILE, H = G.ROWS * G.TILE;

  const pushToast = useCallback((text, kind) => {
    const id = Math.random().toString(36).slice(2);
    setToasts((ts) => [...ts.slice(-3), { id, text, kind }]);
    setTimeout(() => setToasts((ts) => ts.filter((x) => x.id !== id)), 1900);
  }, []);

  const updateInv = useCallback((patch) => {
    const next = { ...invRef.current, ...patch };
    invRef.current = next; setInv(next);
    localStorage.setItem(SAVE_KEY, JSON.stringify(next));
  }, []);
  const gainGold = useCallback((n) => updateInv({ gold: invRef.current.gold + n }), [updateInv]);
  const gainItem = useCallback((id, n) => {
    const items = { ...invRef.current.items }; items[id] = (items[id] || 0) + n; updateInv({ items });
  }, [updateInv]);

  // ---- trading (buttons + LLM ACT) ----
  const trade = useCallback((npc, type, itemId) => {
    const shop = npc.shop; if (!shop) return false;
    const item = G.ITEMS[itemId]; if (!item) return false;
    const cur = invRef.current;
    if (type === "buy") {
      const price = shop.sells && shop.sells[itemId];
      if (price == null) { pushToast(`${npc.name} doesn't sell that`, "bad"); return false; }
      if (cur.gold < price) { pushToast("Not enough gold", "bad"); return false; }
      const items = { ...cur.items, [itemId]: (cur.items[itemId] || 0) + 1 };
      updateInv({ gold: cur.gold - price, items });
      pushToast(`Bought ${item.name}  −${price}g`, "good"); return true;
    } else {
      const price = shop.buys && shop.buys[itemId];
      if (price == null) { pushToast(`${npc.name} won't buy that`, "bad"); return false; }
      if (!cur.items[itemId]) { pushToast(`No ${item.name} to sell`, "bad"); return false; }
      const items = { ...cur.items, [itemId]: cur.items[itemId] - 1 };
      if (items[itemId] <= 0) delete items[itemId];
      updateInv({ gold: cur.gold + price, items });
      pushToast(`Sold ${item.name}  +${price}g`, "good"); return true;
    }
  }, [pushToast, updateInv]);

  const equip = useCallback((id) => {
    const it = G.ITEMS[id]; if (!it) return;
    if (it.type === "weapon") updateInv({ weapon: id });
    else if (it.type === "armor") updateInv({ armor: id });
    pushToast(`Equipped ${it.name}`, "good");
  }, [updateInv, pushToast]);
  const useItem = useCallback((id) => {
    const it = G.ITEMS[id]; const cur = invRef.current;
    if (!it || it.type !== "consumable") return;
    if (cur.hp >= cur.maxHp) { pushToast("Already at full hearts", "bad"); return; }
    const items = { ...cur.items }; items[id]--; if (items[id] <= 0) delete items[id];
    updateInv({ hp: Math.min(cur.maxHp, cur.hp + (it.heal || 0)), items });
    pushToast(`Used ${it.name}  +${it.heal} hearts`, "good");
  }, [updateInv, pushToast]);

  // ---- combat ----
  const attackHero = useCallback((en, ts) => {
    const cur = invRef.current;
    const def = cur.armor ? (G.ITEMS[cur.armor].defense || 0) : 0;
    const dmg = Math.max(1, en.dmg - def);
    const hp = Math.max(0, cur.hp - dmg);
    cmb.current.hurtUntil = ts + 220;
    if (hp <= 0) {
      player.current = { x: 9, y: 9, dir: "down" };
      enemiesRef.current.forEach((e) => { e.x = e.spawnX; e.y = e.spawnY; e.hp = e.maxHp; e.dead = false; });
      updateInv({ hp: cur.maxHp });
      pushToast("You fell! Revived at Mossbrook.", "bad");
    } else {
      updateInv({ hp });
    }
  }, [updateInv, pushToast]);

  const performAttack = useCallback((ts) => {
    if (dlgRef.current || invOpenRef.current) return;
    const cur = invRef.current;
    const w = cur.weapon ? G.ITEMS[cur.weapon] : null;
    if (!w || ts < cmb.current.cdUntil) return;
    cmb.current.cdUntil = ts + w.cd;
    const tiles = G.attackTiles(w, player.current);
    cmb.current.swing = { tiles, until: ts + 170 };
    const killed = [];
    enemiesRef.current.forEach((en) => {
      if (en.dead) return;
      if (!tiles.some((tl) => tl.x === en.x && tl.y === en.y)) return;
      en.hp -= w.power; en.hurtUntil = ts + 170;
      if (w.knockback) {
        const { d } = G.DIRS[player.current.dir];
        const kx = en.x + d[0], ky = en.y + d[1];
        const occ = enemiesRef.current.some((o) => o !== en && !o.dead && o.x === kx && o.y === ky);
        if (!G.isBlocked(kx, ky, themeRef.current) && !occ && !(kx === player.current.x && ky === player.current.y)) { en.x = kx; en.y = ky; }
      }
      if (en.hp <= 0) { en.dead = true; en.reviveAt = ts + 8000; killed.push(en); }
    });
    if (w.dash) {
      const f = G.frontTile(player.current);
      const occ = enemiesRef.current.some((o) => !o.dead && o.x === f.x && o.y === f.y);
      if (!G.isBlocked(f.x, f.y, themeRef.current) && !occ) { player.current.x = f.x; player.current.y = f.y; }
    }
    if (killed.length) {
      let gold = 0;
      killed.forEach(() => { gold += 1 + Math.floor(Math.random() * 3); if (Math.random() < 0.3) gainItem("ore", 1); });
      if (gold) gainGold(gold);
      pushToast(`Defeated ${killed.length} foe${killed.length > 1 ? "s" : ""}${gold ? "  +" + gold + "g" : ""}`, "good");
    }
  }, [gainGold, gainItem, pushToast]);

  // ---- rendering ----
  const render = useCallback((ts) => {
    const ctx = canvasRef.current.getContext("2d");
    ctx.imageSmoothingEnabled = false;
    G.drawWorld(ctx, themeRef.current, frame.current);
    const bob = t.npcBob && frame.current % 2 === 0 ? -2 : 0;
    // enemies
    enemiesRef.current.forEach((en) => {
      if (en.dead) return;
      G.drawSprite(ctx, en.sprite, en.palette, en.x, en.y, bob);
      if (ts < en.hurtUntil) { ctx.fillStyle = "rgba(255,255,255,0.55)"; ctx.fillRect(en.x * G.TILE, en.y * G.TILE, G.TILE, G.TILE); }
    });
    G.NPCS.forEach((n) => G.drawSprite(ctx, n.shape, n.palette, n.x, n.y, bob));
    // swing highlight
    if (ts < cmb.current.swing.until) {
      ctx.fillStyle = "rgba(255,236,120,0.45)";
      cmb.current.swing.tiles.forEach((tl) => ctx.fillRect(tl.x * G.TILE, tl.y * G.TILE, G.TILE, G.TILE));
    }
    // hero
    const p = player.current;
    const heroPal = { H: "#7a4a1a", S: "#f2c89a", E: "#1a1a2a", B: "#3aa04a", W: "#fff", L: "#5a3a1a", A: "#3aa04a" };
    G.drawSprite(ctx, "hero", heroPal, p.x, p.y, 0);
    if (ts < cmb.current.hurtUntil) { ctx.fillStyle = "rgba(226,58,74,0.5)"; ctx.fillRect(p.x * G.TILE, p.y * G.TILE, G.TILE, G.TILE); }
    // facing pip
    const dv = G.DIRS[p.dir].d;
    ctx.fillStyle = "#fff";
    ctx.fillRect(p.x * G.TILE + 14 + dv[0] * 13, p.y * G.TILE + 14 + dv[1] * 13, 4, 4);
    // hearts (top-right)
    const cur = invRef.current, hs = 16, gap = 2;
    for (let i = 0; i < cur.maxHp; i++) {
      const hx = W - 8 - (cur.maxHp - i) * (hs + gap);
      G.drawHeart(ctx, hx, 8, 2, i < cur.hp ? "#e23a4a" : "#3a2030");
    }
  }, [t.npcBob, W]);

  useEffect(() => { render(0); }, [render, t.theme]);

  // ---- realtime game loop ----
  useEffect(() => {
    let raf;
    const loop = (ts) => {
      frame.current = Math.floor(ts / 420);
      const paused = !!dlgRef.current || invOpenRef.current;
      if (!paused) {
        const occupied = new Set(enemiesRef.current.filter((e) => !e.dead).map((e) => e.x + "," + e.y));
        enemiesRef.current.forEach((en) => {
          if (en.dead) {
            if (ts >= en.reviveAt && !occupied.has(en.spawnX + "," + en.spawnY)) {
              en.dead = false; en.x = en.spawnX; en.y = en.spawnY; en.hp = en.maxHp; en.nextAt = ts + en.speed;
              occupied.add(en.x + "," + en.y);
            }
            return;
          }
          if (ts < en.nextAt) return;
          en.nextAt = ts + en.speed;
          occupied.delete(en.x + "," + en.y);
          const act = G.enemyStep(en, player.current, themeRef.current, occupied);
          if (act && act.attack) attackHero(en, ts);
          else if (act) { en.x = act.x; en.y = act.y; }
          occupied.add(en.x + "," + en.y);
        });
      }
      if (cdBarRef.current) {
        const w = invRef.current.weapon ? G.ITEMS[invRef.current.weapon] : null;
        const rem = w ? Math.max(0, cmb.current.cdUntil - ts) : 0;
        const ratio = w ? 1 - rem / w.cd : 1;
        cdBarRef.current.style.width = Math.round(ratio * 100) + "%";
      }
      render(ts);
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, [render, attackHero]);

  // responsive scale
  useEffect(() => {
    const fit = () => {
      const pad = 24;
      const s = Math.min((window.innerWidth - pad) / W, (window.innerHeight - pad) / H);
      setScale(Math.max(0.4, s));
    };
    fit(); window.addEventListener("resize", fit);
    return () => window.removeEventListener("resize", fit);
  }, [W, H]);

  const openDialogue = useCallback((npc) => {
    setInput("");
    setDlg({ npc, conv: [{ role: "assistant", content: npc.greeting }], line: npc.greeting, lastPlayer: "", loading: false });
  }, []);

  const move = useCallback((dx, dy, dir) => {
    const p = player.current; p.dir = dir;
    const nx = p.x + dx, ny = p.y + dy;
    const npc = G.NPCS.find((n) => n.x === nx && n.y === ny);
    if (npc) { openDialogue(npc); return; }
    if (enemiesRef.current.some((e) => !e.dead && e.x === nx && e.y === ny)) return; // blocked by foe — attack it
    if (!G.isBlocked(nx, ny, themeRef.current)) { p.x = nx; p.y = ny; }
  }, [openDialogue]);

  useEffect(() => {
    const onKey = (e) => {
      if (invOpenRef.current) { if (e.key === "Escape" || e.key.toLowerCase() === "i") setInvOpen(false); return; }
      if (dlgRef.current) { if (e.key === "Escape") setDlg(null); return; }
      const k = e.key.toLowerCase();
      if (k === " " || k === "spacebar" || k === "f" || k === "j") { e.preventDefault(); performAttack(performance.now()); }
      else if (k === "i") { e.preventDefault(); setInvOpen(true); }
      else if (k === "arrowup" || k === "w") { e.preventDefault(); move(0, -1, "up"); }
      else if (k === "arrowdown" || k === "s") { e.preventDefault(); move(0, 1, "down"); }
      else if (k === "arrowleft" || k === "a") { e.preventDefault(); move(-1, 0, "left"); }
      else if (k === "arrowright" || k === "d") { e.preventDefault(); move(1, 0, "right"); }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [move, performAttack]);

  const invSummary = useCallback(() => {
    const cur = invRef.current;
    const parts = Object.keys(cur.items).filter((k) => cur.items[k] > 0).map((k) => `${G.ITEMS[k].name} x${cur.items[k]}`);
    return parts.length ? parts.join(", ") : "nothing";
  }, []);

  const send = useCallback(async () => {
    const text = input.trim();
    const cur = dlgRef.current;
    if (!text || !cur || cur.loading) return;
    const npc = cur.npc;
    const conv = [...cur.conv, { role: "user", content: text }];
    setDlg({ ...cur, conv, lastPlayer: text, loading: true });
    setInput("");
    let outText = text;
    if (npc.shop) outText = `(Shop note — Hero's gold: ${invRef.current.gold}g. Hero's items: ${invSummary()}.)\n\n${text}`;
    const messages = [{ role: "user", content: npc.persona }, ...cur.conv, { role: "user", content: outText }];
    let reply;
    try { reply = clean(await window.claude.complete({ messages })); }
    catch (err) { reply = "...the words are lost to the wind. (The connection faltered — try again, hero.)"; }
    if (!reply) reply = "...";
    const lines = reply.split("\n"), kept = [];
    lines.forEach((ln) => {
      const m = ln.match(/^\s*ACT:\s*(buy|sell)\s+([a-z]+)/i);
      if (m && npc.shop) trade(npc, m[1].toLowerCase(), m[2].toLowerCase());
      else kept.push(ln);
    });
    const shown = clean(kept.join("\n")) || "...";
    setDlg((d) => (d && d.npc.id === npc.id
      ? { ...d, conv: [...conv, { role: "assistant", content: reply }], line: shown, loading: false } : d));
  }, [input, invSummary, trade]);

  const box = theme.box;
  const wpn = inv.weapon ? G.ITEMS[inv.weapon] : null;

  return (
    <div className="stage" style={{ background: "#05050a" }}>
      <div className="frame" style={{ width: W, height: H, transform: `scale(${scale})` }}>
        <canvas ref={canvasRef} width={W} height={H} className="screen" />

        <div className="hud" style={{ color: box.accent }}>{theme.title}</div>
        <div className="gold"><span className="coin">●</span>{inv.gold}g</div>
        {!dlg && !invOpen && <div className="hint">↑↓←→ move · SPACE attack · I bag</div>}
        {!dlg && !invOpen && <InventoryBar inv={inv} />}

        {!dlg && !invOpen && wpn && (
          <div className="wpnhud" style={{ boxShadow: `inset 0 0 0 3px ${box.accent}` }}>
            <InvIcon id={inv.weapon} size={26} />
            <div className="wpnmeta">
              <span className="wpnname">{wpn.name}</span>
              <span className="cdtrack"><span className="cdbar" ref={cdBarRef} style={{ background: box.accent }} /></span>
            </div>
          </div>
        )}

        {dlg && <DialogueBox dlg={dlg} box={box} inv={inv} input={input} setInput={setInput}
          onSend={send} onClose={() => setDlg(null)} onTrade={trade} />}

        {invOpen && <InvScreen inv={inv} hp={inv.hp} maxHp={inv.maxHp} box={box}
          onClose={() => setInvOpen(false)} onEquip={equip} onUse={useItem} />}

        <div className="toasts">
          {toasts.map((tt) => <div key={tt.id} className={"toast " + (tt.kind || "")}>{tt.text}</div>)}
        </div>

        {t.scanlines && <div className="scan" />}
      </div>

      <TweaksPanel>
        <TweakSection label="Visual direction" />
        <TweakRadio label="World theme" value={t.theme}
          options={["Hyrule", "Alefgard", "Gaia"]} onChange={(v) => setTweak("theme", v)} />
        <TweakSection label="Flourishes" />
        <TweakToggle label="CRT scanlines" value={t.scanlines} onChange={(v) => setTweak("scanlines", v)} />
        <TweakToggle label="NPC idle bob" value={t.npcBob} onChange={(v) => setTweak("npcBob", v)} />
      </TweaksPanel>
    </div>
  );
}

function InventoryBar({ inv }) {
  const ids = Object.keys(inv.items).filter((k) => inv.items[k] > 0);
  return (
    <div className="invbar">
      <span className="invlabel">BAG</span>
      {ids.length === 0 && <span className="invempty">empty</span>}
      {ids.map((id) => (
        <span key={id} className="invchip" title={G.ITEMS[id].name}>
          <InvIcon id={id} size={22} />
          <span className="invqty">{inv.items[id]}</span>
        </span>
      ))}
    </div>
  );
}

function Portrait({ npc }) {
  const ref = useRef(null);
  useEffect(() => {
    const ctx = ref.current.getContext("2d");
    ctx.imageSmoothingEnabled = false;
    ctx.clearRect(0, 0, 64, 64);
    const shape = G.SHAPES[npc.shape], px = 8;
    for (let r = 0; r < 8; r++) for (let c = 0; c < 8; c++) {
      const ch = shape[r][c]; if (ch === ".") continue;
      const col = npc.palette[ch]; if (!col) continue;
      ctx.fillStyle = col; ctx.fillRect(c * px, r * px, px, px);
    }
  }, [npc]);
  return <canvas ref={ref} width={64} height={64} className="portrait" />;
}

function ShopPanel({ npc, inv, box, onTrade }) {
  const shop = npc.shop;
  const sellIds = shop.sells ? Object.keys(shop.sells) : [];
  const sellableIds = shop.buys ? Object.keys(shop.buys).filter((id) => (inv.items[id] || 0) > 0) : [];
  return (
    <div className="shop" style={{ borderColor: box.accent }}>
      {sellIds.length > 0 && (
        <div className="shop-col">
          <div className="shop-h" style={{ color: box.accent }}>FOR SALE</div>
          <div className="shop-scroll">
            {sellIds.map((id) => {
              const price = shop.sells[id], afford = inv.gold >= price;
              return (
                <button key={id} className="shop-row" disabled={!afford}
                  onClick={() => onTrade(npc, "buy", id)} style={{ color: box.text }}>
                  <InvIcon id={id} size={20} />
                  <span className="shop-name">{G.ITEMS[id].name}</span>
                  <span className="shop-price" style={{ color: box.accent }}>{price}g</span>
                </button>
              );
            })}
          </div>
        </div>
      )}
      {sellableIds.length > 0 && (
        <div className="shop-col">
          <div className="shop-h" style={{ color: box.accent }}>YOU CAN SELL</div>
          <div className="shop-scroll">
            {sellableIds.map((id) => (
              <button key={id} className="shop-row" onClick={() => onTrade(npc, "sell", id)} style={{ color: box.text }}>
                <InvIcon id={id} size={20} />
                <span className="shop-name">{G.ITEMS[id].name} x{inv.items[id]}</span>
                <span className="shop-price" style={{ color: box.accent }}>+{shop.buys[id]}g</span>
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

function DialogueBox({ dlg, box, inv, input, setInput, onSend, onClose, onTrade }) {
  const inputRef = useRef(null);
  useEffect(() => { if (!dlg.loading) inputRef.current && inputRef.current.focus(); }, [dlg.loading]);
  const border2 = box.border2 ? `, inset 0 0 0 7px ${box.border2}` : "";
  const isShop = !!dlg.npc.shop;
  return (
    <div className="dlg" style={{ background: box.bg, boxShadow: `inset 0 0 0 4px ${box.border}${border2}`, color: box.text }}>
      <div className="dlg-top">
        <Portrait npc={dlg.npc} />
        <div className="dlg-body">
          <div className="dlg-namerow">
            <span className="dlg-name" style={{ color: box.accent }}>{dlg.npc.name}</span>
            {isShop && <span className="dlg-gold" style={{ color: box.accent }}>● {inv.gold}g</span>}
          </div>
          {dlg.lastPlayer && <div className="dlg-you">You: {dlg.lastPlayer}</div>}
          <div className="dlg-line">{dlg.loading ? <Dots /> : dlg.line}</div>
        </div>
        <button className="dlg-x" onClick={onClose} style={{ color: box.accent }} aria-label="Close">✕</button>
      </div>
      {isShop && <ShopPanel npc={dlg.npc} inv={inv} box={box} onTrade={onTrade} />}
      <div className="dlg-input">
        <span className="prompt" style={{ color: box.accent }}>&gt;</span>
        <input ref={inputRef} value={input}
          placeholder={dlg.loading ? "..." : (isShop ? "haggle, ask, or buy above" : "say something")}
          disabled={dlg.loading} onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter") onSend(); if (e.key === "Escape") onClose(); }} />
        <button className="dlg-send" onClick={onSend} disabled={dlg.loading}
          style={{ borderColor: box.accent, color: box.accent }}>TALK</button>
      </div>
    </div>
  );
}

function Dots() {
  const [n, setN] = useState(1);
  useEffect(() => { const id = setInterval(() => setN((x) => (x % 3) + 1), 350); return () => clearInterval(id); }, []);
  return <span className="dots">{"." .repeat(n)}</span>;
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
