// Overview dashboard
const { useMemo: useMemoOV } = React;

// Rolling 24-hour incident history. Primary source is the backend
// (/api/incident-history) so every LAN device sees the same timeline
// and the data survives browser-cache clears. localStorage is only a
// tiny resilience layer for the current device when the API is
// temporarily unreachable.
const INCIDENT_HISTORY_KEY = 'pulse.incidentHistory';
const HISTORY_HOURS = 24;
const HISTORY_REFRESH_MS = 60_000;

function _readLocalHistory() {
  try { return JSON.parse(localStorage.getItem(INCIDENT_HISTORY_KEY) || '[]'); }
  catch (_) { return []; }
}
function _writeLocalHistory(samples) {
  try { localStorage.setItem(INCIDENT_HISTORY_KEY, JSON.stringify(samples)); }
  catch (_) { /* quota / private mode */ }
}

function IncidentHistoryBars({ currentCount }) {
  // Fetch authoritative history from backend on mount + every minute.
  // Fall back to whatever was last cached in localStorage if the fetch
  // fails (we never want empty bars just because a probe request took
  // a beat too long).
  const [samples, setSamples] = React.useState(() => _readLocalHistory());

  React.useEffect(() => {
    let cancelled = false;
    async function pull() {
      try {
        const r = await fetch('/api/incident-history?hours=24', { cache: 'no-store' });
        if (!r.ok) throw new Error(`http ${r.status}`);
        const body = await r.json();
        if (cancelled) return;
        const s = Array.isArray(body.samples) ? body.samples : [];
        setSamples(s);
        _writeLocalHistory(s);
      } catch (_) {
        // keep the last good cached value
      }
    }
    pull();
    const t = setInterval(pull, HISTORY_REFRESH_MS);
    return () => { cancelled = true; clearInterval(t); };
  }, []);

  // Bucket samples into HISTORY_HOURS hourly slots. Index 23 is "now";
  // index 0 is "23h ago".
  const buckets = React.useMemo(() => {
    const now = Date.now();
    const slots = Array.from({length: HISTORY_HOURS}, () => ({ max: null, samples: 0 }));
    for (const d of samples) {
      // Accept both the backend schema ({ts, down, crit, off, ...}) and the
      // legacy localStorage schema ({ts, count}) so upgrading a tab doesn't
      // blank the chart.
      const count = (typeof d.down === 'number') ? d.down : (d.count ?? 0);
      const hoursAgo = Math.floor((now - d.ts) / (3600 * 1000));
      const idx = HISTORY_HOURS - 1 - hoursAgo;
      if (idx >= 0 && idx < HISTORY_HOURS) {
        slots[idx].max = Math.max(slots[idx].max ?? 0, count);
        slots[idx].samples++;
      }
    }
    // The final slot always reflects the current live count so the
    // most recent bar updates immediately without waiting for a sample.
    slots[HISTORY_HOURS - 1].max = Math.max(slots[HISTORY_HOURS - 1].max ?? 0, currentCount);
    return slots;
  }, [samples, currentCount]);

  const maxCount = Math.max(1, ...buckets.map(b => b.max || 0));

  return (
    <div className="spark" style={{display:'flex', alignItems:'flex-end', gap:2}}>
      {buckets.map((b, i) => {
        const hoursAgo = HISTORY_HOURS - 1 - i;
        const hasData = b.max !== null;
        const count = b.max || 0;
        const h = hasData ? 4 + (count / maxCount) * 22 : 3;
        const color = count > 0 ? 'var(--crit)' : hasData ? 'var(--border-3)' : 'var(--border)';
        const when = hoursAgo === 0 ? 'now' : `${hoursAgo}h ago`;
        const label = hasData
          ? `${when} · peak ${count} incident${count === 1 ? '' : 's'}`
          : `${when} · no data yet`;
        return <div key={i} title={label}
          style={{flex:1, height: h, background: color, cursor: 'help', transition: 'height 200ms'}} />;
      })}
    </div>
  );
}

