// ─── Evidence ─── read-only node/relationship view of what EPFC activates for
// a query (GET /api/v1/context, packet epfc.context.v1).
//
// Rules this screen keeps:
//   • Read-only. It never creates, flags or completes an LF task, and never
//     writes project state.
//   • The read token is device-local: localStorage under a key WITHOUT the
//     "lf." prefix, because every sync and export path in this app (the
//     OneDrive snapshot in lib-filesync, the undo snapshots and account
//     switch in index.html, the Supabase hydrate in lib-sync) filters on
//     keys starting with "lf.". So the token never reaches Supabase, the
//     OneDrive file the office build reads, or any export.
//   • The token is only ever sent to the approved local API (127.0.0.1 /
//     localhost), never to an arbitrary URL someone typed.
//   • Only real edges are drawn. Nothing about relevance, weight or "why" is
//     invented: a literal-source-path edge means the text cites that file, not
//     that the passage proves anything.
//
// Graph maths lives in lib-evidence.js (pure, unit-tested); this file draws.
(function () {
  const { useState, useEffect, useRef, useCallback, useMemo } = React;
  const EV = window.LFEvidence;

  const REALM_COLOR = { wrh: '#3B82F6', health: '#22C55E', case: '#F97316', lf: '#8B5CF6', other: '#64748B' };
  const TYPE_GLYPH = { document: '▤', chunk: '▦', entity: '◆', task: '✓', event: '◷', other: '•' };
  const EDGE_NOTE = {
    'literal-source-path': 'This text names that file. It does not prove the passage.',
  };

  // ── The canvas ───────────────────────────────────────────────────────────
  function EvidenceGraph({ graph, onOpenSource, onCopyPath }) {
    const wrapRef = useRef(null);
    const [view, setView] = useState({ zoom: 1, pan: { x: 0, y: 0 } });
    const [hover, setHover] = useState(null);      // { kind:'node'|'edge', id }
    const [pinned, setPinned] = useState(null);    // node id, kept after a click
    const [panning, setPanning] = useState(false);
    const panRef = useRef(null);

    const laid = useMemo(() => EV.layoutGraph(graph.nodes, graph.edges), [graph]);

    // Fit the graph whenever a new one arrives.
    useEffect(() => {
      const el = wrapRef.current;
      if (!el) return;
      const r = el.getBoundingClientRect();
      setView(EV.fitTransform(laid.bounds, { width: r.width, height: r.height }));
      setPinned(null); setHover(null);
    }, [laid]);

    const toScreen = useCallback((p) => ({ x: p.x * view.zoom + view.pan.x, y: p.y * view.zoom + view.pan.y }), [view]);

    const onWheel = (e) => {
      e.preventDefault();
      const el = wrapRef.current; if (!el) return;
      const r = el.getBoundingClientRect();
      const mx = e.clientX - r.left, my = e.clientY - r.top;
      setView((v) => {
        const z = Math.max(0.2, Math.min(2.5, v.zoom * (e.deltaY < 0 ? 1.12 : 1 / 1.12)));
        const k = z / v.zoom;
        return { zoom: z, pan: { x: mx - (mx - v.pan.x) * k, y: my - (my - v.pan.y) * k } };
      });
    };
    const onMouseDown = (e) => {
      if (e.button !== 0) return;
      panRef.current = { sx: e.clientX, sy: e.clientY, pan: view.pan };
      setPanning(true);
    };
    useEffect(() => {
      if (!panning) return;
      const move = (e) => {
        const p = panRef.current; if (!p) return;
        setView((v) => ({ ...v, pan: { x: p.pan.x + (e.clientX - p.sx), y: p.pan.y + (e.clientY - p.sy) } }));
      };
      const up = () => setPanning(false);
      window.addEventListener('mousemove', move); window.addEventListener('mouseup', up);
      return () => { window.removeEventListener('mousemove', move); window.removeEventListener('mouseup', up); };
    }, [panning]);

    const focusId = pinned || (hover && hover.kind === 'node' ? hover.id : null);
    const near = useMemo(() => (focusId ? EV.neighbourhood(graph, focusId) : null), [graph, focusId]);
    const details = useMemo(() => (focusId ? EV.nodeDetails(graph, focusId) : null), [graph, focusId]);
    const hoverEdge = hover && hover.kind === 'edge' ? graph.edges.find((e) => e.id === hover.id) : null;
    const dim = (nodeId) => (near && !near.nodes.has(nodeId) ? 0.22 : 1);
    const dimEdge = (edgeId) => (near && !near.edges.has(edgeId) ? 0.1 : 1);
    const labelOf = (id) => (graph.nodes.find((n) => n.id === id) || {}).label || id;

    return (
      <div style={{ position: 'relative', flex: 1, minHeight: 0, borderRadius: 10, overflow: 'hidden', border: '1px solid #1a1a1a' }}>
        <div
          ref={wrapRef}
          onWheel={onWheel}
          onMouseDown={onMouseDown}
          onClick={() => setPinned(null)}
          style={{
            position: 'absolute', inset: 0, overflow: 'hidden', userSelect: 'none',
            cursor: panning ? 'grabbing' : 'grab',
            background: 'radial-gradient(ellipse 140% 80% at 50% 50%, #0c1020 0%, #050505 70%)',
          }}>
          {/* Edges */}
          <svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', overflow: 'visible', pointerEvents: 'none' }}>
            {graph.edges.map((e) => {
              const a = laid.positions[e.from], b = laid.positions[e.to];
              if (!a || !b) return null;
              const p1 = toScreen(a), p2 = toScreen(b);
              const on = hover && hover.kind === 'edge' && hover.id === e.id;
              return (
                <g key={e.id} opacity={dimEdge(e.id)}>
                  <line x1={p1.x} y1={p1.y} x2={p2.x} y2={p2.y}
                    stroke={on ? '#93C5FD' : 'rgba(120,150,220,0.35)'} strokeWidth={on ? 2 : 1} />
                  {/* fat invisible hit line so thin edges are still hoverable */}
                  <line x1={p1.x} y1={p1.y} x2={p2.x} y2={p2.y} stroke="transparent" strokeWidth={12}
                    style={{ pointerEvents: 'stroke' }}
                    onMouseEnter={() => setHover({ kind: 'edge', id: e.id })}
                    onMouseLeave={() => setHover(null)} />
                  {(on || (near && near.edges.has(e.id))) && (
                    <text x={(p1.x + p2.x) / 2} y={(p1.y + p2.y) / 2 - 4} textAnchor="middle"
                      style={{ fontSize: 9, fill: '#7f9cc8', letterSpacing: '0.06em', textTransform: 'uppercase' }}>{e.type}</text>
                  )}
                </g>
              );
            })}
          </svg>

          {/* Nodes */}
          {graph.nodes.map((n) => {
            const p = laid.positions[n.id]; if (!p) return null;
            const s = toScreen(p);
            const color = REALM_COLOR[n.realm] || REALM_COLOR.other;
            const isFocus = focusId === n.id;
            return (
              <div key={n.id}
                onMouseEnter={() => setHover({ kind: 'node', id: n.id })}
                onMouseLeave={() => setHover(null)}
                onClick={(e) => { e.stopPropagation(); setPinned(pinned === n.id ? null : n.id); }}
                title={EV.sourceLabel(n, 90)}
                style={{
                  position: 'absolute', left: s.x, top: s.y, transform: 'translate(-50%,-50%)',
                  width: 'max-content', maxWidth: 240,
                  display: 'flex', alignItems: 'center', gap: 6,
                  padding: '5px 11px 5px 8px', borderRadius: 18,
                  fontSize: 11, lineHeight: 1.25,
                  color: isFocus ? '#fff' : 'rgba(255,255,255,0.72)',
                  background: isFocus ? color + '2e' : color + '14',
                  border: `1px solid ${isFocus ? color : color + '55'}`,
                  boxShadow: isFocus ? `0 0 18px ${color}55` : (n.seed ? `0 0 10px ${color}33` : 'none'),
                  opacity: dim(n.id), transition: 'opacity 0.15s, background 0.15s',
                  cursor: 'pointer', zIndex: isFocus ? 6 : 4,
                }}>
                <span style={{ color, fontSize: 10 }}>{TYPE_GLYPH[n.type] || TYPE_GLYPH.other}</span>
                <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 200 }}>{n.label}</span>
                {n.seed && <span style={{ fontSize: 8, color, letterSpacing: '0.1em' }}>SEED</span>}
              </div>
            );
          })}
        </div>

        {/* Hover / pinned card */}
        {(details || hoverEdge) && (
          <div onClick={(e) => e.stopPropagation()}
            style={{
              position: 'absolute', right: 12, top: 12, width: 330, maxHeight: 'calc(100% - 24px)', overflowY: 'auto',
              background: 'rgba(10,12,18,0.95)', border: '1px solid #232838', borderRadius: 10, padding: 12, zIndex: 20,
              fontSize: 11, color: 'rgba(255,255,255,0.75)', backdropFilter: 'blur(6px)',
            }}>
            {hoverEdge ? (
              <div>
                <div style={{ fontSize: 9, letterSpacing: '0.12em', textTransform: 'uppercase', color: '#7f9cc8' }}>{hoverEdge.type}{hoverEdge.method ? ' · ' + hoverEdge.method : ''}</div>
                <div style={{ marginTop: 6, color: '#fff' }}>{labelOf(hoverEdge.from)} → {labelOf(hoverEdge.to)}</div>
                {(EDGE_NOTE[hoverEdge.method] || hoverEdge.why) && (
                  <div style={{ marginTop: 6 }}>{EDGE_NOTE[hoverEdge.method] || hoverEdge.why}</div>
                )}
                {hoverEdge.edgeClass && <div style={{ marginTop: 6, color: '#64748B' }}>class: {hoverEdge.edgeClass}</div>}
              </div>
            ) : (
              <div>
                <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                  <span style={{ color: REALM_COLOR[details.node.realm] || REALM_COLOR.other }}>{TYPE_GLYPH[details.node.type] || TYPE_GLYPH.other}</span>
                  <span style={{ fontSize: 9, letterSpacing: '0.12em', textTransform: 'uppercase', color: '#7f9cc8' }}>{details.node.realm} · {details.node.type}</span>
                  {pinned && <span style={{ marginLeft: 'auto', fontSize: 9, color: '#475569' }}>pinned · click to release</span>}
                </div>
                <div style={{ marginTop: 6, color: '#fff', fontSize: 12, fontWeight: 600 }}>{details.node.label}</div>
                {(details.node.path || details.node.sourceId) && (
                  <div style={{ marginTop: 6, color: '#94A3B8', wordBreak: 'break-all' }}>{details.source || details.node.sourceId}</div>
                )}
                {details.node.snippet && (
                  <div style={{ marginTop: 8, padding: 8, background: 'rgba(255,255,255,0.03)', borderLeft: '2px solid #334155', borderRadius: 4, whiteSpace: 'pre-wrap' }}>
                    {details.node.snippet}
                    {details.node.excerptTruncated && <div style={{ marginTop: 4, color: '#EAB308', fontSize: 9 }}>Excerpt clipped by EPFC — open the source for the full text.</div>}
                  </div>
                )}
                {(details.node.path || details.node.sourceId) && (
                  <div style={{ marginTop: 8, display: 'flex', gap: 6 }}>
                    <button className="btn btn-ghost btn-sm" onClick={() => onCopyPath && onCopyPath(details.node)}>Copy path</button>
                    {onOpenSource && <button className="btn btn-ghost btn-sm" onClick={() => onOpenSource(details.node)}>Open source</button>}
                  </div>
                )}
                {details.node.revision && <div style={{ marginTop: 6, color: '#3f4a5a', fontSize: 9 }}>{details.node.revision}</div>}
                {details.relationships.length > 0 && (
                  <div style={{ marginTop: 10 }}>
                    <div style={{ fontSize: 9, letterSpacing: '0.12em', textTransform: 'uppercase', color: '#475569' }}>Relationships</div>
                    {details.relationships.map((r) => {
                      const edge = graph.edges.find((e) => e.id === r.id) || {};
                      return (
                        <div key={r.id} style={{ marginTop: 6 }}>
                          <span style={{ color: '#7f9cc8' }}>{r.direction === 'out' ? '→' : '←'} {r.type}</span>{' '}
                          <span style={{ color: '#fff' }}>{labelOf(r.otherId)}</span>
                          {(EDGE_NOTE[edge.method] || r.why) && <div style={{ color: '#64748B' }}>{EDGE_NOTE[edge.method] || r.why}</div>}
                        </div>
                      );
                    })}
                  </div>
                )}
              </div>
            )}
          </div>
        )}

        {/* Zoom controls */}
        <div style={{ position: 'absolute', left: 12, bottom: 12, display: 'flex', gap: 6, zIndex: 20 }}>
          <button className="btn btn-ghost btn-sm" onClick={(e) => { e.stopPropagation(); setView((v) => ({ ...v, zoom: Math.max(0.2, v.zoom / 1.2) })); }}>−</button>
          <button className="btn btn-ghost btn-sm" onClick={(e) => {
            e.stopPropagation();
            const el = wrapRef.current; if (!el) return;
            const r = el.getBoundingClientRect();
            setView(EV.fitTransform(laid.bounds, { width: r.width, height: r.height }));
          }}>Fit</button>
          <button className="btn btn-ghost btn-sm" onClick={(e) => { e.stopPropagation(); setView((v) => ({ ...v, zoom: Math.min(2.5, v.zoom * 1.2) })); }}>+</button>
        </div>
      </div>
    );
  }

  // ── Connection ───────────────────────────────────────────────────────────
  // EPFC's cortex_api runs on this laptop only. The base URL is fixed to the
  // approved local API: the token must never travel to a host someone typed.
  // A dev override is honoured only when it points at this machine, so the
  // token can never be sent to a host someone typed into a field.
  const isLocalApi = (u) => { try { const h = new URL(u).hostname; return h === '127.0.0.1' || h === 'localhost' || h === '::1'; } catch (e) { return false; } };
  const API_BASE = isLocalApi(window.LF_EVIDENCE_API || '') ? String(window.LF_EVIDENCE_API).replace(/\/$/, '') : 'http://127.0.0.1:8788';
  // No "lf." prefix on purpose: that prefix is what gets synced and exported.
  const TOKEN_KEY = 'epfcReadToken';
  const readToken = () => {
    try { return localStorage.getItem(TOKEN_KEY) || sessionStorage.getItem('lf.evidenceToken') || ''; } catch (e) { return ''; }
  };
  const writeToken = (v) => {
    try {
      if (v) localStorage.setItem(TOKEN_KEY, v); else localStorage.removeItem(TOKEN_KEY);
      sessionStorage.removeItem('lf.evidenceToken');   // migrate off the old session key
    } catch (e) {}
  };

  const REALM_KEY = 'epfcRealm';   // device-local UI preference, outside lf.*
  const REALM_OPTS = [['wrh', 'WRH'], ['lf', 'Laminar Flow'], ['all', 'Both']];

  function EvidenceScreen() {
    const [token, setTokenState] = useState(readToken);
    const [realm, setRealmState] = useState(() => { try { return localStorage.getItem(REALM_KEY) || 'wrh'; } catch (e) { return 'wrh'; } });
    const [query, setQuery] = useState('');
    const [state, setState] = useState({ status: 'idle' });   // idle | loading | error | done
    const [graph, setGraph] = useState(null);
    const [showCfg, setShowCfg] = useState(false);
    const [toast, setToast] = useState('');

    const setToken = (v) => { setTokenState(v); writeToken(v); };
    const setRealm = (v) => { setRealmState(v); try { localStorage.setItem(REALM_KEY, v); } catch (e) {} };
    const flash = (m) => { setToast(m); setTimeout(() => setToast(''), 2600); };
    const copyPath = (node) => {
      const p = node.path || node.sourceId || '';
      try { navigator.clipboard.writeText(p); flash('Path copied'); } catch (e) { flash(p); }
    };

    const run = async () => {
      if (!query.trim()) return;
      if (window.LF_OFFLINE) { setState({ status: 'error', error: 'This offline build has no network, and EPFC only answers on the home laptop. Use app.laminarflow.tech there.' }); return; }
      if (!token) { setShowCfg(true); setState({ status: 'error', error: 'Paste the EPFC read token first. It stays on this device and is never synced.' }); return; }
      setState({ status: 'loading' });
      try {
        const u = new URL(API_BASE + '/api/v1/context');
        u.searchParams.set('q', query.trim());
        u.searchParams.set('realm', realm);   // wrh | lf | all
        const res = await fetch(u.toString(), { headers: { authorization: 'Bearer ' + token }, mode: 'cors' });
        if (res.status === 401) throw new Error('EPFC rejected the token (401). Ask it for a fresh read token.');
        if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + res.statusText);
        const g = EV.normalizeContext(await res.json());
        setGraph(g);
        setState({
          status: 'done', warnings: g.warnings, truncated: g.truncated,
          gaps: g.gaps || [], coverage: g.coverage, complete: g.completeWithinScope !== false,
        });
      } catch (err) {
        setGraph(null);
        const msg = String((err && err.message) || err);
        setState({
          status: 'error',
          error: /Failed to fetch|NetworkError|Load failed/i.test(msg)
            ? 'Can\'t reach EPFC at ' + API_BASE + '. It answers only on the home laptop while cortex_api is running, and Chrome may ask once for permission to reach a local address.'
            : msg,
        });
      }
    };

    // POST /api/v1/open — EPFC opens the file on the desktop; copy-path is the
    // fallback whenever it refuses (403), can't find it (404) or is unreachable.
    const openSource = async (node) => {
      const body = node.sourceId ? { source_id: node.sourceId } : { path: node.path };
      try {
        const res = await fetch(API_BASE + '/api/v1/open', {
          method: 'POST', mode: 'cors',
          headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' },
          body: JSON.stringify(body),
        });
        if (res.ok) { flash('Opening on your desktop…'); return; }
        flash(res.status === 403 ? 'EPFC won\'t open that path — copied it instead.'
          : (res.status === 404 ? 'File not found — copied the path instead.' : 'Open failed (' + res.status + ') — copied the path instead.'));
      } catch (e) {
        flash('EPFC unreachable — copied the path instead.');
      }
      copyPath(node);
    };

    const covLine = Object.entries((state.coverage && typeof state.coverage === 'object') ? state.coverage : {})
      .map(([k, v]) => k.toUpperCase() + ': ' + String((v && v.status) || '?') + ((v && v.freshness) ? ' (' + String(v.freshness).slice(0, 10) + ')' : ''))
      .join(' · ');
    const notes = []
      .concat(state.truncated ? ['Partial — EPFC capped this answer.'] : [])
      .concat(state.complete === false ? ['Incomplete within scope.'] : [])
      .concat((state.gaps || []).slice(0, 2))
      .concat((state.warnings || []).slice(0, 2));

    return (
      <div className="screen screen-enter" style={{ display: 'flex', flexDirection: 'column', gap: 10, height: '100%' }}>
        <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
          <input
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            onKeyDown={(e) => { if (e.key === 'Enter') run(); }}
            placeholder="What are you looking for? e.g. mechanical peer review comments"
            style={{ flex: 1, background: '#0b0b0b', border: '1px solid #1f1f1f', borderRadius: 8, padding: '9px 12px', color: '#ddd', fontSize: 12, fontFamily: 'inherit' }}
          />
          <button className="btn btn-primary btn-sm" onClick={run} disabled={state.status === 'loading'}>
            {state.status === 'loading' ? 'Searching…' : 'Search'}
          </button>
          <div style={{ display: 'flex', gap: 2, padding: 2, background: '#0b0b0b', border: '1px solid #1f1f1f', borderRadius: 8 }}>
            {REALM_OPTS.map(([id, label]) => (
              <button key={id} onClick={() => setRealm(id)} title={id === 'lf' ? 'First Laminar Flow query takes a moment while the tree loads' : ''}
                style={{
                  background: realm === id ? '#1d283a' : 'transparent', color: realm === id ? '#cbd5e1' : '#555',
                  border: 'none', borderRadius: 6, padding: '5px 9px', fontSize: 10, fontWeight: 700,
                  letterSpacing: '0.06em', textTransform: 'uppercase', cursor: 'pointer', fontFamily: 'inherit',
                }}>{label}</button>
            ))}
          </div>
          <button className="btn btn-ghost btn-sm" onClick={() => setShowCfg((v) => !v)} title={token ? 'EPFC token saved on this device' : 'Paste the EPFC read token'}>
            {token ? '⚙' : '⚙ !'}
          </button>
        </div>

        {showCfg && (
          <div style={{ padding: 10, background: '#0b0b0b', border: '1px solid #1f1f1f', borderRadius: 8 }}>
            <label style={{ fontSize: 10, color: '#666' }}>EPFC read token — saved on this device only; never synced, never in the OneDrive file
              <input type="password" value={token} onChange={(e) => setToken(e.target.value)} placeholder="paste it once"
                style={{ width: '100%', marginTop: 4, background: '#111', border: '1px solid #222', borderRadius: 6, padding: '6px 8px', color: '#ddd', fontSize: 11, fontFamily: 'inherit' }} />
            </label>
            <div style={{ marginTop: 6, display: 'flex', alignItems: 'center', gap: 10 }}>
              <span style={{ fontSize: 9, color: '#3a3a3a', letterSpacing: '0.06em' }}>Endpoint {API_BASE} · read-only routes · home laptop only</span>
              {token && <button className="btn btn-ghost btn-sm" style={{ marginLeft: 'auto' }} onClick={() => setToken('')}>Forget token</button>}
            </div>
          </div>
        )}

        {state.status === 'error' && (
          <div style={{ padding: 10, borderRadius: 8, background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.2)', color: '#EF4444', fontSize: 11 }}>{state.error}</div>
        )}
        {state.status === 'done' && notes.length > 0 && (
          <div style={{ padding: 8, borderRadius: 8, background: 'rgba(234,179,8,0.06)', border: '1px solid rgba(234,179,8,0.18)', color: '#EAB308', fontSize: 10 }}>{notes.join(' · ')}</div>
        )}

        {graph && graph.nodes.length > 0 ? (
          <EvidenceGraph graph={graph} onOpenSource={openSource} onCopyPath={copyPath} />
        ) : (
          <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#333', fontSize: 11, letterSpacing: '0.1em', textTransform: 'uppercase' }}>
            {state.status === 'loading' ? (realm === 'wrh' ? 'Asking EPFC…' : 'Asking EPFC… the first Laminar Flow query takes a moment') : (state.status === 'done' ? 'No evidence for that query' : 'Ask a question to see the evidence EPFC activates')}
          </div>
        )}

        <div style={{ display: 'flex', gap: 10, alignItems: 'center', fontSize: 9, color: '#2a2a2a', letterSpacing: '0.08em', textTransform: 'uppercase' }}>
          <span>Read-only · scroll to zoom · drag to pan · hover a node or link · click to pin</span>
          {covLine && <span style={{ color: '#3a3a3a' }}>Index {covLine}</span>}
          {toast && <span style={{ marginLeft: 'auto', color: '#7f9cc8', textTransform: 'none', letterSpacing: 0, fontSize: 10 }}>{toast}</span>}
        </div>
      </div>
    );
  }

  window.EvidenceScreen = EvidenceScreen;
  window.EvidenceGraph = EvidenceGraph;
})();
