/* Reports section — admin-only cross-strategy reports, one page per report.
 *
 * Entry lag (#/reports/entry-lag): for every setup, the trader's own price
 * ("CMP") vs the market when his message reached us, whether a follower would
 * still be inside his entry range, and what the lag cost in P&L. All the maths
 * is server-side (src/entry_lag.js) so this file only filters, sorts and draws.
 *
 * Reuses widgets-signals.jsx globals (fmtNum, fmtStamp, signedPct, signedR,
 * pnlColor, ledgerChip, PERIOD_CHIPS, ReportSummaryCard, reportAdversePct,
 * reportAdverseColor, SignalChartModal) — it loads after that file.
 */

const { useState: useStateRP, useEffect: useEffectRP, useMemo: useMemoRP } = React;

// Status chip per range verdict. Colour carries meaning, not category: green =
// a follower is fine, red = he's late or the trade's already decided, muted =
// nothing to judge.
const LAG_STATUS = {
  in_range:   { label: 'In range',        tone: 'bull',    tip: '"CMP till X": price was at or better than X on arrival (at or below it for a long), so a follower gets his entry. Otherwise: inside his CMP-to-second-entry zone, or within the gate\'s tolerance of his CMP.' },
  wait_entry: { label: 'Wait for entry',  tone: 'gold',    tip: '"CMP till X": price was past X on arrival (above it for a long). X is his entry, so a follower waits for price to come back to X — P&L assumes a limit at X that only fills if it trades there within 7 days.' },
  would_miss: { label: 'Would miss',      tone: 'bear',    tip: 'Price had already run past his CMP (or the top of his zone) toward profit by more than the tolerance — a follower would be chasing.' },
  better:     { label: 'Better entry',    tone: 'info',    tip: 'Price had pulled back toward the stop since he posted — a follower gets a better price than he did.' },
  past_bound: { label: 'Past DCA bound',  tone: 'gold',    tip: 'Price was already beyond the far end of his zone (below his floor for a long, above his ceiling for a short), still short of the stop.' },
  resting:    { label: 'Resting',         tone: 'neutral', tip: 'Plain limit order: price hadn\'t come to his level yet when the message arrived.' },
  fills_now:  { label: 'Fills at once',   tone: 'gold',    tip: 'Plain limit order: price was already through his level on arrival, so the order fills immediately at a worse price.' },
  sl_first:   { label: 'Stopped first',   tone: 'bear',    tip: 'The stop traded before the message reached us, or price was already through it on arrival. Nobody following could have entered.' },
  tp1_first:  { label: 'TP1 first',       tone: 'bear',    tip: 'The first target traded before the message reached us, or price was already past it on arrival. The move was over before a follower could act.' },
  no_lag:     { label: 'Real-time',       tone: 'neutral', tip: 'Real-time feed and no price stated in the message: his CMP is the price when it arrived, so there is no lag to measure.' },
  no_ref:     { label: 'No reference',    tone: 'neutral', tip: 'Nothing to compare: no Bybit price at arrival (coin not listed, or a signal from before prices were stored), or a delayed post whose price at his post time could not be fetched.' },
};
const LAG_STATUS_ORDER = ['would_miss', 'tp1_first', 'sl_first', 'wait_entry', 'fills_now', 'past_bound', 'in_range', 'better', 'resting', 'no_lag', 'no_ref'];

const ENTRY_TYPE_LABEL = {
  market_cmp: 'CMP', dca_zone: 'CMP till', split: 'CMP + 2nd', limit: 'Limit', unspecified: 'Unstated',
};

const CMP_SOURCE_TIP = {
  stated: 'The number the trader wrote next to "CMP" in the message.',
  at_trader_post: 'No number in the message — the Bybit price at the minute he originally posted (the source channel is a delayed mirror).',
};

const EXIT_BASIS_LABEL = {
  tp_hit: 'TP1 (sim)', sl_hit: 'SL (sim)', no_resolution: '7d (sim)', sim_open: 'open (sim)', live: 'live', closed: 'closed',
};
const EXIT_BASIS_TIP = {
  sim_open: 'Simulated: neither stop nor TP1 yet, the 7-day window is still running — marked at the latest 15m close.',
  tp_hit: 'Simulated fixed-stop / first-target exit: reached TP1.',
  sl_hit: 'Simulated fixed-stop / first-target exit: stopped out.',
  no_resolution: 'Simulated exit: neither stop nor TP1 within 7 days, marked at the 7-day close.',
  live: 'Still open — marked to the live Bybit price.',
  closed: 'The price the signal was actually closed at (our real close, or the trader\'s close).',
};

function lagChip(status) {
  const s = LAG_STATUS[status] || { label: status || '—', tone: 'neutral', tip: '' };
  return <span className={'chip ' + s.tone} title={s.tip} style={{ fontSize: 9.5, whiteSpace: 'nowrap' }}>{s.label}</span>;
}

function fmtDelay(min) {
  if (min == null) return null;
  if (min < 60) return Math.round(min) + 'm';
  const h = Math.floor(min / 60), m = Math.round(min % 60);
  return h + 'h ' + String(m).padStart(2, '0') + 'm';
}

function pctOrDash(v) { return v != null ? signedPct(v) : '—'; }

// A lag cost shown from the follower's side (cost 0.4R -> "−0.40R"); exactly zero
// reads as a plain "0.00R", not "+0.00R" — no lag cost isn't a gain.
function costSigned(cost, unit) {
  if (cost == null) return '—';
  if (Math.abs(cost) < 0.005) return '0.00' + unit;
  return (cost > 0 ? '−' : '+') + Math.abs(cost).toFixed(2) + unit;
}

const LAG_SORTS = {
  arrived: r => r.arrived_at ? Date.parse(r.arrived_at) : null,
  symbol: r => (r.symbol || '').toLowerCase(),
  strategy: r => (r.strategy || '').toLowerCase(),
  gap: r => r.gap_pct != null ? r.gap_pct : r.limit_gap_pct,
  gap_r: r => r.gap_r,
  status: r => LAG_STATUS_ORDER.indexOf(r.status),
  pnl: r => r.pnl_pct,
  cost: r => r.lag_cost_pct,
  delay: r => r.delay_min,
};

