/* Operations console.
 *
 * The token lives in memory only — never localStorage, never a cookie — so
 * closing the tab ends the session and nothing is left on a shared machine.
 *
 * Every value here is read from the ledger. Nothing is computed twice: if a
 * number looks wrong, the ledger is wrong, and that is the point of showing it. */

import * as charts from '/charts.js';

let token = null;
let timer = null;

const $ = (id) => document.getElementById(id);

/* Money is stored in millisatoshis. Operators think in sats. */
const sats = (msat) => Math.round((msat || 0) / 1000).toLocaleString();
const signed = (msat) => (msat > 0 ? '+' : '') + sats(msat);

function el(tag, attrs, ...children) {
  const node = document.createElement(tag);
  for (const [k, v] of Object.entries(attrs || {})) {
    if (k === 'class') node.className = v;
    else if (k === 'text') node.textContent = v;
    else node.setAttribute(k, v);
  }
  for (const c of children) {
    if (c == null) continue;
    node.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
  }
  return node;
}

function clear(node) {
  while (node.firstChild) node.removeChild(node.firstChild);
}

async function api(path) {
  const res = await fetch(path, { headers: { Authorization: 'Bearer ' + token } });
  if (!res.ok) throw new Error(`${res.status}`);
  return res.json();
}

/* ---------------- gate ---------------- */

async function unlock() {
  token = $('token').value.trim();
  try {
    await api('/admin/api/overview');
  } catch (e) {
    $('gate-msg').textContent =
      e.message === '401' ? 'Rejected.' : 'Unavailable: ' + e.message;
    token = null;
    return;
  }
  $('gate').hidden = true;
  $('console').hidden = false;
  await refreshAll();
  // Poll rather than stream: the console is read-only and a few seconds of
  // staleness costs nothing, whereas another websocket per operator does.
  timer = setInterval(refreshAll, 5000);
}

/* ---------------- refresh ---------------- */

async function refreshAll() {
  try {
    await Promise.all([loadOverview(), loadActivePanel()]);
    $('livedot').classList.remove('stale');
  } catch {
    // A failed poll marks the display stale rather than blanking it: old
    // numbers with a warning beat no numbers.
    $('livedot').classList.add('stale');
  }
}

async function loadOverview() {
  const d = await api('/admin/api/overview');

  $('hdr-pot').textContent = sats(d.house_pot_msat);
  $('k-pot').textContent = sats(d.house_pot_msat);
  $('k-owed').textContent = sats(d.owed_to_players);
  $('k-fees').textContent = sats(d.fees_all_time_msat);
  $('k-fees-24h').textContent = sats(d.fees_24h_msat) + ' in 24h';

  const margin = $('k-margin');
  margin.textContent = signed(d.gross_margin_24h);
  margin.className = 'tile-value ' + (d.gross_margin_24h >= 0 ? 'up' : 'down');
  $('k-volume').textContent = sats(d.wagered_24h_msat) + ' wagered';

  $('k-players').textContent = d.players_total.toLocaleString();
  $('k-active').textContent = d.players_active_24h + ' active 24h';

  // The books check is the one number that must never be wrong.
  const books = $('k-books');
  books.textContent = d.books_balanced ? 'BALANCED' : 'IMBALANCE';
  $('tile-books').classList.toggle('bad', !d.books_balanced);
  $('k-conservation').textContent = d.books_balanced
    ? 'sums to zero'
    : `off by ${d.conservation_msat} msat`;
}

function activePanel() {
  const on = document.querySelector('.opsnav button.on');
  return on ? on.dataset.panel : 'dash';
}

async function loadActivePanel() {
  switch (activePanel()) {
    case 'dash': return loadDashboard();
    case 'players': return loadPlayers();
    case 'ledger': return loadLedger();
    case 'rounds': return loadRounds();
    case 'risk': return loadRisk();
  }
}

/* ---------------- dashboard ---------------- */

