// Fabric page — the org's knowledge graph (Overview / Graph / Timeline / Catalog)
//
// Every tab reads the live `/skill/graph` snapshot. There is no demo dataset
// behind this page: figures come from deriveFabricStats() / deriveLearningEvents()
// over what the backend returned, and anything the snapshot can't support
// (industry, ARR, account health, vendor logos) is simply not shown.

const { useState: useFb, useEffect: useFbE, useRef: useFbR, useMemo: useFbM } = React;

function FabricPage({ openEntity, setRoute, reveal, onRevealConsumed }) {
  const [tab, setTab] = useFb('overview');
  const [snapshot, setSnapshot] = useFb(null);
  // Starts true: the fetch below is kicked off in an effect, which runs AFTER
  // the first paint. Starting false would commit one frame of empty content
  // under the hero before the loading state appears.
  const [snapLoading, setSnapLoading] = useFb(true);
  const [snapError, setSnapError] = useFb(null);
  const [reloadNonce, setReloadNonce] = useFb(0);
  const [tick, setTick] = useFb(0);

  // Relative stamps ("3 min ago") and rolling windows ("+N · 24h", the 14-day
  // sparkline) are functions of the current time, not only of the snapshot.
  // Without a clock they would freeze at load and quietly go stale — a page
  // left open would keep insisting the graph was last updated a minute ago.
  useFbE(() => {
    const t = setInterval(() => setTick(v => v + 1), 60000);
    return () => clearInterval(t);
  }, []);

  // Load graph snapshot once (and on reload). Also subscribe to live
  // updates pushed by applyMutationResponse / applyAskGraphDelta so edits
  // performed inside the side panel (or elsewhere in the app) reflect in
  // the graph view without a manual refresh.
  useFbE(() => {
    let cancelled = false;
    setSnapLoading(true);
    setSnapError(null);
    fetchGraphSnapshotCached()
      .then(s => { if (!cancelled) { setSnapshot(s); setSnapLoading(false); } })
      .catch(e => { if (!cancelled) { setSnapError(e.message || String(e)); setSnapLoading(false); } });
    const unsub = typeof subscribeGraphSnapshot === 'function'
      ? subscribeGraphSnapshot(s => { if (!cancelled) setSnapshot(s); })
      : null;
    return () => { cancelled = true; if (unsub) unsub(); };
  }, [reloadNonce]);

  const reloadSnapshot = () => { invalidateGraphSnapshot(); setReloadNonce(n => n + 1); };

  // The stream and the aggregates are wanted by the hero AND by two tabs.
  // Derived once here so a large graph is walked once per snapshot rather than
  // once per consumer — and so every surface on the page necessarily quotes the
  // same numbers.
  const learningEvents = useFbM(
    () => (snapshot ? deriveLearningEvents(snapshot) : []),
    [snapshot]
  );
  const stats = useFbM(
    () => deriveFabricStats(snapshot, learningEvents),
    [snapshot, learningEvents, tick]   // `tick`: the 24h/14-day windows move with the clock
  );
  const origins = useFbM(() => fabOriginBreakdown(snapshot), [snapshot]);

  // A learned-concept reveal request (from the global toast) forces the graph
  // tab so the reveal sequence has a canvas to play on.
  useFbE(() => {
    if (reveal && reveal.ids && reveal.ids.length) setTab('graph');
  }, [reveal && reveal.token]);

  return (
    <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
      {/* Hero header — the organization's graph at a glance */}
      <FabricHero stats={stats} origins={origins} loading={snapLoading}
        error={snapError} reload={reloadSnapshot} setTab={setTab} tab={tab}/>

      {/* Tab content. `tick` is passed to the time-rendering tabs purely as a
          re-render trigger, so their relative stamps age with the clock. */}
      <div style={{ flex: 1, overflowY: 'auto' }}>
        {tab === 'overview' && <FabricOverviewTab snapshot={snapshot} stats={stats} origins={origins} events={learningEvents} tick={tick} loading={snapLoading} error={snapError} reload={reloadSnapshot} openEntity={openEntity} setRoute={setRoute} setTab={setTab}/>}
        {tab === 'graph'    && <GraphTab snapshot={snapshot} loading={snapLoading} error={snapError} reload={reloadSnapshot} openEntity={openEntity} reveal={reveal} onRevealConsumed={onRevealConsumed} />}
        {tab === 'timeline' && <TimelineTab events={learningEvents} tick={tick} loading={snapLoading} error={snapError} reload={reloadSnapshot} snapshot={snapshot}/>}
        {tab === 'catalog'  && <CatalogTab snapshot={snapshot} loading={snapLoading} error={snapError} reload={reloadSnapshot} openEntity={openEntity} />}
      </div>
    </div>
  );
}