function sortLagRows(rows, key, dir) {
  const get = LAG_SORTS[key] || LAG_SORTS.arrived;
  const d = dir === 'asc' ? 1 : -1;
  return [...rows].sort((a, b) => {
    const av = get(a), bv = get(b);
    if (av == null && bv == null) return 0;
    if (av == null) return 1;          // nulls last either way
    if (bv == null) return -1;
    if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * d;
    return String(av).localeCompare(String(bv), 'en-GB') * d;
  });
}

function toCsv(rows) {
  const cols = [
    ['arrived_at', 'arrived_utc'], ['trader_posted_at', 'trader_posted_utc'], ['delay_min', 'delay_min'],
    ['strategy', 'strategy'], ['symbol', 'symbol'], ['side', 'side'], ['entry_type', 'entry_type'],
    ['trader_cmp', 'trader_cmp'], ['cmp_source', 'cmp_source'], ['entry_ref', 'trader_entry'],
    ['entry_ref_source', 'trader_entry_source'], ['arrival', 'price_at_arrival'],
    ['gap_pct', 'gap_pct'], ['gap_r', 'gap_r'], ['bound', 'zone_bound'], ['limit_low', 'limit_low'],
    ['limit_high', 'limit_high'], ['limit_gap_pct', 'limit_gap_pct'], ['status', 'status'], ['sl', 'sl'],
    ['sl_dist_pct', 'sl_dist_pct'], ['tps', 'tps'], ['tp1_dist_pct', 'tp1_dist_pct'],
    ['rr_at_cmp', 'rr_at_cmp'], ['rr_at_arrival', 'rr_at_arrival'], ['exit_price', 'exit_price'],
    ['exit_basis', 'exit_basis'], ['fill', 'fill'], ['fill_after_h', 'fill_after_h'],
    ['pnl_pct', 'pnl_pct_ours'], ['pnl_r', 'pnl_r_ours'], ['pnl_pct_at_cmp', 'pnl_pct_his'],
    ['pnl_r_at_cmp', 'pnl_r_his'], ['lag_cost_pct', 'lag_cost_pct'], ['lag_cost_r', 'lag_cost_r'], ['real_fill', 'real_fill'],
    ['real_fill_vs_cmp_pct', 'real_fill_vs_cmp_pct'], ['real_pnl', 'real_pnl_usd'], ['signal_id', 'signal_id'],
  ];
  const esc = v => {
    if (v == null) return '';
    const s = Array.isArray(v) ? v.join(' ') : String(v);
    return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
  };
  return [cols.map(c => c[1]).join(',')]
    .concat(rows.map(r => cols.map(c => esc(r[c[0]])).join(',')))
    .join('\n');
}

