// Vigor — browser front-end driving combat-core (via combat-wasm) one tick per
// wall-clock frame. The engine is authoritative; this file is pure presentation:
// build a Game, push the player's intent, render the state it hands back.

import init, { Game } from "./pkg/combat_wasm.js";

// Monster roster for the start screen. ids match the config stat_blocks.
const ROSTER = [
  { id: "rat",        name: "Dire Rat",       blurb: "Fast and fragile — a flurry of filthy bites." },
  { id: "skeleton",   name: "Skeleton Knight", blurb: "Brittle bone, a wicked notched blade." },
  { id: "battlemage", name: "Dark Sorcerer",  blurb: "Fireballs with a long, punishable wind-up." },
  { id: "golem",      name: "Stone Golem",    blurb: "A slow wall of stone. Immense pool, heavy blows." },
  { id: "troll",      name: "Cave Troll",     blurb: "Huge, regenerates, hits like a landslide." },
  { id: "wraith",     name: "Wraith",         blurb: "Curses your ceiling — it drains your maximum away." },
];

const PLAYER = "duelist";

let cfgText = null;       // raw config JSON string (handed to the engine)
let abilityIndex = {};    // id -> { name, cast_time } from the config (for any caster)
let tickMs = 100;         // ms per tick (1000 / tick_rate)

let game = null;
let loopId = null;
let over = false;
let enemyId = 1;
let curFoe = null;
let playerAbilities = [];
let prevDown = {};        // id -> was-down, to fire the KO shake once
let casts = {};           // actorId -> { id, total } to drive the telegraph bar

const $ = (sel) => document.querySelector(sel);
const round = (x) => Math.round(x);

async function boot() {
  await init();
  // Cache-bust so config edits are always picked up (the browser otherwise
  // serves a stale fetch and the game runs old numbers).
  cfgText = await (await fetch("../configs/default.json?v=" + Date.now())).text();
  const cfg = JSON.parse(cfgText);
  tickMs = Math.max(20, Math.round(1000 / (cfg.tick_rate || 10)));
  for (const a of cfg.abilities || []) {
    abilityIndex[a.id] = { name: a.name, cast_time: a.cast_time || 0 };
  }
  buildRoster();
  $("#again").onclick = () => startBattle(curFoe);
  $("#choose").onclick = () => {
    $("#result").classList.add("hidden");
    $("#battle").classList.add("hidden");
    $("#start").classList.remove("hidden");
  };
  $("#auto-toggle").onclick = toggleAuto;
  window.addEventListener("keydown", onKey);
}

function buildRoster() {
  const r = $("#roster");
  r.innerHTML = "";
  for (const foe of ROSTER) {
    const el = document.createElement("div");
    el.className = "foe";
    el.innerHTML = `
      <img src="./portraits/${foe.id}.png" alt="${foe.name}" loading="lazy" />
      <div class="fmeta"><div class="fname">${foe.name}</div>
      <div class="fblurb">${foe.blurb}</div></div>`;
    el.onclick = () => startBattle(foe);
    r.appendChild(el);
  }
}

function startBattle(foe) {
  curFoe = foe;
  over = false;
  prevDown = {};
  casts = {};
  if (loopId) clearInterval(loopId);

  const seed = Math.floor(Math.random() * 1e9);
  try {
    game = new Game(cfgText, PLAYER, foe.id, BigInt(seed));
  } catch (e) {
    alert("Could not start battle: " + e);
    return;
  }
  enemyId = 1;
  playerAbilities = JSON.parse(game.abilities());
  game.setTarget(enemyId);

  // Scene chrome.
  $("#start").classList.add("hidden");
  $("#result").classList.add("hidden");
  $("#battle").classList.remove("hidden");
  $("#enemy-portrait").src = `./portraits/${foe.id}.png`;
  $("#enemy-name").textContent = foe.name;
  $("#player-name").textContent = "Duelist";
  $("#floaters").innerHTML = "";
  $("#log").innerHTML = "";
  mountVbar("#enemy-vbar");
  mountVbar("#player-vbar");
  buildActionBar();

  applyState(JSON.parse(game.state()));
  loopId = setInterval(step, tickMs);
}

function step() {
  if (over) return;
  const s = JSON.parse(game.tick());
  applyState(s);
  for (const ev of s.events) handleEvent(ev, s);
  if (s.outcome) endBattle(s.outcome);
}

// ── rendering ──────────────────────────────────────────────────────────
function mountVbar(sel) {
  $(sel).innerHTML = `
    <div class="vbar">
      <div class="vbar-cap"></div>
      <div class="vbar-fill"></div>
      <div class="vbar-ceiling"></div>
      <div class="vbar-num"></div>
    </div>`;
}