// Auto-scale bytes-per-second to a human-friendly unit. Uses binary
// multiples (KB=1024) like most Linux tools do for network rates.
function _fmtRate(bps) {
  const v = Math.max(0, bps || 0);
  if (v < 1024)               return { val: v.toFixed(0),              unit: 'B/s'  };
  if (v < 1024 * 1024)        return { val: (v / 1024).toFixed(1),     unit: 'KB/s' };
  if (v < 1024 * 1024 * 1024) return { val: (v / 1048576).toFixed(2),  unit: 'MB/s' };
  return { val: (v / 1073741824).toFixed(2), unit: 'GB/s' };
}

function NetThroughputKPI() {
  const [snap, setSnap] = React.useState(null);
  React.useEffect(() => {
    let cancelled = false;
    async function pull() {
      try {
        const r = await fetch('/api/netstats', { cache: 'no-store' });
        if (!r.ok) return;
        const body = await r.json();
        if (!cancelled) setSnap(body);
      } catch (_) { /* keep last */ }
    }
    pull();
    const t = setInterval(pull, 5000);
    return () => { cancelled = true; clearInterval(t); };
  }, []);

  const history = snap?.history || [];
  const rx = snap?.totalRxPerSec || 0;
  const tx = snap?.totalTxPerSec || 0;
  const total = rx + tx;
  const tot = _fmtRate(total);
  const rxf = _fmtRate(rx);
  const txf = _fmtRate(tx);
  const ifaceNames = Object.keys(snap?.interfaces || {});
  const activeIfaces = ifaceNames.filter(n => {
    const i = snap.interfaces[n];
    return (i.rxPerSec + i.txPerSec) > 0;
  });
  const ifaceHint = activeIfaces.length
    ? activeIfaces.join(', ')
    : (snap && snap.ready ? 'idle' : 'sampling…');

  // Sparkline values = total throughput per sample in KB/s for readable scale.
  const values = history.map(h => (h.rxPerSec + h.txPerSec) / 1024);

  return (
    <div className="kpi" title={`host machine NIC throughput · ${ifaceHint}`}>
      <div className="label">Net throughput · host</div>
      <div className="val">
        {snap && snap.ready ? tot.val : '—'}
        <span style={{fontSize:14, color:'var(--text-faint)'}}> {snap && snap.ready ? tot.unit : ''}</span>
      </div>
      <div className="delta" style={{color: 'var(--text-dim)'}}>
        ↓ rx {rxf.val} {rxf.unit} · ↑ tx {txf.val} {txf.unit}
      </div>
      <div className="spark">
        {values.length >= 2
          ? <Sparkline values={values} color="var(--accent)" />
          : <div style={{fontSize: 10, color:'var(--text-mute)', paddingTop: 8}}>collecting samples…</div>}
      </div>
    </div>
  );
}

function Sparkline({ values, color = 'var(--accent)', fill = true, height = 30, width = 160 }) {
  if (!values || !values.length) return null;
  const min = Math.min(...values);
  const max = Math.max(...values);
  const range = max - min || 1;
  const n = values.length;
  const pts = values.map((v, i) => {
    const x = (i / (n - 1)) * width;
    const y = height - ((v - min) / range) * (height - 4) - 2;
    return `${x.toFixed(1)},${y.toFixed(1)}`;
  }).join(' ');
  const last = values[n - 1];
  const lastX = width;
  const lastY = height - ((last - min) / range) * (height - 4) - 2;
  return (
    <svg width={width} height={height} style={{display:'block', overflow:'visible'}}>
      {fill && (
        <polyline
          points={`0,${height} ${pts} ${width},${height}`}
          fill={color} opacity="0.12" stroke="none"
        />
      )}
      <polyline points={pts} fill="none" stroke={color} strokeWidth="1.25" />
      <circle cx={lastX} cy={lastY} r="2" fill={color} />
    </svg>
  );
}

