/**
 * Browser entry point — wires the DOM to `game.ts`.
 *
 * Presentation only. Every question of what is legal, what it means, or what the board looks like
 * is answered by the engine or by the shared view helpers.
 */

import type { Game } from './game.ts';
import {
  actionMenu,
  handPlayable,
  currentActor,
  fromSave,
  newGame,
  submit,
  toSave,
  view,
} from './game.ts';

const SAVE_KEY = 'station-master.save.v1';

let game: Game;
/** Which card or track piece is picked, waiting for a location. */
let selected: string | null = null;
/** A square picked on the board, waiting for a rotation. */
let pendingAt: string | null = null;

const $ = (id: string): HTMLElement => {
  const el = document.getElementById(id);
  if (!el) throw new Error(`missing element: ${id}`);
  return el as HTMLElement;
};

const esc = (s: string): string =>
  s.replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c] ?? c);

// ---------------------------------------------------------------------------

function start(): void {
  const params = new URLSearchParams(location.search);
  const requested = params.get('seed');

  const saved = load();
  if (saved && requested === null) {
    game = fromSave(saved);
  } else {
    // A seed in the URL makes a game shareable and reproducible: same link, same deal.
    const seed = requested !== null ? Number(requested) || 1 : Math.floor(Math.random() * 1e9);
    game = newGame(seed);
  }
  render();
}