function updateVbar(sel, a) {
  const bar = $(sel).querySelector(".vbar");
  const pct = (v) => `${Math.max(0, Math.min(100, (v / a.max_vigor) * 100))}%`;
  bar.querySelector(".vbar-cap").style.width = pct(a.cap);
  bar.querySelector(".vbar-fill").style.width = pct(a.vigor);
  bar.querySelector(".vbar-ceiling").style.left = pct(a.cap);
  bar.querySelector(".vbar-num").textContent = `${round(a.vigor)} · cap ${round(a.cap)} / ${round(a.max_vigor)}`;
  bar.classList.toggle("dazed", a.stagger === "dazed");
  bar.classList.toggle("down", a.stagger === "unconscious" || a.stagger === "dead");
}

function applyState(s) {
  for (const a of s.actors) {
    const isEnemy = a.team !== 0;
    const card = isEnemy ? "#enemy-card" : "#player-card";
    updateVbar(isEnemy ? "#enemy-vbar" : "#player-vbar", a);
    $(isEnemy ? "#enemy-level" : "#player-level").textContent = `Lv ${a.level}`;
    renderStatuses(isEnemy ? "#enemy-status" : "#player-status", a);
    $(card).classList.toggle("down", a.stagger === "unconscious" || a.stagger === "dead");

    // Enemy cast telegraph.
    if (isEnemy) renderCast(a);

    if (!isEnemy) updateActionBar(a);
  }
}

function renderStatuses(sel, a) {
  const row = $(sel);
  const chips = [];
  if (a.stagger === "dazed") chips.push(["dazed", "DAZED"]);
  for (const st of a.statuses) chips.push([st, label(st)]);
  row.innerHTML = chips.map(([c, t]) => `<span class="chip ${c}">${t}</span>`).join("");
}

function label(st) {
  return ({
    dot: "poison", hot: "mending", regen_sabotage: "enervated",
    fatigue_amp: "weary", mitigation: "warded",
  })[st] || st;
}

function renderCast(a) {
  const wrap = $("#enemy-cast");
  const card = $("#enemy-card");
  if (a.casting) {
    const info = abilityIndex[a.casting.ability] || { name: a.casting.ability, cast_time: a.casting.remaining };
    if (!casts[a.id]) casts[a.id] = { id: a.casting.ability, total: Math.max(1, a.casting.remaining) };
    const total = abilityIndex[a.casting.ability]?.cast_time || casts[a.id].total;
    const frac = Math.max(0, Math.min(1, (total - a.casting.remaining) / total));
    wrap.classList.remove("hidden");
    wrap.querySelector(".cast-label").textContent = `casting ${info.name}…`;
    wrap.querySelector(".cast-fill").style.width = `${frac * 100}%`;
    card.classList.add("casting");
  } else {
    delete casts[a.id];
    wrap.classList.add("hidden");
    card.classList.remove("casting");
  }
}

// ── action bar ─────────────────────────────────────────────────────────
function buildActionBar() {
  const bar = $("#actionbar");
  bar.innerHTML = "";
  playerAbilities.forEach((ab, i) => {
    const b = document.createElement("button");
    b.className = "ability";
    b.dataset.kind = ab.kind;
    b.dataset.id = ab.id;
    const cost = ab.fill_cost > 0 ? `${round(ab.fill_cost)} vigor` : "free";
    const cast = ab.cast_time > 0 ? ` · ${(ab.cast_time / 10).toFixed(1)}s cast` : "";
    b.innerHTML = `<span class="akey">${i + 1}</span>
      <span class="aname">${ab.name}</span>
      <span class="acost">${cost}${cast}</span>
      <span class="acd hidden"></span>`;
    b.onclick = () => useAbility(ab.id);
    bar.appendChild(b);
  });
}

function updateActionBar(player) {
  const cdMap = {};
  for (const c of player.cooldowns) cdMap[c.id] = c.remaining;
  for (const b of $("#actionbar").children) {
    const ab = playerAbilities.find((x) => x.id === b.dataset.id);
    const cd = cdMap[ab.id] || 0;
    const unaffordable = ab.fill_cost > player.vigor;
    const cdEl = b.querySelector(".acd");
    if (cd > 0) {
      cdEl.classList.remove("hidden");
      cdEl.textContent = (cd / 10).toFixed(1);
    } else {
      cdEl.classList.add("hidden");
    }
    b.disabled = cd > 0 || unaffordable || player.stagger !== "normal";
  }
}