// One group's section in the status grid. Owns its own drop-zone state
// (drop-target / drop-same highlight). Drag-enabled tiles and drop zones
// only render if `canDrag` is true — i.e. the viewer is an admin.
function StatusGroupSection({ group, nodes, onSelect, canDrag, onGroupMove }) {
  const [isOver, setIsOver] = React.useState(false);
  const dnd = window.useDragState ? window.useDragState() : { activeNodeId: null, sourceGroup: null };
  const dragging = !!dnd.activeNodeId;
  const sameGroup = dragging && dnd.sourceGroup === group;

  function onDragOver(e) {
    if (!canDrag || !dragging) return;
    e.preventDefault();                 // required to allow drop
    e.dataTransfer.dropEffect = sameGroup ? 'none' : 'move';
    if (!isOver) setIsOver(true);
  }
  function onDragLeave(e) {
    // Only clear when leaving the container, not when hovering over a child.
    if (e.currentTarget.contains(e.relatedTarget)) return;
    if (isOver) setIsOver(false);
  }
  function onDrop(e) {
    if (!canDrag || !dragging) return;
    e.preventDefault();
    setIsOver(false);
    const nodeId = e.dataTransfer.getData('text/plain');
    if (!nodeId) return;
    if (dnd.sourceGroup === group) return;   // same-group drop = no-op
    if (typeof onGroupMove === 'function') onGroupMove(nodeId, group);
  }

  const hint = (dragging && !sameGroup && isOver)
    ? <span className="chip accent" style={{fontSize: 9, marginLeft: 8}}>drop to move here</span>
    : (dragging && sameGroup)
    ? <span className="chip" style={{fontSize: 9, marginLeft: 8, background: 'var(--surface-3)', color: 'var(--text-mute)'}}>same group</span>
    : null;

  const classes = [
    'heatmap-group',
    (canDrag && dragging && !sameGroup && isOver) ? 'drop-target' : '',
    (canDrag && dragging && sameGroup) ? 'drop-same' : '',
  ].filter(Boolean).join(' ');

  return (
    <div className={classes} style={{ marginBottom: 10, padding: '2px 4px', borderRadius: 2 }}
         onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}>
      <div style={{display:'flex', fontSize:10, color:'var(--text-faint)', letterSpacing:'0.1em', textTransform:'uppercase', marginBottom: 4}}>
        <span style={{width: 110}}>{group}</span>
        <span>{nodes.filter(n => n.status==='ok').length}/{nodes.length} up</span>
        {hint}
      </div>
      <div style={{display:'grid', gridTemplateColumns:'repeat(12, 1fr)', gap: 3}}>
        {nodes.map(n => {
          const isThisDragging = dnd.activeNodeId === n.id;
          const anom = n.anomaly && n.anomaly.active;
          const tileTitle = anom
            ? `${n.hostname} · ${n.status} · ${n.latency.toFixed(0)}ms · ANOMALY z=${n.anomaly.z.toFixed(1)}σ (baseline ${Math.round(n.anomaly.baselineMean)}ms)${canDrag ? ' · drag to another group to move' : ''}`
            : `${n.hostname} · ${n.status} · ${n.latency.toFixed(0)}ms${canDrag ? ' · drag to another group to move' : ''}`;
          return (
            <button key={n.id}
              className={`heatmap-tile ${isThisDragging ? 'dragging' : ''} ${anom ? 'anomaly' : ''}`}
              draggable={canDrag}
              onDragStart={canDrag ? (e) => {
                e.dataTransfer.effectAllowed = 'move';
                e.dataTransfer.setData('text/plain', n.id);
                window.setDragActive && window.setDragActive(n.id, n.group);
              } : undefined}
              onDragEnd={canDrag ? () => {
                window.clearDragActive && window.clearDragActive();
              } : undefined}
              onClick={() => onSelect(n)}
              title={tileTitle}
              style={{aspectRatio: '2',
                border: anom ? '1.5px solid var(--warn)' : '1px solid transparent',
                borderRadius: 1,
                background: n.status === 'ok' ? 'oklch(0.72 0.16 150 / 0.7)'
                  : n.status === 'crit' ? 'oklch(0.66 0.22 27 / 0.85)'
                  : n.status === 'warn' ? 'oklch(0.75 0.15 85 / 0.75)'
                  : n.status === 'maint' ? 'oklch(0.75 0.15 85 / 0.75)'
                  : 'oklch(0.38 0 0)',
                cursor: canDrag ? 'grab' : 'pointer',
                position: 'relative',
                display:'flex', alignItems:'center', justifyContent:'center',
                color: n.status === 'off' ? 'var(--text-faint)' : '#071018', fontSize: 9, fontWeight: 600,
                animation: n.status === 'crit'
                  ? 'flash 1s ease-in-out infinite'
                  : anom ? 'flash 2s ease-in-out infinite' : 'none',
                overflow: 'hidden',
                opacity: isThisDragging ? 0.35 : 1,
                boxShadow: anom ? '0 0 0 2px oklch(0.75 0.15 85 / 0.35)' : 'none',
              }}>
              {n.displayName.split('.')[0].slice(0,6)}
            </button>
          );
        })}
      </div>
    </div>
  );
}

