// Screen-level components, wired to the live indexer API.
const { useState: uS, useEffect: uE, useMemo: uM, useRef: uR } = React;

// Helpers from boot.js. Read via window because Babel-standalone hoists
// top-level const/let to var on the global object, and a same-named local
// shim would shadow the real helper and recurse into itself.
const _powerUsd = (p) => (window.powerUsd ? window.powerUsd(p) : '');
const _stakeUsd = (l) => (window.stakeUsd ? window.stakeUsd(l) : '');

function useFetch(path, deps) {
  const [state, setState] = uS({ loading: true, data: null, error: null });
  uE(() => {
    let alive = true;
    setState({ loading: true, data: null, error: null });
    api(path)
      .then((d) => alive && setState({ loading: false, data: d, error: null }))
      .catch((e) => alive && setState({ loading: false, data: null, error: String(e) }));
    return () => { alive = false; };
  }, deps || [path]);
  return state;
}

// A lock only counts while its unlock time is still in the future
// ("" / epoch-zero / past timestamps mean unlocked).
function lockActive(v) {
  if (!v) return false;
  const t = new Date(v).getTime();
  return Number.isFinite(t) && t > Date.now();
}
function LockCell({ until }) {
  return lockActive(until)
    ? <span className="lock-pill"><LockIcon />{dt(until)}</span>
    : <span className="muted small">—</span>;
}

function Loading({ label }) {
  return <div className="boot" style={{ height: 220 }}><span className="spin" />{label || 'Loading…'}</div>;
}
function ErrorBox({ msg }) {
  return <div className="card" style={{ padding: 22 }}><span className="muted">Could not load: {msg}</span></div>;
}

// =========================== Reporters ===========================
function ReportersScreen({ navigate }) {
  // Default to Performance — that's the view selectors come here for.
  const [sub, setSub] = uS('perf');
  return (
    <div className="screen">
      <div className="screen-head">
        <div><h2 className="screen-title">Reporters</h2></div>
      </div>
      <div className="subtabs">
        <button className={'subtabs-btn' + (sub === 'perf' ? ' active' : '')} onClick={() => setSub('perf')}>
          Performance
        </button>
        <button className={'subtabs-btn' + (sub === 'stake' ? ' active' : '')} onClick={() => setSub('stake')}>
          Stake
        </button>
      </div>
      {sub === 'stake' ? <ReportersStakeView navigate={navigate} /> : <ReportersPerfView navigate={navigate} />}
    </div>
  );
}

