const { useState, useEffect, useRef, useMemo, useCallback } = React;

const fmt = new Intl.NumberFormat('en-US');
const fmt2 = new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 });
const fmt0 = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 });
const ms2t = (ms, cs = true) => {
  if (ms == null) return '–';
  const h = Math.floor(ms / 3600000), m = Math.floor(ms % 3600000 / 60000), s = Math.floor(ms % 60000 / 1000), c = Math.floor(ms % 1000 / 10);
  const p = n => String(n).padStart(2, '0');
  return `${p(h)}:${p(m)}:${p(s)}` + (cs ? `.${p(c)}` : '');
};
const parseT = s => { if (!s) return null; const m = s.match(/^(\d{1,2}):?(\d{2})?:?(\d{2})?(?:\.(\d{1,3}))?$/); if (!m) return null; const frac = m[4] ? Math.round(+('0.' + m[4]) * 1000) : 0; return ((+m[1]) * 3600 + (+(m[2] || 0)) * 60 + (+(m[3] || 0))) * 1000 + frac; };
const dateLabel = d => new Date(+d.slice(0, 4), +d.slice(4, 6) - 1, +d.slice(6)).toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
const priceStr = r => r.source === 'REZEF' ? (r.price / 100).toFixed(2) + ' ₪' : fmt2.format(r.price);
const api = async (url) => { if (window.localApi) return window.localApi(url); const r = await fetch(url); if (!r.ok) throw new Error(`${r.status} ${url}`); return r.json(); };

const ALL_GROUPS = ['options', 'stocks', 'bonds'];
const EMPTY = { source: '', market: '', itype: '', base: '', q: '', t0: '', t1: '', minq: '', minv: '', agg: '', phase: '', sectors: null };
function qs(f) {
  const p = new URLSearchParams();
  for (const [k, v] of Object.entries(f)) {
    if (k === 'sectors') { if (v != null) p.set('sectors', v); continue; }
    if (v === '' || v == null) continue;
    if (k === 'groups' && v.split(',').length === ALL_GROUPS.length) continue;
    if (k === 't0') { const t = parseT(v); if (t !== null) p.set('t0', t); }
    else if (k === 't1') { const t = parseT(v); if (t !== null) p.set('t1', t + (v.includes('.') ? 9 : v.split(':').length >= 3 ? 999 : 59999)); }
    else p.set(k, v);
  }
  return p.toString();
}

function Chips({ options, value, onChange }) {
  return <div className="chips">{options.map(([v, l]) => <span key={v} className={'chip' + (value === v ? ' on' : '')} onClick={() => onChange(v)}>{l}</span>)}</div>;
}

function Rail({ meta, filters, setFilters, count }) {
  // selects and chips apply immediately; text fields apply on blur or Enter
  const [text, setText] = useState(filters);
  useEffect(() => { setText(filters); }, [filters]);
  const commit = () => { const t = { ...filters }; for (const k of ['q', 't0', 't1', 'minq', 'minv']) t[k] = text[k]; if (JSON.stringify(t) !== JSON.stringify(filters)) setFilters(t); };
  const now = k => v => setFilters(f => ({ ...f, [k]: v && v.target ? v.target.value : v }));
  const edit = k => e => setText(t => ({ ...t, [k]: e.target.value }));
  const onKey = e => { if (e.key === 'Enter') { e.target.blur(); } };
  const reset = () => setFilters(EMPTY);
  return (
    <aside className="rail">
      <label>Source<Chips options={[['', 'All'], ['REZEF', 'Rezef · cash'], ['MAOF', 'MAOF · derivatives']]} value={filters.source} onChange={now('source')} /></label>
      <label>Market<select value={filters.market} onChange={now('market')}><option value="">All markets</option>{meta.markets.map(m => <option key={m}>{m}</option>)}</select></label>
      <label>Instrument type<select value={filters.itype} onChange={now('itype')}><option value="">All types</option>{meta.types.map(m => <option key={m}>{m}</option>)}</select></label>
      <label>Underlying (derivatives)<select value={filters.base} onChange={now('base')}><option value="">All</option>{meta.bases.map(m => <option key={m}>{m}</option>)}</select></label>
      <label>Symbol / name / number<input value={text.q} onChange={edit('q')} onBlur={commit} onKeyDown={onKey} placeholder="TEVA, טבע, 00629014, T35-P0024…" /></label>
      <div className="row2">
        <label>From (hh:mm)<input value={text.t0} onChange={edit('t0')} onBlur={commit} onKeyDown={onKey} placeholder="09:45" /></label>
        <label>To<input value={text.t1} onChange={edit('t1')} onBlur={commit} onKeyDown={onKey} placeholder="17:35" /></label>
      </div>
      <div className="row2">
        <label>Min qty<input type="number" min="0" value={text.minq} onChange={edit('minq')} onBlur={commit} onKeyDown={onKey} placeholder="0" /></label>
        <label>Min value<input type="number" min="0" value={text.minv} onChange={edit('minv')} onBlur={commit} onKeyDown={onKey} placeholder="0" /></label>
      </div>
      <label>Aggressor<Chips options={[['', 'Both'], ['B', 'Buy'], ['S', 'Sell']]} value={filters.agg} onChange={now('agg')} /></label>
      <label>Phase (Rezef)<Chips options={[['', 'All'], ['O', 'Open'], ['T', 'Continuous'], ['C', 'Close']]} value={filters.phase} onChange={now('phase')} /></label>
      <button className="act ghost" onClick={reset}>Reset filters</button>
      <p className="hint">{count == null ? '' : `${fmt.format(count)} trades match`}</p>
    </aside>
  );
}