function Overview({ nodes, alerts, onSelect, currentUser, onGroupMove }) {
  const stats = useMemoOV(() => {
    const live = nodes.filter(n => n.status === 'ok' || n.status === 'maint');
    const avg = live.reduce((s, n) => s + n.latency, 0) / (live.length || 1);
    const sorted = [...live.map(n => n.latency)].sort((a,b)=>a-b);
    const p99 = sorted[Math.floor(sorted.length * 0.99)] || 0;
    const netTotal = nodes.reduce((s, n) => s + n.netIn + n.netOut, 0);
    const critCount = nodes.filter(n => n.status === 'crit').length;
    const offCount = nodes.filter(n => n.status === 'off').length;
    const anomalyCount = nodes.filter(n => n.anomaly && n.anomaly.active).length;
    const uptime = ((nodes.length - offCount - critCount) / nodes.length) * 100;
    const histLen = nodes[0]?.latencyHist.length || 0;
    const fleetLat = [];
    const fleetNet = [];
    for (let i = 0; i < histLen; i++) {
      let ls = 0, lc = 0, ns = 0;
      for (const n of nodes) {
        if (n.status !== 'off') { ls += n.latencyHist[i]; lc++; }
        ns += n.netHist[i];
      }
      fleetLat.push(ls / (lc || 1));
      fleetNet.push(ns);
    }
    return { avg, p99, netTotal, critCount, offCount, anomalyCount, uptime, fleetLat, fleetNet };
  }, [nodes]);

  const byGroup = useMemoOV(() => {
    const m = {};
    for (const n of nodes) {
      if (!m[n.group]) m[n.group] = { total: 0, ok: 0, crit: 0, off: 0, maint: 0, lat: 0 };
      m[n.group].total++;
      m[n.group][n.status]++;
      if (n.status !== 'off') m[n.group].lat += n.latency;
    }
    return m;
  }, [nodes]);

  const recentAlerts = alerts.slice(0, 5);

  return (
    <div className="overview">
      <div className="kpi">
        <div className="label">Availability</div>
        <div className="val">{stats.uptime.toFixed(2)}<span style={{fontSize:14, color:'var(--text-faint)'}}>%</span></div>
        <div className={`delta ${stats.offCount === 0 && stats.critCount === 0 ? 'up' : 'down'}`}>
          {stats.offCount === 0 && stats.critCount === 0 ? '↑ all endpoints up' : `↓ ${stats.offCount + stats.critCount} down`}
        </div>
        <div className="spark"><Sparkline values={stats.fleetLat.map(v => 100 - Math.min(99, v/20))} color="var(--ok)" /></div>
      </div>
      <div className="kpi">
        <div className="label">Avg response time</div>
        <div className="val">{stats.avg.toFixed(0)}<span style={{fontSize:14, color:'var(--text-faint)'}}> ms</span></div>
        <div className="delta">p99 {stats.p99.toFixed(0)}ms · sla &lt; 800ms</div>
        <div className="spark"><Sparkline values={stats.fleetLat} color="var(--accent)" /></div>
      </div>
      <NetThroughputKPI />
      <div className="kpi">
        <div className="label">Open incidents</div>
        <div className="val" style={{color: stats.critCount ? 'var(--crit)' : 'var(--text)'}}>{stats.critCount + stats.offCount}</div>
        <div className="delta down">
          {alerts.filter(a=>a.severity==='crit'&&!a.ack).length} unacked · {alerts.filter(a=>a.severity==='warn').length} warnings
          {stats.anomalyCount > 0 && (
            <span style={{marginLeft: 8, color: 'var(--warn)'}}>
              · {stats.anomalyCount} anomal{stats.anomalyCount === 1 ? 'y' : 'ies'}
            </span>
          )}
        </div>
        <IncidentHistoryBars currentCount={stats.critCount + stats.offCount} />
      </div>

      {/* Heatmap grouped by category */}
      <div className="card grid-3">
        <div className="card-head">
          <h3>Endpoints · status grid</h3>
          <span className="sub">{nodes.length} endpoints · live</span>
          <span className="spacer" />
          <div className="legend">
            <span className="legend-item"><span className="dot ok"/> up</span>
            <span className="legend-item"><span className="dot crit"/> down</span>
            <span className="legend-item"><span className="dot maint"/> maint</span>
            <span className="legend-item"><span className="dot off"/> offline</span>
          </div>
        </div>
        <div className="card-body">
          {window.GROUPS.map(group => {
            const g = nodes.filter(n => n.group === group);
            if (!g.length) return null;
            return <StatusGroupSection key={group} group={group} nodes={g}
                      onSelect={onSelect}
                      canDrag={!!(currentUser && currentUser.isAdmin)}
                      onGroupMove={onGroupMove} />;
          })}
        </div>
      </div>

      {/* Groups breakdown */}
      <div className="card">
        <div className="card-head"><h3>By group</h3></div>
        <div className="card-body" style={{padding:0}}>
          {Object.entries(byGroup).map(([group, s]) => (
            <div key={group} style={{display:'grid', gridTemplateColumns:'90px 1fr auto', gap: 10, padding:'8px 12px', borderBottom:'1px solid var(--border)', fontSize:12, alignItems:'center'}}>
              <span style={{color:'var(--text-dim)', fontWeight:600, fontSize:10, letterSpacing:'0.08em'}}>{group}</span>
              <div style={{display:'flex', height: 8, borderRadius:1, overflow:'hidden', background:'var(--surface-3)'}}>
                {s.ok > 0 && <div style={{flex: s.ok, background:'var(--ok)'}} />}
                {s.maint > 0 && <div style={{flex: s.maint, background:'var(--warn)'}} />}
                {s.crit > 0 && <div style={{flex: s.crit, background:'var(--crit)'}} />}
                {s.off > 0 && <div style={{flex: s.off, background:'var(--off)'}} />}
              </div>
              <span style={{fontSize:11, color:'var(--text-faint)'}}>{s.ok}/{s.total}</span>
            </div>
          ))}
        </div>
      </div>

      {/* Latency distribution */}
      <div className="card grid-2">
        <div className="card-head">
          <h3>Response time · 60s window</h3>
          <span className="sub">p50 / p95 / p99</span>
        </div>
        <div className="card-body">
          <LatencyChart nodes={nodes} />
        </div>
      </div>

      {/* Alerts preview */}
      <div className="card grid-2">
        <div className="card-head"><h3>Recent alerts</h3><span className="spacer"/><span className="sub">{alerts.length} total</span></div>
        <div>
          {recentAlerts.map(a => {
            const node = nodes.find(n => n.id === a.node);
            return (
              <div key={a.id} className="alert-row" style={{gridTemplateColumns:'80px 70px 1fr auto'}}>
                <span className="ts">{window.fmtRel(a.ts)}</span>
                <span className={`chip ${a.severity === 'crit' ? 'crit' : a.severity === 'warn' ? 'warn' : 'accent'}`}>{a.severity}</span>
                <span className="msg">
                  {a.title}
                  <small>{node?.hostname} · {a.body}</small>
                </span>
                <span style={{color: a.ack ? 'var(--text-mute)' : 'var(--accent)', fontSize:10, letterSpacing:'0.08em', textTransform:'uppercase'}}>
                  {a.ack ? 'ack' : 'new'}
                </span>
              </div>
            );
          })}
        </div>
      </div>

      {/* Events timeline */}
      <div className="card grid-4">
        <div className="card-head"><h3>Events · last 60 minutes</h3><span className="spacer"/><span className="sub">{String(new Date().getHours()).padStart(2,'0')}:00 local</span></div>
        <div className="card-body">
          <EventsTimeline alerts={alerts} />
        </div>
      </div>
    </div>
  );
}

