/* ============================================================================
   Fleet dashboard — live view over the SignalR /hub/status feed (Phase 4).
   Vanilla JS, no framework. Subscribes to the "fleet" group and re-renders the
   aggregate strip + filterable/sortable PLC table on every push.
   ========================================================================= */
'use strict';

(function () {
  // ── State ──────────────────────────────────────────────────────────────
  let latest = null;                 // last StatusResponse
  const prevPdu = new Map();         // plc name → { forwarded, t } for rate calc
  const rateByName = new Map();      // plc name → PDU/s
  const filter = { search: '', state: '', problemsOnly: false };
  const sort = { key: 'name', dir: 1 };

  // ── Helpers ────────────────────────────────────────────────────────────
  const $ = (id) => document.getElementById(id);

  function num(n) {
    if (n === null || n === undefined) return '—';
    return n.toLocaleString('en-US');
  }

  function ratio(hit, miss) {
    const total = (hit || 0) + (miss || 0);
    if (total === 0) return null;
    return Math.round((100 * hit) / total);
  }

  function exceptionTotal(b) {
    const e = b.exceptionsByCode || {};
    return (e.code01 || 0) + (e.code02 || 0) + (e.code03 || 0) +
           (e.code04 || 0) + (e.codeOther || 0);
  }

  function isProblem(plc) {
    return plc.listener.state === 'recovering'
        || exceptionTotal(plc.backend) > 0
        || (plc.backend.backendHeartbeatsFailed || 0) > 0
        || (plc.backend.connectsFailed || 0) > 0;
  }

  function stateChip(state) {
    const cls = state === 'bound' ? 'chip-ok'
              : state === 'recovering' ? 'chip-warn'
              : 'chip-idle';
    return `<span class="chip ${cls}">${state}</span>`;
  }

  function ratioCell(hit, miss) {
    const r = ratio(hit, miss);
    if (r === null) return '<span class="s-idle">—</span>';
    return `${r}%<span class="ratio-sub"> ${num(hit)}</span>`;
  }

  function keepaliveCell(b) {
    const sent = b.backendHeartbeatsSent || 0;
    const failed = b.backendHeartbeatsFailed || 0;
    if (sent === 0 && failed === 0) return '<span class="s-idle">—</span>';
    if (failed > 0) return `<span class="s-bad">${num(sent)} · ${failed} fail</span>`;
    return num(sent);
  }

  // ── Rate computation ───────────────────────────────────────────────────
  function updateRates(snapshot) {
    const now = performance.now();
    for (const plc of snapshot.plcs) {
      const cur = plc.pdus.forwarded;
      const prev = prevPdu.get(plc.name);
      if (prev && now > prev.t) {
        const dt = (now - prev.t) / 1000;
        const rate = Math.max(0, (cur - prev.forwarded) / dt);
        rateByName.set(plc.name, rate);
      }
      prevPdu.set(plc.name, { forwarded: cur, t: now });
    }
  }

  function rateOf(name) {
    return rateByName.has(name) ? rateByName.get(name) : null;
  }

  // ── Aggregate strip ────────────────────────────────────────────────────
  function renderAggregates(s) {
    $('ag-bound').textContent = s.listeners.bound;
    $('ag-configured').textContent = '/ ' + s.listeners.configured;

    let clients = 0, exceptions = 0, recovering = 0;
    let cacheHit = 0, cacheMiss = 0, fleetRate = 0;
    for (const plc of s.plcs) {
      clients += plc.clients.connected;
      exceptions += exceptionTotal(plc.backend);
      if (plc.listener.state === 'recovering') recovering++;
      cacheHit += plc.backend.cacheHitCount || 0;
      cacheMiss += plc.backend.cacheMissCount || 0;
      const r = rateOf(plc.name);
      if (r !== null) fleetRate += r;
    }

    $('ag-clients').textContent = num(clients);
    $('ag-pdurate').textContent = rateByName.size ? Math.round(fleetRate).toLocaleString('en-US') : '—';
    $('ag-recovering').textContent = recovering;
    $('ag-exceptions').textContent = num(exceptions);

    const cr = ratio(cacheHit, cacheMiss);
    $('ag-cache').textContent = cr === null ? '—' : cr + '%';

    $('ag-bound').className = s.listeners.bound < s.listeners.configured ? 's-warn' : '';
    $('ag-recovering-card').className = 'agg-card' + (recovering > 0 ? ' caution' : '');
    $('ag-exceptions-card').className = 'agg-card' + (exceptions > 0 ? ' alert' : '');
  }

  // ── Table ──────────────────────────────────────────────────────────────
  function sortKey(plc, key) {
    switch (key) {
      case 'name':       return plc.name.toLowerCase();
      case 'host':       return plc.host.toLowerCase();
      case 'state':      return plc.listener.state;
      case 'clients':    return plc.clients.connected;
      case 'pdurate':    return rateOf(plc.name) || 0;
      case 'rtt':        return plc.backend.lastRoundTripMs || 0;
      case 'exceptions': return exceptionTotal(plc.backend);
      case 'coalesce':   return ratio(plc.backend.coalescedHitCount, plc.backend.coalescedMissCount) || -1;
      case 'cache':      return ratio(plc.backend.cacheHitCount, plc.backend.cacheMissCount) || -1;
      case 'keepalive':  return plc.backend.backendHeartbeatsSent || 0;
      default:           return plc.name;
    }
  }

  function visiblePlcs(s) {
    let rows = s.plcs.slice();
    const q = filter.search.trim().toLowerCase();
    if (q) rows = rows.filter(p => p.name.toLowerCase().includes(q) || p.host.toLowerCase().includes(q));
    if (filter.state) rows = rows.filter(p => p.listener.state === filter.state);
    if (filter.problemsOnly) rows = rows.filter(isProblem);
    rows.sort((a, b) => {
      const ka = sortKey(a, sort.key), kb = sortKey(b, sort.key);
      if (ka < kb) return -1 * sort.dir;
      if (ka > kb) return 1 * sort.dir;
      return a.name.localeCompare(b.name);
    });
    return rows;
  }

  function renderTable(s) {
    const rows = visiblePlcs(s);
    const tbody = $('plc-rows');
    $('row-count').textContent = `${rows.length} of ${s.plcs.length}`;

    if (rows.length === 0) {
      tbody.innerHTML = '<tr><td colspan="10" class="empty-row">No PLCs match the current filter.</td></tr>';
      return;
    }

    tbody.innerHTML = rows.map(plc => {
      const b = plc.backend;
      const rate = rateOf(plc.name);
      const excn = exceptionTotal(b);
      const recovHint = plc.listener.state === 'recovering' && plc.listener.lastBindError
        ? ` title="${escapeAttr(plc.listener.lastBindError)}"` : '';
      return `<tr data-name="${escapeAttr(plc.name)}"${recovHint}>
        <td class="plc-name">${escapeHtml(plc.name)}</td>
        <td class="plc-host">${escapeHtml(plc.host)}:${plc.listenPort}</td>
        <td>${stateChip(plc.listener.state)}</td>
        <td class="num">${plc.clients.connected}</td>
        <td class="num">${rate === null ? '—' : Math.round(rate)}</td>
        <td class="num">${(b.lastRoundTripMs || 0).toFixed(1)}</td>
        <td class="num ${excn > 0 ? 's-bad' : ''}">${excn}</td>
        <td class="num">${ratioCell(b.coalescedHitCount, b.coalescedMissCount)}</td>
        <td class="num">${ratioCell(b.cacheHitCount, b.cacheMissCount)}</td>
        <td class="num">${keepaliveCell(b)}</td>
      </tr>`;
    }).join('');
  }

  function escapeHtml(s) {
    return String(s).replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
  }
  function escapeAttr(s) {
    return escapeHtml(s).replace(/"/g, '&quot;');
  }

  // ── Render orchestration ───────────────────────────────────────────────
  function render() {
    if (!latest) return;
    renderAggregates(latest);
    renderTable(latest);
  }

  function onSnapshot(s) {
    latest = s;
    updateRates(s);
    const up = s.service.uptimeSeconds;
    $('svc-meta').textContent =
      `v${s.service.version}  ·  up ${formatUptime(up)}  ·  reloads ${s.service.configReloadCount}`;
    render();
  }

  function formatUptime(sec) {
    const d = Math.floor(sec / 86400);
    const h = Math.floor((sec % 86400) / 3600);
    const m = Math.floor((sec % 3600) / 60);
    if (d > 0) return `${d}d ${h}h`;
    if (h > 0) return `${h}h ${m}m`;
    return `${m}m`;
  }

  // ── Connection-state pill ──────────────────────────────────────────────
  function setConn(state, text) {
    const pill = $('conn');
    pill.dataset.state = state;
    $('conn-text').textContent = text || state;
  }

  // ── Wire up filter / sort controls ─────────────────────────────────────
  function wireControls() {
    $('f-search').addEventListener('input', e => { filter.search = e.target.value; render(); });
    $('f-state').addEventListener('change', e => { filter.state = e.target.value; render(); });
    $('f-problems').addEventListener('change', e => { filter.problemsOnly = e.target.checked; render(); });

    document.querySelectorAll('th.sortable').forEach(th => {
      th.addEventListener('click', () => {
        const key = th.dataset.sort;
        if (sort.key === key) { sort.dir *= -1; }
        else { sort.key = key; sort.dir = 1; }
        document.querySelectorAll('th.sortable').forEach(h => h.classList.remove('sorted-asc', 'sorted-desc'));
        th.classList.add(sort.dir === 1 ? 'sorted-asc' : 'sorted-desc');
        render();
      });
    });

    $('plc-rows').addEventListener('click', e => {
      const tr = e.target.closest('tr[data-name]');
      if (!tr) return;
      window.open('/plc/' + encodeURIComponent(tr.dataset.name), '_blank');
    });
  }

  // ── SignalR ────────────────────────────────────────────────────────────
  function connect() {
    const connection = new signalR.HubConnectionBuilder()
      .withUrl('/hub/status')
      .withAutomaticReconnect([0, 1000, 2000, 5000, 10000])
      .build();

    connection.on('fleet', onSnapshot);

    connection.onreconnecting(() => setConn('connecting', 'reconnecting'));
    connection.onreconnected(() => { setConn('connected'); connection.invoke('SubscribeFleet'); });
    connection.onclose(() => setConn('disconnected', 'disconnected'));

    async function start() {
      try {
        setConn('connecting', 'connecting');
        await connection.start();
        await connection.invoke('SubscribeFleet');
        setConn('connected');
      } catch {
        setConn('disconnected', 'retrying');
        setTimeout(start, 3000);
      }
    }
    start();
  }

  // ── Boot ───────────────────────────────────────────────────────────────
  document.addEventListener('DOMContentLoaded', () => {
    wireControls();
    document.querySelector('th[data-sort="name"]').classList.add('sorted-asc');
    connect();
  });
})();