function render(): void {
  const f = view(game);
  const menu = actionMenu(game);

  // Which squares the selected card or track piece may go on. Highlighting them is what turns the
  // coordinate list into a board: you pick the thing, then click where it goes.
  const chosen = menu.placeable.flatMap((g) => g.items).find((it) => it.subjectKey === selected);
  const spotsAt = new Map<string, { label: string; index: number }[]>();
  for (const sp of chosen?.spots ?? []) {
    const key = `${sp.coord.row},${sp.coord.col}`;
    spotsAt.set(key, [...(spotsAt.get(key) ?? []), sp]);
  }

  $('clock').textContent = `Day ${f.day} · Stage ${f.stage} — ${f.clock}`;
  $('phase').textContent = f.phase;
  $('revenue').textContent = String(f.revenue);
  const obj = $('objective');
  obj.textContent = f.objective.note;
  obj.className = f.objective.onPace ? 'pace good' : 'pace behind';
  $('seed').textContent = String(game.seed);

  // -- division
  $('division').innerHTML = f.division
    .map(
      (n) =>
        `<div class="node ${n.kind === 'office' ? 'office' : ''}">` +
        `<div class="nm">${esc(n.label)}` +
        (n.gradeUp
          ? ` <span class="grade">${n.gradeUp === 'east' ? 'climbs E ▲' : '▲ W climbs'}</span>`
          : '') +
        `</div>` +
        (n.modifiers.length ? `<div class="mlmods">${n.modifiers.map(esc).join(' · ')}</div>` : '') +
        `<div class="regions">` +
        n.trains
          .map(
            (r) =>
              `<div class="region">` +
              r
                .map(
                  (t) =>
                    `<span class="chip">${esc(t.label)}</span>` +
                    `<div class="consist">${t.consist.length ? esc(t.consist.join(', ')) : 'empty'}</div>`,
                )
                .join('') +
              `</div>`,
          )
          .join('') +
        `</div></div>`,
    )
    .join('');

  // -- grid
  // Include the legal squares in the bounds. A placement is usually just OUTSIDE the current
  // extent — that is what extending the district means — so a grid sized to the existing cards
  // would highlight nothing at exactly the moment highlighting matters.
  const spotCoords = [...spotsAt.keys()].map((k) => k.split(',').map(Number));
  const rows = [...f.cells.map((c) => c.row), ...spotCoords.map((c) => c[0]!)];
  const cols = [...f.cells.map((c) => c.col), ...spotCoords.map((c) => c[1]!)];
  const r0 = Math.min(...rows);
  const r1 = Math.max(...rows);
  const c0 = Math.min(...cols);
  const c1 = Math.max(...cols);
  const grid = $('grid');
  grid.style.gridTemplateColumns = `repeat(${c1 - c0 + 1}, minmax(104px, 1fr))`;
  let cells = '';
  for (let r = r1; r >= r0; r--) {
    for (let c = c0; c <= c1; c++) {
      const cell = f.cells.find((x) => x.row === r && x.col === c);
      const spots = spotsAt.get(`${r},${c}`);
      if (!cell) {
        // An empty square is still a legal destination — render it as a target rather than a gap,
        // or the most common placement of all would have nothing to click.
        cells += spots
          ? `<div class="cell ghost legal" data-at="${r},${c}">` +
            `<div class="nm">place here</div><div class="dim coord">(${r},${c})</div>` +
            (spots.length > 1 ? `<div class="dim">${spots.length} rotations</div>` : '') +
            `</div>`
          : '<div></div>';
        continue;
      }
      // The full explanation goes in `title` so hovering any card says what it does, with a short
      // version printed on the card itself — a name alone tells a player nothing once it is played.
      cells +=
        `<div class="cell ${cell.running ? 'run ' : ''}${cell.kind === 'office' ? 'office ' : ''}` +
        `${cell.kind === 'modifier' ? 'modifier ' : ''}${spots ? 'legal' : ''}"` +
        `${spots ? ` data-at="${r},${c}"` : ''} title="${esc(cell.label)} — ${esc(cell.what)}">` +
        `<div class="nm">${esc(cell.label)}</div>` +
        `<div class="dim coord">(${r},${c})</div>` +
        `<div class="dim what">${esc(short(cell.what))}</div>` +
        (cell.enhancements.length
          ? `<div class="enh">${cell.enhancements.map(esc).join(' · ')}</div>`
          : '') +
        (cell.tray ? `<div class="crew">🚂 ${esc(cell.tray)}</div>` : '') +
        (cell.cars.length ? `<div class="cars">${esc(cell.cars.join(', '))}</div>` : '') +
        `</div>`;
    }
  }
  grid.innerHTML = cells;

  // Clicking a highlighted square plays there. With more than one rotation available it narrows to
  // that square instead, so the rotation is a second click rather than a guess.
  for (const el of Array.from(grid.querySelectorAll('[data-at]'))) {
    const node = el as HTMLElement;
    node.onclick = () => {
      const spots = spotsAt.get(node.dataset['at'] ?? '') ?? [];
      if (spots.length === 1) {
        const intent = menu.options[spots[0]!.index];
        if (intent) submit(game, intent);
        selected = null;
        pendingAt = null;
      } else if (spots.length > 1) {
        pendingAt = node.dataset['at'] ?? null;
      }
      render();
    };
  }

  // -- facilities
  $('facs').innerHTML =
    f.facilities.length === 0
      ? '<div class="dim">no facilities yet</div>'
      : f.facilities
          .map(
            (x) =>
              `<div class="fac"><div class="nm">${esc(x.name)}</div>` +
              `<div class="dim">${esc(x.commodity)} · ${esc(x.flow)} · laborers ${esc(x.laborers)}</div>` +
              `<div class="boxes"><span class="dim">green</span>${boxes(x.green, x.greenCap)}</div>` +
              `<div class="boxes"><span class="dim">MEN AT WORK</span>` +
              x.maw
                .map(
                  (m) => `<span class="box ${m ? 'm' : 'empty'}">${m ? esc(m) : '·'}</span>`,
                )
                .join('') +
              `</div>` +
              `<div class="boxes"><span class="dim">red</span>${boxes(x.red, x.redCap)}</div>` +
              `<div class="boxes"><span class="dim">siding</span>${boxes(x.track, x.trackCap)}</div>` +
              `<div class="fstat ${x.jammed ? 'bad' : x.canFinish ? 'good' : 'idle'}">` +
              (x.jammed
                ? 'JAMMED — a load is on WORK with no car spotted; the siding is locked'
                : x.canFinish
                  ? 'ready — a matching empty car is spotted'
                  : 'no car spotted — a load started here would jam') +
              `</div></div>`,
          )
          .join('');

  // Name AND effect. A hand of names alone tells a player nothing about what they can do.
  const cardRow = (name: string, why: string, playable: boolean | null): string =>
    `<div class="handcard${playable === false ? ' unplayable' : ''}"><b>${esc(name)}</b>` +
    (playable === false ? ' <span class="tag">not playable yet</span>' : '') +
    (why ? `<div class="dim what">${esc(why)}</div>` : '') +
    `</div>`;

  const canPlay = handPlayable(game);
  $('hand').innerHTML = f.hand.length
    ? f.hand.map((h, i) => cardRow(h, f.handWhat[i] ?? '', canPlay[i] ?? null)).join('')
    : '<span class="dim">empty</span>';
  $('depts').innerHTML = f.departments.length
    ? f.departments.map((d, i) => cardRow(d, f.departmentsWhat[i] ?? '', null)).join('')
    : '<span class="dim">none</span>';

  const total = f.trackSupply.reduce((n, t) => n + t.left, 0);
  $('supply').innerHTML =
    `<div class="dim">${total} pieces left · one may be laid per turn, during the DRAW option</div>` +
    f.trackSupply
      .map(
        (t) =>
          `<span class="card ${t.left === 0 ? 'gone' : ''}">${esc(t.piece)} <b>${t.left}</b></span>`,
      )
      .join(' ');

  $('blocked').innerHTML =
    f.blocked.length === 0
      ? '<li class="dim">nothing blocked</li>'
      : f.blocked
          .map((b) => `<li class="sev-${b.severity}"><b>${esc(b.where)}</b> — ${esc(b.why)}</li>`)
          .join('');

  // -- log
  const log = $('log');
  log.innerHTML = game.log
    .slice(-60)
    .map((l) => `<div class="line t-${l.tone}">${esc(l.text)}</div>`)
    .join('');
  log.scrollTop = log.scrollHeight;

  renderActions(menu);
  save();
}