function LatencyChart({ nodes }) {
  const histLen = nodes[0]?.latencyHist.length || 0;
  const p50 = [], p95 = [], p99 = [];
  for (let i = 0; i < histLen; i++) {
    const vals = nodes.filter(n => n.status !== 'off').map(n => n.latencyHist[i]).sort((a,b)=>a-b);
    p50.push(vals[Math.floor(vals.length * 0.5)] || 0);
    p95.push(vals[Math.floor(vals.length * 0.95)] || 0);
    p99.push(vals[Math.floor(vals.length * 0.99)] || 0);
  }
  const W = 560, H = 160;
  const all = [...p50, ...p95, ...p99];
  const max = Math.max(...all, 100);
  const pts = (arr) => arr.map((v, i) => `${(i/(arr.length-1))*W},${H - (v/max)*(H-10) - 4}`).join(' ');

  return (
    <svg viewBox={`0 0 ${W} ${H + 20}`} width="100%" style={{display:'block', overflow:'visible'}}>
      {[0, 0.25, 0.5, 0.75, 1].map(p => (
        <line key={p} x1="0" x2={W} y1={H - p*(H-10) - 4} y2={H - p*(H-10) - 4} stroke="var(--border)" strokeDasharray="2 3" />
      ))}
      {[0.25, 0.5, 0.75, 1].map(p => (
        <text key={p} x="2" y={H - p*(H-10) - 6} fill="var(--text-mute)" fontSize="9" fontFamily="var(--mono)">
          {Math.round(max * p)}ms
        </text>
      ))}
      <polyline points={`0,${H} ${pts(p99)} ${W},${H}`} fill="var(--crit)" opacity="0.12" />
      <polyline points={pts(p99)} fill="none" stroke="var(--crit)" strokeWidth="1.25" />
      <polyline points={pts(p95)} fill="none" stroke="var(--warn)" strokeWidth="1.25" />
      <polyline points={pts(p50)} fill="none" stroke="var(--accent)" strokeWidth="1.25" />
      <g fontFamily="var(--mono)" fontSize="10">
        <circle cx="10" cy={H+14} r="3" fill="var(--accent)"/><text x="18" y={H+18} fill="var(--text-dim)">p50</text>
        <circle cx="60" cy={H+14} r="3" fill="var(--warn)"/><text x="68" y={H+18} fill="var(--text-dim)">p95</text>
        <circle cx="110" cy={H+14} r="3" fill="var(--crit)"/><text x="118" y={H+18} fill="var(--text-dim)">p99</text>
      </g>
    </svg>
  );
}