async function loadDashboard() {
  const d = await api('/admin/api/revenue');

  const days = d.daily || [];
  charts.balanceChart($('chart-revenue'),
    days.map((x) => ({ BalanceAfter: x.net_msat })));

  // Stakes in against payouts out, as a simple two-series comparison.
  charts.crashHistoryChart($('chart-flow'),
    days.map((x) => Math.max(1, x.stakes_in_msat / Math.max(1, x.paid_out_msat))));

  const grid = $('fee-schedule');
  clear(grid);
  const s = d.fee_schedule || {};
  const rows = [
    ['rake', s.rake_percent],
    ['rounding unit', s.rounding_unit],
    ['minimum payout', s.minimum_payout],
    ['game rtp', s.game_rtp_percent],
    ['effective rtp', s.effective_rtp_percent],
    ['worst case rounding', s.worst_case_rounding_per_payout],
  ];
  for (const [k, v] of rows) {
    grid.appendChild(el('div', {},
      el('span', { class: 'k', text: k }),
      el('span', { class: 'v', text: v || '—' })));
  }
}

/* ---------------- players ---------------- */

let playersCache = [];

async function loadPlayers() {
  const d = await api('/admin/api/players');
  playersCache = d.players || [];
  renderPlayers();
}

function renderPlayers() {
  const q = $('player-filter').value.trim().toLowerCase();
  const body = $('tbl-players').querySelector('tbody');
  clear(body);

  for (const p of playersCache) {
    if (q && !p.nickname.toLowerCase().includes(q) && !p.pubkey.includes(q)) continue;
    const tr = el('tr', {});
    tr.appendChild(el('td', { class: 'dim', text: String(p.id) }));
    tr.appendChild(el('td', { text: p.nickname || '—' }));
    tr.appendChild(el('td', { class: 'num pos', text: sats(p.balance_msat) }));
    tr.appendChild(el('td', { class: 'num dim', text: String(p.bets) }));
    tr.appendChild(el('td', { class: 'num', text: sats(p.wagered_msat) }));
    tr.appendChild(el('td', { class: 'num', text: sats(p.won_msat) }));
    tr.appendChild(el('td', {
      class: 'num ' + (p.net_msat >= 0 ? 'pos' : 'neg'),
      text: signed(p.net_msat),
    }));
    tr.appendChild(el('td', {
      class: 'dim',
      text: p.last_seen ? new Date(p.last_seen).toLocaleString() : '—',
    }));
    body.appendChild(tr);
  }
}

/* ---------------- ledger ---------------- */

async function loadLedger() {
  const d = await api('/admin/api/transactions');
  const body = $('tbl-ledger').querySelector('tbody');
  clear(body);

  for (const e of d.transactions || []) {
    const tr = el('tr', {});
    tr.appendChild(el('td', { class: 'dim', text: String(e.id) }));

    const cls = e.kind === 'operating_fee' ? 'pill fee'
      : e.kind === 'payout' ? 'pill payout' : 'pill';
    tr.appendChild(el('td', {}, el('span', { class: cls, text: e.kind })));

    tr.appendChild(el('td', { class: 'dim', text: e.round_id ? String(e.round_id) : '—' }));
    tr.appendChild(el('td', { text: e.nickname || String(e.account_id) }));
    tr.appendChild(el('td', {
      class: 'num ' + (e.amount_msat >= 0 ? 'pos' : 'neg'),
      text: signed(e.amount_msat),
    }));
    tr.appendChild(el('td', { class: 'num dim', text: sats(e.balance_before) }));
    tr.appendChild(el('td', { class: 'num', text: sats(e.balance_after) }));
    tr.appendChild(el('td', { class: 'dim', text: new Date(e.created_at).toLocaleTimeString() }));
    body.appendChild(tr);
  }
}

/* ---------------- rounds ---------------- */