// --------- SHARED STATES ---------
// Loading / error / empty are the normal states of a page with no fallback
// dataset, so they get first-class rendering rather than a blank panel.

function FabricLoading({ label }) {
  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 }}>{label}</div>
    </div>
  );
}

function FabricError({ error, reload }) {
  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>
      {reload && (
        <button className="btn sm" style={{ marginTop: 16 }} onClick={reload}>
          <Icons.Refresh size={12}/> Retry
        </button>
      )}
    </div>
  );
}

function FabricEmpty({ setRoute }) {
  return (
    <div style={{ padding: '56px 32px', textAlign: 'center', maxWidth: 460, margin: '0 auto' }}>
      <Icons.Graph size={30} style={{ color: 'var(--text-tertiary)' }}/>
      <div className="t-h3" style={{ marginTop: 12 }}>Nothing indexed yet</div>
      <div className="text-secondary t-small" style={{ marginTop: 6 }}>
        Connect Databricks or Slack, or upload a file — the entities, relationships
        and concepts this org learns from them show up here.
      </div>
      {setRoute && (
        <button className="btn sm accent" style={{ marginTop: 18 }} onClick={() => setRoute('connectors')}>
          Connect a source →
        </button>
      )}
    </div>
  );
}

// --------- HERO HEADER ---------

