/* ============================================================================
   Connection-detail page — live per-PLC view over /hub/status (Phase 5).
   Subscribes to one PLC's "plc" group; renders grouped counter cards and the
   real-time debug view (per-tag PLC-side raw BCD vs client-side decoded value).
   ========================================================================= */
'use strict';

(function () {
  // ── PLC name from the URL path: /plc/{name} ────────────────────────────
  const plcName = decodeURIComponent(location.pathname.replace(/^\/plc\//, ''));

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

  document.title = `mbproxy — ${plcName}`;
  $('crumb-name').textContent = plcName;
  $('plc-name').textContent = plcName;

  // ── Helpers ────────────────────────────────────────────────────────────
  function num(n) {
    if (n === null || n === undefined) return '—';
    return n.toLocaleString('en-US');
  }
  function escapeHtml(s) {
    return String(s).replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
  }
  function hex4(n) { return '0x' + (n & 0xffff).toString(16).toUpperCase().padStart(4, '0'); }

  function formatAge(sec) {
    if (sec === null || sec === undefined) return '—';
    if (sec < 1) return 'now';
    if (sec < 60) return sec.toFixed(1) + 's';
    const m = Math.floor(sec / 60);
    if (m < 60) return `${m}m ${Math.floor(sec % 60)}s`;
    return `${Math.floor(m / 60)}h ${m % 60}m`;
  }
  function shortTime(iso) {
    try { return new Date(iso).toLocaleTimeString('en-US', { hour12: false }); }
    catch { return iso; }
  }

  // ── Card renderer ──────────────────────────────────────────────────────
  // rows: array of [key, value, optionalClass]; extra: raw HTML appended.
  function card(title, rows, extra) {
    const body = rows.map(([k, v, cls]) =>
      `<div class="kv"><span class="k">${escapeHtml(k)}</span>` +
      `<span class="v ${cls || ''}">${v}</span></div>`).join('');
    return `<div class="metric-card">
      <div class="panel-head">${escapeHtml(title)}</div>
      ${body}${extra || ''}
    </div>`;
  }

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

  // ── Render: PLC counters ───────────────────────────────────────────────
  function renderPlc(plc) {
    $('notice').hidden = true;
    $('cards').hidden = false;

    $('plc-sub').textContent = `${plc.host}:${plc.listenPort}`;
    $('plc-state').innerHTML = stateChip(plc.listener.state);

    const b = plc.backend, p = plc.pdus, l = plc.listener;
    const e = b.exceptionsByCode || {};
    const excnTotal = (e.code01 || 0) + (e.code02 || 0) + (e.code03 || 0) +
                      (e.code04 || 0) + (e.codeOther || 0);

    const cards = [];

    // Listener
    cards.push(card('Listener', [
      ['State', stateChip(l.state)],
      ['Recovery attempts', num(l.recoveryAttempts), l.recoveryAttempts > 0 ? 'warn' : ''],
      ['Last bind error', l.lastBindError ? escapeHtml(l.lastBindError) : '—',
        l.lastBindError ? 'bad' : ''],
    ]));

    // Clients
    const clientLines = (plc.clients.remoteEndpoints || []).map(c =>
      `<div class="client-line">${escapeHtml(c.remote)}` +
      `<span class="pdu"> · ${num(c.pdusForwarded)} PDUs · since ${shortTime(c.connectedAtUtc)}</span></div>`
    ).join('');
    cards.push(card('Clients', [
      ['Connected', num(plc.clients.connected)],
    ], clientLines));

    // PDU traffic
    cards.push(card('PDU traffic', [
      ['Forwarded', num(p.forwarded)],
      ['FC03 read HR', num(p.byFc.fc03)],
      ['FC04 read IR', num(p.byFc.fc04)],
      ['FC06 write single', num(p.byFc.fc06)],
      ['FC16 write multiple', num(p.byFc.fc16)],
      ['Other FCs', num(p.byFc.other)],
      ['BCD slots rewritten', num(p.rewrittenSlots)],
      ['Partial-BCD warnings', num(p.partialBcdWarnings), p.partialBcdWarnings > 0 ? 'warn' : ''],
      ['Invalid-BCD warnings', num(p.invalidBcdWarnings), p.invalidBcdWarnings > 0 ? 'warn' : ''],
    ]));

    // Backend health
    cards.push(card('Backend health', [
      ['Connects ok', num(b.connectsSuccess)],
      ['Connects failed', num(b.connectsFailed), b.connectsFailed > 0 ? 'bad' : ''],
      ['Round-trip (EWMA)', (b.lastRoundTripMs || 0).toFixed(1) + ' ms'],
      ['Exceptions total', num(excnTotal), excnTotal > 0 ? 'bad' : ''],
      ['· 01 illegal function', num(e.code01)],
      ['· 02 illegal address', num(e.code02)],
      ['· 03 illegal value', num(e.code03)],
      ['· 04 device failure', num(e.code04)],
      ['· other', num(e.codeOther)],
    ]));

    // Multiplexer
    cards.push(card('Multiplexer', [
      ['In flight', num(b.inFlight)],
      ['Max in flight', num(b.maxInFlight)],
      ['TxId wraps', num(b.txIdWraps)],
      ['Disconnect cascades', num(b.disconnectCascades), b.disconnectCascades > 0 ? 'warn' : ''],
      ['Queue depth', num(b.queueDepth), b.queueDepth > 0 ? 'warn' : ''],
    ]));

    // Coalescing
    cards.push(card('Read coalescing', [
      ['Hits', num(b.coalescedHitCount)],
      ['Misses', num(b.coalescedMissCount)],
      ['Hit ratio', ratioText(b.coalescedHitCount, b.coalescedMissCount)],
      ['Resp. to dead upstream', num(b.coalescedResponseToDeadUpstream)],
    ]));

    // Cache
    cards.push(card('Response cache', [
      ['Hits', num(b.cacheHitCount)],
      ['Misses', num(b.cacheMissCount)],
      ['Hit ratio', ratioText(b.cacheHitCount, b.cacheMissCount)],
      ['Invalidations', num(b.cacheInvalidations)],
      ['Entries', num(b.cacheEntryCount)],
      ['Approx. bytes', num(b.cacheBytes)],
    ]));

    // Keepalive
    cards.push(card('Keepalive', [
      ['Heartbeats sent', num(b.backendHeartbeatsSent)],
      ['Heartbeats failed', num(b.backendHeartbeatsFailed), b.backendHeartbeatsFailed > 0 ? 'bad' : ''],
      ['Idle disconnects', num(b.backendIdleDisconnects), b.backendIdleDisconnects > 0 ? 'warn' : ''],
    ]));

    // Bytes
    cards.push(card('Bytes', [
      ['Upstream in', num(plc.bytes.upstreamIn)],
      ['Upstream out', num(plc.bytes.upstreamOut)],
    ]));

    $('cards').innerHTML = cards.join('');
  }

  function ratioText(hit, miss) {
    const total = (hit || 0) + (miss || 0);
    if (total === 0) return '—';
    return Math.round((100 * hit) / total) + '%';
  }

  // ── Render: PLC removed by hot-reload ──────────────────────────────────
  function renderMissing() {
    $('notice').hidden = false;
    $('cards').hidden = true;
    $('cards').innerHTML = '';
    $('plc-sub').textContent = 'not configured';
    $('plc-state').innerHTML = '<span class="chip chip-idle">removed</span>';
  }

  // ── Render: debug view ─────────────────────────────────────────────────
  function renderDebug(debug) {
    const cap = $('capture-state');
    cap.dataset.armed = String(debug.captureArmed);
    cap.textContent = debug.captureArmed ? 'capture armed' : 'capture idle';

    const tbody = $('debug-rows');
    if (!debug.tags || debug.tags.length === 0) {
      tbody.innerHTML = '<tr><td colspan="6" class="empty-row">No BCD tags configured for this PLC.</td></tr>';
      return;
    }

    tbody.innerHTML = debug.tags.map(t => {
      if (!t.hasValue) {
        return `<tr class="no-traffic">
          <td>${t.address} <span class="ratio-sub">${hex4(t.address)}</span></td>
          <td>${t.width}-bit</td>
          <td colspan="3">no traffic yet</td>
          <td class="num">—</td>
        </tr>`;
      }
      const dirCls = t.direction === 'write' ? 'dir-write' : 'dir-read';
      const stale = (t.ageSeconds || 0) > 30 ? ' stale' : '';
      return `<tr class="${stale.trim()}">
        <td>${t.address} <span class="ratio-sub">${hex4(t.address)}</span></td>
        <td>${t.width}-bit</td>
        <td><span class="dir-tag ${dirCls}">${t.direction}</span></td>
        <td class="num raw">${escapeHtml(t.rawHex)}</td>
        <td class="num dec">${num(t.decodedValue)}</td>
        <td class="num">${formatAge(t.ageSeconds)}</td>
      </tr>`;
    }).join('');
  }

  // ── Snapshot handler ───────────────────────────────────────────────────
  function onDetail(detail) {
    if (detail.plc) renderPlc(detail.plc);
    else renderMissing();
    renderDebug(detail.debug || { captureArmed: false, tags: [] });
  }

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

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

    connection.on('plc', onDetail);

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

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

  document.addEventListener('DOMContentLoaded', connect);
})();
