// Fabric — Timeline & Catalog tabs. Split out of fabric.jsx. Loads after fabric.jsx.
const { useState: useFx, useMemo: useFxM } = React;

// --------- TIMELINE TAB — the graph's own learning history ---------
//
// Every entry is a learning event reconstructed from the `/skill/graph`
// snapshot (deriveLearningEvents). Structural facts the graph read from a
// source sit on the left, inferences it drew sit on the right, so the spine
// reads as "what we were told" vs "what we worked out".

const TL_LANES = {
  entity:   { side: 'left',  color: '#0F766E', label: 'Entities' },
  schema:   { side: 'left',  color: '#6B21A8', label: 'Schema' },
  relation: { side: 'right', color: '#4F46E5', label: 'Relationships' },
  rerank:   { side: 'right', color: '#B45309', label: 'Re-ranking' },
  dedupe:   { side: 'right', color: '#475569', label: 'Merges' },
};

const TL_PAGE = 40;

// `tick` is not read: it is the page clock, passed in purely so the relative
// stamps below re-render as they age instead of freezing at snapshot load.
function TimelineTab({ events, tick, loading, error, reload, snapshot }) {
  const [hidden, setHidden] = useFx({});        // lane key → true when filtered out
  const [limit, setLimit] = useFx(TL_PAGE);

  // Undone events are the Learning page's business (it offers "Recover");
  // this tab shows the history as it currently stands.
  const active = useFxM(
    () => (events || []).filter(e => e.undo_state !== 'undo'),
    [events]
  );
  const counts = useFxM(() => {
    const c = {};
    active.forEach(e => { c[e.type] = (c[e.type] || 0) + 1; });
    return c;
  }, [active]);
  const shown = useFxM(
    () => active.filter(e => !hidden[e.type]),
    [active, hidden]
  );

  if (error && !snapshot) return <FabricError error={error} reload={reload}/>;
  if (!snapshot) return loading ? <FabricLoading label="Loading learning history…"/> : null;

  if (active.length === 0) {
    return (
      <div style={{ padding: '56px 32px', textAlign: 'center', maxWidth: 460, margin: '0 auto' }}>
        <Icons.Sparkle size={28} style={{ color: 'var(--text-tertiary)' }}/>
        <div className="t-h3" style={{ marginTop: 12 }}>No learning history yet</div>
        <div className="text-secondary t-small" style={{ marginTop: 6 }}>
          Entities, relationships and merges show up here as the graph learns them
          from your sources and conversations.
        </div>
      </div>
    );
  }

  const toggle = (k) => setHidden(h => ({ ...h, [k]: !h[k] }));

  return (
    <div style={{ padding: '24px 32px', maxWidth: 1080, margin: '0 auto' }}>
      {/* Lane filters — each carries its real count */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 20, flexWrap: 'wrap' }}>
        <span className="t-micro text-tertiary">Lanes</span>
        {Object.entries(TL_LANES).map(([k, v]) => {
          const off = !!hidden[k];
          return (
            <button key={k} onClick={() => toggle(k)} title={off ? 'Show' : 'Hide'} style={{
              display: 'inline-flex', alignItems: 'center', gap: 6,
              padding: '4px 10px', borderRadius: 14,
              background: 'var(--surface)',
              border: '1px solid var(--border-subtle)',
              fontSize: 11, fontWeight: 500,
              opacity: off ? 0.45 : 1,
            }}>
              <span style={{ width: 8, height: 8, borderRadius: 2, background: v.color }}/>
              {v.label}
              <span className="mono-sm mono text-tertiary">{(counts[k] || 0).toLocaleString()}</span>
            </button>
          );
        })}
        <div style={{ flex: 1 }}/>
        <span className="mono-sm mono text-tertiary">
          {shown.length.toLocaleString()} of {active.length.toLocaleString()} events
        </span>
      </div>

      {shown.length === 0 ? (
        <div className="card" style={{ padding: 24, textAlign: 'center' }}>
          <span className="t-small text-secondary">Every lane is hidden — re-enable one above.</span>
        </div>
      ) : (
        <div style={{ position: 'relative' }}>
          {/* Center line */}
          <div style={{
            position: 'absolute', left: '50%', top: 0, bottom: 0,
            width: 1, background: 'var(--border)',
            transform: 'translateX(-0.5px)',
          }}/>

          <div style={{ display: 'flex', flexDirection: 'column', gap: 4, paddingTop: 18 }}>
            {shown.slice(0, limit).map(ev => <TimelineEvent key={ev.id} ev={ev}/>)}
          </div>

          <div style={{ display: 'flex', justifyContent: 'center', marginTop: 12, paddingBottom: 40 }}>
            {shown.length > limit ? (
              <button className="btn sm" onClick={() => setLimit(l => l + TL_PAGE)}>
                Load older activity ({(shown.length - limit).toLocaleString()} more)
              </button>
            ) : (
              <span className="mono-sm mono text-tertiary">That's the whole history.</span>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

function TimelineEvent({ ev }) {
  const lane = TL_LANES[ev.type] || { side: 'right', color: 'var(--text-tertiary)', label: ev.type };
  const onLeft = lane.side === 'left';

  return (
    <div style={{
      display: 'grid',
      gridTemplateColumns: '1fr 40px 1fr',
      alignItems: 'start',
      minHeight: 80,
    }}>
      {/* Left side */}
      <div style={{ paddingRight: 20, textAlign: 'right' }}>
        {onLeft && <EventCard ev={ev} lane={lane} side="left"/>}
      </div>

      {/* Center: dot + connector */}
      <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', paddingTop: 10, position: 'relative' }}>
        {/* Connector line from dot to card */}
        <div style={{
          position: 'absolute', top: 16,
          [onLeft ? 'right' : 'left']: '50%',
          width: 16, height: 1, background: 'var(--border)',
        }}/>
        <div style={{
          width: 14, height: 14, borderRadius: '50%',
          background: 'var(--surface)', border: `2px solid ${lane.color}`,
          position: 'relative', zIndex: 1,
        }}/>
        <div className="mono-sm mono text-tertiary" style={{ marginTop: 6, fontSize: 10, whiteSpace: 'nowrap' }}>
          {formatRelativeTime(ev.at)}
        </div>
      </div>

      {/* Right side */}
      <div style={{ paddingLeft: 20 }}>
        {!onLeft && <EventCard ev={ev} lane={lane} side="right"/>}
      </div>
    </div>
  );
}

function EventCard({ ev, lane, side }) {
  // Zone-less backend timestamps are UTC — parcleParseGraphTime pins them
  // before they are shown in the viewer's own zone.
  const at = parcleParseGraphTime(ev.at);
  const stamp = isNaN(at) ? 'undated' : new Date(at).toLocaleString();
  return (
    <div style={{
      display: 'inline-block',
      textAlign: 'left',
      maxWidth: 440, width: '100%',
      background: 'var(--surface)',
      border: '1px solid var(--border-subtle)',
      borderLeft: side === 'right' ? `3px solid ${lane.color}` : '1px solid var(--border-subtle)',
      borderRight: side === 'left' ? `3px solid ${lane.color}` : '1px solid var(--border-subtle)',
      borderRadius: 8,
      padding: '10px 14px',
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4, flexWrap: 'wrap' }}>
        <span style={{ width: 8, height: 8, borderRadius: 2, background: lane.color, flexShrink: 0 }}/>
        <span className="mono-sm mono text-tertiary" style={{ fontSize: 11 }}>
          {lane.label} · {stamp}
        </span>
        {ev.origin && (
          <span className="chip" style={{ fontSize: 10 }}>{fabOriginLabel(ev.origin)}</span>
        )}
      </div>
      <div style={{ fontSize: 13, lineHeight: '20px', fontWeight: 500 }}>{ev.title}</div>
      {ev.body && (
        <div className="t-small text-secondary" style={{ marginTop: 4, whiteSpace: 'pre-wrap' }}>{ev.body}</div>
      )}
      {ev.evidence_ref && (
        <div className="mono-sm mono text-tertiary" style={{ marginTop: 6, fontSize: 10, wordBreak: 'break-all' }}>
          {ev.evidence_ref}
        </div>
      )}
    </div>
  );
}

// --------- CATALOG TAB — snapshot-driven entity catalog ---------

function CatalogTab({ snapshot, loading, error, reload, openEntity }) {
  const [q, setQ] = useFx('');
  const [cat, setCat] = useFx('all');
  const [typ, setTyp] = useFx('all');
  const [dom, setDom] = useFx('all');

  const view = useFxM(() => {
    if (!snapshot) return { rows: [], total: 0 };
    const nodes = allGraphNodes(snapshot);
    const edges = allGraphEdges(snapshot);
    const nodeIndex = buildGraphNodeIndex(nodes);
    const { outByNode, inByNode } = buildGraphAdjacency(edges);
    const enriched = nodes.map(n => {
      const out = outByNode.get(n.id) || [];
      const inE = inByNode.get(n.id) || [];
      const domain = deriveNodeDomain(n, out, nodeIndex);
      const confEdges = [...out, ...inE].filter(e => e.confidence != null);
      const avgConf = confEdges.length
        ? confEdges.reduce((s, e) => s + e.confidence, 0) / confEdges.length
        : null;
      return {
        id: n.id,
        category: n.category,
        type: n.type || null,
        name: n.name,
        // Synonyms can be legacy strings or structured SynonymEntry objects
        // (`{name, at, origin, evidence_ref, state}`) per AskGraphDelta. Skip
        // soft-undone entries and flatten to active names for search/render.
        synonyms: (n.synonyms || [])
          .map(s => (typeof s === 'string' ? s : (s && s.state !== 'undo' ? s.name : null)))
          .filter(s => typeof s === 'string' && s),
        origin: n.origin,
        domain,
        edgeCount: out.length + inE.length,
        avgConf,
      };
    });
    const total = enriched.length;
    const qn = q.trim().toLowerCase();
    const filtered = enriched.filter(r => {
      if (cat !== 'all' && r.category !== cat) return false;
      if (typ !== 'all') {
        if (typ === 'concept') { if (r.category !== 'concept') return false; }
        else { if (r.type !== typ) return false; }
      }
      if (dom !== 'all' && r.domain !== dom) return false;
      if (qn) {
        const hay = (r.name + ' ' + r.synonyms.join(' ') + ' ' + r.id).toLowerCase();
        if (!hay.includes(qn)) return false;
      }
      return true;
    });
    filtered.sort((a, b) => b.edgeCount - a.edgeCount);
    return { rows: filtered, total };
  }, [snapshot, q, cat, typ, dom]);

  if (loading && !snapshot) {
    return (
      <div style={{ padding: '48px 32px', textAlign: 'center' }}>
        <div className="skeleton" style={{ width: 220, height: 18, margin: '0 auto' }}/>
        <div className="mono-sm mono text-tertiary" style={{ marginTop: 10 }}>Loading graph catalog…</div>
      </div>
    );
  }
  if (error) {
    return (
      <div style={{ padding: 40, textAlign: 'center' }}>
        <Icons.AlertTri size={28} style={{ color: 'var(--error)' }}/>
        <div className="t-h3" style={{ marginTop: 10, color: 'var(--error)' }}>Failed to load graph</div>
        <div className="text-secondary t-small" style={{ marginTop: 4 }}>{error}</div>
        <button className="btn sm" style={{ marginTop: 16 }} onClick={reload}>
          <Icons.Refresh size={12}/> Retry
        </button>
      </div>
    );
  }
  if (!snapshot) return null;

  const MAX = 200;

  return (
    <div style={{ padding: '24px 32px' }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' }}>
        <h2 className="t-h2" style={{ margin: 0 }}>Graph catalog</h2>
        <span className="mono-sm mono text-tertiary">
          {view.rows.length.toLocaleString()} shown · {view.total.toLocaleString()} total nodes
        </span>
      </div>

      <div style={{ display: 'flex', gap: 10, marginTop: 16, alignItems: 'center', flexWrap: 'wrap' }}>
        <div style={{ position: 'relative', flex: 1, maxWidth: 380, minWidth: 200 }}>
          <Icons.Search size={13} style={{ position: 'absolute', left: 12, top: 10, color: 'var(--text-tertiary)' }}/>
          <input className="input" placeholder="Search name, id, synonyms…" value={q} onChange={e => setQ(e.target.value)} style={{ paddingLeft: 34 }}/>
        </div>
        <SelectBtn label="Category" value={cat} onChange={setCat} options={[
          ['all', 'All'], ['factual', 'Factual'], ['concept', 'Concept'],
        ]}/>
        <SelectBtn label="Type" value={typ} onChange={setTyp} options={[
          ['all', 'All'], ['table', 'Table'], ['column', 'Column'],
          ['document', 'Document'], ['chunk', 'Chunk'], ['concept', 'Concept'],
        ]}/>
        <SelectBtn label="Domain" value={dom} onChange={setDom} options={[
          ['all', 'All'], ['db', 'DB'], ['doc', 'Docs'], ['bridge', 'Bridge'], ['orphan', 'Orphan'],
        ]}/>
      </div>

      <div className="card" style={{ marginTop: 16, overflow: 'hidden' }}>
        <table className="parcle">
          <thead>
            <tr>
              <th style={{ width: 80 }}>Category</th>
              <th style={{ width: 95 }}>Type</th>
              <th>Name</th>
              <th style={{ width: 80 }}>Domain</th>
              <th style={{ width: 140 }}>Confidence</th>
              <th style={{ width: 80 }}>Edges</th>
              <th style={{ width: 80 }}></th>
            </tr>
          </thead>
          <tbody>
            {view.rows.slice(0, MAX).map(r => {
              const dc = DOMAIN_COLORS[r.domain] || DOMAIN_COLORS.orphan;
              return (
                <tr key={r.id}
                  onClick={() => openEntity && openEntity({ id: r.id, type: r.type || r.category, label: r.name, sub: r.domain })}
                  style={{ cursor: 'pointer' }}>
                  <td><span className={`chip ${r.category === 'concept' ? 'accent' : ''}`} style={{ fontSize: 10 }}>{r.category}</span></td>
                  <td className="mono-sm mono text-secondary">{r.type || '—'}</td>
                  <td>
                    <div style={{ fontWeight: 500 }}>{r.name}</div>
                    <div className="mono-sm mono text-tertiary" style={{ fontSize: 10, wordBreak: 'break-all' }}>{r.id}</div>
                    {r.synonyms.length > 0 && (
                      <div style={{ marginTop: 4, display: 'flex', gap: 3, flexWrap: 'wrap' }}>
                        {r.synonyms.slice(0, 4).map((s, i) => <span key={s + '_' + i} className="chip" style={{ fontSize: 10 }}>{s}</span>)}
                        {r.synonyms.length > 4 && <span className="text-tertiary" style={{ fontSize: 10 }}>+{r.synonyms.length - 4}</span>}
                      </div>
                    )}
                  </td>
                  <td>
                    <span className="chip" style={{ background: dc + '22', color: dc, border: 0, fontSize: 10 }}>{r.domain}</span>
                  </td>
                  <td>{r.avgConf != null ? <ConfBar conf={r.avgConf}/> : <span className="text-tertiary">—</span>}</td>
                  <td className="mono-sm mono text-secondary">{r.edgeCount}</td>
                  <td><button className="btn sm ghost">Open <Icons.ChevronR size={11}/></button></td>
                </tr>
              );
            })}
          </tbody>
        </table>
        {view.rows.length > MAX && (
          <div style={{ padding: '10px 18px', borderTop: '1px solid var(--border-subtle)', textAlign: 'center' }} className="mono-sm mono text-tertiary">
            Showing first {MAX} of {view.rows.length.toLocaleString()} — narrow filters to see more
          </div>
        )}
        {view.rows.length === 0 && (
          <div style={{ padding: 30, textAlign: 'center' }} className="t-small text-tertiary">
            No nodes match the current filters.
          </div>
        )}
      </div>
    </div>
  );
}

function SelectBtn({ label, value, onChange, options }) {
  return (
    <label style={{ position: 'relative', display: 'inline-flex', alignItems: 'center' }}>
      <select value={value} onChange={e => onChange(e.target.value)}
        className="btn"
        style={{
          padding: '0 28px 0 12px', height: 32,
          appearance: 'none', WebkitAppearance: 'none', MozAppearance: 'none',
          fontSize: 13, fontWeight: 500,
          background: 'var(--surface)', color: 'var(--text-primary)',
          cursor: 'pointer',
        }}>
        {options.map(([v, l]) => <option key={v} value={v}>{label}: {l}</option>)}
      </select>
      <Icons.ChevronD size={11} style={{ position: 'absolute', right: 10, pointerEvents: 'none', color: 'var(--text-tertiary)' }}/>
    </label>
  );
}