function FabricHero({ stats, origins, loading, error, reload, tab, setTab }) {
  const orgName = fabOrgName();
  const initial = [...String(orgName)][0];

  const comp = stats && stats.composition;
  const summary = comp ? [
    [comp.tables,    'table'],
    [comp.documents, 'document'],
    [comp.concepts,  'concept'],
    [comp.edges,     'relationship'],
  ].filter(([n]) => n > 0).map(([n, word]) =>
    `${n.toLocaleString()} ${word}${n === 1 ? '' : 's'}`).join(' · ') : '';

  return (
    <div style={{
      borderBottom: '1px solid var(--border-subtle)',
      padding: '24px 32px 0',
      background: 'var(--bg)',
    }}>
      {/* Row 1: icon + title + actions (actions wrap below on narrow) */}
      <div style={{ display: 'flex', alignItems: 'flex-start', gap: 20, flexWrap: 'wrap' }}>
        {/* Org icon */}
        <div style={{
          width: 56, height: 56, borderRadius: 12,
          background: 'linear-gradient(135deg, #E7F4F2, #D4ECE8)',
          border: '1px solid var(--border-subtle)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          flexShrink: 0,
          position: 'relative',
        }}>
          <span style={{ fontSize: 22, fontWeight: 600, color: '#0A5A54' }}>
            {initial ? initial.toUpperCase() : '·'}
          </span>
          <span style={{
            position: 'absolute', bottom: -5, right: -5,
            padding: '1px 6px', borderRadius: 10,
            background: 'var(--surface)', border: '1px solid var(--border)',
            fontSize: 10, fontWeight: 500, color: 'var(--text-secondary)',
          }}>Org</span>
        </div>

        {/* Title column */}
        <div style={{ flex: 1, minWidth: 260 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', rowGap: 4 }}>
            <h1 className="t-h1" style={{ margin: 0 }}>{orgName}</h1>
            <span className="chip" style={{ fontSize: 11 }}>Knowledge graph</span>
          </div>
          <div className="t-body text-secondary" style={{ marginTop: 6, minHeight: 20 }}>
            {error ? <span style={{ color: 'var(--error)' }}>Graph unavailable — {error}</span>
              : !stats ? (loading ? 'Loading graph…' : '')
              : stats.isEmpty ? 'No entities indexed yet'
              : summary}
          </div>
        </div>

        {/* Actions */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'flex-end', flexShrink: 0 }}>
          {stats && stats.lastUpdatedAt != null && (
            <span className="mono-sm mono text-tertiary" style={{ whiteSpace: 'nowrap' }}>
              Last updated {formatRelativeTime(new Date(stats.lastUpdatedAt).toISOString())}
            </span>
          )}
          <button className="btn sm ghost" onClick={reload} disabled={loading}>
            <Icons.Refresh size={12}/> {loading ? 'Refreshing…' : 'Refresh'}
          </button>
        </div>
      </div>

      {/* Row 2: where the knowledge came from — node counts per producer */}
      {origins && origins.length > 0 && (
        <div style={{ marginTop: 14, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
          <span className="t-micro text-tertiary" style={{ whiteSpace: 'nowrap' }}>LEARNED FROM</span>
          {origins.map(o => (
            <span key={o.origin} className="chip" style={{ fontSize: 11, whiteSpace: 'nowrap' }}>
              {o.label} · {o.count.toLocaleString()}
            </span>
          ))}
        </div>
      )}

      {/* Tabs */}
      <div style={{ marginTop: 20, display: 'flex', gap: 4, flexWrap: 'wrap' }}>
        {[
          ['overview', 'Overview'],
          ['graph',    'Graph'],
          ['timeline', 'Learning timeline'],
          ['catalog',  'Catalog'],
        ].map(([id, label]) => (
          <button key={id} onClick={() => setTab(id)} style={{
            padding: '10px 14px',
            fontSize: 13, fontWeight: tab === id ? 500 : 400,
            color: tab === id ? 'var(--text-primary)' : 'var(--text-secondary)',
            borderBottom: `2px solid ${tab === id ? 'var(--accent)' : 'transparent'}`,
            marginBottom: -1,
            whiteSpace: 'nowrap',
          }}>{label}</button>
        ))}
      </div>
    </div>
  );
}

// --------- OVERVIEW TAB ---------

const FAB_METRIC_CARDS = [
  { key: 'entities',  label: 'Entities indexed',    note: 'tables, columns, documents, chunks and concepts' },
  { key: 'relations', label: 'Relationships',       note: 'mapping, dependency, relation and category edges' },
  { key: 'merges',    label: 'Cross-source merges', note: 'duplicates folded into an existing concept' },
  { key: 'learnings', label: 'Learning events',     note: 'every change the graph recorded' },
];

// Named FabricOverviewTab, not OverviewTab: drawers.jsx already owns that name
// for the connector drawer, and every top-level declaration in these scripts
// lands in ONE shared global scope — whichever file loaded last would win and
// silently render the wrong component in the other's tab.
function FabricOverviewTab({ snapshot, stats, origins, events, tick, loading, error, reload, openEntity, setRoute, setTab }) {
  const hubs = useFbM(() => fabTopNodes(snapshot, 8), [snapshot]);
  const recent = useFbM(
    () => (events || []).filter(e => e.undo_state !== 'undo').slice(0, 6),
    [events]
  );

  if (error && !snapshot) return <FabricError error={error} reload={reload}/>;
  if (!snapshot) return loading ? <FabricLoading label="Loading knowledge graph…"/> : null;
  if (stats.isEmpty) return <FabricEmpty setRoute={setRoute}/>;

  return (
    <div style={{ padding: '24px 32px', display: 'flex', flexDirection: 'column', gap: 24 }}>
      {/* A refresh that failed leaves the previous snapshot on screen. Say so
          loudly — otherwise these numbers read as current when they are not. */}
      {error && (
        <div className="card" style={{ padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 10, borderColor: 'var(--error)' }}>
          <Icons.AlertTri size={14} style={{ color: 'var(--error)', flexShrink: 0 }}/>
          <span className="t-small" style={{ flex: 1 }}>
            Couldn't refresh the graph — {error}. Everything below is from the last successful load.
          </span>
          <button className="btn sm" onClick={reload} disabled={loading}>
            <Icons.Refresh size={12}/> Retry
          </button>
        </div>
      )}

      {/* Aggregate stats strip */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
        {FAB_METRIC_CARDS.map(card => (
          <FabricMetricCard key={card.key} label={card.label} note={card.note} metric={stats[card.key]}/>
        ))}
      </div>

      {/* Two columns: left = provenance, right = graph hubs */}
      <div style={{ display: 'grid', gridTemplateColumns: '1.15fr 1fr', gap: 20 }}>

        {/* Where the knowledge came from */}
        <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
          <div style={{ padding: '14px 18px', borderBottom: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', gap: 10 }}>
            <Icons.Sparkle size={14} style={{ color: 'var(--accent)' }}/>
            <div style={{ fontSize: 13, fontWeight: 500 }}>Where this knowledge came from</div>
            <div style={{ flex: 1 }}/>
            <span className="mono-sm mono text-tertiary">
              {(stats.composition.tables + stats.composition.columns + stats.composition.documents
                + stats.composition.chunks + stats.composition.concepts).toLocaleString()} nodes
            </span>
          </div>
          <div style={{ padding: '6px 18px 14px' }}>
            {origins.map(o => (
              <div key={o.origin} style={{ marginTop: 12 }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
                  <span className="t-small">{o.label}</span>
                  <span className="mono-sm mono text-secondary">
                    {o.count.toLocaleString()} · {Math.round(o.share * 100)}%
                  </span>
                </div>
                <Progress value={Math.round(o.share * 100)} height={3}/>
              </div>
            ))}
          </div>
          <div style={{ padding: '10px 18px', borderTop: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', gap: 10, background: 'var(--bg)' }}>
            <span className="t-small text-secondary" style={{ flex: 1 }}>
              Producer recorded on each node by the backend.
            </span>
            <button className="btn sm ghost" onClick={() => setTab('catalog')}>Browse catalog →</button>
          </div>
        </div>

        {/* Most connected nodes */}
        <div className="card" style={{ padding: 0 }}>
          <div style={{ padding: '14px 18px', borderBottom: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center' }}>
            <div style={{ fontSize: 13, fontWeight: 500 }}>Most connected</div>
            <div style={{ flex: 1 }}/>
            <span className="mono-sm mono text-tertiary">by semantic edges</span>
          </div>
          {hubs.length === 0 ? (
            <div className="t-small text-tertiary" style={{ padding: 24, textAlign: 'center' }}>
              No semantic relationships yet — they appear as the graph links concepts to data.
            </div>
          ) : hubs.map(n => (
            <button key={n.id}
              onClick={() => openEntity && openEntity({ id: n.id, type: n.type, label: n.name, sub: fabOriginLabel(n.origin) })}
              style={{
                width: '100%', display: 'flex', alignItems: 'center', gap: 12,
                padding: '10px 18px', textAlign: 'left',
                borderBottom: '1px solid var(--border-subtle)',
              }}>
              <span className={`chip ${n.category === 'concept' ? 'accent' : ''}`} style={{ fontSize: 10, flexShrink: 0 }}>{n.type}</span>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{n.name}</div>
                <div className="mono-sm mono text-tertiary">{fabOriginLabel(n.origin)}</div>
              </div>
              <span className="mono-sm mono text-secondary" style={{ flexShrink: 0 }}>{n.degree}</span>
              <Icons.ChevronR size={12} style={{ color: 'var(--text-tertiary)', flexShrink: 0 }}/>
            </button>
          ))}
        </div>
      </div>

      {/* Recent learning */}
      <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
        <div style={{ padding: '14px 18px', borderBottom: '1px solid var(--border-subtle)', display: 'flex', alignItems: 'center', gap: 10 }}>
          <div style={{ fontSize: 13, fontWeight: 500 }}>Recent learning</div>
          <div style={{ flex: 1 }}/>
          <span className="mono-sm mono text-tertiary">
            {stats.learnings.last24h > 0
              ? `${stats.learnings.last24h.toLocaleString()} in the last 24h`
              : 'nothing in the last 24h'}
          </span>
          <button className="btn sm ghost" onClick={() => setTab('timeline')}>Timeline →</button>
        </div>
        {recent.length === 0 ? (
          <div className="t-small text-tertiary" style={{ padding: 24, textAlign: 'center' }}>
            No learning events recorded yet.
          </div>
        ) : recent.map(row => <LearningRowMini key={row.id} row={row}/>)}
      </div>
    </div>
  );
}

// One overview metric. The 24h delta and the sparkline are omitted (not zeroed)
// when the underlying rows carry no usable timestamps, so the card never
// implies a trend it can't evidence.
function FabricMetricCard({ label, note, metric }) {
  return (
    <div className="card" style={{ padding: 16 }}>
      <div className="t-micro text-tertiary" title={note}>{label}</div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginTop: 6 }}>
        <span style={{ fontSize: 22, fontWeight: 600, letterSpacing: '-0.01em' }}>
          {metric.total.toLocaleString()}
        </span>
        {metric.last24h > 0 && (
          <span className="mono-sm mono" style={{ color: 'var(--success)' }}>+{metric.last24h} · 24h</span>
        )}
      </div>
      {metric.spark ? (
        <div style={{ marginTop: 10 }}>
          <Sparkline data={metric.spark} color="var(--accent)" height={28} filled />
          <div className="mono-sm mono text-tertiary" style={{ fontSize: 10, marginTop: 4 }}>
            last {FAB_SPARK_DAYS} days
          </div>
        </div>
      ) : (
        <div className="mono-sm mono text-tertiary" style={{ fontSize: 10, marginTop: 14 }}>
          no dated activity in the last {FAB_SPARK_DAYS} days
        </div>
      )}
    </div>
  );
}

function ConfBar({ conf }) {
  const pct = Math.round(conf * 100);
  const color = conf >= 0.95 ? 'var(--success)' : conf >= 0.85 ? 'var(--accent)' : 'var(--warning)';
  return (
    <div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, marginTop: 2 }}>
      <div style={{ width: 36, height: 3, borderRadius: 2, background: 'var(--border-subtle)' }}>
        <div style={{ width: `${pct}%`, height: '100%', borderRadius: 2, background: color }}/>
      </div>
      <span className="mono-sm mono" style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>{conf.toFixed(2)}</span>
    </div>
  );
}

// A derived learning event (graph_learning.jsx), compact. The full row with
// undo / edit / delete lives on the Learning page.
function LearningRowMini({ row }) {
  const statusStyle = {
    auto:          { chip: 'accent',  text: 'Auto' },
    applied:       { chip: 'success', text: 'Applied' },
    needs_review:  { chip: 'warning', text: 'Review' },
  }[row.status] || { chip: '', text: row.status || 'Recorded' };
  return (
    <div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: '12px 18px', borderBottom: '1px solid var(--border-subtle)' }}>
      <div style={{ width: 28, flexShrink: 0 }}>
        <LearningGlyph type={row.type} />
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 13, fontWeight: 500 }}>{row.title}</div>
        <div className="t-small text-secondary" style={{ marginTop: 2, whiteSpace: 'pre-wrap' }}>{row.body}</div>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4, flexShrink: 0 }}>
        <span className="mono-sm mono text-tertiary">{formatRelativeTime(row.at)}</span>
        <span className={`chip ${statusStyle.chip}`} style={{ fontSize: 10 }}>{statusStyle.text}</span>
      </div>
    </div>
  );
}

function LearningGlyph({ type }) {
  const conf = {
    entity:   { color: '#0F766E', icon: <path d="M12 3l9 6v6l-9 6-9-6V9z M3 9l9 6 9-6 M12 15v6"/> },
    relation: { color: '#4F46E5', icon: <path d="M5 12h14 M13 6l6 6-6 6"/> },
    rerank:   { color: '#B45309', icon: <path d="M7 4v16 M13 8v12 M19 12v8 M3 20h18"/> },
    // dedupe subsumes what used to be a separate `synonym` type — both are concept merges
    dedupe:   { color: '#475569', icon: <><path d="M9 9h10v10H9z"/><path d="M5 5h10v10H5z" fill="rgba(71,85,105,0.12)"/></> },
    gap:      { color: '#B91C1C', icon: <path d="M12 3l10 18H2z M12 10v5M12 18v.5"/> },
    schema:   { color: '#6B21A8', icon: <><path d="M4 4h16v5H4z"/><path d="M4 11h16v5H4z"/><path d="M4 18h16v3H4z"/></> },
  }[type] || { color: '#6B7280', icon: <circle cx="12" cy="12" r="4"/> };
  return (
    <div style={{
      width: 28, height: 28, borderRadius: 7,
      background: conf.color + '18',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }}>
      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={conf.color} strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
        {conf.icon}
      </svg>
    </div>
  );
}

Object.assign(window, { FabricPage, FabricLoading, FabricError, FabricEmpty, LearningGlyph });