function EventsTimeline({ alerts }) {
  const now = Date.now();
  const windowMs = 60 * 60 * 1000;
  const W = 1000, H = 60;
  return (
    <svg viewBox={`0 0 ${W} ${H + 20}`} width="100%" style={{display:'block', overflow:'visible'}}>
      <line x1="0" x2={W} y1={H/2} y2={H/2} stroke="var(--border-2)" />
      {Array.from({length: 13}).map((_, i) => {
        const x = (i / 12) * W;
        return (
          <g key={i}>
            <line x1={x} x2={x} y1={H/2 - 4} y2={H/2 + 4} stroke="var(--border-3)" />
            <text x={x} y={H+14} fill="var(--text-mute)" fontSize="10" fontFamily="var(--mono)" textAnchor="middle">
              -{60 - i*5}m
            </text>
          </g>
        );
      })}
      {alerts.map(a => {
        const age = now - a.ts;
        if (age > windowMs) return null;
        const x = W - (age / windowMs) * W;
        const color = a.severity === 'crit' ? 'var(--crit)' : a.severity === 'warn' ? 'var(--warn)' : 'var(--accent)';
        return (
          <g key={a.id}>
            <line x1={x} x2={x} y1={H/2 - 18} y2={H/2 + 18} stroke={color} strokeWidth="1" opacity="0.6" />
            <circle cx={x} cy={H/2} r="4" fill={color} />
            <text x={x + 6} y={H/2 - 8} fill="var(--text-dim)" fontSize="10" fontFamily="var(--mono)">{a.title.slice(0, 26)}</text>
          </g>
        );
      })}
      <line x1={W-1} x2={W-1} y1="0" y2={H} stroke="var(--accent)" />
      <text x={W-4} y="10" fill="var(--accent)" fontSize="10" fontFamily="var(--mono)" textAnchor="end">NOW</text>
    </svg>
  );
}

Object.assign(window, { Overview, Sparkline });