function Stats({ s }) {
  if (!s) return <div className="stats" />;
  const by = Object.fromEntries(s.by_source.map(x => [x.source, x]));
  const rz = by.REZEF || { trades: 0, instruments: 0, value: 0 }, mf = by.MAOF || { trades: 0, instruments: 0, value: 0 };
  const tiles = [
    ['Trades selected', fmt.format(s.total), `of ${fmt.format(s.total_day)} on the day`],
    ['Rezef · cash market', fmt.format(rz.trades), `${rz.instruments} instruments · ${fmt0.format(rz.value / 1e6)} M NIS`],
    ['MAOF · derivatives', fmt.format(mf.trades), `${mf.instruments} contracts · ${fmt0.format(mf.value / 1e6)} M premium`],
    ['Buy-initiated', s.buy_share == null ? '–' : (100 * s.buy_share).toFixed(1) + '%', 'buy order arrived after the sell order'],
    ['Time span', `${ms2t(s.first, false)} – ${ms2t(s.last, false)}`, 'first and last selected trade'],
  ];
  for (const f of (s.flow || [])) if (f.net != null) tiles.push([`Net flow · ${f.source === 'MAOF' ? 'derivatives (Δ-adjusted)' : 'cash market'}`,
    (f.net >= 0 ? '+' : '−') + fmt0.format(Math.abs(f.net) / 1e6) + ' M', `long minus short initiated, of ${fmt0.format(f.gross / 1e6)} M gross, in underlying NIS`]);
  return <div className="stats">{tiles.map(([l, v, sub]) => <div className="stat" key={l}><div className="l">{l}</div><div className="v mono">{v}</div><div className="s">{sub}</div></div>)}</div>;
}