async function loadRounds() {
  const d = await api('/admin/api/rounds');
  const body = $('tbl-rounds').querySelector('tbody');
  clear(body);

  for (const r of d.rounds || []) {
    const tr = el('tr', {});
    tr.appendChild(el('td', { class: 'dim', text: String(r.id) }));
    tr.appendChild(el('td', { text: r.game }));

    const crash = r.crash_point
      ? (r.crash_point / 4294967296).toFixed(2) + '×'
      : '—';
    tr.appendChild(el('td', { class: 'num', text: crash }));
    tr.appendChild(el('td', { class: 'num dim', text: String(r.players) }));
    tr.appendChild(el('td', { class: 'num', text: sats(r.staked_msat) }));
    tr.appendChild(el('td', { class: 'num', text: sats(r.paid_msat) }));
    tr.appendChild(el('td', { class: 'num pos', text: sats(r.rake_msat) }));
    tr.appendChild(el('td', {
      class: 'num ' + (r.house_result_msat >= 0 ? 'pos' : 'neg'),
      text: signed(r.house_result_msat),
    }));

    // The seed cell is the honest one: sealed until settlement, and there is
    // no control that opens it early.
    const seedCell = el('td', { class: 'mono' });
    if (r.voided_at) {
      seedCell.appendChild(el('span', { class: 'pill void', text: 'void' }));
    } else if (r.server_seed) {
      seedCell.textContent = r.server_seed.slice(0, 16) + '…';
    } else {
      seedCell.appendChild(el('span', { class: 'pill', text: 'sealed' }));
    }
    tr.appendChild(seedCell);
    body.appendChild(tr);
  }
}

/* ---------------- risk ---------------- */

async function loadRisk() {
  const d = await api('/admin/api/risk');

  const flags = $('risk-flags');
  clear(flags);
  const items = [
    {
      n: d.withdrawals_to_review, t: 'withdrawals awaiting your approval',
      level: d.withdrawals_to_review > 0 ? 'warn' : 'ok',
    },
    {
      n: d.pending_withdrawals, t: 'withdrawals queued or sending',
      level: 'ok',
    },
    {
      n: d.unresolved_rounds, t: 'rounds unresolved past the staleness window',
      level: d.unresolved_rounds > 0 ? 'warn' : 'ok',
    },
    {
      n: d.books_balanced ? 0 : d.conservation_msat,
      t: d.books_balanced ? 'ledger imbalance — books sum to zero'
                          : 'LEDGER IMBALANCE — investigate immediately',
      level: d.books_balanced ? 'ok' : 'bad',
    },
  ];
  for (const it of items) {
    flags.appendChild(el('div', { class: 'flag ' + it.level },
      el('span', { class: 'n', text: String(it.n) }),
      el('span', { class: 't', text: it.t })));
  }

  const body = $('tbl-winners').querySelector('tbody');
  clear(body);
  for (const wnr of d.top_winners || []) {
    const tr = el('tr', {});
    tr.appendChild(el('td', { class: 'dim', text: String(wnr.account_id) }));
    tr.appendChild(el('td', { text: wnr.nickname || '—' }));
    tr.appendChild(el('td', { class: 'num pos', text: signed(wnr.net_msat) }));
    tr.appendChild(el('td', { class: 'num dim', text: String(wnr.bets) }));
    body.appendChild(tr);
  }
}

/* ---------------- wiring ---------------- */

function selectPanel(name) {
  document.querySelectorAll('.opsnav button').forEach((b) =>
    b.classList.toggle('on', b.dataset.panel === name));
  document.querySelectorAll('.opspanel').forEach((p) =>
    (p.hidden = p.id !== 'panel-' + name));
  loadActivePanel().catch(() => $('livedot').classList.add('stale'));
}

$('unlock').onclick = () => unlock();
$('token').onkeydown = (e) => { if (e.key === 'Enter') unlock(); };
$('player-filter').oninput = renderPlayers;
document.querySelectorAll('.opsnav button').forEach((b) =>
  (b.onclick = () => selectPanel(b.dataset.panel)));

window.addEventListener('beforeunload', () => {
  if (timer) clearInterval(timer);
  token = null;
});