/** First clause only — the cards are small, and the full text is on the tooltip. */
function short(what: string): string {
  const head = what.split(' · ')[0] ?? what;
  return head.length > 64 ? head.slice(0, 61) + '…' : head;
}

function boxes(items: string[], cap: number): string {
  let out = '';
  for (let i = 0; i < Math.max(cap, items.length); i++) {
    const v = items[i];
    out += `<span class="box ${v ? 'f' : 'empty'}">${v ? esc(v) : '·'}</span>`;
  }
  return out || '<span class="dim">—</span>';
}

function renderActions(menu: ReturnType<typeof actionMenu>): void {
  const el = $('actions');

  if (game.state.status !== 'active') {
    const o = game.state.outcome;
    el.innerHTML =
      `<div class="over ${o?.result === 'win' ? 'win' : 'loss'}">` +
      `${o?.result === 'win' ? 'YOU WIN' : 'GAME OVER'} — ${esc(String(o?.reason ?? ''))}<br>` +
      `final Revenue ${game.state.players[0]?.revenue ?? 0} against a target of ${view(game).objective.target}</div>` +
      `<button id="again">new game</button>`;
    $('again').onclick = () => {
      clearSave();
      location.search = '';
    };
    return;
  }

  if (menu.direct.length === 0 && menu.placeable.length === 0) {
    el.innerHTML = '<div class="dim">nothing to decide — the engine is running the Division</div>';
    return;
  }

  const apply = (index: number): void => {
    const intent = menu.options[index];
    if (intent) submit(game, intent);
    selected = null;
    pendingAt = null;
    render();
  };

  let html = menu.direct
    .map(
      (g) =>
        `<div class="grp"><h3>${esc(g.title)}</h3>` +
        g.actions.map((a) => `<button class="act" data-i="${a.index}">${esc(a.label)}</button>`).join('') +
        `</div>`,
    )
    .join('');

  // Subject first, location second. Picking a card then a square is how the choice is actually made;
  // one flat list of every card-square-rotation combination was unreadable.
  for (const group of menu.placeable) {
    html += `<div class="grp"><h3>${esc(group.title)}</h3>`;
    for (const item of group.items) {
      const open = selected === item.subjectKey;
      html +=
        `<button class="subj ${open ? 'open' : ''}" data-subj="${esc(item.subjectKey)}">` +
        `${esc(item.subject)} <span class="dim">${item.spots.length} spot${item.spots.length === 1 ? '' : 's'}</span>` +
        `</button>`;
      if (open) {
        // Once a square is picked, show only that square's rotations — the rest is noise.
        const shown = pendingAt
          ? item.spots.filter((sp) => `${sp.coord.row},${sp.coord.col}` === pendingAt)
          : item.spots;
        html +=
          `<div class="spots"><div class="dim">` +
          (pendingAt ? `choose a rotation for (${esc(pendingAt.replace(',', ', '))}):` : 'click a highlighted square, or:') +
          `</div>` +
          shown.map((sp) => `<button class="act" data-i="${sp.index}">${esc(sp.label)}</button>`).join('') +
          `</div>`;
      }
    }
    html += `</div>`;
  }
  el.innerHTML = html;

  for (const b of Array.from(el.querySelectorAll('button.act'))) {
    const node = b as HTMLElement;
    node.onclick = () => apply(Number(node.dataset['i']));
  }
  for (const b of Array.from(el.querySelectorAll('button.subj'))) {
    const node = b as HTMLElement;
    node.onclick = () => {
      const key = node.dataset['subj'] ?? null;
      selected = selected === key ? null : key;
      pendingAt = null;
      render();
    };
  }
}

// ---------------------------------------------------------------------------
// Saving. localStorage only — nothing leaves the browser.
// ---------------------------------------------------------------------------

function save(): void {
  try {
    localStorage.setItem(SAVE_KEY, JSON.stringify(toSave(game)));
  } catch {
    // A full or disabled localStorage must not take the game down with it.
  }
}

function load(): ReturnType<typeof toSave> | null {
  try {
    const raw = localStorage.getItem(SAVE_KEY);
    return raw ? (JSON.parse(raw) as ReturnType<typeof toSave>) : null;
  } catch {
    return null;
  }
}

function clearSave(): void {
  try {
    localStorage.removeItem(SAVE_KEY);
  } catch {
    /* nothing to do */
  }
}

start();