function useAbility(id) {
  if (!game || over) return;
  game.useAbility(id, enemyId);
}

function toggleAuto() {
  const btn = $("#auto-toggle");
  const on = !btn.classList.contains("on");
  btn.classList.toggle("on", on);
  if (game) game.setAutoAttack(on);
}

function onKey(e) {
  if ($("#battle").classList.contains("hidden")) return;
  if (e.key === " ") { e.preventDefault(); toggleAuto(); return; }
  const n = parseInt(e.key, 10);
  if (n >= 1 && n <= playerAbilities.length) {
    useAbility(playerAbilities[n - 1].id);
  }
}

// ── events → juice ─────────────────────────────────────────────────────
function nameOf(s, id) {
  const a = s.actors.find((x) => x.id === id);
  return a ? a.name : `#${id}`;
}

function handleEvent(ev, s) {
  switch (ev.t) {
    case "hit": {
      const toPlayer = ev.tgt === 0;
      const big = ev.outcome === "dazed" || ev.outcome === "unconscious" || ev.outcome === "killed";
      const tag = ev.outcome === "dazed" ? " DAZE!" : ev.outcome === "unconscious" ? " KO!" : ev.outcome === "killed" ? " ✶" : "";
      floater(`${round(ev.dmg)}${tag}`, toPlayer ? "down" : "up", "hit", big);
      if (ev.dmg > 0) shake(toPlayer ? "#player-card" : "#enemy-card");
      logLine(`${nameOf(s, ev.src)} hits ${nameOf(s, ev.tgt)} for ${round(ev.dmg)}${tag}`, big ? "lbig" : "lhit");
      break;
    }
    case "ceiling": {
      floater(`-${round(ev.fatigue)} cap`, ev.tgt === 0 ? "down" : "up", "curse", true);
      logLine(`${nameOf(s, ev.src)} curses ${nameOf(s, ev.tgt)}: ceiling −${round(ev.fatigue)}`, "lcurse");
      break;
    }
    case "recovery": {
      if (ev.heal > 0 || ev.revived) floater(ev.revived ? "REVIVE" : `+${round(ev.heal)} cap`, ev.tgt === 0 ? "down" : "up", "heal", ev.revived);
      logLine(`${nameOf(s, ev.tgt)} recovers ${round(ev.heal)} ceiling${ev.revived ? " (revived!)" : ""}`, "lheal");
      break;
    }
    case "cast":
      logLine(`${nameOf(s, ev.actor)} begins ${abilityIndex[ev.ability]?.name || ev.ability}…`, "lcast");
      break;
    case "fizzle":
      floater("fizzle", ev.actor === 0 ? "down" : "up", "curse", false);
      logLine(`${nameOf(s, ev.actor)}'s ${abilityIndex[ev.ability]?.name || ev.ability} fizzles`, "");
      break;
    case "interrupt":
      logLine(`${nameOf(s, ev.actor)}'s ${abilityIndex[ev.ability]?.name || ev.ability} is interrupted!`, "lbig");
      break;
    case "death":
      logLine(`${nameOf(s, ev.actor)} falls.`, "lbig");
      break;
  }
}

let floatSeq = 0;
function floater(text, dir, kind, big) {
  const f = document.createElement("div");
  f.className = `floater ${dir} ${kind}${big ? " crit" : ""}`;
  f.textContent = text;
  f.style.left = `${50 + (floatSeq++ % 5 - 2) * 12}%`;
  $("#floaters").appendChild(f);
  setTimeout(() => f.remove(), 1200);
}

function shake(sel) {
  const el = $(sel);
  el.classList.remove("hurt");
  void el.offsetWidth; // reflow to restart the animation
  el.classList.add("hurt");
}

function logLine(text, cls) {
  const log = $("#log");
  const line = document.createElement("div");
  if (cls) line.className = cls;
  line.textContent = text;
  log.appendChild(line);
  while (log.children.length > 40) log.removeChild(log.firstChild);
  log.scrollTop = log.scrollHeight;
}

function endBattle(outcome) {
  over = true;
  if (loopId) clearInterval(loopId);
  const win = outcome === "win_player";
  const card = $("#result .result-card");
  card.classList.toggle("win", win);
  card.classList.toggle("lose", !win);
  $("#result-title").textContent = win ? "VICTORY" : outcome === "draw" ? "MUTUAL RUIN" : "DEFEAT";
  $("#result-sub").textContent = win
    ? `${curFoe.name} falls before you.`
    : outcome === "draw" ? "You fell together." : `${curFoe.name} has bested you.`;
  $("#result").classList.remove("hidden");
}

boot();
