// Shared live/debug helpers for the three visual node-graph editors (LogicEditor,
// ControlLogicEditor, SyntheticGraphEditor). When an editor's "Debug" toggle is on,
// the graph is evaluated online and each node shows a small value badge plus an
// "active" highlight, refreshed at human speed (~0.5 s). The per-node values come
// from different backends per editor (synthetic = server trace endpoint, panel =
// a client-side dry-run engine, control = a server WS push), but they all funnel
// into the same `DebugSnapshot` shape and the same rendering helpers below.
import { useState, useEffect, useRef } from 'preact/hooks';

/** Live state for a single node while debug mode is on. */
export interface NodeDebugState {
  value?: any;              // last computed value (scalar number, array, bool, string)
  type?: 'scalar' | 'array';
  approx?: boolean;         // value is a rough single-shot estimate (stateful DSP nodes)
  error?: string;           // evaluation error for this node
  active?: boolean;         // node fired / produced a value this tick
}

/** Per-node debug state, keyed by node id. */
export type DebugSnapshot = Map<string, NodeDebugState>;

/** Round-trips a value into a compact label for the on-node badge. */
export function formatBadge(s: NodeDebugState | undefined): string {
  if (!s) return '';
  if (s.error) return '!';
  const v = s.value;
  if (v == null) return '—';
  if (Array.isArray(v)) return `[${v.length}]`;
  if (typeof v === 'number') {
    if (!isFinite(v)) return String(v);
    if (Number.isInteger(v)) return String(v);
    return v.toPrecision(4).replace(/\.?0+$/, '');
  }
  if (typeof v === 'boolean') return v ? 'true' : 'false';
  const str = String(v);
  return str.length > 10 ? str.slice(0, 9) + '…' : str;
}

/** Full, untruncated value for the badge's `title` tooltip. */
export function badgeTitle(s: NodeDebugState | undefined): string {
  if (!s) return '';
  if (s.error) return s.error;
  const v = s.value;
  if (Array.isArray(v)) return `[${v.map(n => (typeof n === 'number' ? n.toPrecision(6) : String(n))).join(', ')}]`;
  return v == null ? '' : String(v);
}

/** Extra class(es) for a node element given its current debug state. */
export function nodeDebugClass(s: NodeDebugState | undefined): string {
  if (!s) return '';
  if (s.error) return 'flow-node-debug-error';
  if (s.active) return 'flow-node-active';
  return '';
}

/**
 * Polls `fetcher` every `intervalMs` while `enabled`, returning the latest
 * snapshot (empty map when disabled). Used by editors whose backend is a
 * request/response trace (synthetic) or any pull-based source. Overlapping
 * polls are prevented; a poll that resolves after the hook is disabled/unmounted
 * is discarded.
 */
export function useFlowDebug(
  enabled: boolean,
  fetcher: () => Promise<DebugSnapshot | null>,
  intervalMs = 500,
): DebugSnapshot {
  const [snap, setSnap] = useState<DebugSnapshot>(() => new Map());
  const fetcherRef = useRef(fetcher);
  fetcherRef.current = fetcher;

  useEffect(() => {
    if (!enabled) { setSnap(new Map()); return; }
    let alive = true;
    let inFlight = false;
    const tick = async () => {
      if (inFlight) return;
      inFlight = true;
      try {
        const next = await fetcherRef.current();
        if (alive && next) setSnap(next);
      } catch { /* transient; next tick retries */ }
      finally { inFlight = false; }
    };
    tick();
    const h = setInterval(tick, intervalMs);
    return () => { alive = false; clearInterval(h); };
  }, [enabled, intervalMs]);

  return snap;
}