function ReportersStakeView({ navigate }) {
  const { loading, data, error } = useFetch('/api/reporters');
  // List of reporters seen submitting anything in the last ~24h. Used to
  // downgrade "active" -> "Not reported recently" for registered
  // reporters who haven't actually posted in a while.
  const active24h = useFetch('/api/reporters/active-24h');
  const [sort, setSort] = uS('power');
  if (loading) return <Loading label="Loading reporters…" />;
  if (error) return <ErrorBox msg={error} />;
  const rows = [...(data || [])].sort((a, b) => {
    if (sort === 'selectors') return b.num_selectors - a.num_selectors;
    if (sort === 'commission') return Number(b.commission_rate) - Number(a.commission_rate);
    return Number(b.power) - Number(a.power);
  });
  // Share = % of total reporting power, so the biggest reporter shows
  // its actual network share (e.g. 30%) instead of 100%.
  const total = Math.max(1, rows.reduce((s, r) => s + Number(r.power || 0), 0));
  const activeSet = new Set(active24h.data || []);
  // If the active-24h endpoint hasn't returned anything yet (cold start)
  // skip the downgrade so we don't flag everyone as idle.
  const knowsActivity = activeSet.size > 0;
  const statusOf = (r) => r.jailed ? 'jailed'
    : (knowsActivity && !activeSet.has(r.reporter_address)) ? 'idle'
    : 'active';
  return (
    <React.Fragment>
      <div className="card table-card">
        <table className="t">
          <thead>
            <tr>
              <th style={{ width: 44 }}>#</th><th>Reporter</th>
              <th className={'right th-sort' + (sort === 'power' ? ' on' : '')} onClick={() => setSort('power')}>Power{sort === 'power' ? ' ▾' : ''}</th>
              <th className="right" style={{ width: 90, whiteSpace: 'nowrap' }}>Power %</th>
              <th className={'right th-sort' + (sort === 'commission' ? ' on' : '')} onClick={() => setSort('commission')}>Commission{sort === 'commission' ? ' ▾' : ''}</th>
              <th className={'right th-sort' + (sort === 'selectors' ? ' on' : '')} onClick={() => setSort('selectors')}>Selectors{sort === 'selectors' ? ' ▾' : ''}</th>
              <th>Trend</th><th>Status</th>
            </tr>
          </thead>
          <tbody>
            {rows.map((r) => (
              <tr key={r.reporter_address} className="row-clickable" onClick={() => navigate('reporter/' + r.reporter_address)}>
                <td className="rank">{r.rank}</td>
                <td><AddressBlock addr={r.reporter_address} moniker={r.moniker}
                  to={'reporter/' + r.reporter_address} navigate={navigate} /></td>
                <td className="right mono">{fmt(r.power)}
                  <div className="usd-sub muted small">{_powerUsd(r.power)}</div>
                </td>
                <td className="right mono">{(Number(r.power) / total * 100).toFixed(1)}%</td>
                <td className="right mono">{(Number(r.commission_rate) * 100).toFixed(1)}%</td>
                <td className="right mono">{r.num_selectors}</td>
                <td><Sparkline data={r.spark} width={110} height={28} /></td>
                <td><StatusPill status={statusOf(r)} /></td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </React.Fragment>
  );
}

function ReportersPerfView({ navigate }) {
  const { loading, data, error } = useFetch('/api/reporters/perf');
  const [win, setWin] = uS('1');
  const reporterIndex = useFetch('/api/reporters'); // for power / commission / selectors / status
  if (loading) return <Loading label="Loading performance…" />;
  if (error) return <ErrorBox msg={error} />;
  const rows = (data && data.rows) || [];
  const byAddr = new Map((reporterIndex.data || []).map((r) => [r.reporter_address, r]));
  const W = ['1', '5', '15', '30'];
  const rK = 'r_' + win;
  const mK = 'm_' + win;
  // Sort by reports desc — most-active reporters at the top.
  const sorted = [...rows].sort((a, b) => Number(b[rK] || 0) - Number(a[rK] || 0));
  return (
    <React.Fragment>
      <div className="seg" style={{ marginBottom: 14 }}>
        {W.map((w) => (
          <button key={w} className={'seg-btn' + (win === w ? ' active' : '')} onClick={() => setWin(w)}>{w}D</button>
        ))}
      </div>
      <div className="card table-card">
        <table className="t">
          <thead><tr>
            <th style={{ width: 44 }}>#</th>
            <th>Reporter</th>
            <th className="right">Commission</th>
            <th className="right" style={{ width: 110 }}>Reliability</th>
            <th className="right">Reported</th>
            <th className="right">Missed</th>
            <th>Status</th>
          </tr></thead>
          <tbody>
            {sorted.length === 0 && (
              <tr><td colSpan="7" className="muted" style={{ padding: '24px 18px' }}>
                No data yet — performance is tracked going forward only. Come back after a few hours.
              </td></tr>
            )}
            {sorted.map((r, i) => {
              const reported = Number(r[rK] || 0);
              const missed = Number(r[mK] || 0);
              const denom = reported + missed;
              const rel = denom > 0 ? (reported / denom) * 100 : null;
              const meta = byAddr.get(r.reporter);
              const moniker = meta ? meta.moniker : '';
              const comm = meta ? Number(meta.commission_rate) : null;
              const jailed = meta ? meta.jailed : false;
              const relCol = rel == null ? 'var(--mut)' : rel >= 99 ? '#4ade80' : rel >= 95 ? '#fbbf24' : '#fb7185';
              return (
                <tr key={r.reporter} className="row-clickable" onClick={() => navigate('reporter/' + r.reporter)}>
                  <td className="rank">{i + 1}</td>
                  <td><AddressBlock addr={r.reporter} moniker={moniker}
                    to={'reporter/' + r.reporter} navigate={navigate} /></td>
                  <td className="right mono">{comm == null ? '—' : (comm * 100).toFixed(1) + '%'}</td>
                  <td className="right mono"><b style={{ color: relCol }}>{rel == null ? '—' : rel.toFixed(2) + '%'}</b></td>
                  <td className="right mono">{fmt(reported)}</td>
                  <td className="right mono">{missed > 0 ? <b style={{ color: '#fb7185' }}>{fmt(missed)}</b> : '0'}</td>
                  <td><StatusPill status={jailed ? 'jailed' : 'active'} /></td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </React.Fragment>
  );
}

// =========================== Reporter Detail ===========================
function ReporterDetail({ addr, navigate }) {
  const { loading, data, error } = useFetch('/api/reporter/' + addr, [addr]);
  if (loading) return <Loading label="Loading reporter…" />;
  if (error || !data || !data.reporter) return <ErrorBox msg={error || 'not found'} />;
  const r = data.reporter;
  return (
    <div className="screen">
      <a className="back" {...navLink(navigate, 'reporters')}>
        <svg width="13" height="13" viewBox="0 0 14 14"><path d="M9 2L4 7l5 5" stroke="currentColor" fill="none" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /></svg>
        All reporters
      </a>
      <div className="card hero">
        <div className="hero-main">
          <div style={{ minWidth: 0 }}>
            <div className="hero-name">{r.moniker || 'Reporter'}<StatusPill status={r.jailed ? 'jailed' : 'active'} /></div>
            <div className="hero-addr mono">{r.reporter_address}</div>
          </div>
        </div>
        <div className="hero-stats">
          <div><div className="hero-stat-l">Power</div>
            <div className="hero-stat-v">{fmt(r.power)}</div>
            <div className="usd-sub muted small">{_powerUsd(r.power)}</div></div>
          <div><div className="hero-stat-l">Commission</div><div className="hero-stat-v">{(Number(r.commission_rate) * 100).toFixed(1)}%</div></div>
          <div><div className="hero-stat-l">Selectors</div><div className="hero-stat-v">{r.num_selectors}</div></div>
          <div><div className="hero-stat-l">As of block</div><div className="hero-stat-v mono">{fmt(r.height)}</div></div>
        </div>
      </div>
      <div className="grid-2">
        <div className="card chart-card"><AreaChart points={data.history} valueFn={(p) => Number(p.power)} fmtVal={compact} label="Power over time" /></div>
        <div className="card chart-card"><AreaChart points={data.history} valueFn={(p) => Number(p.num_selectors)} fmtVal={fmt} label="Selector count over time" /></div>
      </div>
      <div className="card table-card">
        <div className="card-head">
          <div><div className="card-title">Current selectors</div>
            <div className="muted small">{data.selectors.length} addresses delegating to this reporter</div></div>
        </div>
        <table className="t">
          <thead><tr><th>Selector</th><th className="right">Stake (TRB)</th><th>Locked until</th></tr></thead>
          <tbody>
            {data.selectors.length === 0 && <tr><td colSpan="3" className="muted">None</td></tr>}
            {data.selectors.map((s) => (
              <tr key={s.selector_address} className="row-clickable" onClick={() => navigate('selector/' + s.selector_address)}>
                <td><AddressBlock addr={s.selector_address}
                  to={'selector/' + s.selector_address} navigate={navigate} /></td>
                <td className="right mono">{trb(s.stake_amount)}
                  <div className="usd-sub muted small">{_stakeUsd(s.stake_amount)}</div>
                </td>
                <td><LockCell until={s.locked_until} /></td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <div className="card table-card section-gap">
        <div className="card-head"><div><div className="card-title">Transactions</div></div></div>
        <DetailTxs rows={data.events} navigate={navigate} viewer={addr} />
      </div>
    </div>
  );
}

// =========================== Selectors ===========================
function SelectorsScreen({ navigate }) {
  const { loading, data, error } = useFetch('/api/selectors');
  const [q, setQ] = uS('');
  if (loading) return <Loading label="Loading selectors…" />;
  if (error) return <ErrorBox msg={error} />;
  const filt = (data || []).filter((s) => !q ||
    s.selector_address.toLowerCase().includes(q.toLowerCase()) ||
    (s.reporter_moniker || '').toLowerCase().includes(q.toLowerCase()));
  return (
    <div className="screen">
      <div className="screen-head">
        <div><h2 className="screen-title">Selectors</h2></div>
        <div className="search-inline"><SearchIcon />
          <input placeholder="Filter selector or reporter…" value={q} onChange={(e) => setQ(e.target.value)} /></div>
      </div>
      <div className="card table-card">
        <table className="t">
          <thead><tr><th>Selector</th><th>Selecting reporter</th><th className="right">Stake (TRB)</th><th>Locked until</th></tr></thead>
          <tbody>
            {filt.map((s) => (
              <tr key={s.selector_address} className="row-clickable" onClick={() => navigate('selector/' + s.selector_address)}>
                <td><AddressBlock addr={s.selector_address}
                  to={'selector/' + s.selector_address} navigate={navigate} /></td>
                <td><AddressBlock addr={s.reporter_address} moniker={s.reporter_moniker}
                  to={'reporter/' + s.reporter_address} navigate={navigate} /></td>
                <td className="right mono">{trb(s.stake_amount)}
                  <div className="usd-sub muted small">{_stakeUsd(s.stake_amount)}</div>
                </td>
                <td><LockCell until={s.locked_until} /></td>
              </tr>
            ))}
            {filt.length === 0 && <tr><td colSpan="4" className="muted">No selectors match.</td></tr>}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// =========================== Selector Detail ===========================
function SelectorDetail({ addr, navigate }) {
  const { loading, data, error } = useFetch('/api/selector/' + addr, [addr]);
  const [tab, setTab] = uS('activity');
  const [rangeSel, setRangeSel] = uS('30');
  const [customDays, setCustomDays] = uS('90');
  if (loading) return <Loading label="Loading selector…" />;
  if (error || !data) return <ErrorBox msg={error || 'not found'} />;
  const cur = (data.current && data.current[0]) || {};
  const claims = data.claims || [];
  const stake = Number(cur.stake_amount) || 0;
  const totalClaimed = claims.reduce((s, c) => s + (Number(c.amount) || 0), 0);
  const now = Date.now();
  const sumSince = (days) => {
    const cut = now - days * 864e5;
    return claims.reduce((s, c) =>
      s + (new Date(c.block_time).getTime() >= cut ? (Number(c.amount) || 0) : 0), 0);
  };
  const pct = (v) => (Number.isFinite(v) && v >= 0 ? v.toFixed(v < 10 ? 2 : 1) + '%' : '—');
  const apr = (days) => (stake > 0
    ? pct((sumSince(days) * (365 / days)) / stake * 100) : '—');
  const apr7 = apr(7);
  const apr30 = apr(30);
  const claimDaily = (() => {
    const m = {};
    for (const c of claims) {
      const d = String(c.block_time || '').slice(0, 10);
      if (d) m[d] = (m[d] || 0) + (Number(c.amount) || 0);
    }
    return Object.keys(m).sort()
      .map((d) => ({ block_time: d + 'T00:00:00Z', amount: m[d] }));
  })();
  const rangeDays = rangeSel === 'custom'
    ? Math.max(1, parseInt(customDays, 10) || 1) : Number(rangeSel);
  const chartFrom = now - rangeDays * 864e5;
  const claimChart = claimDaily.filter((p) => new Date(p.block_time).getTime() >= chartFrom);
  return (
    <div className="screen">
      <a className="back" {...navLink(navigate, 'selectors')}>
        <svg width="13" height="13" viewBox="0 0 14 14"><path d="M9 2L4 7l5 5" stroke="currentColor" fill="none" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /></svg>
        All selectors
      </a>
      <div className="card hero">
        <div className="hero-main">
          <div style={{ minWidth: 0 }}>
            <div className="hero-name">Selector</div>
            <div className="hero-addr mono">{addr}</div>
          </div>
        </div>
        <div className="hero-stats">
          <div><div className="hero-stat-l">Selecting</div>
            <div className="hero-stat-v">{cur.reporter_address
              ? <a className="link" {...navLink(navigate, 'reporter/' + cur.reporter_address)}>{cur.reporter_moniker || short(cur.reporter_address)}</a>
              : '—'}</div></div>
          <div><div className="hero-stat-l">Stake</div>
            <div className="hero-stat-v">{trb(cur.stake_amount)} <span className="unit">TRB</span></div>
            <div className="usd-sub muted small">{_stakeUsd(cur.stake_amount)}</div></div>
          <div><div className="hero-stat-l">Locked until</div>
            <div className="hero-stat-v small">{lockActive(cur.locked_until)
              ? <span className="lock-pill">{dt(cur.locked_until)}</span>
              : <span className="muted">Unlocked</span>}</div></div>
          <div><div className="hero-stat-l">As of block</div><div className="hero-stat-v mono">{fmt(cur.height)}</div></div>
        </div>
      </div>
      <div className="seg" style={{ margin: '16px 0' }}>
        <button className={'seg-btn' + (tab === 'activity' ? ' active' : '')} onClick={() => setTab('activity')}>Activity</button>
        <button className={'seg-btn' + (tab === 'earnings' ? ' active' : '')} onClick={() => setTab('earnings')}>Earnings</button>
      </div>

      {tab === 'activity' && (
        <React.Fragment>
          <div className="card chart-card">
            <AreaChart points={data.history} valueFn={(p) => Number(p.stake_amount) / 1e6} fmtVal={compact} label="Delegation change" height={200} />
          </div>
          <div className="card table-card section-gap">
            <div className="card-head"><div><div className="card-title">Transactions</div></div></div>
            <DetailTxs rows={data.events} navigate={navigate} />
          </div>
        </React.Fragment>
      )}

      {tab === 'earnings' && (
        <React.Fragment>
          <div className="card hero">
            <div className="hero-stats" style={{ width: '100%', justifyContent: 'space-between' }}>
              <div><div className="hero-stat-l">Available to claim</div>
                <div className="hero-stat-v">{trb(cur.available_tips)} <span className="unit">TRB</span></div></div>
              <div><div className="hero-stat-l">Total claimed</div>
                <div className="hero-stat-v">{trb(totalClaimed)} <span className="unit">TRB</span></div></div>
              <div><div className="hero-stat-l">APR · 7-day</div>
                <div className="hero-stat-v" style={{ color: 'var(--accent)' }}>{apr7}</div></div>
              <div><div className="hero-stat-l">APR · 30-day</div>
                <div className="hero-stat-v" style={{ color: 'var(--accent)' }}>{apr30}</div></div>
              <div><div className="hero-stat-l">Claims</div>
                <div className="hero-stat-v">{fmt(claims.length)}</div></div>
            </div>
          </div>
          <div className="card chart-card">
            <div className="card-head" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
              <div className="card-title">Daily tip claims (TRB)</div>
              <div className="seg" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
                {[['7', '7d'], ['30', '30d'], ['custom', 'Custom']].map(([k, l]) => (
                  <button key={k} className={'seg-btn' + (rangeSel === k ? ' active' : '')} onClick={() => setRangeSel(k)}>{l}</button>
                ))}
                {rangeSel === 'custom' && (
                  <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}>
                    <input type="number" min="1" value={customDays}
                      onChange={(e) => setCustomDays(e.target.value)}
                      style={{ width: 64 }} aria-label="Custom days" />
                    <span className="muted small">days</span>
                  </span>
                )}
              </div>
            </div>
            <AreaChart points={claimChart} valueFn={(p) => p.amount / 1e6} fmtVal={compact} label={`Last ${rangeDays} days`} height={200} />
          </div>
          <div className="card table-card section-gap">
            <div className="card-head"><div><div className="card-title">Tip claims</div></div></div>
            <table className="t">
              <thead><tr>
                <th style={{ width: 170 }}>Block</th><th className="right">Amount</th>
                <th>Validator</th><th style={{ width: 96 }}>Tx</th>
              </tr></thead>
              <tbody>
                {claims.length === 0 && <tr><td colSpan="4" className="muted" style={{ padding: '24px 18px' }}>No tip claims yet.</td></tr>}
                {claims.map((c, i) => {
                  const ex = window.EXPLORER ? window.EXPLORER + c.tx_hash : null;
                  return (
                    <tr key={c.tx_hash + i}>
                      <td><div className="block-cell">
                        <span className="mono">{fmt(c.height)}</span>
                        <span className="muted small" title={dt(c.block_time)}>{ago(c.block_time)}</span>
                      </div></td>
                      <td className="right"><b className="mono">{trb(c.amount)}</b> <span className="unit">TRB</span></td>
                      <td><span className="mono muted" title={c.validator}>{short(c.validator)}</span></td>
                      <td>{ex
                        ? <a className="tx-link mono" href={ex} target="_blank" rel="noopener">{c.tx_hash.slice(0, 8)}…<ExtIcon /></a>
                        : <span className="tx-link mono">{c.tx_hash.slice(0, 8)}…</span>}</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        </React.Fragment>
      )}
    </div>
  );
}

// =========================== Transactions (global) ===========================
const KIND_OPTS = [
  { key: 'create_reporter', label: 'Create reporter' },
  { key: 'select_reporter', label: 'Select reporter' },
  { key: 'switch_reporter', label: 'Switch reporter' },
  { key: 'delegate', label: 'Delegate' },
  { key: 'undelegate', label: 'Undelegate' },
  { key: 'redelegate', label: 'Redelegate' },
];

function TxsScreen({ navigate }) {
  const [kinds, setKinds] = uS([]);
  const [page, setPage] = uS(1);
  const limit = 25;
  uE(() => { setPage(1); }, [kinds]);
  const qs = `kinds=${kinds.join(',')}&page=${page}&limit=${limit}`;
  const { loading, data, error } = useFetch('/api/txs?' + qs, [qs]);
  const total = data ? data.total : 0;
  const pages = Math.max(1, Math.ceil(total / limit));
  const typeFilter = <ColumnFilter label="Type" options={KIND_OPTS} value={kinds} onChange={setKinds} />;
  return (
    <div className="screen">
      <div className="screen-head">
        <div><h2 className="screen-title">Transactions</h2></div>
      </div>
      <div className="card">
        {error ? <ErrorBox msg={error} />
          : (loading && !data) ? <Loading label="Loading transactions…" />
          : <TxTable rows={data ? data.rows : []} navigate={navigate} typeFilter={typeFilter} />}
        <div className="pager">
          <span className="muted small">{fmt(total)} txs · page {page} / {fmt(pages)}</span>
          <div className="pager-btns">
            <button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}>← Prev</button>
            <button onClick={() => setPage((p) => Math.min(pages, p + 1))} disabled={page >= pages}>Next →</button>
          </div>
        </div>
      </div>
    </div>
  );
}

// =========================== Tx Table (shared) ===========================
// Deterministic routing: a wallet (the acting selector) always opens the
// wallet page; a reporter (moniker) always opens the reporter page.
function PartyCell({ e, navigate }) {
  const W = (a) => 'selector/' + a;
  const R = (a) => 'reporter/' + a;
  const arrow = <span className="arrow">→</span>;
  const dot = <span className="arrow">·</span>;
  const re = /tellor1[0-9a-z]{20,}/g, rev = /tellorvaloper1[0-9a-z]+/g;

  if (e.kind === 'create_reporter')
    return <AddressBlock addr={e.reporter_address} to={R(e.reporter_address)} navigate={navigate} />;

  if (e.kind === 'select_reporter')
    return <div className="parties">
      <AddressBlock addr={e.selector_address} to={W(e.selector_address)} navigate={navigate} />{arrow}
      <AddressBlock addr={e.reporter_address} to={R(e.reporter_address)} navigate={navigate} />
    </div>;

  if (e.kind === 'switch_reporter') {
    const m = (e.detail || '').match(re) || [];
    const prev = m[0];
    const next = e.reporter_address || m[1];
    return <div className="parties">
      <AddressBlock addr={e.selector_address} to={W(e.selector_address)} navigate={navigate} />
      {dot}
      {prev && <AddressBlock addr={prev} to={R(prev)} navigate={navigate} />}
      {prev && next && arrow}
      {next && <AddressBlock addr={next} to={R(next)} navigate={navigate} />}
    </div>;
  }

  if (e.kind === 'redelegate') {
    const v = (e.detail || '').match(rev) || [];
    return <div className="parties">
      <AddressBlock addr={e.selector_address} to={W(e.selector_address)} navigate={navigate} />
      {v.length >= 2 && <React.Fragment>{dot}
        <AddressBlock addr={v[0]} to={'validator/' + v[0]} navigate={navigate} />
        {arrow}
        <AddressBlock addr={v[1]} to={'validator/' + v[1]} navigate={navigate} />
      </React.Fragment>}
    </div>;
  }

  // delegate / undelegate: wallet -> validator
  return <div className="parties">
    <AddressBlock addr={e.selector_address} to={W(e.selector_address)} navigate={navigate} />
    {e.validator && <React.Fragment>{arrow}
      <AddressBlock addr={e.validator} to={'validator/' + e.validator} navigate={navigate} />
    </React.Fragment>}
  </div>;
}

// Header-anchored type filter (replaces the toolbar button).
function ColumnFilter({ label, options, value, onChange }) {
  const [open, setOpen] = uS(false);
  const ref = uR(null);
  uE(() => {
    const c = (ev) => { if (ref.current && !ref.current.contains(ev.target)) setOpen(false); };
    document.addEventListener('mousedown', c);
    return () => document.removeEventListener('mousedown', c);
  }, []);
  const toggle = (k) => {
    const s = new Set(value);
    s.has(k) ? s.delete(k) : s.add(k);
    onChange([...s]);
  };
  return (
    <span className="colf" ref={ref}>
      <button className={'colf-btn' + (value.length ? ' on' : '')} onClick={() => setOpen((o) => !o)}>
        {label}{value.length > 0 && <span className="colf-n">{value.length}</span>}
        <svg width="9" height="9" viewBox="0 0 10 10"><path d="M2 3.5l3 3 3-3" stroke="currentColor" strokeWidth="1.6" fill="none" strokeLinecap="round" strokeLinejoin="round" /></svg>
      </button>
      {open && (
        <div className="dd-panel colf-panel" onClick={(e) => e.stopPropagation()}>
          <div className="dd-list">
            {options.map((o) => (
              <label key={o.key} className="dd-opt">
                <input type="checkbox" checked={value.includes(o.key)} onChange={() => toggle(o.key)} />
                <span>{o.label}</span>
              </label>
            ))}
          </div>
          <div className="dd-foot">
            <button onClick={() => onChange([])}>Clear</button>
            <button className="primary" onClick={() => setOpen(false)}>Done</button>
          </div>
        </div>
      )}
    </span>
  );
}

// Detail-page tx table: type filter lives in the column header.
function DetailTxs({ rows, navigate, viewer }) {
  const [kinds, setKinds] = uS([]);
  const filt = (rows || []).filter((e) => !kinds.length || kinds.includes(e.kind));
  const typeFilter = <ColumnFilter label="Type" options={KIND_OPTS} value={kinds} onChange={setKinds} />;
  return <TxTable rows={filt} compact navigate={navigate} typeFilter={typeFilter} viewer={viewer} />;
}

// Decide how a switch_reporter event should be labelled relative to the
// reporter being viewed: an inbound switch (this reporter gained a
// selector) or an outbound one (lost a selector). Falls back to the
// generic kind when there's no viewer context (e.g. the global tx feed
// or a selector detail page).
function displayKind(e, viewer) {
  if (!viewer || e.kind !== 'switch_reporter') return e.kind;
  if (e.reporter_address === viewer) return 'switch_in';
  const m = (e.detail || '').match(/tellor1[0-9a-z]{20,}/);
  if (m && m[0] === viewer) return 'switch_out';
  return e.kind;
}

function TxTable({ rows, compact, navigate, typeFilter, viewer }) {
  let last = '';
  return (
    <table className="t">
      <thead><tr>
        <th style={{ width: compact ? 130 : 170 }}>Block</th>
        <th>{typeFilter || 'Type'}</th>
        <th className="right">Amount</th><th>Parties</th><th style={{ width: 96 }}>Tx</th>
      </tr></thead>
      <tbody>
        {(!rows || !rows.length) && (
          <tr><td colSpan="5" className="muted" style={{ padding: '24px 18px' }}>No transactions match the filters.</td></tr>
        )}
        {(rows || []).map((e, i) => {
          const day = new Date(e.block_time).toDateString();
          const showDay = !compact && day !== last;
          last = day;
          const ex = window.EXPLORER ? window.EXPLORER + e.tx_hash : null;
          return (
            <React.Fragment key={e.tx_hash + i}>
              {showDay && <tr className="day-row"><td colSpan="5">
                <span className="day-chip">{new Date(e.block_time).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })}</span>
              </td></tr>}
              <tr>
                <td><div className="block-cell">
                  <span className="mono">{fmt(e.height)}</span>
                  <span className="muted small" title={dt(e.block_time)}>{ago(e.block_time)}</span>
                </div></td>
                <td><TxBadge kind={displayKind(e, viewer)} /></td>
                <td className="right">{e.amount
                  ? <span><b className="mono">{trb(e.amount)}</b> <span className="unit">TRB</span></span>
                  : <span className="muted">—</span>}</td>
                <td><PartyCell e={e} navigate={navigate} /></td>
                <td>{ex
                  ? <a className="tx-link mono" href={ex} target="_blank" rel="noopener">{e.tx_hash.slice(0, 8)}…<ExtIcon /></a>
                  : <span className="tx-link mono">{e.tx_hash.slice(0, 8)}…</span>}</td>
              </tr>
            </React.Fragment>
          );
        })}
      </tbody>
    </table>
  );
}

function SearchIcon() { return <svg width="14" height="14" viewBox="0 0 14 14"><circle cx="6" cy="6" r="4.2" stroke="currentColor" fill="none" strokeWidth="1.5" /><path d="M9.5 9.5l3 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /></svg>; }
function LockIcon() { return <svg width="11" height="11" viewBox="0 0 12 12"><rect x="2.5" y="5.5" width="7" height="5" rx="1" stroke="currentColor" fill="none" strokeWidth="1.2" /><path d="M4 5.5V3.5a2 2 0 0 1 4 0V5.5" stroke="currentColor" fill="none" strokeWidth="1.2" strokeLinecap="round" /></svg>; }
function ExtIcon() { return <svg width="10" height="10" viewBox="0 0 10 10" style={{ marginLeft: 4, opacity: 0.6 }}><path d="M3.5 2h4.5v4.5M8 2L3.5 6.5M3 4v4h4" stroke="currentColor" fill="none" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" /></svg>; }

// =========================== Reports (live) ===========================
const RING_MS = 30 * 60 * 1000;

function priceFmt(v) {
  if (v == null || !Number.isFinite(v)) return '—';
  const a = Math.abs(v);
  if (a >= 1000) return v.toLocaleString(undefined, { maximumFractionDigits: 2 });
  if (a >= 1) return v.toFixed(4);
  if (a === 0) return '0';
  return v.toPrecision(4);
}

function useReportStream() {
  const [state, setState] = uS({ reports: [], misses: [], head: 0, connected: false });
  uE(() => {
    const es = new EventSource('/api/reports/stream');
    let ring = [];
    let misses = [];
    let head = 0;
    const pruneArr = (a, cap) => {
      const cut = Date.now() - RING_MS;
      if (a.length > cap) a = a.slice(a.length - cap);
      let i = 0;
      while (i < a.length && a[i].ts < cut) i++;
      return i ? a.slice(i) : a;
    };
    const flush = () => {
      ring = pruneArr(ring, 8000);
      misses = pruneArr(misses, 2000);
      setState({ reports: ring.slice(), misses: misses.slice(), head, connected: true });
    };
    es.onerror = () => setState((s) => ({ ...s, connected: false }));
    es.onmessage = (ev) => {
      let m; try { m = JSON.parse(ev.data); } catch { return; }
      if (m.type === 'snapshot') {
        ring = (m.reports || []).slice();
        misses = (m.misses || []).slice();
        head = m.head || head;
      } else if (m.type === 'block') {
        ring = ring.concat(m.reports || []);
        head = m.height || head;
      } else if (m.type === 'miss') {
        misses = misses.concat(m.misses || []);
      } else return;
      flush();
    };
    return () => es.close();
  }, []);
  return state;
}

function RTag({ addr, navigate, compact }) {
  let name = (window.MON && window.MON[addr]) || (addr ? addr.slice(0, 9) + '…' : '');
  if (compact && name.length > 12) name = name.slice(0, 11) + '…';
  return <a className={'link rtag' + (compact ? ' mono' : '')}
    title={(window.MON && window.MON[addr]) ? addr : ''}
    {...navLink(navigate, 'reporter/' + addr)}>{name}</a>;
}

// Reporter row used in the bridge deposit detail. Plain colour-coded
// text (green = voted, red = didn't report) instead of pill chips.
function RepChip({ addr, power, tone, navigate }) {
  if (!addr) return null;
  const name = (window.MON && window.MON[addr]) || (addr.slice(0, 9) + '…' + addr.slice(-4));
  return (
    <div className={'rep-row rep-' + tone}>
      <a title={addr} {...navLink(navigate, 'reporter/' + addr)}>
        <span className="rep-name">{name}</span>
      </a>
      {power != null && Number(power) > 0 && (
        <span className="rep-power">power {compact(Number(power))}</span>
      )}
    </div>
  );
}

function ReportsScreen({ navigate }) {
  const [sub, setSub] = uS('reports');
  const { reports, misses, head, connected } = useReportStream();

  const { blocks, reporters, totalReporters, byCell, rows, powerMap } = uM(() => {
    const bset = new Set(); const rstat = new Map();
    for (const r of reports) {
      bset.add(r.height);
      const s = rstat.get(r.reporter) || { n: 0, p: 0 };
      s.n += 1; s.p = Math.max(s.p, r.power);
      rstat.set(r.reporter, s);
    }
    const blocks = [...bset].sort((a, b) => b - a).slice(0, 60);
    const bkeep = new Set(blocks);
    // Reporters ordered by voting power desc (highest on the left), with
    // report frequency as a tie-break. Keep the active set capped at 12.
    const ranked = [...rstat.entries()].sort((a, b) => b[1].p - a[1].p || b[1].n - a[1].n);
    const totalReporters = ranked.length;
    const reporters = ranked.slice(0, 12).map((e) => e[0]);
    const powerMap = new Map([...rstat.entries()].map(([a, s]) => [a, s.p]));
    const byCell = new Map();       // `${h}|${rep}` -> [{symbol,base}]
    const rowMap = new Map();       // `${h}|${qid}` -> {height,symbol,base,vals:{rep:value}}
    for (const r of reports) {
      if (!bkeep.has(r.height)) continue;
      const ck = r.height + '|' + r.reporter;
      (byCell.get(ck) || byCell.set(ck, []).get(ck)).push(r);
      const rk = r.height + '|' + r.query_id;
      let row = rowMap.get(rk);
      if (!row) { row = { height: r.height, symbol: r.symbol, base: r.base, vals: {} }; rowMap.set(rk, row); }
      row.vals[r.reporter] = r.value;
    }
    // Per-row median across reporting values — used to highlight reporters
    // whose value drifts from what everyone else submitted that block.
    const rows = [...rowMap.values()]
      .sort((a, b) => b.height - a.height || a.symbol.localeCompare(b.symbol))
      .slice(0, 200)
      .map((row) => {
        const arr = Object.values(row.vals).filter((v) => Number.isFinite(v) && v > 0);
        let median = null;
        if (arr.length) {
          const s = arr.slice().sort((a, b) => a - b);
          const mid = s.length >> 1;
          median = s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
        }
        return { ...row, median };
      });
    return { blocks, reporters, byCell, rows, powerMap, totalReporters };
  }, [reports]);

  // Live missed-report grid (same axes as Reports: blocks x reporters,
  // cells = asset chips for missed rounds). Only blocks where at least
  // one miss happened are rendered, so the table stays short.
  const missGrid = uM(() => {
    const bset = new Set();
    for (const m of misses) bset.add(m.height);
    const mBlocks = [...bset].sort((a, b) => b - a).slice(0, 60);
    const bkeep = new Set(mBlocks);
    const missByCell = new Map();
    for (const m of misses) {
      if (!bkeep.has(m.height)) continue;
      const ck = m.height + '|' + m.reporter;
      (missByCell.get(ck) || missByCell.set(ck, []).get(ck)).push(m);
    }
    return { mBlocks, missByCell };
  }, [misses]);

  const st = (k, l) => (
    <button className={'seg-btn' + (sub === k ? ' active' : '')} onClick={() => setSub(k)}>{l}</button>
  );

  return (
    <div className="screen">
      <div className="screen-head">
        <div><h2 className="screen-title">Reports</h2>
          <p className="screen-sub muted">
            Live oracle reports · head {fmt(head)} ·{' '}
            <span style={{ color: connected ? 'var(--accent)' : '#fb7185' }}>
              {connected ? '● live' : '○ reconnecting'}
            </span> · {fmt(reports.length)} in last 30 min
            {sub !== 'summary' && totalReporters > reporters.length &&
              <> · top {reporters.length} of {totalReporters} reporters</>}
          </p></div>
      </div>
      <div className="seg" style={{ margin: '4px 0 16px' }}>
        {st('reports', 'Reports')}{st('values', 'Values')}{st('missed', 'Missed')}{st('summary', 'Missed Summary')}
      </div>

      {(sub === 'reports' || sub === 'values') && reports.length === 0 && (
        <div className="card"><div className="muted" style={{ padding: '28px 18px' }}>Waiting for live reports…</div></div>
      )}

      {sub === 'reports' && reports.length > 0 && (
        <div className="card rgrid-wrap">
          <table className="t rgrid">
            <thead><tr>
              <th className="rgrid-stick">Block</th>
              {reporters.map((rp) => <th key={rp}><RTag addr={rp} navigate={navigate} compact /></th>)}
            </tr></thead>
            <tbody>
              {blocks.map((h) => (
                <tr key={h}>
                  <td className="rgrid-stick mono">{fmt(h)}</td>
                  {reporters.map((rp) => {
                    const cell = byCell.get(h + '|' + rp) || [];
                    return <td key={rp}>{cell.map((r, i) => <CoinChip key={i} symbol={r.symbol} base={r.base} size={16} />)}</td>;
                  })}
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}

      {sub === 'values' && reports.length > 0 && (
        <div className="card rgrid-wrap">
          <table className="t rgrid">
            <thead><tr>
              <th className="rgrid-stick">Block</th><th>Asset</th>
              {reporters.map((rp) => <th key={rp} className="right"><RTag addr={rp} navigate={navigate} compact /></th>)}
            </tr></thead>
            <tbody>
              {rows.map((row) => (
                <tr key={row.height + row.symbol}>
                  <td className="rgrid-stick mono">{fmt(row.height)}</td>
                  <td><CoinChip symbol={row.symbol} base={row.base} size={16} /></td>
                  {reporters.map((rp) => {
                    if (!(rp in row.vals)) return <td key={rp} className="mono right" />;
                    const v = row.vals[rp];
                    const med = row.median;
                    const dev = (med && med > 0 && Number.isFinite(v)) ? ((v - med) / med) * 100 : null;
                    const ad = dev == null ? 0 : Math.abs(dev);
                    const cls = ad >= 3 ? 'dev dev-bad' : ad >= 1 ? 'dev dev-warn' : 'dev dev-ok';
                    return (
                      <td key={rp} className="mono right">
                        {priceFmt(v)}
                        {dev != null && ad >= 0.01 && (
                          <span className={cls}>{ad.toFixed(2)}%</span>
                        )}
                      </td>
                    );
                  })}
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}

      {sub === 'missed' && (
        missGrid.mBlocks.length === 0 ? (
          <div className="card"><div className="muted" style={{ padding: '28px 18px' }}>
            No missed report in past 30 mins.
          </div></div>
        ) : (
          <div className="card rgrid-wrap">
            <table className="t rgrid">
              <thead><tr>
                <th className="rgrid-stick">Block</th>
                {reporters.map((rp) => <th key={rp}><RTag addr={rp} navigate={navigate} compact /></th>)}
              </tr></thead>
              <tbody>
                {missGrid.mBlocks.map((h) => (
                  <tr key={h}>
                    <td className="rgrid-stick mono">{fmt(h)}</td>
                    {reporters.map((rp) => {
                      const cell = missGrid.missByCell.get(h + '|' + rp) || [];
                      return <td key={rp}>{cell.map((m, i) => <CoinChip key={i} symbol={m.symbol} base={m.base} size={16} />)}</td>;
                    })}
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )
      )}

      {sub === 'summary' && <MissedTab navigate={navigate} powerMap={powerMap} />}
    </div>
  );
}

function MissedTab({ navigate, powerMap }) {
  const [data, setData] = uS(null);
  const [win, setWin] = uS('1');
  uE(() => {
    let alive = true;
    const load = () => api('/api/reports/missed').then((d) => alive && setData(d)).catch(() => {});
    load();
    const id = setInterval(load, 60000);
    return () => { alive = false; clearInterval(id); };
  }, []);
  const windows = (data && data.windows) || [1, 3, 6, 12, 24];
  const rows = (data && data.rows) || [];
  const mKey = 'm_' + win;
  const { assets, reporters, mat, rTotals, grand } = uM(() => {
    // Show ALL reporters / assets we have any data for in the window so
    // the matrix doubles as a comparison view — clean reporters show as
    // a column of ticks instead of being filtered out.
    const aMeta = new Map(), aTot = new Map(), rMiss = new Map();
    const aSeen = new Set(), rSeen = new Set();
    const m = {};
    let grand = 0;
    for (const r of rows) {
      const miss = Number(r[mKey] || 0);
      if (!aMeta.has(r.query_id)) {
        const sym = r.symbol || r.query_id.slice(0, 6);
        aMeta.set(r.query_id, { qid: r.query_id, symbol: sym, base: sym.replace(/\/.*$/, '') });
      }
      aSeen.add(r.query_id);
      rSeen.add(r.reporter);
      if (miss > 0) {
        aTot.set(r.query_id, (aTot.get(r.query_id) || 0) + miss);
        rMiss.set(r.reporter, (rMiss.get(r.reporter) || 0) + miss);
        grand += miss;
      }
      (m[r.reporter] = m[r.reporter] || {})[r.query_id] = miss;
    }
    // Assets: misses-first (worst on top), then any clean ones.
    const assets = [...aSeen].sort((a, b) =>
      (aTot.get(b) || 0) - (aTot.get(a) || 0) || a.localeCompare(b)
    ).map((q) => aMeta.get(q));
    // Reporters: always ordered by voting power desc, so the columns
    // stay in the same place regardless of which time window is picked
    // (matches the Reports / Values / live-Missed tables).
    const pow = (rp) => (powerMap && powerMap.get(rp)) || 0;
    const reporters = [...rSeen].sort((a, b) =>
      pow(b) - pow(a) || (rMiss.get(b) || 0) - (rMiss.get(a) || 0)
    );
    return { assets, reporters, mat: m, rTotals: rMiss, grand };
  }, [rows, mKey, powerMap]);

  if (!data) return <div className="card"><Loading label="Loading summary…" /></div>;

  const seg = (
    <div className="seg">
      {windows.map((w) => (
        <button key={w} className={'seg-btn' + (win === String(w) ? ' active' : '')} onClick={() => setWin(String(w))}>{w}h</button>
      ))}
    </div>
  );

  return (
    <div className="card table-card">
      <div className="card-head" style={{ display: 'flex', alignItems: 'center', gap: 16, justifyContent: 'space-between' }}>
        <div>
          <div className="card-title">Missed reports</div>
        </div>
        {seg}
      </div>
      {reporters.length === 0 ? (
        <div className="muted" style={{ padding: '24px 18px' }}>No missed reports recorded in last {win}h</div>
      ) : (
        <div className="m-grid-wrap">
          <table className="t m-grid">
            <thead><tr>
              <th>Asset</th>
              {reporters.map((rp) => (
                <th key={rp}><RTag addr={rp} navigate={navigate} compact /></th>
              ))}
            </tr></thead>
            <tbody>
              {assets.map((a) => (
                <tr key={a.qid}>
                  <td><CoinChip symbol={a.symbol} base={a.base} size={16} /></td>
                  {reporters.map((rp) => {
                    const v = (mat[rp] || {})[a.qid];
                    if (v == null) return <td key={rp} className="na">—</td>;
                    return v > 0
                      ? <td key={rp} className="miss mono">{fmt(v)}</td>
                      : <td key={rp} className="ok">✓</td>;
                  })}
                </tr>
              ))}
            </tbody>
            <tfoot>
              <tr>
                <td>Total ({fmt(grand)})</td>
                {reporters.map((rp) => (
                  <td key={rp} className="mono"><b>{fmt(rTotals.get(rp) || 0)}</b></td>
                ))}
              </tr>
            </tfoot>
          </table>
        </div>
      )}
    </div>
  );
}

// =========================== Disputes ===========================

// Browser-friendly hex -> ascii decoder for the small ABI strings we use.
function _hexToUtf8(hex) {
  if (!hex) return '';
  let s = '';
  for (let i = 0; i < hex.length; i += 2) s += String.fromCharCode(parseInt(hex.slice(i, i + 2), 16));
  try { return decodeURIComponent(escape(s)); } catch { return s; }
}
function _readAbiString(hex, byteOff) {
  const p = byteOff * 2;
  const len = parseInt(hex.slice(p, p + 64) || '0', 16) || 0;
  return _hexToUtf8(hex.slice(p + 64, p + 64 + len * 2));
}

// Decode a SpotPrice query_data into "<ASSET>/<CURRENCY>".
function decodeSpotPriceSymbol(queryData) {
  if (!queryData) return '';
  const h = String(queryData).replace(/^0x/i, '');
  if (h.length < 128) return '';
  try {
    const off2 = parseInt(h.slice(64, 128), 16);
    const argsLen = parseInt(h.slice(off2 * 2, off2 * 2 + 64), 16) || 0;
    const args = h.slice(off2 * 2 + 64, off2 * 2 + 64 + argsLen * 2);
    if (args.length < 128) return '';
    const a1 = parseInt(args.slice(0, 64), 16);
    const a2 = parseInt(args.slice(64, 128), 16);
    const asset = _readAbiString(args, a1).toUpperCase();
    const cur = _readAbiString(args, a2).toUpperCase();
    return asset && cur ? `${asset}/${cur}` : asset;
  } catch { return ''; }
}

// Decode a disputed report's value into something human-readable, based
// on the query_type that came with the dispute event.
function decodeDisputeValue(queryType, hex) {
  if (!hex) return null;
  const h = hex.replace(/^0x/i, '');
  try {
    if (queryType === 'SpotPrice') {
      // 32-byte uint scaled by 1e18.
      if (h.length < 64) return null;
      const n = Number(BigInt('0x' + h.slice(0, 64))) / 1e18;
      return Number.isFinite(n) ? { kind: 'price', value: n } : null;
    }
    if (queryType === 'TRBBridge') {
      // abi.encode(address recipient, uint256 amount, uint256 depositId, bool)
      // we just need recipient (right 40 chars of word 0) and amount (word 1).
      if (h.length < 64 * 2) return null;
      const recipient = '0x' + h.slice(24, 64);
      const amt = Number(BigInt('0x' + h.slice(64, 128))) / 1e6; // loya -> TRB
      return { kind: 'bridge', recipient, amount: amt };
    }
  } catch { /* fallthrough */ }
  return null;
}

function prettyDisputeValue(d) {
  // The "Disputed report" column wants a short label only — query_type
  // plus the asset for SpotPrice. Full decoded value (price / amount /
  // recipient) lives in the expanded panel.
  if (!d.query_type) return null;
  if (d.query_type === 'SpotPrice') {
    const sym = decodeSpotPriceSymbol(d.query_data);
    return sym || null;
  }
  return null;
}

function DisputesScreen({ navigate }) {
  const { loading, data, error } = useFetch('/api/disputes');
  const [open, setOpen] = uS(null); // dispute_id of expanded row
  if (loading) return <Loading label="Loading disputes…" />;
  if (error) return <ErrorBox msg={error} />;
  const rows = data || [];
  return (
    <div className="screen">
      <div className="screen-head">
        <div><h2 className="screen-title">Disputes</h2></div>
      </div>
      <div className="card table-card">
        <table className="t">
          <thead><tr>
            <th style={{ width: 50 }}>ID</th>
            <th>Disputer</th>
            <th>Reporter</th>
            <th>Category</th>
            <th>Disputed report</th>
            <th className="right">Fee (TRB)</th>
            <th className="right" style={{ width: 80 }}>Yes</th>
            <th className="right" style={{ width: 80 }}>No</th>
            <th style={{ width: 130 }}>Block</th>
          </tr></thead>
          <tbody>
            {rows.length === 0 && (
              <tr><td colSpan="9" className="muted" style={{ padding: '24px 18px' }}>No disputes recorded.</td></tr>
            )}
            {rows.map((d) => <DisputeRow key={d.dispute_id} d={d} open={open === d.dispute_id} setOpen={setOpen} navigate={navigate} />)}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function DisputeRow({ d, open, setOpen, navigate }) {
  const t = d.tally || {};
  const total = BigInt(t.total_power || '0');
  const pct = (n) => total > 0n ? (Number(BigInt(n || '0') * 1000n / total) / 10).toFixed(1) + '%' : '0%';
  const catTone = d.category && d.category.includes('MAJOR') ? 'tone-red'
    : d.category && d.category.includes('MINOR') ? 'tone-amber'
    : 'tone-mint';
  const decoded = decodeDisputeValue(d.query_type, d.value_hex);
  const pretty = prettyDisputeValue(d);
  return (
    <React.Fragment>
      <tr className="row-clickable" onClick={() => setOpen(open ? null : d.dispute_id)}>
        <td className="mono">#{d.dispute_id}</td>
        <td><AddressBlock addr={d.disputer} /></td>
        <td><AddressBlock addr={d.reporter} /></td>
        <td><span className={'tx-badge ' + catTone}>{(d.category || '').replace(/^DISPUTE_CATEGORY_/, '')}</span></td>
        <td>
          {d.query_type
            ? <><b>{d.query_type}</b>{pretty && <span className="muted small"> · {pretty}</span>}</>
            : <span className="muted small">—</span>}
        </td>
        <td className="right mono">{trb(d.fee_paid)} {d.pay_from_bond ? <span className="muted small">(from bond)</span> : null}</td>
        <td className="right mono">{total > 0n ? <span style={{ color: '#4ade80' }}>{pct(t.SUPPORT)}</span> : <span className="muted">—</span>}</td>
        <td className="right mono">{total > 0n ? <span style={{ color: '#fb7185' }}>{pct(t.AGAINST)}</span> : <span className="muted">—</span>}</td>
        <td><div className="block-cell"><span className="mono">{fmt(d.height)}</span><span className="muted small" title={dt(d.block_time)}>{ago(d.block_time)}</span></div></td>
      </tr>
      {open && (
        <tr className="dispute-detail">
          <td colSpan="9">
            <div className="dispute-detail-grid">
              <div>
                <div className="muted small">What was disputed</div>
                <ul className="dispute-other">
                  <li>Query type: <span className="mono">{d.query_type || '—'}</span></li>
                  {decoded && decoded.kind === 'price' && (() => {
                    const sym = decodeSpotPriceSymbol(d.query_data);
                    return <>
                      {sym && <li>Asset: <span className="mono">{sym}</span></li>}
                      <li>Reported price: <span className="mono">{decoded.value.toLocaleString(undefined, { maximumFractionDigits: 6 })}</span></li>
                    </>;
                  })()}
                  {decoded && decoded.kind === 'bridge' && <>
                    <li>Bridge amount: <span className="mono">{decoded.amount.toLocaleString(undefined, { maximumFractionDigits: 6 })} TRB</span></li>
                    <li>Bridge recipient: <span className="mono small">{decoded.recipient}</span></li>
                  </>}
                  {d.query_id && <li>Query id: <span className="mono small">{d.query_id}</span></li>}
                  {d.report_block ? <li>Reported at block: <span className="mono">{fmt(d.report_block)}</span> <span className="muted small">{d.report_ts ? '· ' + dt(d.report_ts) : ''}</span></li> : null}
                </ul>
                <div className="muted small" style={{ marginTop: 10 }}>Raw value (hex)</div>
                <div className="mono small" style={{ wordBreak: 'break-all', marginTop: 4 }}>{d.value_hex || '—'}</div>
              </div>
              <div>
                <div className="muted small">Outcome</div>
                <ul className="dispute-other">
                  <li>Fee paid: <span className="mono">{trb(d.fee_paid)} TRB</span> {d.pay_from_bond ? <span className="muted small">(from bond)</span> : null}</li>
                  <li>Total fee owed: <span className="mono">{trb(d.total_fee)} TRB</span></li>
                  {BigInt(d.stake_deducted || '0') > 0n && (
                    <li>Stake slashed: <span className="mono">{trb(d.stake_deducted)} TRB</span></li>
                  )}
                  {d.jailed_duration && d.jailed_duration !== '0' && (
                    <li>Reporter jailed: <span className="mono">{d.jailed_duration}s</span></li>
                  )}
                  <li>Tx: <span className="mono small">{d.tx_hash || '—'}</span></li>
                </ul>
              </div>
              <div style={{ gridColumn: '1 / -1' }}>
                <div className="muted small" style={{ marginBottom: 6 }}>Votes ({(d.votes || []).length})</div>
                <table className="t" style={{ fontSize: '.82rem' }}>
                  <thead><tr><th>Voter</th><th className="right">Power</th><th>Choice</th><th>Block</th></tr></thead>
                  <tbody>
                    {(d.votes || []).length === 0 && <tr><td colSpan="4" className="muted">No votes yet.</td></tr>}
                    {(d.votes || []).map((v, i) => {
                      const raw = (v.choice || '').replace(/^VOTE_/, '');
                      const ch = raw === 'SUPPORT' ? 'Yes' : raw === 'AGAINST' ? 'No' : raw === 'INVALID' ? 'Invalid' : raw;
                      const col = raw === 'SUPPORT' ? '#4ade80' : raw === 'AGAINST' ? '#fb7185' : '#fbbf24';
                      return (
                        <tr key={i}>
                          <td><AddressBlock addr={v.voter} to={'reporter/' + v.voter} navigate={navigate} /></td>
                          <td className="right mono">{compact(Number(v.voter_power || 0))}</td>
                          <td><span style={{ color: col, fontWeight: 600 }}>{ch}</span></td>
                          <td><span className="mono small">{fmt(v.height)}</span> <span className="muted small">{ago(v.block_time)}</span></td>
                        </tr>
                      );
                    })}
                  </tbody>
                </table>
              </div>
            </div>
          </td>
        </tr>
      )}
    </React.Fragment>
  );
}

// =========================== Bridge ===========================

// Decode the TRBBridge query_data ABI to pull out the destination chain
// info if available. For now we just label these as "Layer -> EVM" since
// the only kind on mainnet today is the EVM withdraw. Returns a short
// "EVM" label for now; future deposit support can extend this.
function bridgeKind(_w) { return 'Layer → EVM'; }

function evmAddr(s) {
  if (!s) return '—';
  const a = (s.startsWith('0x') ? s : '0x' + s);
  return a.slice(0, 8) + '…' + a.slice(-6);
}

function BridgeScreen({ navigate }) {
  const [sub, setSub] = uS('deposits');
  return (
    <div className="screen">
      <div className="screen-head">
        <div><h2 className="screen-title">Bridge</h2></div>
      </div>
      <div className="seg" style={{ margin: '4px 0 16px' }}>
        <button className={'seg-btn' + (sub === 'deposits' ? ' active' : '')} onClick={() => setSub('deposits')}>Deposits (ETH → Layer)</button>
        <button className={'seg-btn' + (sub === 'withdrawals' ? ' active' : '')} onClick={() => setSub('withdrawals')}>Withdrawals (Layer → EVM)</button>
      </div>
      {sub === 'deposits' ? <BridgeDepositsView navigate={navigate} /> : <BridgeWithdrawalsView navigate={navigate} />}
    </div>
  );
}

function BridgeDepositsView({ navigate }) {
  const { loading, data, error } = useFetch('/api/bridge/deposits');
  const active = useFetch('/api/reporters/active-24h');
  const [open, setOpen] = uS(null);
  if (loading) return <Loading label="Loading deposits…" />;
  if (error) return <ErrorBox msg={error} />;
  const rows = data || [];
  const activeSet = new Set(active.data || []);
  return (
    <div className="card table-card">
      <table className="t">
        <thead><tr>
          <th style={{ width: 60 }}>ID</th>
          <th>Direction</th>
          <th>From (EVM)</th>
          <th>To (Layer)</th>
          <th className="right">Amount</th>
          <th>Reporters</th>
          <th style={{ width: 130 }}>Block</th>
        </tr></thead>
        <tbody>
          {rows.length === 0 && <tr><td colSpan="7" className="muted" style={{ padding: '24px 18px' }}>No deposits indexed yet — backfilling…</td></tr>}
          {rows.map((d) => (
            <BridgeDepositRow key={d.deposit_id} d={d} activeSet={activeSet}
              open={open === d.deposit_id} setOpen={setOpen} navigate={navigate} />
          ))}
        </tbody>
      </table>
    </div>
  );
}

function BridgeDepositRow({ d, activeSet, open, setOpen, navigate }) {
  const trbAmt = Number(d.amount || 0) / 1e18; // deposit amount is 1e18-scaled
  const reported = d.reporters || [];
  const reporterSet = new Set(reported.map((r) => r.reporter));
  // "Missed" = recently-active reporters who didn't sign this deposit.
  const missed = activeSet.size > 0 ? [...activeSet].filter((a) => !reporterSet.has(a)) : [];
  return (
    <React.Fragment>
      <tr className="row-clickable" onClick={() => setOpen(open ? null : d.deposit_id)}>
        <td className="mono">#{d.deposit_id}</td>
        <td><span className="tx-badge tone-sky">ETH → Layer</span></td>
        <td className="mono small" title={d.evm_sender}>{evmAddr(d.evm_sender.replace(/^0x/, ''))}</td>
        <td><AddressBlock addr={d.layer_recipient} /></td>
        <td className="right mono">
          {trbAmt.toLocaleString(undefined, { maximumFractionDigits: 6 })} <span className="unit">TRB</span>
          {window.TRB_PRICE > 0 && (
            <div className="usd-sub muted small">{window.usd(trbAmt * window.TRB_PRICE)}</div>
          )}
        </td>
        <td>
          <span style={{ color: '#4ade80', fontWeight: 600 }}>{reported.length} reported</span>
          {activeSet.size > 0 && missed.length > 0 && <>
            <span className="muted small">{' · '}</span>
            <span style={{ color: '#fb7185', fontWeight: 600 }}>{missed.length} missed</span>
          </>}
        </td>
        <td><div className="block-cell"><span className="mono">{fmt(d.first_seen_height)}</span><span className="muted small" title={dt(d.first_seen_time)}>{ago(d.first_seen_time)}</span></div></td>
      </tr>
      {open && (
        <tr className="dispute-detail">
          <td colSpan="7">
            <div className="dispute-detail-grid" style={{ gridTemplateColumns: '1fr 1fr 1fr' }}>
              <div>
                <div className="muted small">Details</div>
                <ul className="dispute-other">
                  <li>Deposit ID: <span className="mono">{d.deposit_id}</span></li>
                  <li>From (EVM): <span className="mono small">{d.evm_sender}</span></li>
                  <li>To (Layer): <span className="mono small">{d.layer_recipient}</span></li>
                  <li>Query id: <span className="mono small">{d.query_id}</span></li>
                </ul>
              </div>
              <div>
                <div className="muted small">Reporters who Voted ({reported.length})</div>
                {reported.length === 0
                  ? <div className="muted small" style={{ marginTop: 6 }}>No reporter records indexed yet.</div>
                  : <div className="rep-list">
                    {reported.map((r, i) => (
                      <RepChip key={i} addr={r.reporter} power={r.power} tone="ok" navigate={navigate} />
                    ))}
                  </div>}
              </div>
              <div>
                <div className="muted small">Did not Report ({missed.length})</div>
                {activeSet.size === 0
                  ? <div className="muted small" style={{ marginTop: 6 }}>Active reporter set not yet known.</div>
                  : missed.length === 0
                    ? <div className="muted small" style={{ marginTop: 6 }}>All active reporters submitted.</div>
                    : <div className="rep-list">
                      {missed.map((a, i) => (
                        <RepChip key={i} addr={a} tone="bad" navigate={navigate} />
                      ))}
                    </div>}
              </div>
            </div>
          </td>
        </tr>
      )}
    </React.Fragment>
  );
}

function BridgeWithdrawalsView({ navigate }) {
  const { loading, data, error } = useFetch('/api/bridge');
  const [open, setOpen] = uS(null);
  if (loading) return <Loading label="Loading withdrawals…" />;
  if (error) return <ErrorBox msg={error} />;
  const rows = data || [];
  return (
    <div className="card table-card">
      <table className="t">
        <thead><tr>
          <th style={{ width: 60 }}>ID</th>
          <th>Direction</th>
          <th>From (Layer)</th>
          <th>To (EVM)</th>
          <th className="right">Amount</th>
          <th>Attestation</th>
          <th style={{ width: 130 }}>Block</th>
        </tr></thead>
        <tbody>
          {rows.length === 0 && <tr><td colSpan="7" className="muted" style={{ padding: '24px 18px' }}>No withdrawals indexed yet — backfilling…</td></tr>}
          {rows.map((w) => (
            <BridgeRow key={w.withdraw_id} w={w}
              open={open === w.withdraw_id} setOpen={setOpen} navigate={navigate} />
          ))}
        </tbody>
      </table>
    </div>
  );
}

function BridgeRow({ w, open, setOpen, navigate }) {
  const trbAmt = Number(w.amount || 0) / 1e6;
  // aggregate_power is reported in loya-equivalent voting power; show
  // it in the same TRB-equivalent units the rest of the dashboard uses.
  // No % of network — network power has grown over time and the
  // comparison would be misleading for historical withdrawals.
  const aggInUnits = Number(w.aggregate_power || 0) / 1e6;
  return (
    <React.Fragment>
      <tr className="row-clickable" onClick={() => setOpen(open ? null : w.withdraw_id)}>
        <td className="mono">#{w.withdraw_id}</td>
        <td><span className="tx-badge tone-violet">Layer → EVM</span></td>
        <td><AddressBlock addr={w.sender} /></td>
        <td className="mono small" title={'0x' + w.recipient_evm}>{evmAddr(w.recipient_evm)}</td>
        <td className="right mono">
          {trbAmt.toLocaleString(undefined, { maximumFractionDigits: 6 })} <span className="unit">TRB</span>
          {window.TRB_PRICE > 0 && (
            <div className="usd-sub muted small">{window.usd(trbAmt * window.TRB_PRICE)}</div>
          )}
        </td>
        <td><span className="mono">{compact(aggInUnits)}</span></td>
        <td><div className="block-cell"><span className="mono">{fmt(w.height)}</span><span className="muted small" title={dt(w.block_time)}>{ago(w.block_time)}</span></div></td>
      </tr>
      {open && (
        <tr className="dispute-detail">
          <td colSpan="7">
            <div className="dispute-detail-grid">
              <div>
                <div className="muted small">Withdrawal</div>
                <ul className="dispute-other">
                  <li>ID: <span className="mono">{w.withdraw_id}</span></li>
                  <li>Amount: <span className="mono">{trbAmt.toLocaleString(undefined, { maximumFractionDigits: 6 })} TRB</span></li>
                  <li>From (Layer): <span className="mono small">{w.sender}</span></li>
                  <li>To (EVM): <span className="mono small">0x{w.recipient_evm}</span></li>
                </ul>
              </div>
              <div>
                <div className="muted small">Attestation</div>
                <ul className="dispute-other">
                  <li>Aggregate power: <span className="mono">{compact(aggInUnits)}</span></li>
                  <li>Query id: <span className="mono small">{w.query_id}</span></li>
                  <li>Tx: <span className="mono small">{w.tx_hash || '—'}</span></li>
                </ul>
              </div>
            </div>
          </td>
        </tr>
      )}
    </React.Fragment>
  );
}

// =========================== Validators ===========================
function ValidatorsScreen({ navigate }) {
  const { loading, data, error } = useFetch('/api/validators');
  if (loading) return <Loading label="Loading validators…" />;
  if (error) return <ErrorBox msg={error} />;
  const rows = data || [];
  // Share = % of total bonded stake, so the largest validator shows
  // its real network share rather than 100%.
  const totalT = Math.max(1, rows.reduce((s, v) => s + Number(v.tokens || 0), 0));
  return (
    <div className="screen">
      <div className="screen-head">
        <div><h2 className="screen-title">Validators</h2></div>
      </div>
      <div className="card table-card">
        <table className="t">
          <thead><tr>
            <th style={{ width: 44 }}>#</th>
            <th>Validator</th>
            <th className="right">Stake</th>
            <th className="right" style={{ width: 90, whiteSpace: 'nowrap' }}>Stake %</th>
            <th className="right">Commission</th>
            <th>Status</th>
          </tr></thead>
          <tbody>
            {rows.length === 0 && <tr><td colSpan="6" className="muted" style={{ padding: '24px 18px' }}>No validators indexed yet — refreshing…</td></tr>}
            {rows.map((v, i) => {
              const tokens = Number(v.tokens || 0);
              const share = (tokens / totalT) * 100;
              return (
                <tr key={v.operator_address} className="row-clickable" onClick={() => navigate('validator/' + v.operator_address)}>
                  <td className="rank">{i + 1}</td>
                  <td><AddressBlock addr={v.operator_address} moniker={v.moniker}
                    to={'validator/' + v.operator_address} navigate={navigate} /></td>
                  <td className="right mono">{(tokens / 1e6).toLocaleString(undefined, { maximumFractionDigits: 2 })}
                    <div className="usd-sub muted small">{_stakeUsd(v.tokens)}</div>
                  </td>
                  <td className="right mono">{share.toFixed(1)}%</td>
                  <td className="right mono">{(Number(v.commission_rate) * 100).toFixed(1)}%</td>
                  <td><StatusPill status={v.jailed ? 'jailed' : v.status === 'BOND_STATUS_BONDED' ? 'active' : 'inactive'} /></td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function ValidatorDetail({ addr, navigate }) {
  const { loading, data, error } = useFetch('/api/validator/' + addr, [addr]);
  if (loading) return <Loading label="Loading validator…" />;
  if (error || !data || !data.validator) return <ErrorBox msg={error || 'not found'} />;
  const v = data.validator;
  const events = data.events || [];
  const tokens = Number(v.tokens || 0);
  return (
    <div className="screen">
      <a className="back" {...navLink(navigate, 'validators')}>
        <svg width="13" height="13" viewBox="0 0 14 14"><path d="M9 2L4 7l5 5" stroke="currentColor" fill="none" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /></svg>
        All validators
      </a>
      <div className="card hero">
        <div className="hero-main">
          <div style={{ minWidth: 0 }}>
            <div className="hero-name addr-name-validator">{v.moniker || 'Validator'}
              <StatusPill status={v.jailed ? 'jailed' : v.status === 'BOND_STATUS_BONDED' ? 'active' : 'inactive'} /></div>
            <div className="hero-addr mono">{v.operator_address}</div>
          </div>
        </div>
        <div className="hero-stats">
          <div><div className="hero-stat-l">Stake</div>
            <div className="hero-stat-v">{(tokens / 1e6).toLocaleString(undefined, { maximumFractionDigits: 2 })} <span className="unit">TRB</span></div>
            <div className="usd-sub muted small">{_stakeUsd(v.tokens)}</div></div>
          <div><div className="hero-stat-l">Commission</div><div className="hero-stat-v">{(Number(v.commission_rate) * 100).toFixed(1)}%</div></div>
          <div><div className="hero-stat-l">Status</div><div className="hero-stat-v small">{(v.status || '').replace(/^BOND_STATUS_/, '')}</div></div>
          <div><div className="hero-stat-l">Account</div><div className="hero-stat-v mono small">{v.account_address ? short(v.account_address) : '—'}</div></div>
        </div>
      </div>
      <div className="card">
        <div className="card-head"><div><div className="card-title">Profile</div></div></div>
        <div style={{ padding: '14px 18px' }}>
          <ul className="dispute-other">
            {v.identity && <li>Identity: <span className="mono">{v.identity}</span></li>}
            {v.website && <li>Website: <a className="link" href={v.website} target="_blank" rel="noopener">{v.website}</a></li>}
            {v.security_contact && <li>Security contact: <span className="mono">{v.security_contact}</span></li>}
            {v.details && <li>Details: <span>{v.details}</span></li>}
            <li>Account address: <span className="mono small">{v.account_address || '—'}</span></li>
            <li>Delegator shares: <span className="mono">{v.delegator_shares}</span></li>
          </ul>
        </div>
      </div>
      <div className="card table-card section-gap">
        <div className="card-head"><div><div className="card-title">Transactions</div></div></div>
        <DetailTxs rows={events} navigate={navigate} />
      </div>
    </div>
  );
}

Object.assign(window, {
  ReportersScreen, ReporterDetail, SelectorsScreen, SelectorDetail, TxsScreen,
  ReportsScreen, DisputesScreen, BridgeScreen,
  ValidatorsScreen, ValidatorDetail,
  TxTable, KIND_OPTS, SearchIcon, LockIcon, ExtIcon,
});