function downloadCsv(rows) {
  const blob = new Blob([toCsv(rows)], { type: 'text/csv' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'entry-lag-' + new Date().toISOString().slice(0, 10) + '.csv';
  a.click();
  setTimeout(() => URL.revokeObjectURL(a.href), 1000);
}

// ── filters ────────────────────────────────────────────────────────────────
function LagFilterBar({ strategies, f, setF }) {
  const inp = {
    padding: '4px 7px', borderRadius: 6, fontSize: 11, background: 'var(--bg-elev)',
    border: '1px solid var(--border-soft)', color: 'var(--text)', fontFamily: 'var(--font-mono)',
  };
  const label = { fontSize: 10.5, color: 'var(--text-4)' };
  const set = (k, v) => setF(prev => ({ ...prev, [k]: v }));
  const toggleIn = (k, v) => setF(prev => {
    const cur = prev[k];
    return { ...prev, [k]: cur.includes(v) ? cur.filter(x => x !== v) : [...cur, v] };
  });
  return (
    <div className="card" style={{ display: 'flex', gap: 16, alignItems: 'center', flexWrap: 'wrap', padding: '10px 14px', marginBottom: 14 }}>
      <span style={{ display: 'flex', gap: 4, alignItems: 'center', flexWrap: 'wrap' }}>
        <span style={label}>Strategy</span>
        <button onClick={() => set('strategies', [])} style={ledgerChip(!f.strategies.length)}>All</button>
        {(strategies || []).map(s => (
          <button key={s.id} onClick={() => toggleIn('strategies', s.id)} title={s.name}
            style={ledgerChip(f.strategies.includes(s.id))}>{s.public_name}</button>
        ))}
      </span>
      <span style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
        <span style={label}>Coin</span>
        <input value={f.symbol} onChange={e => set('symbol', e.target.value.toUpperCase())}
          placeholder="BTC" style={{ ...inp, width: 80 }} />
      </span>
      <span style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
        <span style={label}>Direction</span>
        {[['all', 'All'], ['long', 'Long'], ['short', 'Short']].map(([id, l]) => (
          <button key={id} onClick={() => set('side', id)} style={ledgerChip(f.side === id)}>{l}</button>
        ))}
      </span>
      <span style={{ display: 'flex', gap: 4, alignItems: 'center', flexWrap: 'wrap' }}>
        <span style={label} title="How the trader stated his entry">Entry</span>
        <button onClick={() => set('types', [])} style={ledgerChip(!f.types.length)}>All</button>
        {Object.entries(ENTRY_TYPE_LABEL).map(([id, l]) => (
          <button key={id} onClick={() => toggleIn('types', id)} style={ledgerChip(f.types.includes(id))}>{l}</button>
        ))}
      </span>
      <span style={{ display: 'flex', gap: 4, alignItems: 'center', flexWrap: 'wrap' }}>
        <span style={label} title="Filters by when the message reached us">Period</span>
        {PERIOD_CHIPS.map(([id, l]) => (
          <button key={id} onClick={() => set('period', id)} style={ledgerChip(f.period === id)}>{l}</button>
        ))}
        {f.period === 'custom' && (
          <span style={{ display: 'flex', gap: 6, alignItems: 'center', marginLeft: 2 }}>
            <input type="date" value={f.since} onChange={e => set('since', e.target.value)} style={inp} />
            <span style={{ color: 'var(--text-4)', fontSize: 11 }}>to</span>
            <input type="date" value={f.until} onChange={e => set('until', e.target.value)} style={inp} />
          </span>
        )}
      </span>
      <label style={{ display: 'flex', gap: 6, alignItems: 'center', fontSize: 11, color: 'var(--text-3)', cursor: 'pointer' }}
             title="Hide rows with nothing to compare (real-time posts with no stated price, coins with no Bybit price)">
        <input type="checkbox" checked={f.comparableOnly} onChange={e => set('comparableOnly', e.target.checked)} />
        Comparable only
      </label>
    </div>
  );
}

// ── summary ────────────────────────────────────────────────────────────────
function LagSummary({ s, statusFilter, setStatusFilter }) {
  const missN = (s.status.would_miss || 0) + (s.never_filled_n || 0) + (s.status.tp1_first || 0) + (s.status.sl_first || 0);
  const judgedN = s.n - (s.status.no_lag || 0) - (s.status.no_ref || 0);
  return (
    <>
      <div style={{ display: 'flex', gap: 10, marginBottom: 10, flexWrap: 'wrap' }}>
        <ReportSummaryCard label="Median gap" value={s.median_gap_pct != null ? reportAdversePct(s.median_gap_pct) : '—'}
          color={reportAdverseColor(s.median_gap_pct)}
          subtitle={s.with_ref ? `${s.adverse_n} of ${s.with_ref} worse for a follower` : 'no comparable rows'}
          title="The trader's entry (X for &quot;CMP till X&quot;, his CMP otherwise) vs the price when his message reached us. Positive (red) = worse for a follower: price already higher for a long, lower for a short." />
        <ReportSummaryCard label="Median gap (R)" value={s.median_gap_r != null ? signedR(s.median_gap_r) : '—'}
          color={reportAdverseColor(s.median_gap_r)}
          subtitle="in units of his initial risk"
          title="The same gap as a fraction of the distance from his entry to his stop — a 2% gap on a 4% stop is half the trade's risk. Median, so one stop parked next to the entry can't skew it." />
        <ReportSummaryCard label="Missed the entry" value={judgedN ? `${missN} / ${judgedN}` : '—'}
          color={missN ? 'var(--bear)' : 'var(--text)'}
          subtitle={`${s.status.would_miss || 0} chasing · ${s.never_filled_n || 0} never back to entry · ${(s.status.tp1_first || 0) + (s.status.sl_first || 0)} decided before arrival`}
          title="Would miss + &quot;CMP till X&quot; setups whose price never came back to X within 7 days + TP1 first + Stopped first, out of every setup that could be judged (everything except Real-time and No reference)." />
        <ReportSummaryCard label="Delay (mirror)" value={s.median_delay_min != null ? fmtDelay(s.median_delay_min) : '—'}
          subtitle={s.delayed_n ? `median over ${s.delayed_n} delayed post${s.delayed_n === 1 ? '' : 's'}` : 'no delayed posts'}
          title="Time from the trader's original post to the message reaching us — only known for the delayed mirror channel, which stamps 'Originally posted'." />
        <ReportSummaryCard label="P&L if entered on arrival" value={s.avg_pnl_pct != null ? signedPct(s.avg_pnl_pct) : '—'}
          color={s.avg_pnl_pct != null ? pnlColor(s.avg_pnl_pct) : undefined}
          subtitle={s.total_pnl_r != null ? `avg ${signedR(s.avg_pnl_r)} · total ${signedR(s.total_pnl_r)} over ${s.pnl_n}` : null}
          title="Average gross move from the price at arrival to the exit (simulated fixed stop / first target when resolved, else the live price, else the actual close). Before fees." />
        <ReportSummaryCard label="Couldn't get in" value={s.missed_n ? `${s.missed_winners_n} won · ${s.missed_losers_n} lost` : '—'}
          color={s.missed_winners_n > s.missed_losers_n ? 'var(--bear)' : s.missed_n ? 'var(--bull)' : undefined}
          subtitle={s.missed_n ? `of ${s.missed_n} setups a follower never entered` : 'every judged setup was enterable'}
          title="Setups a follower never got into — decided before arrival, or price never came back to his entry within 7 days — split by how the trader's own trade went. Winners missed are the lag's real cost; losers missed are a saving." />
        <ReportSummaryCard label="Lag cost (median)" value={s.median_lag_cost_r != null ? costSigned(s.median_lag_cost_r, 'R') : '—'}
          color={s.median_lag_cost_r != null ? pnlColor(-s.median_lag_cost_r) : undefined}
          subtitle={s.paired_n ? `${costSigned(s.median_lag_cost_pct, '%')} · median over ${s.paired_n} trades` : 'no comparable trades'}
          title="Per setup: our result minus his (a setup we couldn't enter counts as 0 for us). Median, not a sum — one stop parked next to the entry can turn an ordinary move into dozens of R. Negative = the lag cost us." />
      </div>
      <div style={{ display: 'flex', gap: 5, flexWrap: 'wrap', marginBottom: 12, alignItems: 'center' }}>
        <span style={{ fontSize: 10.5, color: 'var(--text-4)', marginRight: 2 }}>Status</span>
        <button onClick={() => setStatusFilter([])} style={ledgerChip(!statusFilter.length)}>All {s.n}</button>
        {LAG_STATUS_ORDER.filter(k => s.status[k]).map(k => (
          <button key={k} title={LAG_STATUS[k].tip}
            onClick={() => setStatusFilter(prev => prev.includes(k) ? prev.filter(x => x !== k) : [...prev, k])}
            style={ledgerChip(statusFilter.includes(k))}>
            {LAG_STATUS[k].label} {s.status[k]}
          </button>
        ))}
      </div>
    </>
  );
}

// ── table ──────────────────────────────────────────────────────────────────
function LagMessageModal({ row, onClose }) {
  if (!row) return null;
  return (
    <div style={{
      position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.55)', zIndex: 50,
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20,
    }} onClick={onClose}>
      <div onClick={e => e.stopPropagation()} className="card" style={{ width: 520, maxWidth: '100%', maxHeight: '80vh', display: 'flex', flexDirection: 'column' }}>
        <div className="card-h" style={{ justifyContent: 'space-between' }}>
          <span style={{ fontWeight: 600, fontSize: 13 }}>{row.symbol} {row.side} · {row.strategy}</span>
          <button onClick={onClose} style={{
            background: 'transparent', border: 'none', cursor: 'pointer', color: 'var(--text-4)', fontSize: 16, padding: 0,
          }}>×</button>
        </div>
        <div className="card-b" style={{ overflowY: 'auto' }}>
          <div className="mono" style={{ fontSize: 10, color: 'var(--text-4)', marginBottom: 8 }}>
            {row.trader_posted_at && <>posted {fmtStamp(row.trader_posted_at)} · </>}
            arrived {fmtStamp(row.arrived_at)}
          </div>
          <div style={{ fontSize: 12.5, color: 'var(--text)', whiteSpace: 'pre-wrap', lineHeight: 1.5, wordBreak: 'break-word' }}>
            {row.message || '(no text — image only)'}
          </div>
        </div>
      </div>
    </div>
  );
}

function LagTable({ rows, onViewMessage, onViewChart }) {
  const [sort, setSort] = useStateRP({ key: 'arrived', dir: 'desc' });
  const [page, setPage] = useStateRP(0);
  const PAGE = 50;
  const sorted = useMemoRP(() => sortLagRows(rows, sort.key, sort.dir), [rows, sort]);
  useEffectRP(() => setPage(0), [rows, sort]);
  const pages = Math.max(1, Math.ceil(sorted.length / PAGE));
  const shown = sorted.slice(page * PAGE, page * PAGE + PAGE);

  const th = { textAlign: 'left', fontSize: 9.5, color: 'var(--text-4)', letterSpacing: '0.09em',
               textTransform: 'uppercase', padding: '0 8px 7px', fontWeight: 500, whiteSpace: 'nowrap' };
  const td = { padding: '7px 8px', borderTop: '1px solid var(--border-soft)', fontSize: 11.5, verticalAlign: 'top' };
  const mono = { ...td, fontFamily: 'var(--font-mono)' };
  const sub = { display: 'block', fontSize: 9.5, color: 'var(--text-4)', marginTop: 1 };
  const Th = ({ k, children, title }) => (
    <th style={{ ...th, cursor: k ? 'pointer' : undefined, color: sort.key === k ? 'var(--gold)' : th.color }} title={title}
        onClick={k ? () => setSort(s => ({ key: k, dir: s.key === k && s.dir === 'desc' ? 'asc' : 'desc' })) : undefined}>
      {children}{sort.key === k ? (sort.dir === 'desc' ? ' ↓' : ' ↑') : ''}
    </th>
  );
  const linkBtn = {
    background: 'transparent', border: 'none', cursor: 'pointer', color: 'var(--text-3)', padding: 0,
    fontSize: 11, textDecoration: 'underline', textDecorationStyle: 'dotted', textUnderlineOffset: 3,
  };

  if (!rows.length) return <div style={{ padding: 20, color: 'var(--text-4)', fontSize: 12 }}>No setups match these filters.</div>;

  return (
    <div className="card" style={{ padding: '12px 6px 6px' }}>
      <div style={{ overflowX: 'auto' }}>
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead><tr>
            <Th k="arrived" title="When the message reached us (UTC). Below it: when the trader originally posted, for the delayed mirror feed.">time</Th>
            <Th k="strategy">strategy</Th>
            <Th k="symbol">coin</Th>
            <th style={th} title="How the trader stated his entry: CMP, CMP till X, CMP + second entry, a plain limit, or not stated.">entry</th>
            <th style={th} title="The trader's entry: X for &quot;CMP till X&quot; (his CMP below it for context); the number he wrote next to CMP, or the Bybit price when he posted (delayed feed); for a plain limit, his order level.">trader entry</th>
            <th style={th} title="Bybit price (1-minute close) when the message reached us.">at arrival</th>
            <Th k="gap" title="Arrival vs his CMP (or vs his limit level). Positive (red) = worse for a follower: price already higher for a long, lower for a short.">gap %</Th>
            <Th k="gap_r" title="The same gap in units of his initial risk (CMP to stop).">gap R</Th>
            <Th k="status" title="Where the arrival price sat against his plan. Hover a chip for its meaning.">range</Th>
            <th style={th} title="Initial stop as he posted it, and how far it sat from the arrival price.">SL</th>
            <th style={th} title="Targets as he posted them; TP1 distance from the arrival price below.">TPs</th>
            <th style={th} title="Reward:risk to TP1 from his entry → from ours. What the lag did to the setup.">R:R</th>
            <Th k="pnl" title="Gross P&L if we had entered on arrival (a limit at its level, once reached), out at the simulated fixed-stop/first-target exit, else the live price, else the actual close. Before fees.">P&L on arrival</Th>
            <Th k="cost" title="His P&L minus ours. A setup we couldn't enter counts as 0 for us. Positive = what the lag cost.">lag cost</Th>
            <th style={th} title="What we actually paid on a real position (size-weighted across accounts), and its gap vs his CMP.">our fill</th>
            <th style={th}></th>
          </tr></thead>
          <tbody>
            {shown.map(r => {
              const cost = r.lag_cost_pct;
              const isLimit = r.entry_type === 'limit';
              const isTill = r.entry_ref_source === 'till';
              const gap = isLimit ? r.limit_gap_pct : r.gap_pct;
              return (
                <tr key={r.signal_id}>
                  <td style={{ ...td, color: 'var(--text-3)', whiteSpace: 'nowrap' }}>
                    {fmtStamp(r.arrived_at)}
                    {r.delay_min != null && (
                      <span style={sub} title={'Trader posted ' + fmtStamp(r.trader_posted_at)}>+{fmtDelay(r.delay_min)} after post</span>
                    )}
                  </td>
                  <td style={{ ...td, color: 'var(--text-2)', whiteSpace: 'nowrap' }}>{r.strategy}</td>
                  <td style={{ ...td, fontWeight: 600, whiteSpace: 'nowrap' }}>
                    {r.symbol}
                    <span style={{ ...sub, color: r.side === 'long' ? 'var(--bull)' : 'var(--bear)', fontWeight: 400 }}>{r.side}</span>
                  </td>
                  <td style={{ ...td, color: 'var(--text-3)', whiteSpace: 'nowrap' }}>
                    {ENTRY_TYPE_LABEL[r.entry_type] || r.entry_type}
                    {isTill && <span style={sub} title="His entry: X or better">enter {r.side === 'short' ? '≥' : '≤'} {fmtNum(r.bound)}</span>}
                    {!isTill && r.bound != null && <span style={sub} title="His second entry">2nd {fmtNum(r.bound)}</span>}
                  </td>
                  <td style={mono}>
                    {isLimit
                      ? <span title="His limit order level">{fmtNum(r.limit_low)}{r.limit_high != null && r.limit_high !== r.limit_low ? '–' + fmtNum(r.limit_high) : ''}</span>
                      : isTill
                        ? <span title="His entry: the X of &quot;CMP till X&quot;">{fmtNum(r.bound)}</span>
                        : <span title={CMP_SOURCE_TIP[r.cmp_source] || ''}>{fmtNum(r.trader_cmp)}</span>}
                    {isLimit && r.limit_low != null && <span style={sub}>limit order</span>}
                    {isTill && r.trader_cmp != null && (
                      <span style={sub} title={'His CMP when he posted, for context. ' + (CMP_SOURCE_TIP[r.cmp_source] || '')}>CMP {fmtNum(r.trader_cmp)}</span>
                    )}
                    {!isTill && r.cmp_source && <span style={sub}>{r.cmp_source === 'stated' ? 'stated' : 'at his post'}</span>}
                  </td>
                  <td style={mono}>{fmtNum(r.arrival)}</td>
                  {isLimit ? (
                    // Distance to a resting order isn't a worse entry — it's how far
                    // price must travel to fill — so it stays neutral.
                    <td style={{ ...mono, color: 'var(--text-3)' }}
                        title="Limit order: how far the market sat from his order level on arrival (not slippage — the order fills at his price, if reached).">
                      {gap != null ? Math.abs(gap).toFixed(2) + '%' : '—'}
                      {gap != null && <span style={sub}>{r.status === 'resting' ? 'to fill' : 'through it'}</span>}
                    </td>
                  ) : (
                    <td style={{ ...mono, color: reportAdverseColor(gap) }}>{reportAdversePct(gap)}</td>
                  )}
                  <td style={{ ...mono, color: reportAdverseColor(r.gap_r) }}>{r.gap_r != null ? signedR(r.gap_r) : '—'}</td>
                  <td style={td}>{lagChip(r.status)}</td>
                  <td style={{ ...mono, color: 'var(--text-3)' }}>
                    {fmtNum(r.sl)}
                    {r.sl_dist_pct != null && <span style={sub}>{r.sl_dist_pct.toFixed(1)}% away</span>}
                  </td>
                  <td style={{ ...mono, color: 'var(--text-3)', maxWidth: 150 }}>
                    {r.tps && r.tps.length ? r.tps.map(fmtNum).join(' · ') : '—'}
                    {r.tp1_dist_pct != null && <span style={sub}>TP1 {r.tp1_dist_pct.toFixed(1)}% away</span>}
                  </td>
                  <td style={{ ...mono, whiteSpace: 'nowrap' }}>
                    {r.rr_at_cmp != null ? (
                      <span title="R:R to TP1 from his entry → from ours">
                        <span style={{ color: 'var(--text-3)' }}>{r.rr_at_cmp.toFixed(2)}</span>
                        {' → '}
                        <span style={{ color: r.rr_at_arrival != null && r.rr_at_arrival < r.rr_at_cmp ? 'var(--bear)' : 'var(--text)' }}>
                          {r.rr_at_arrival != null ? r.rr_at_arrival.toFixed(2) : '—'}
                        </span>
                      </span>
                    ) : r.rr_at_arrival != null ? (
                      <span title={isLimit ? 'R:R to TP1 from his limit level' : 'R:R to TP1 from the arrival price'}>{r.rr_at_arrival.toFixed(2)}</span>
                    ) : '—'}
                  </td>
                  <td style={{ ...mono, color: r.pnl_pct != null ? pnlColor(r.pnl_pct) : 'var(--text-4)' }}>
                    {r.pending ? <span style={{ color: 'var(--text-4)' }} title="The entry level hasn't been reached yet — nothing to price.">not filled</span>
                      : r.fill === 'never' ? <span style={{ color: 'var(--text-4)' }} title="Price never came back to his entry within 7 days of arrival — a follower waiting at X never got in.">never filled</span>
                      : r.sim_note === 'candles_failed' ? <span style={{ color: 'var(--text-4)' }} title="Bybit didn't return candles for this one — reload to retry.">couldn't load</span>
                      : r.sim_note === 'no_levels' ? <span style={{ color: 'var(--text-4)' }} title="No stop or no first target in his post — nothing to simulate an exit against.">no TP/SL</span>
                      : pctOrDash(r.pnl_pct)}
                    {r.fill === 'later' && r.fill_after_h != null && (
                      <span style={sub} title="When price came back to his entry, after the message reached us">filled +{r.fill_after_h}h</span>
                    )}
                    {r.pnl_r != null && <span style={{ ...sub, color: pnlColor(r.pnl_r) }}>{signedR(r.pnl_r)}</span>}
                    {r.exit_basis && r.pnl_pct != null && (
                      <span style={sub} title={EXIT_BASIS_TIP[r.exit_basis]}>{EXIT_BASIS_LABEL[r.exit_basis] || r.exit_basis}</span>
                    )}
                  </td>
                  <td style={{ ...mono, color: cost != null ? reportAdverseColor(cost) : 'var(--text-4)' }}
                      title={r.pnl_pct_at_cmp != null ? `His result: ${signedPct(r.pnl_pct_at_cmp)}${r.pnl_r_at_cmp != null ? ' / ' + signedR(r.pnl_r_at_cmp) : ''}` : undefined}>
                    {cost != null ? reportAdversePct(cost) : r.pnl_pct_at_cmp != null ? <span title="His result — ours isn't priced yet">his {signedPct(r.pnl_pct_at_cmp)}</span> : '—'}
                    {cost != null && r.pnl_pct_at_cmp != null && <span style={sub}>his {signedPct(r.pnl_pct_at_cmp)}</span>}
                  </td>
                  <td style={{ ...mono, color: 'var(--text-3)' }}>
                    {fmtNum(r.real_fill)}
                    {r.real_fill_vs_cmp_pct != null && (
                      <span style={{ ...sub, color: reportAdverseColor(r.real_fill_vs_cmp_pct) }}>{reportAdversePct(r.real_fill_vs_cmp_pct)} vs his entry</span>
                    )}
                    {r.real_pnl != null && <span style={{ ...sub, color: pnlColor(r.real_pnl) }}>{fmtUsd(r.real_pnl)}</span>}
                  </td>
                  <td style={{ ...td, whiteSpace: 'nowrap' }}>
                    <button onClick={() => onViewMessage(r)} style={linkBtn} title="Read the trader's message">msg</button>
                    {' · '}
                    <button onClick={() => onViewChart(r.signal_id)} style={linkBtn} title="Price chart with his levels">chart</button>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
      {pages > 1 && (
        <div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end', alignItems: 'center', padding: '10px 8px 4px' }}>
          <span style={{ fontSize: 11, color: 'var(--text-4)' }}>{page * PAGE + 1}–{Math.min(sorted.length, page * PAGE + PAGE)} of {sorted.length}</span>
          <button disabled={page === 0} onClick={() => setPage(p => p - 1)} style={pgBtn}>‹</button>
          <button disabled={page >= pages - 1} onClick={() => setPage(p => p + 1)} style={pgBtn}>›</button>
        </div>
      )}
    </div>
  );
}

// ── page ───────────────────────────────────────────────────────────────────
const LAG_FILTERS_DEFAULT = {
  strategies: [], symbol: '', side: 'all', types: [], period: '1m', since: '', until: '', comparableOnly: true,
};

function W_EntryLagReport() {
  const [strategies, setStrategies] = useStateRP(null);
  const [f, setF] = useStateRP(LAG_FILTERS_DEFAULT);
  const [symbolDeb, setSymbolDeb] = useStateRP('');
  const [statusFilter, setStatusFilter] = useStateRP([]);
  const [data, setData] = useStateRP(null);
  const [err, setErr] = useStateRP(null);
  const [loading, setLoading] = useStateRP(false);
  const [msgRow, setMsgRow] = useStateRP(null);
  const [chartSignalId, setChartSignalId] = useStateRP(null);

  useEffectRP(() => {
    fetch('/api/admin/strategies', { credentials: 'include' }).then(r => r.json())
      .then(j => setStrategies(j.ok ? j.data : []))
      .catch(() => setStrategies([]));
  }, []);

  useEffectRP(() => {
    const t = setTimeout(() => setSymbolDeb(f.symbol.trim()), 350);
    return () => clearTimeout(t);
  }, [f.symbol]);

  useEffectRP(() => {
    let alive = true;
    const qs = new URLSearchParams();
    if (f.strategies.length) qs.set('strategy_id', f.strategies.join(','));
    if (symbolDeb) qs.set('symbol', symbolDeb);
    if (f.side !== 'all') qs.set('side', f.side);
    if (f.types.length) qs.set('entry_type', f.types.join(','));
    if (f.period === 'custom') {
      if (f.since) qs.set('since', f.since);
      if (f.until) qs.set('until', f.until + 'T23:59:59Z');
    } else if (f.period !== 'all') {
      qs.set('period', f.period);
    }
    setLoading(true);
    fetch('/api/reports/entry-lag?' + qs.toString(), { credentials: 'include' }).then(r => {
      if (!r.ok && r.headers.get('content-type') && !r.headers.get('content-type').includes('json')) {
        throw new Error(`Request failed (HTTP ${r.status}) — try again`);
      }
      return r.json();
    }).then(j => {
      if (!alive) return;
      if (j.ok) { setData(j.data); setErr(null); } else setErr(j.error || 'Could not load the report');
    }).catch(e => { if (alive) setErr(String(e.message || e)); })
      .finally(() => { if (alive) setLoading(false); });
    return () => { alive = false; };
  }, [f.strategies, symbolDeb, f.side, f.types, f.period, f.since, f.until]);

  // Comparable-only and the status chips filter client-side, so the summary is
  // recomputed over exactly the rows on screen.
  const visible = useMemoRP(() => {
    if (!data) return [];
    let rows = data.rows;
    if (f.comparableOnly) rows = rows.filter(r => r.status !== 'no_lag' && r.status !== 'no_ref');
    return rows;
  }, [data, f.comparableOnly]);
  const shown = useMemoRP(() => statusFilter.length ? visible.filter(r => statusFilter.includes(r.status)) : visible,
    [visible, statusFilter]);
  const summary = useMemoRP(() => summarizeLag(visible), [visible]);

  return (
    <div>
      <LagFilterBar strategies={strategies} f={f} setF={setF} />
      {err && <div style={{ padding: 14, color: 'var(--bear)', fontSize: 12 }}>{err}</div>}
      {!data && !err && <div style={{ padding: 20, color: 'var(--text-4)', fontSize: 12 }}>Loading report…</div>}
      {data && (
        <div style={{ opacity: loading ? 0.55 : 1, transition: 'opacity 0.15s' }}>
          <LagSummary s={summary} statusFilter={statusFilter} setStatusFilter={setStatusFilter} />
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, marginBottom: 8, flexWrap: 'wrap' }}>
            <span style={{ fontSize: 10.5, color: 'var(--text-4)' }}>
              Tolerance {data.tolerance_r}R (gate setting) · P&L is gross, before fees
              {data.marks_stale && ' · live prices stale'}
              {data.paths_failed > 0 && ` · ${data.paths_failed} delayed post${data.paths_failed === 1 ? '' : 's'} couldn't be priced at post time`}
              {data.truncated && ' · showing the latest 2000 setups — narrow the period'}
            </span>
            <button onClick={() => downloadCsv(shown)} disabled={!shown.length} style={ledgerChip(false)}
              title="Download the rows on screen as CSV">Export CSV</button>
          </div>
          <LagTable rows={shown} onViewMessage={setMsgRow} onViewChart={setChartSignalId} />
        </div>
      )}
      <LagMessageModal row={msgRow} onClose={() => setMsgRow(null)} />
      <SignalChartModal signalId={chartSignalId} onClose={() => setChartSignalId(null)} />
    </div>
  );
}

// Client-side twin of src/entry_lag.js's computeEntryLagSummary, so the cards
// follow the "Comparable only" toggle without another round-trip. Kept to the
// same fields and the same medians-not-means rule.
function summarizeLag(rows) {
  const vals = k => rows.map(r => r[k]).filter(v => v != null);
  const median = a => {
    if (!a.length) return null;
    const s = [...a].sort((x, y) => x - y), m = Math.floor(s.length / 2);
    return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
  };
  const sum = a => a.reduce((x, y) => x + y, 0);
  const gaps = vals('gap_pct'), pnlR = vals('pnl_r'), pnlPct = vals('pnl_pct'), delays = vals('delay_min');
  const costR = vals('lag_cost_r'), costPct = vals('lag_cost_pct');
  const status = {};
  rows.forEach(r => { status[r.status] = (status[r.status] || 0) + 1; });
  return {
    n: rows.length, with_ref: gaps.length, status,
    median_gap_pct: median(gaps), median_gap_r: median(vals('gap_r')),
    adverse_n: gaps.filter(v => v > 0).length,
    median_delay_min: median(delays), delayed_n: delays.length,
    pnl_n: pnlR.length,
    avg_pnl_pct: pnlPct.length ? sum(pnlPct) / pnlPct.length : null,
    avg_pnl_r: pnlR.length ? sum(pnlR) / pnlR.length : null,
    total_pnl_r: pnlR.length ? sum(pnlR) : null,
    never_filled_n: rows.filter(r => r.fill === 'never').length,
    ...(() => {
      const missed = rows.filter(r => r.status === 'sl_first' || r.status === 'tp1_first' || r.fill === 'never');
      return { missed_n: missed.length, missed_winners_n: missed.filter(r => r.pnl_r_at_cmp > 0).length,
               missed_losers_n: missed.filter(r => r.pnl_r_at_cmp < 0).length };
    })(),
    paired_n: costR.length,
    median_lag_cost_r: median(costR),
    median_lag_cost_pct: median(costPct),
  };
}

// The Reports section: a sub-nav across report pages (#/reports/<page>). One
// page today; each new report is another entry here plus its component.
// ── Reconciliation ─────────────────────────────────────────────────────────
// What the worker's full exchange check (ai/src/full_reconcile.py, every 5 min) found on each
// account and what it did. Read-only: the worker owns every action; this only shows them.
const RECON_KIND = {
  restore_sl:             { label: 'Stop restored',            tone: 'bull' },
  cancel_order:           { label: 'Leftover cancelled',       tone: 'bull' },
  restore_skipped:        { label: 'Already fine on re-check', tone: 'neutral' },
  restore_sl_failed:      { label: 'Stop restore FAILED',      tone: 'bear' },
  cancel_failed:          { label: 'Cancel FAILED',            tone: 'bear' },
  sl_wrong_side:          { label: 'No stop · price past it',  tone: 'bear' },
  no_sl_no_level:         { label: 'No stop · none to restore', tone: 'bear' },
  untracked_position:     { label: 'Untracked position',       tone: 'gold' },
  position_side_mismatch: { label: 'Side mismatch',            tone: 'gold' },
  ambiguous_position:     { label: 'Ambiguous position',       tone: 'gold' },
  unknown_signal_order:   { label: 'Order names wrong signal', tone: 'gold' },
  foreign_order:          { label: 'Not our order',            tone: 'neutral' },
};

function reconChip(kind) {
  const k = RECON_KIND[kind] || { label: kind, tone: 'neutral' };
  return <span className={'chip ' + k.tone} style={{ fontSize: 9.5, whiteSpace: 'nowrap' }}>{k.label}</span>;
}

function W_ReconciliationReport() {
  const [data, setData] = useStateRP(null);
  const [err, setErr] = useStateRP(null);
  const [showInfo, setShowInfo] = useStateRP(false);

  useEffectRP(() => {
    let alive = true;
    const load = () => fetch('/api/reports/reconciliation', { credentials: 'include' }).then(r => r.json())
      .then(j => { if (!alive) return; if (j.ok) { setData(j.data); setErr(null); } else setErr(j.error || 'Could not load'); })
      .catch(e => { if (alive) setErr(String(e.message || e)); });
    load();
    const id = setInterval(load, 60000);
    return () => { alive = false; clearInterval(id); };
  }, []);

  if (err) return <div style={{ padding: 14, color: 'var(--bear)', fontSize: 12 }}>{err}</div>;
  if (!data) return <div style={{ padding: 20, color: 'var(--text-4)', fontSize: 12 }}>Loading…</div>;

  const th = { textAlign: 'left', fontSize: 9.5, color: 'var(--text-4)', letterSpacing: '0.09em',
               textTransform: 'uppercase', padding: '0 8px 7px', fontWeight: 500 };
  const td = { padding: '7px 8px', borderTop: '1px solid var(--border-soft)', fontSize: 11.5, verticalAlign: 'top' };
  const byId = Object.fromEntries(data.accounts.map(a => [a.id, a]));

  return (
    <div>
      <div style={{ fontSize: 11, color: 'var(--text-4)', marginBottom: 12, lineHeight: 1.5 }}>
        Every 5 minutes each account with API keys is compared with Bybit — all positions and open orders, not
        just the ones we think are open. It only ever <b>restores a missing stop</b> from the signal, or
        <b> cancels an order of ours whose trade is over</b> (never by age). Everything else is left for you and
        emailed once.
        <label style={{ marginLeft: 10, cursor: 'pointer' }}>
          <input type="checkbox" checked={showInfo} onChange={e => setShowInfo(e.target.checked)} /> show info items
        </label>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
        {data.accounts.map(a => {
          const items = (a.findings || []).filter(f => showInfo || f.severity === 'important' || f.done);
          const standing = (a.findings || []).filter(f => f.severity === 'important' && !f.done);
          return (
            <div key={a.id} className="card">
              <div className="card-h" style={{ justifyContent: 'space-between' }}>
                <span style={{ fontWeight: 600, fontSize: 13 }}>
                  {a.label}
                  <span className={'chip ' + (a.env === 'real' ? 'bear' : 'neutral')} style={{ fontSize: 9.5, marginLeft: 8 }}>{a.env}</span>
                  <span style={{ color: 'var(--text-4)', fontWeight: 400, fontSize: 11, marginLeft: 8 }}>{a.owner_email}</span>
                </span>
                <span style={{ fontSize: 11, color: a.full_reconcile_error ? 'var(--bear)' : !a.full_reconcile_at ? 'var(--text-4)' : standing.length ? 'var(--gold)' : 'var(--bull)' }}>
                  {a.full_reconcile_error ? 'check failed'
                    : !a.full_reconcile_at ? 'not checked yet'
                    : standing.length ? `${standing.length} need${standing.length === 1 ? 's' : ''} a look` : 'matches Bybit'}
                  <span style={{ color: 'var(--text-4)', marginLeft: 8 }}>{a.full_reconcile_at ? 'checked ' + fmtStamp(a.full_reconcile_at) : ''}</span>
                </span>
              </div>
              <div className="card-b">
                {a.full_reconcile_error && <div style={{ color: 'var(--bear)', fontSize: 11.5, marginBottom: 8 }}>{a.full_reconcile_error}</div>}
                {a.reconciliation_alert && (
                  <div style={{ color: 'var(--gold)', fontSize: 11.5, marginBottom: 8 }}>
                    New entries on this account are paused by a position-reconcile alert: {a.reconciliation_alert}
                  </div>
                )}
                {items.length ? (
                  <table style={{ width: '100%', borderCollapse: 'collapse' }}>
                    <tbody>
                      {items.map((f, i) => (
                        <tr key={i}>
                          <td style={{ ...td, width: 190 }}>{reconChip(f.kind)}</td>
                          <td style={{ ...td, fontWeight: 600, width: 70 }}>{f.symbol}</td>
                          <td style={{ ...td, color: 'var(--text-2)' }}>
                            {f.detail}
                            {f.error && <span style={{ display: 'block', color: 'var(--text-4)', fontSize: 10.5 }}>{f.error}</span>}
                          </td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                ) : <div style={{ color: 'var(--text-4)', fontSize: 11.5 }}>{a.full_reconcile_at ? 'Nothing to report.' : 'Waiting for the first check.'}</div>}
              </div>
            </div>
          );
        })}
      </div>
      <div className="card" style={{ marginTop: 14 }}>
        <div className="card-h"><span style={{ fontWeight: 600, fontSize: 13 }}>Actions taken</span></div>
        <div className="card-b">
          {data.actions.length ? (
            <table style={{ width: '100%', borderCollapse: 'collapse' }}>
              <thead><tr><th style={th}>when</th><th style={th}>account</th><th style={th}>what</th><th style={th}>coin</th><th style={th}>detail</th></tr></thead>
              <tbody>
                {data.actions.map(x => (
                  <tr key={x.id}>
                    <td style={{ ...td, color: 'var(--text-3)', whiteSpace: 'nowrap' }}>{fmtStamp(x.at)}</td>
                    <td style={td}>{(byId[x.account_id] || {}).label || x.account_id}</td>
                    <td style={td}>{reconChip(x.kind)}</td>
                    <td style={{ ...td, fontWeight: 600 }}>{x.symbol}</td>
                    <td style={{ ...td, color: 'var(--text-2)' }}>{x.detail}{x.error && <span style={{ display: 'block', color: 'var(--text-4)', fontSize: 10.5 }}>{x.error}</span>}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          ) : <div style={{ color: 'var(--text-4)', fontSize: 11.5 }}>No actions yet.</div>}
        </div>
      </div>
    </div>
  );
}

const REPORT_PAGES = [
  { id: 'entry-lag', label: 'Entry lag', blurb: 'The trader\'s entry vs the price when his message reached us — and what the lag cost' },
  { id: 'reconciliation', label: 'Reconciliation', blurb: 'Every account compared with Bybit — what\'s wrong, and what was fixed automatically' },
];

function W_ReportsSection({ page, onNavigate }) {
  const current = REPORT_PAGES.find(p => p.id === page) || REPORT_PAGES[0];
  return (
    <div>
      <div style={{ display: 'flex', gap: 4, alignItems: 'center', marginBottom: 12, flexWrap: 'wrap' }}>
        {REPORT_PAGES.map(p => (
          <button key={p.id} onClick={() => onNavigate('reports/' + p.id)} style={ledgerChip(p.id === current.id)}>{p.label}</button>
        ))}
        <span style={{ fontSize: 11, color: 'var(--text-4)', marginLeft: 8 }}>{current.blurb}</span>
      </div>
      {current.id === 'entry-lag' && <W_EntryLagReport />}
      {current.id === 'reconciliation' && <W_ReconciliationReport />}
    </div>
  );
}