const START_MIN = 9 * 60 + 40, END_MIN = 17 * 60 + 40, BINS = END_MIN - START_MIN;
const START_MS = START_MIN * 60000, END_MS = END_MIN * 60000;
const msToT = (ms, withCs) => { const t = Math.floor(ms / 1000), h = Math.floor(t / 3600), m = Math.floor(t % 3600 / 60), x = t % 60, cs = Math.floor(ms % 1000 / 10); const p = n => String(n).padStart(2, '0'); return `${p(h)}:${p(m)}:${p(x)}` + (withCs ? '.' + p(cs) : ''); };
// bin width in ms from the window length in ms: 10 ms up to 30 s, 100 ms up to 5 min, 1 s up to 30 min, then 5 s / 15 s / 60 s
const binFor = len => (len <= 30000 ? 10 : len <= 300000 ? 100 : len <= 1800000 ? 1000 : len <= 7200000 ? 5000 : len <= 14400000 ? 15000 : 60000);
const binLabel = b => (b < 1000 ? b + ' ms' : b === 1000 ? '1 second' : (b / 1000) + ' seconds');
const minToHHMM = m => `${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;
const fmtM = v => (Math.abs(v) >= 1e9 ? (v / 1e9).toFixed(1) + 'B' : Math.abs(v) >= 1e6 ? (v / 1e6).toFixed(0) + 'M' : Math.abs(v) >= 1e3 ? (v / 1e3).toFixed(0) + 'k' : String(Math.round(v)));

const PALETTE = ['#2a78d6', '#eb6834', '#1baf7a', '#eda100', '#e87ba4', '#008300', '#6250d6', '#e34948', '#0fa3b1', '#b5651d', '#7f8c3a', '#c2185b', '#5c6bc0', '#8d6e63', '#26a69a', '#ff7043', '#9ccc65', '#ab47bc', '#789262', '#d4a017', '#4db6ac', '#ef5350', '#7e57c2', '#8a8a8a'];
// fixed colour per sector name, independent of the day and of which sectors are present
const SECTOR_ORDER = ['Technology', 'Real estate & construction', 'Derivatives', 'Trade & services', 'Corporate bonds · unlinked', 'Corporate bonds · CPI-linked', 'Banks', 'Insurance',
  'Energy, oil & gas', 'Industry', 'Financial services', 'ETFs', 'Biomed', 'Investment & holdings', 'Government bonds · unlinked', 'Government bonds · CPI-linked',
  'MAKAM (T-bills)', 'Bond ETFs', 'Bonds · FX-linked', 'Financial instruments', 'Commercial paper', 'sector 83', 'sector 77', 'Unclassified'];
const hashStr = t => { let h = 0; for (let i = 0; i < t.length; i++) h = (h * 31 + t.charCodeAt(i)) >>> 0; return h; };
const sectorColor = (name) => { const i = SECTOR_ORDER.indexOf(name); return PALETTE[(i >= 0 ? i : SECTOR_ORDER.length + hashStr(name || '')) % PALETTE.length]; };

function SectorChips({ all, value, onChange }) {
  // value: null = every sector active; otherwise '|'-joined list of active sector names ('' = none)
  const active = value == null ? new Set(all.map(x => x.sector)) : new Set(value ? value.split('|') : []);
  const emit = set => onChange(set.size === all.length ? null : all.filter(x => set.has(x.sector)).map(x => x.sector).join('|'));
  const toggle = name => { const n = new Set(active); if (n.has(name)) n.delete(name); else n.add(name); emit(n); };
  return <div style={{ marginBottom: 10 }}>
    <div className="chips" style={{ alignItems: 'center' }}>
      <span className="hint" style={{ marginRight: 4 }}>{active.size} of {all.length} sectors</span>
      <span className="chip" onClick={() => onChange(null)}>All</span><span className="chip" onClick={() => onChange('')}>None</span>
      <span style={{ width: 1, height: 18, background: 'var(--line)', margin: '0 4px' }} />
      {all.map(x => <span key={x.sector} className={'chip' + (active.has(x.sector) ? ' on' : '')} onClick={() => toggle(x.sector)} role="button" tabIndex={0}
        title={`${fmt.format(x.trades)} trades · ${x.assets} instruments${x.super_sector ? ' · ' + x.super_sector : ''}`}><span style={{ display: 'inline-block', width: 9, height: 9, borderRadius: 2, background: sectorColor(x.sector, all), marginRight: 6, verticalAlign: '0' }} />{x.sector} <span style={{ opacity: .7 }}>{fmt.format(x.trades)}</span></span>)}
    </div>
  </div>;
}

function Histogram({ mags, bin, sectors, sel, onSelect, onBack, onHover }) {
  const ref = useRef(null);
  const ovl = useRef(null);
  const drag = useRef(null);
  const [hover, setHover] = useState(null);
  const [tmp, setTmp] = useState(null);
  const H = 240, PAD = { l: 52, r: 16, t: 14, b: 22 };
  // visible window in seconds; bars are `bin` seconds wide
  const s0 = sel ? sel[0] : START_MS, s1 = sel ? sel[1] : END_MS - 1;   // ms
  const b0 = Math.floor(s0 / bin), b1 = Math.floor(s1 / bin), NB = b1 - b0 + 1;
  const geom = () => { const c = ref.current; const W = c.getBoundingClientRect().width || c.clientWidth || 900; const pw = W - PAD.l - PAD.r; return { W, pw, bw: pw / NB }; };
  const xToSec = x => { const { bw } = geom(); const b = Math.max(b0, Math.min(b1, b0 + Math.floor((x - PAD.l) / bw))); return b; };
  const ph = H - PAD.t - PAD.b, mid = PAD.t + ph / 2;
  const series = useMemo(() => {
    const per = new Map(), pos = new Map(), neg = new Map(), cnt = new Map();
    for (const r of (mags || [])) { const b = r.minute; if (!per.has(b)) { per.set(b, []); pos.set(b, 0); neg.set(b, 0); cnt.set(b, 0); } per.get(b).push(r); cnt.set(b, cnt.get(b) + r.orders); if (r.sum_m > 0) pos.set(b, pos.get(b) + r.sum_m); else neg.set(b, neg.get(b) + r.sum_m); }
    for (const l of per.values()) l.sort((a, b) => Math.abs(b.sum_m) - Math.abs(a.sum_m));
    return { per, pos, neg, cnt };
  }, [mags]);
  const ymax = useMemo(() => { let amax = 1; for (let b = b0; b <= b1; b++) { amax = Math.max(amax, series.pos.get(b) || 0, -(series.neg.get(b) || 0)); } const step = Math.pow(10, Math.floor(Math.log10(amax))); return Math.ceil(amax / step) * step; }, [series, b0, b1]);
  const yOf = m => mid - (m / ymax) * ph / 2;
  const span = s1 - s0 + 1; const tick = span > 5 * 3600000 ? 3600000 : span > 2 * 3600000 ? 1800000 : span > 3600000 ? 600000 : span > 1200000 ? 300000 : span > 300000 ? 60000 : span > 60000 ? 10000 : span > 20000 ? 5000 : span > 5000 ? 1000 : span > 1000 ? 200 : 50;
  const label = t => (tick >= 60000 ? msToT(t).slice(0, 5) : tick >= 1000 ? msToT(t) : msToT(t, true));

  const drawData = useCallback(() => {
    const c = ref.current; if (!c) return;
    const ctx = c.getContext('2d'); const { W, bw } = geom(); const dpr = window.devicePixelRatio || 1;
    c.width = W * dpr; c.height = H * dpr; ctx.scale(dpr, dpr);
    const cs = getComputedStyle(document.documentElement);
    const muted = cs.getPropertyValue('--muted').trim(), line = cs.getPropertyValue('--line').trim(), accent = cs.getPropertyValue('--accent').trim();
    ctx.clearRect(0, 0, W, H);
    if (tmp) { const [a, b] = tmp; const x0 = PAD.l + (Math.min(a, b) - b0) * bw, x1 = PAD.l + (Math.max(a, b) - b0 + 1) * bw; ctx.fillStyle = accent; ctx.globalAlpha = 0.14; ctx.fillRect(x0, PAD.t, x1 - x0, ph); ctx.globalAlpha = 1; }
    ctx.font = '11px IBM Plex Mono, monospace'; ctx.strokeStyle = line; ctx.fillStyle = muted; ctx.textAlign = 'right';
    for (const g of [-1, -0.5, 0, 0.5, 1]) { const v = g * ymax, y = yOf(v); ctx.beginPath(); ctx.moveTo(PAD.l, y); ctx.lineTo(W - PAD.r, y); ctx.stroke(); ctx.fillText((v > 0 ? '+' : '') + (Math.abs(v) >= 10 ? v.toFixed(0) : v.toFixed(1)), PAD.l - 6, y + 4); }
    const w = Math.max(1, bw - (bw > 3 ? 1 : 0));
    for (let b = b0; b <= b1; b++) {
      const l = series.per.get(b); if (!l) continue; const x = PAD.l + (b - b0) * bw; let up = 0, dn = 0;
      for (const r of l) {
        if (!r.sum_m) continue; ctx.fillStyle = sectorColor(r.sector, sectors);
        if (r.sum_m > 0) { const y0 = yOf(up), y1 = yOf(up + r.sum_m); ctx.fillRect(x, y1, w, Math.max(0.5, y0 - y1)); up += r.sum_m; }
        else { const y0 = yOf(dn), y1 = yOf(dn + r.sum_m); ctx.fillRect(x, y0, w, Math.max(0.5, y1 - y0)); dn += r.sum_m; }
      }
    }
    ctx.strokeStyle = muted; ctx.beginPath(); ctx.moveTo(PAD.l, mid); ctx.lineTo(W - PAD.r, mid); ctx.stroke();
    ctx.fillStyle = muted; ctx.textAlign = 'center';
    for (let t = Math.ceil(s0 / tick) * tick; t <= s1; t += tick) ctx.fillText(label(t), PAD.l + (t / bin - b0) * bw, H - 6);
  }, [series, sectors, ymax, b0, b1, bin, s0, s1, tmp && tmp[0], tmp && tmp[1]]);

  const drawOverlay = useCallback(() => {
    const o = ovl.current; if (!o) return;
    const ctx = o.getContext('2d'); const { W, bw } = geom(); const dpr = window.devicePixelRatio || 1;
    o.width = W * dpr; o.height = H * dpr; ctx.scale(dpr, dpr); ctx.clearRect(0, 0, W, H);
    if (hover == null || hover < b0 || hover > b1) return;
    const muted = getComputedStyle(document.documentElement).getPropertyValue('--muted').trim();
    const x = PAD.l + (hover - b0) * bw; ctx.strokeStyle = muted; ctx.setLineDash([3, 3]); ctx.strokeRect(x - 0.5, PAD.t, Math.max(2, bw) + 1, ph); ctx.setLineDash([]);
  }, [hover, b0, b1]);

  useEffect(() => { drawData(); window.addEventListener('resize', drawData); return () => window.removeEventListener('resize', drawData); }, [drawData]);
  useEffect(() => { drawOverlay(); }, [drawOverlay]);

  const pos = e => { const r = ref.current.getBoundingClientRect(); return xToSec(e.clientX - r.left); };
  const onDown = e => { if (e.button !== 0) return; const b = pos(e); drag.current = b; setTmp([b, b]); };
  const onMove = e => { const b = pos(e); if (b !== hover) { setHover(b); if (onHover) onHover([b * bin, (b + 1) * bin - 1]); } if (drag.current != null) setTmp(t => (t && t[1] === b ? t : [drag.current, b])); };
  const onUp = e => { if (drag.current == null) return; const a = drag.current, b = pos(e); drag.current = null; setTmp(null); const lo = Math.min(a, b) * bin, hi = (Math.max(a, b) + 1) * bin - 1; onSelect([Math.max(START_MS, lo), Math.min(END_MS - 1, hi)]); };
  const onLeave = () => { setHover(null); if (onHover) onHover(null); if (drag.current != null) { drag.current = null; setTmp(null); } };
  const reset = e => { e.preventDefault(); onSelect(null); };
  const back = e => { e.preventDefault(); (onBack || (() => onSelect(null)))(); };
  const f2 = v => v == null ? '–' : (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v).toFixed(2);
  const hl = hover != null && series.per.get(hover); const hv = hl ? { t: msToT(hover * bin, bin < 1000), n: series.cnt.get(hover), tot: series.pos.get(hover) + series.neg.get(hover), list: hl.slice(0, 4) } : null;
  return (
    <div className="panel">
      <h2>Σ trade magnitude per {binLabel(bin)}, by sector{sel ? <span className="mono" style={{ marginLeft: 8 }}>{msToT(s0, bin < 1000)}–{msToT(s1 + 1, bin < 1000)}</span> : null}
        <span>{hv ? <span className="mono">{hv.t} · {fmt.format(hv.n)} orders · total {f2(hv.tot)} · {hv.list.map(r => <span key={r.sector} style={{ marginLeft: 8 }}><span style={{ display: 'inline-block', width: 8, height: 8, background: sectorColor(r.sector, sectors), borderRadius: 2, marginRight: 4 }} />{r.sector} {f2(r.sum_m)}</span>)}</span>
          : <span>stacked by active sector · bars are {binLabel(bin)} wide (down to 10 ms once the window is ≤ 30 s) · drag to zoom · right-click: one zoom level back · double-click: whole day</span>}</span></h2>
      <div style={{ position: 'relative', height: H }}>
        <canvas ref={ref} height={H} role="img" aria-label="Stacked per-sector trade magnitude per time bin; drag to zoom, right-click to reset" style={{ position: 'absolute', inset: 0 }} />
        <canvas ref={ovl} height={H} aria-hidden="true" style={{ position: 'absolute', inset: 0, cursor: 'crosshair' }}
          onMouseDown={onDown} onMouseMove={onMove} onMouseUp={onUp} onMouseLeave={onLeave} onDoubleClick={reset} onContextMenu={back} />
      </div>
    </div>
  );
}

function SectorTrades({ day, filters, sector }) {
  const [page, setPage] = useState(0);
  const [sort, setSort] = useState({ key: 'abs_magnitude', desc: true });
  const [data, setData] = useState(null);
  const SIZE = 50;
  const f = sector == null ? filters : { ...filters, sectors: sector };
  useEffect(() => { setData(null); api(`/api/day/${day}/trades?${qs(f)}&page=${page}&size=${SIZE}&sort=${sort.key}&desc=${sort.desc}`).then(setData); }, [day, JSON.stringify(f), page, sort]);
  const onSort = k => { setSort(s => ({ key: k, desc: s.key === k ? !s.desc : (k !== 'time' && k !== 'instrument') })); setPage(0); };
  return <div style={{ padding: '6px 0 10px 24px', borderLeft: '3px solid var(--accent)', margin: '4px 0 8px' }}>
    {data ? <><Tape data={data} onPick={() => {}} sort={sort} onSort={onSort} /><Pager total={data.total} page={page} size={SIZE} setPage={setPage} /></> : <p className="hint">loading…</p>}
  </div>;
}

function Totals({ data, sectors, day, filters }) {
  const [open, setOpen] = useState(null);   // null = closed, '' = all active sectors, or a sector name
  if (!data || !data.total) return <p className="hint">No scored orders in this window.</p>;
  const f1 = v => (v == null ? '' : (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v).toFixed(1));
  const NCOLS = 8;
  const Row = ({ r, name, bold, color, key_ }) => { const isOpen = open === key_; return <React.Fragment>
    <tr className="click" style={bold ? { fontWeight: 600 } : {}} onClick={() => setOpen(isOpen ? null : key_)} title="click to show the trades">
      <td><span style={{ display: 'inline-block', width: 12, color: 'var(--muted)' }}>{isOpen ? '▾' : '▸'}</span>{color ? <span style={{ display: 'inline-block', width: 9, height: 9, borderRadius: 2, background: color, marginRight: 6 }} /> : null}{name}</td>
      <td className="num mono">{fmt.format(r.orders)}</td><td className="num mono">{fmt.format(r.trades)}</td>
      <td className="num mono" style={{ color: 'var(--buy)' }}>{fmt.format(r.long_orders)}</td><td className="num mono" style={{ color: 'var(--sell)' }}>{fmt.format(r.short_orders)}</td>
      <td className="num mono" style={{ color: 'var(--buy)' }}>+{r.long_m.toFixed(1)}</td><td className="num mono" style={{ color: 'var(--sell)' }}>−{r.short_m.toFixed(1)}</td>
      <td className="num mono" style={{ color: r.net_m > 0 ? 'var(--buy)' : r.net_m < 0 ? 'var(--sell)' : undefined, fontWeight: 600 }}>{f1(r.net_m)}</td>
    </tr>
    {isOpen && <tr><td colSpan={NCOLS} style={{ padding: 0, whiteSpace: 'normal' }}><SectorTrades day={day} filters={filters} sector={key_ === '' ? null : key_} /></td></tr>}
  </React.Fragment>; };
  return <div className="tblwrap"><table><thead><tr><th>Sector</th><th className="num">Orders</th><th className="num">Trades</th><th className="num" title="orders whose aggressor is long the underlying">Long orders</th><th className="num" title="orders whose aggressor is short the underlying">Short orders</th>
    <th className="num">Σ long magnitude</th><th className="num">Σ short magnitude</th><th className="num">Net</th></tr></thead>
    <tbody><Row r={data.total} name="All active sectors" bold key_="" />{data.rows.map(r => <Row key={r.sector} r={r} name={r.sector} color={sectorColor(r.sector)} key_={r.sector} />)}</tbody></table></div>;
}

function Pager({ total, page, size, setPage }) {
  const pages = Math.max(1, Math.ceil(total / size));
  return <div className="pager"><span>{fmt.format(total)} rows · page {page + 1} of {fmt.format(pages)}</span>
    <button disabled={page === 0} onClick={() => setPage(page - 1)}>Prev</button><button disabled={page >= pages - 1} onClick={() => setPage(page + 1)}>Next</button></div>;
}

const Sector = ({ r }) => r.sector ? <span title={r.sector_code ? `code ${r.sector_code}` : ''}>{r.sector}{r.sub_sector ? <span style={{ color: 'var(--muted)' }}> · {r.sub_sector}</span> : null}</span> : <span className="hint">–</span>;
const Inst = ({ r }) => r.source === 'MAOF' ? <span>{r.symbol}</span> : <span>{r.symbol} <span style={{ color: 'var(--muted)' }}>{r.name_he}</span></span>;

const detail = r => [r.source === 'REZEF' ? 'cash market · ' + r.market : 'derivatives · ' + (r.base_asset_name || ''),
  r.instrument_type, r.series, r.strike != null ? 'strike ' + r.strike : null, r.expiry ? 'expiry ' + r.expiry.slice(6) + '/' + r.expiry.slice(4, 6) : null,
  r.phase ? 'phase ' + r.phase : null, r.buy_order != null ? 'orders ' + r.buy_order + ' / ' + r.sell_order : null, r.trade_no != null ? 'trade #' + r.trade_no : null, r.dir != null ? (r.dir > 0 ? 'long-initiated' : 'short-initiated') : null, 'id ' + r.instrument].filter(Boolean).join(' · ');
const Signed = ({ v }) => v == null ? <td className="num mono"></td> : <td className="num mono" style={{ color: v > 0 ? 'var(--buy)' : v < 0 ? 'var(--sell)' : undefined }}>{(v >= 0 ? '+' : '−') + fmt0.format(Math.abs(v))}</td>;
const Th = ({ k, label, num, sort, onSort }) => <th className={(num ? 'num ' : '') + 'sortable'} onClick={() => onSort(k)} title="click to sort">{label}{sort && sort.key === k ? (sort.desc ? ' \u25be' : ' \u25b4') : ''}</th>;
const mag = r => (r.size_z == null ? null : (r.dir || 1) * Math.tanh(Math.max(0, r.size_z) / 3));   // in [-1, 1]: sign = direction, |m| = tanh(z/3): z 3 -> 0.76, z 5 -> 0.93
const rowStyle = m => ({});   // no row shading; the magnitude cell carries the colour
const MagCell = ({ m }) => m == null ? <td className="num mono"></td> : <td className="num mono" style={{ fontWeight: Math.abs(m) >= 0.76 ? 600 : 400, color: Math.abs(m) < 0.3 ? undefined : m > 0 ? 'var(--buy)' : 'var(--sell)' }}>{(m > 0 ? '+' : m < 0 ? '−' : '') + Math.abs(m).toFixed(2)}</td>;
const ZCell = ({ z }) => { if (z == null) return <td className="num mono"></td>; const a = Math.min(1, Math.max(0, (z - 1) / 5)); return <td className="num mono" style={{ fontWeight: z >= 3 ? 600 : 400, background: z >= 2 ? `rgba(11,110,122,${0.12 + 0.5 * a})` : undefined }} title={z >= 3 ? 'unusually large order for this instrument' : ''}>{z.toFixed(2)}</td>; };
const Dir = ({ d }) => <td>{d == null ? '' : <span className={'pill ' + (d > 0 ? 'B' : 'S')}>{d > 0 ? '▲ long' : '▼ short'}</span>}</td>;

function Tape({ data, onPick, sort, onSort, hl }) {
  const rows = data.rows;
  const inHl = r => hl && r.time_ms >= hl[0] && r.time_ms <= hl[1];
  return <div className="tblwrap"><table><thead><tr><Th k="time" label="Time" sort={sort} onSort={onSort} /><Th k="instrument" label="Instrument" sort={sort} onSort={onSort} /><Th k="sector" label="Sector" sort={sort} onSort={onSort} /><Th k="size" label="Size" num sort={sort} onSort={onSort} /><Th k="price" label="Price" num sort={sort} onSort={onSort} /><Th k="value" label="Value" num sort={sort} onSort={onSort} /><Th k="side" label="Side" sort={sort} onSort={onSort} /><Th k="dir" label="Direction" sort={sort} onSort={onSort} /><Th k="magnitude" label="Trade magnitude" num sort={sort} onSort={onSort} /><Th k="abs_magnitude" label="|magnitude|" num sort={sort} onSort={onSort} /></tr></thead>
    <tbody>{rows.map((r, j) => <tr key={r.seq} className={'click' + (j > 0 && r.time_ms !== rows[j - 1].time_ms ? ' batch-start' : '') + (inHl(r) ? ' hl' : '')} onClick={() => onPick(r)} title={detail(r) + (r.delta_notional != null ? ` · Δ-size ${fmt0.format(r.delta_notional)} NIS · size pct ${r.size_pct}` : '')} style={rowStyle(mag(r))}>
      <td className="mono">{ms2t(r.time_ms)}</td>
      <td><span className={'pill ' + r.source} style={{ marginRight: 6 }}>{r.source === 'REZEF' ? 'RZ' : 'MF'}</span><Inst r={r} /></td>
      <td><Sector r={r} /></td>
      <td className="num mono">{fmt.format(r.qty)}</td><td className="num mono">{priceStr(r)}</td><td className="num mono">{fmt0.format(r.value)}</td>
      <td><span className={'pill ' + r.aggressor} title="aggressor side: which order crossed the spread">{r.aggressor === 'B' ? 'BUY' : 'SELL'}</span></td>
      <Dir d={r.dir} /><MagCell m={mag(r)} /><td className="num mono">{mag(r) == null ? '' : Math.abs(mag(r)).toFixed(2)}</td></tr>)}</tbody></table></div>;
}

function Instruments({ data, onPick, sort, onSort }) {
  const mx = Math.max(1, ...data.rows.map(r => r.trades));
  return <div className="tblwrap"><table><thead><tr><Th k="instrument" label="Instrument" sort={sort} onSort={onSort} /><Th k="sector" label="Sector" sort={sort} onSort={onSort} /><Th k="trades" label="Trades" num sort={sort} onSort={onSort} /><th></th><Th k="size" label="Size" num sort={sort} onSort={onSort} /><Th k="value" label="Value" num sort={sort} onSort={onSort} /><Th k="net_flow" label="Net flow" num sort={sort} onSort={onSort} /><th className="num">Long %</th><Th k="magnitude" label="Max magnitude" num sort={sort} onSort={onSort} /><th className="num">First</th><th className="num">Last</th><Th k="first" label="First t" sort={sort} onSort={onSort} /><Th k="last" label="Last t" sort={sort} onSort={onSort} /></tr></thead>
    <tbody>{data.rows.map(r => { const rz = r.source === 'REZEF'; const px = v => rz ? (v / 100).toFixed(2) : fmt2.format(v);
      const longShare = r.gross_flow ? (r.gross_flow + r.net_flow) / (2 * r.gross_flow) : null;
      return <tr key={r.instrument} className="click" onClick={() => onPick(r)} title={detail(r)}>
      <td><span className={'pill ' + r.source} style={{ marginRight: 6 }}>{rz ? 'RZ' : 'MF'}</span><Inst r={r} /></td>
      <td><Sector r={r} /></td>
      <td className="num mono">{fmt.format(r.trades)}</td><td><div className="bar"><i style={{ width: (100 * r.trades / mx) + '%' }} /></div></td>
      <td className="num mono">{fmt.format(r.qty)}</td><td className="num mono">{fmt0.format(r.value)}</td><Signed v={r.net_flow} />
      <td className="num mono">{longShare == null ? '' : (100 * longShare).toFixed(0)}</td><td className="num mono">{r.max_z == null ? '' : Math.tanh(Math.max(0, r.max_z) / 3).toFixed(2)}</td>
      <td className="num mono">{px(r.first_px)}</td><td className="num mono">{px(r.last_px)}</td><td className="mono">{ms2t(r.first, false)}</td><td className="mono">{ms2t(r.last, false)}</td></tr>; })}</tbody></table></div>;
}

function Batches({ data }) {
  return <div>
    <p className="hint" style={{ marginBottom: 8 }}>{fmt.format(data.distinct_timestamps)} distinct timestamps in the selection; {fmt.format(data.total)} carry more than one trade — one aggressive order sweeping several resting orders or several contracts at once. Sorted by size.</p>
    <div className="tblwrap"><table><thead><tr><th>Time</th><th className="num">Trades</th><th className="num">Instruments</th><th>Side mix</th><th className="num">Qty</th><th className="num">Value</th><th className="num">Net flow</th><th>Instruments hit</th></tr></thead>
      <tbody>{data.rows.map(r => <tr key={r.time_ms}><td className="mono">{ms2t(r.time_ms)}</td><td className="num mono">{r.trades}</td><td className="num mono">{r.instruments}</td>
        <td><span className="pill B">{r.buys} buy</span> <span className="pill S">{r.trades - r.buys} sell</span> <span className="hint">aggressor</span></td><td className="num mono">{fmt.format(r.qty)}</td><td className="num mono">{fmt0.format(r.value)}</td><Signed v={r.net_flow} />
        <td>{r.symbols.join(', ')}{r.instruments > 8 ? ' …' : ''}</td></tr>)}</tbody></table></div></div>;
}

function InstrumentModal({ day, inst, onClose }) {
  const [d, setD] = useState(null);
  useEffect(() => { api(`/api/day/${day}/instrument/${inst.instrument}`).then(setD); }, [day, inst]);
  const rz = inst.source === 'REZEF'; const px = v => rz ? (v / 100).toFixed(2) : fmt2.format(v);
  return <div className="overlay" onClick={onClose}><div className="modal" onClick={e => e.stopPropagation()}>
    <header><h3><Inst r={inst} /> <span className="mono" style={{ color: 'var(--muted)', fontWeight: 400 }}>{inst.instrument}</span></h3>
      <span className="hint">{d ? `${fmt.format(d.total)} trades on ${dateLabel(day)}` : 'loading…'}</span><button className="act ghost" onClick={onClose}>Close</button></header>
    {d && <div className="tblwrap"><table><thead><tr><th>Time</th><th className="num">Price</th><th className="num">Qty</th><th className="num">Value</th><th>Side</th><th>Direction</th><th className="num">Trade magnitude</th></tr></thead>
      <tbody>{d.rows.map((r, j) => <tr key={r.trade_no} className={j > 0 && r.time_ms !== d.rows[j - 1].time_ms ? 'batch-start' : ''} style={rowStyle(mag(r))}><td className="mono">{ms2t(r.time_ms)}</td><td className="num mono">{px(r.price)}</td><td className="num mono">{fmt.format(r.qty)}</td><td className="num mono">{fmt0.format(r.value)}</td>
        <td><span className={'pill ' + r.aggressor}>{r.aggressor === 'B' ? 'BUY' : 'SELL'}</span></td><Dir d={r.dir} /><MagCell m={mag(r)} /></tr>)}</tbody></table></div>}
  </div></div>;
}

function DayView({ day, onStatus }) {
  const [meta, setMeta] = useState(null);
  const [filters, setFilters] = useState(EMPTY);
  const [summary, setSummary] = useState(null);
  const [minutes, setMinutes] = useState([]);
  const [mags, setMags] = useState([]);
  const [index, setIndex] = useState([]);
  const [tab, setTab] = useState('tape');
  const [totals, setTotals] = useState(null);
  const [page, setPage] = useState(0);
  const [table, setTable] = useState(null);
  const [sort, setSort] = useState({ key: 'time', desc: false });
  const onSort = k => { setSort(s => ({ key: k, desc: s.key === k ? !s.desc : (k !== 'time' && k !== 'instrument') })); setPage(0); };
  const [pick, setPick] = useState(null);
  const [hl, setHl] = useState(null);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);
  const SIZE = 100;
  const t0m = parseT(filters.t0), t1m = parseT(filters.t1);
  const selBins = t0m != null && t1m != null ? [t0m, t1m] : null;   // ms
  const bin = binFor(selBins ? selBins[1] - selBins[0] + 1 : END_MS - START_MS);
  useEffect(() => { if (onStatus) onStatus({ bin, window: selBins ? `${msToT(selBins[0], bin < 1000)}–${msToT(selBins[1] + 1, bin < 1000)}` : 'whole day', canShift: !!selBins, atStart: !selBins || selBins[0] <= START_MS, atEnd: !selBins || selBins[1] >= END_MS - 1, shift: shiftWin }); }, [bin, selBins && selBins[0], selBins && selBins[1], filters]);
  const zoomHist = useRef([]);   // stack of previous windows (null = whole day)
  const applyWin = b => setF(b ? { ...filters, t0: msToT(b[0], true), t1: msToT(b[1], true) } : { ...filters, t0: '', t1: '' });
  const onSelectBins = b => { if (b) zoomHist.current.push(selBins); else zoomHist.current = []; applyWin(b); };
  const onZoomBack = () => { const prev = zoomHist.current.length ? zoomHist.current.pop() : null; applyWin(prev); };
  // window end is stored inclusively at 10 ms precision (hh:mm:ss.cc), so the true length is end + 10 ms - start
  const shiftWin = dir => { if (!selBins) return; const len = selBins[1] + 10 - selBins[0]; let a = selBins[0] + dir * len; a = Math.max(START_MS, Math.min(END_MS - len, a)); if (a === selBins[0]) return; applyWin([a, a + len - 10]); };
  useEffect(() => { const h = e => { if (e.target && /INPUT|SELECT|TEXTAREA/.test(e.target.tagName)) return; if (e.key === 'ArrowLeft') shiftWin(-1); else if (e.key === 'ArrowRight') shiftWin(1); }; window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h); }, [selBins && selBins[0], selBins && selBins[1], filters]);

  useEffect(() => { setMeta(null); api(`/api/day/${day}/meta`).then(setMeta).catch(e => setErr(String(e))); setFilters(f => ({ ...EMPTY, sectors: f.sectors })); setPage(0); zoomHist.current = []; }, [day]);   // new day: keep the sector selection, reset the time window
  useEffect(() => {
    if (!meta) return; const q = qs(filters); setBusy(true);
    Promise.all([api(`/api/day/${day}/summary?${q}`), api(`/api/day/${day}/magnitude?${q}&bin_ms=${bin}`), api(`/api/day/${day}/totals?${q}`)]).then(([s, g, t]) => { setSummary(s); setMags(g); setTotals(t); }).catch(e => setErr(String(e))).finally(() => setBusy(false));
  }, [meta, filters, day, bin]);
  useEffect(() => {
    if (!meta || tab !== 'tape') return; const q = qs(filters); setBusy(true);
    api(`/api/day/${day}/${tab === 'tape' ? 'trades' : tab === 'inst' ? 'instruments' : 'batches'}?${q}&page=${page}&size=${SIZE}&sort=${sort.key}&desc=${sort.desc}`).then(setTable).catch(e => setErr(String(e))).finally(() => setBusy(false));
  }, [meta, filters, day, tab, page, sort]);

  const setF = f => { setFilters(f); setPage(0); };
  if (err) return <div className="err">{err}</div>;
  if (!meta) return <p className="hint" style={{ padding: 24 }}>Loading {day}…</p>;
  return <div className={busy ? 'loading' : ''}>
    <div className="wrap single">
      <div className="main">
        <SectorChips all={meta.sectors || []} value={filters.sectors} onChange={v => setF({ ...filters, sectors: v })} />
        <Histogram mags={mags} bin={bin} sectors={meta.sectors || []} sel={selBins} onSelect={onSelectBins} onBack={onZoomBack} onHover={setHl} />
        <div className="panel">
          <div className="tabs"><button className={'tab' + (tab === 'tape' ? ' on' : '')} onClick={() => setTab('tape')}>Tape</button><button className={'tab' + (tab === 'totals' ? ' on' : '')} onClick={() => setTab('totals')}>Totals by direction</button>
            <span className="hint" style={{ marginLeft: 'auto', alignSelf: 'center' }}>{summary ? `${fmt.format(summary.total)} of ${fmt.format(summary.total_day)} trades` : ''}{filters.t0 ? ` · window ${filters.t0}–${filters.t1}` : ' · whole day'}{table ? (table.total ? ` · showing rows ${table.page * table.size + 1}–${Math.min(table.total, (table.page + 1) * table.size)}` : ' · no trades in this window') : ''}{filters.sectors != null ? ` · ${filters.sectors ? filters.sectors.split('|').length : 0} sectors` : ''}</span></div>
          {tab === 'tape' && table && <Tape data={table} onPick={setPick} sort={sort} onSort={onSort} hl={hl} />}
          {tab === 'totals' && <Totals data={totals} sectors={meta.sectors || []} day={day} filters={filters} />}
          {tab === 'tape' && table && <Pager total={table.total} page={page} size={SIZE} setPage={setPage} />}
        </div>
      </div>
    </div>
    {pick && <InstrumentModal day={day} inst={pick} onClose={() => setPick(null)} />}
  </div>;
}

function Files() {
  const [files, setFiles] = useState(null);
  useEffect(() => { api('/api/files').then(setFiles); }, []);
  if (!files) return <p className="hint" style={{ padding: 24 }}>Loading…</p>;
  const groups = {}; for (const f of files) (groups[f.kind] = groups[f.kind] || []).push(f);
  const mb = n => (n / 1e6).toFixed(1) + ' MB';
  return <div className="files">
    <p className="hint">Every file the extraction scripts have written under <span className="mono">data/</span>. Click a name to download it.</p>
    {Object.entries(groups).map(([k, fs]) => <div key={k}><h2>{k} · {fs.length} files · {mb(fs.reduce((s, f) => s + f.size, 0))}</h2>
      <div className="panel tblwrap"><table><thead><tr><th>File</th><th className="num">Rows</th><th className="num">Size</th></tr></thead><tbody>
        {fs.map(f => <tr key={f.path}><td><a href={f.href || `/api/download/${f.path}`}>{f.path}</a></td><td className="num mono">{f.rows == null ? '' : fmt.format(f.rows)}</td><td className="num mono">{mb(f.size)}</td></tr>)}</tbody></table></div></div>)}
  </div>;
}

function App() {
  const [days, setDays] = useState([]);
  const [day, setDay] = useState(null);
  const [view, setView] = useState('day');
  const [status, setStatus] = useState(null);
  useEffect(() => { api('/api/days').then(d => { setDays(d); if (d.length) setDay(d[d.length - 1].date); }); }, []);   // open on the most recent day
  const idx = days.findIndex(d => d.date === day);
  return <div>
    <div className="top" style={{ display: 'grid', gridTemplateColumns: '1fr auto 1fr', alignItems: 'center' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
        <h1>TASE data browser</h1>
        <nav><a className={view === 'day' ? 'on' : ''} onClick={() => setView('day')}>Day tape</a>{!window.localApi && <a className={view === 'files' ? 'on' : ''} onClick={() => setView('files')}>Files</a>}</nav>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'center' }}>
        {view === 'day' && status && <>
          <button className="act ghost" disabled={!status.canShift || status.atStart} onClick={() => status.shift(-1)} title="previous window of the same length (← key)">‹ Prev</button>
          <span className="mono" style={{ fontSize: 15, fontWeight: 500, padding: '6px 14px', border: '1px solid var(--line)', borderRadius: 999, color: 'var(--ink)', background: 'var(--surface2)' }} title="current bar width of the graphs · current time window">bars: <span style={{ color: 'var(--accent)' }}>{binLabel(status.bin)}</span> · {status.window}</span>
          <button className="act ghost" disabled={!status.canShift || status.atEnd} onClick={() => status.shift(1)} title="next window of the same length (→ key)">Next ›</button></>}
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'flex-end' }}>
        {view === 'day' && day && <>
          <button className="act ghost" disabled={idx <= 0} onClick={() => setDay(days[idx - 1].date)}>‹</button>
          <select value={day} onChange={e => setDay(e.target.value)}>{days.map(d => <option key={d.date} value={d.date}>{dateLabel(d.date)} · {fmt.format(d.rows)} trades</option>)}</select>
          <button className="act ghost" disabled={idx >= days.length - 1} onClick={() => setDay(days[idx + 1].date)}>›</button></>}
      </div>
    </div>
    {view === 'day' ? (day ? <DayView day={day} onStatus={setStatus} /> : <p className="hint" style={{ padding: 24 }}>No daily files yet — run <span className="mono">python tase_hft/daily_trades.py --from … --to …</span></p>) : <Files />}
  </div>;
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
