// Dashboard — landing page. Stat cards, equity chart, ticker, trades, alerts.
//
// Mock-free as of M11.1 cleanup: every panel renders an empty / loading
// state until the bot publishes real data. We never fall back to
// hard-coded sample numbers — that masked which paths were live vs.
// scaffolding and made it impossible for an operator to tell at a
// glance.
const { Card, Stat, Pill, Copy, fmtTs, fmtNum, fmtPct, since } = window.UI;
const RC = window.Recharts;

// adapt a backend apiTrade (alt_qty, crypto_qty, ts_ms) to the table shape
// used throughout the UI (qty, price, fee, ts).
function adaptTrade(t) {
  const alt = parseFloat(t.alt_qty || '0') || 0;
  const crypto = parseFloat(t.crypto_qty || '0') || 0;
  // Implied execution price = bridge spent / alt received. Real fee is
  // not on the API yet (M11.2 will add it via api_events); show 0 for
  // now so the column never reads a hardcoded mock value.
  const price = alt > 0 ? crypto / alt : 0;
  return {
    id: t.id,
    ts: t.ts_ms,
    from: t.from || '—',
    to: t.to || '—',
    qty: alt,
    price,
    fee: 0,
    state: t.state || 'PENDING',
    client_order_id: t.client_order_id || '',
    account: '', // filled by caller
  };
}

function adaptAlert(a) {
  return {
    kind: a.kind || 'event',
    severity: a.severity || 'info',
    reason: a.message || a.code || '',
    at: a.at_ms || 0,
    account: '',
  };
}

function adaptEquity(arr) {
  return (arr || []).map(p => ({ t: p.t_ms, v: parseFloat(p.v) || 0 }));
}

function PageDashboard({ app, setApp }) {
  const [window_, setWindow] = React.useState('24h');

  // Resolve the active account from the live list. While accounts are
  // loading or the operator's chosen account vanished from the config,
  // render a placeholder so we never invent an account.
  const acct = (app.accounts || []).find(a => a.id === app.account) || null;
  const accountID = acct && acct.accountID;

  const { data: dash, error, loading, refresh } = window.API.useAPI(
    () => accountID ? window.API.get('/api/v1/dashboard', { account_id: accountID }) : Promise.resolve(null),
    [accountID],
  );

  // Auto-refresh whenever a trade or equity event is broadcast on the
  // SSE bus so the cards land freshly-confirmed jumps within ~1s.
  // Depend on the events array identity (every push returns a fresh
  // ref) so the effect keeps re-running once the buffer caps out.
  const { events: dashEvents } = window.API.useSSE(['trade', 'equity'], 50);
  React.useEffect(() => {
    if (!dashEvents.length) return;
    refresh();
  }, [dashEvents]);

  const equityData = adaptEquity(dash && dash.equity_curve);
  const haveEquity = equityData.length >= 2;
  const startV = haveEquity ? equityData[0].v : 0;
  const endV = haveEquity ? equityData[equityData.length - 1].v : 0;
  const pnl = endV - startV;
  const pnlPct = startV ? pnl / startV : 0;

  const recentTrades = (dash && Array.isArray(dash.recent_trades))
    ? dash.recent_trades.map(t => ({ ...adaptTrade(t), account: acct ? acct.id : '' }))
    : [];

  const openAlerts = (dash && Array.isArray(dash.alerts))
    ? dash.alerts.map(a => ({ ...adaptAlert(a), account: acct ? acct.id : '' }))
    : [];

  const currentCoin = (dash && dash.current_coin) || (acct && acct.current_coin) || '';
  const outbox = (dash && dash.outbox) || null;

  // Symbol list for the ticker column comes from the configured account
  // (coin/<bridge> for each non-bridge coin). Empty until accounts load.
  const tickerSymbols = React.useMemo(() => {
    if (!acct || !Array.isArray(acct.coins) || !acct.bridge) return [];
    return acct.coins
      .filter(c => c !== acct.bridge)
      .map(c => `${c}/${acct.bridge}`);
  }, [acct && acct.id, acct && (acct.coins || []).join(','), acct && acct.bridge]);

  if (!app.accountsLoaded) {
    return (
      <div className="p-6 mono text-[12px] text-ink-3">loading accounts…</div>
    );
  }
  if (!acct) {
    return (
      <div className="p-6 mono text-[12px] text-ink-2">
        no account configured · set up <span className="text-ink-0">accounts[]</span> in your <span className="text-ink-0">bot.yaml</span> and restart the bot.
      </div>
    );
  }

  const liveTag = haveEquity ? 'live' : (loading ? 'loading…' : (error ? 'api error' : 'no data yet'));

  return (
    <div className="p-4 space-y-4">
      {openAlerts.length > 0 && (
        <div className="bg-neg-2/20 border border-neg-1/40 flex items-stretch">
          <div className="w-1 bg-neg-1"></div>
          <div className="flex-1 p-3 flex items-start gap-3">
            <Icon.AlertTri size={16} className="text-neg-0 mt-0.5"/>
            <div className="flex-1">
              <div className="flex items-baseline gap-2">
                <h3 className="text-[12px] font-medium text-ink-0">{openAlerts.length} active risk event{openAlerts.length > 1 ? 's' : ''}</h3>
                <span className="mono text-[10.5px] text-ink-3">latest {since(Math.max(...openAlerts.map(a => a.at)))}</span>
              </div>
              <ul className="mt-1.5 space-y-0.5">
                {openAlerts.slice(0, 3).map((a, i) => (
                  <li key={i} className="mono text-[11px] text-ink-1 flex items-center gap-2">
                    <Pill tone={a.severity === 'error' ? 'neg' : a.severity === 'warn' ? 'amb' : 'inf'}>{a.kind.replace('_',' ')}</Pill>
                    <span className="text-ink-2">{a.reason}</span>
                    <span className="text-ink-3">— {a.account}</span>
                  </li>
                ))}
              </ul>
            </div>
          </div>
        </div>
      )}

      {/* Stat cards row */}
      <div className="grid grid-cols-6 gap-px bg-line-0">
        <Stat label="current coin" value={currentCoin || '—'} sub={liveTag} />
        <Stat label={`equity / ${acct.bridge}`}
              value={haveEquity ? fmtNum(endV, 0) : '—'}
              delta={haveEquity ? fmtPct(pnlPct, 2) : ''}
              deltaTone={pnl >= 0 ? 'pos' : 'neg'}
              sub={haveEquity ? `${window_} window` : 'no data yet'} />
        <Stat label="24h p&l"
              value={haveEquity ? fmtPct(pnlPct, 2) : '—'}
              sub={haveEquity ? `${pnl >= 0 ? '+' : ''}${fmtNum(pnl, 2)} ${acct.bridge}` : '—'}
              deltaTone={pnl >= 0 ? 'pos' : 'neg'} />
        <Stat label="recent trades"
              value={String(recentTrades.length)}
              sub={recentTrades.length ? 'last 10' : 'none yet'} />
        <Stat label="scout iter / s" value={String(app.scoutRate || 0)} sub="from status spine" />
        <Stat label="alerts (24h)"
              value={String(openAlerts.length)}
              sub={openAlerts.length ? 'see banner' : 'clean'} />
      </div>

      {/* Main row: equity chart big, ticker strip narrow */}
      <div className="grid grid-cols-12 gap-4">
        <Card title="equity curve" sub={`${acct.id} · ${acct.bridge}`} className="col-span-9"
          action={
            <div className="flex gap-px">
              {['1h','24h','7d','30d','all'].map(w => (
                <button key={w} onClick={() => setWindow(w)}
                  className={`px-2 h-6 mono text-[10.5px] uppercase tracking-[0.1em] border ${window_ === w ? 'border-line-2 bg-bg-3 text-ink-0' : 'border-line-0 bg-bg-1 text-ink-3 hover:text-ink-1'}`}>{w}</button>
              ))}
            </div>
          }
        >
          {haveEquity
            ? <EquityChart data={equityData} startV={startV} bridge={acct.bridge} />
            : <EmptyPanel icon={<Icon.Dashboard size={24}/>} title="no equity samples yet"
                         hint={loading ? 'loading…' : 'snapshots accumulate once balances + prices flow (M11.2)'} />}
        </Card>

        <Card title="ticker" sub="bbo" className="col-span-3" padded={false}>
          <TickerColumn symbols={tickerSymbols} />
        </Card>
      </div>

      {/* Holdings — coin count growth, the bot's actual target */}
      <Card title="holdings" sub="coin count growth · bot's actual target" padded={false}>
        <HoldingsTable accountID={accountID} />
      </Card>

      {/* Trades + outbox status */}
      <div className="grid grid-cols-12 gap-4">
        <Card title="last 10 trades" sub={acct.id} className="col-span-8" padded={false}>
          {recentTrades.length
            ? <TradesTable trades={recentTrades} compact />
            : <EmptyPanel icon={<Icon.Trades size={20}/>} title="no trades yet"
                         hint="filled jumps will appear here once the dispatcher loop is live (M11.2)" />}
        </Card>
        <Card title="outbox" sub="queue state" className="col-span-4" padded={false}>
          <OutboxMini stats={outbox} />
        </Card>
      </div>
    </div>
  );
}

// HoldingsTable — per-coin first vs current balance + delta. Bot's
// optimisation target is to grow `current_qty` over time; the equity
// curve is a side-effect (USD value = qty × price). This card answers
// the question: "Did my coin count actually grow?"
function HoldingsTable({ accountID }) {
  const { data, error, loading, refresh } = window.API.useAPI(
    () => accountID ? window.API.get('/api/v1/holdings', { account_id: accountID }) : Promise.resolve(null),
    [accountID],
  );
  // Refresh on every confirmed trade or equity tick so the numbers
  // stay current without a polling loop. Depend on the events array
  // identity (each push = fresh ref) — using events.length stalls
  // once the buffer caps out.
  const { events } = window.API.useSSE(['trade', 'equity'], 20);
  React.useEffect(() => {
    if (events.length) refresh();
  }, [events]);

  if (loading && !data) {
    return <div className="px-3 py-6 mono text-[11.5px] text-ink-3 text-center">loading…</div>;
  }
  if (error) {
    return <div className="px-3 py-6 mono text-[11.5px] text-neg-0 text-center">api error: {String(error.message || error)}</div>;
  }
  const items = (data && Array.isArray(data.items)) ? data.items : [];
  if (items.length === 0) {
    return (
      <div className="px-3 py-6 mono text-[11.5px] text-ink-3 text-center">
        no balance snapshots yet — the equity loop writes one row per minute, first sample lands within 60s of boot.
      </div>
    );
  }

  // Sort: current coin first, then initial coin, then by USD value desc.
  const sorted = items.slice().sort((a, b) => {
    if (a.is_current !== b.is_current) return a.is_current ? -1 : 1;
    if (a.is_initial !== b.is_initial) return a.is_initial ? -1 : 1;
    return parseFloat(b.current_usd_val || '0') - parseFloat(a.current_usd_val || '0');
  });

  return (
    <div className="overflow-auto scrollbar">
      <table className="w-full text-[11.5px]">
        <thead className="text-ink-3 mono text-[10px] uppercase tracking-[0.1em]">
          <tr className="border-b border-line-0">
            <th className="text-left font-medium px-3 py-1.5">coin</th>
            <th className="text-right font-medium px-3 py-1.5">first qty</th>
            <th className="text-right font-medium px-3 py-1.5">current qty</th>
            <th className="text-right font-medium px-3 py-1.5">Δ qty</th>
            <th className="text-right font-medium px-3 py-1.5">Δ %</th>
            <th className="text-right font-medium px-3 py-1.5">usd value</th>
          </tr>
        </thead>
        <tbody className="mono">
          {sorted.map((h, i) => {
            const dq = parseFloat(h.delta_qty || '0');
            const dp = parseFloat(h.delta_pct || '0');
            const tone = dq > 0 ? 'pos' : dq < 0 ? 'neg' : 'ink';
            return (
              <tr key={h.coin} className={`border-b border-line-0 ${i % 2 ? 'bg-bg-1' : 'bg-bg-1/50'}`}>
                <td className="px-3 py-1.5">
                  <div className="flex items-center gap-2">
                    <span className="text-ink-0">{h.coin}</span>
                    {h.is_current && <Pill tone="pos">current</Pill>}
                    {h.is_initial && !h.is_current && <Pill tone="amb">initial</Pill>}
                  </div>
                </td>
                <td className="px-3 py-1.5 text-right text-ink-2 num">{fmtQty(h.first_qty)}</td>
                <td className="px-3 py-1.5 text-right text-ink-0 num">{fmtQty(h.current_qty)}</td>
                <td className={`px-3 py-1.5 text-right num ${tone === 'pos' ? 'text-pos-0' : tone === 'neg' ? 'text-neg-0' : 'text-ink-3'}`}>
                  {dq > 0 ? '+' : ''}{fmtQty(h.delta_qty)}
                </td>
                <td className={`px-3 py-1.5 text-right num ${tone === 'pos' ? 'text-pos-0' : tone === 'neg' ? 'text-neg-0' : 'text-ink-3'}`}>
                  {h.delta_pct ? (dp > 0 ? '+' : '') + h.delta_pct + '%' : '—'}
                </td>
                <td className="px-3 py-1.5 text-right text-ink-1 num">{h.current_usd_val ? '$' + h.current_usd_val : '—'}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

function fmtQty(s) {
  if (!s) return '0';
  const v = parseFloat(s);
  if (!isFinite(v)) return s;
  if (Math.abs(v) >= 1000) return v.toFixed(2);
  if (Math.abs(v) >= 1) return v.toFixed(4);
  if (Math.abs(v) >= 0.01) return v.toFixed(6);
  return v.toFixed(8);
}

function EmptyPanel({ icon, title, hint }) {
  return (
    <div className="px-3 py-10 flex flex-col items-center justify-center gap-2 text-center">
      <div className="text-ink-3">{icon}</div>
      <div className="mono text-[11.5px] text-ink-2">{title}</div>
      {hint && <div className="mono text-[10.5px] text-ink-3 max-w-[42ch] leading-relaxed">{hint}</div>}
    </div>
  );
}

function EquityChart({ data, startV, bridge }) {
  const min = Math.min(...data.map(d => d.v));
  const max = Math.max(...data.map(d => d.v));
  const pad = (max - min) * 0.1 || (max * 0.001) || 1;
  const trend = data[data.length-1].v >= startV ? '#5fcf95' : '#e07262';
  return (
    <div style={{ height: 260 }}>
      <RC.ResponsiveContainer width="100%" height="100%">
        <RC.AreaChart data={data} margin={{ top: 8, right: 12, left: 0, bottom: 0 }}>
          <defs>
            <linearGradient id="eq" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stopColor={trend} stopOpacity={0.18}/>
              <stop offset="100%" stopColor={trend} stopOpacity={0}/>
            </linearGradient>
          </defs>
          <RC.CartesianGrid stroke="#1f2630" strokeDasharray="0" vertical={false} />
          <RC.XAxis dataKey="t" tickFormatter={(t) => fmtTs(t,{short:true})} stroke="#3a424d" tick={{ fontSize: 10, fontFamily: 'JetBrains Mono', fill: '#5b6573' }} axisLine={{ stroke: '#1f2630' }} tickLine={false} minTickGap={50}/>
          <RC.YAxis domain={[min - pad, max + pad]} stroke="#3a424d" tick={{ fontSize: 10, fontFamily: 'JetBrains Mono', fill: '#5b6573' }} axisLine={false} tickLine={false} width={60} tickFormatter={(v) => fmtNum(v,0)} />
          <RC.ReferenceLine y={startV} stroke="#7d8896" strokeDasharray="3 3" label={{ value: `start ${fmtNum(startV,0)}`, position: 'insideTopLeft', fill: '#7d8896', fontSize: 10, fontFamily: 'JetBrains Mono' }}/>
          <RC.Tooltip
            content={({ active, payload }) => {
              if (!active || !payload?.length) return null;
              const p = payload[0].payload;
              const delta = p.v - startV;
              return (
                <div className="bg-bg-2 border border-line-1 px-2.5 py-1.5 mono text-[11px]">
                  <div className="text-ink-3">{fmtTs(p.t)}</div>
                  <div className="text-ink-0">{fmtNum(p.v, 2)} {bridge}</div>
                  <div className={delta >= 0 ? 'text-pos-0' : 'text-neg-0'}>{fmtPct(delta/startV, 2)} · {delta >= 0 ? '+' : ''}{fmtNum(delta,2)}</div>
                </div>
              );
            }}
            cursor={{ stroke: '#7aaed8', strokeDasharray: '2 2' }}
          />
          <RC.Area type="monotone" dataKey="v" stroke={trend} strokeWidth={1.25} fill="url(#eq)" isAnimationActive={false} />
        </RC.AreaChart>
      </RC.ResponsiveContainer>
    </div>
  );
}

// TickerColumn — one row per configured pair. Rows start with placeholder
// dashes; every SSE 'ticker' event for a matching symbol fills the row
// in. We deliberately do NOT seed prices from any local source: an
// empty row is more honest than a stale number.
function TickerColumn({ symbols }) {
  const { connected, events } = window.API.useSSE(['ticker'], 200);
  // rows: { symbol → { last, change, flash, ts } }
  const [rowMap, setRowMap] = React.useState({});

  // Reset row map when the configured symbol set changes (e.g. after
  // accounts hydrate or the operator switches accounts).
  React.useEffect(() => {
    const blank = {};
    for (const s of symbols) blank[s] = { last: null, change: null, flash: null, ts: 0 };
    setRowMap(blank);
  }, [symbols.join(',')]);

  // Apply the latest SSE event onto the matching row.
  React.useEffect(() => {
    if (!events.length) return;
    const last = events[events.length - 1];
    if (last.topic !== 'ticker' || !last.data) return;
    const sym = last.data.symbol || last.data.Symbol;
    if (!sym) return;
    setRowMap(prev => {
      if (!(sym in prev)) return prev; // ignore symbols outside our config
      const row = prev[sym];
      const lastPx = parseFloat(last.data.last ?? last.data.price ?? 0) || row.last;
      const change = (last.data.change != null) ? parseFloat(last.data.change) : row.change;
      const flash = row.last == null ? 'up' : (lastPx >= row.last ? 'up' : 'dn');
      return { ...prev, [sym]: { last: lastPx, change, flash, ts: Date.now() } };
    });
    // events array identity changes on every push (useSSE returns a
    // fresh slice), so depending on the array — not its length —
    // keeps this effect firing once the 200-event tail caps out.
  }, [events]);

  // Clear flash highlight after a short delay so it pulses rather than
  // sticks. We mutate the row map but only the flash field.
  React.useEffect(() => {
    const id = setInterval(() => {
      setRowMap(prev => {
        let dirty = false;
        const out = {};
        for (const k of Object.keys(prev)) {
          if (prev[k].flash) {
            out[k] = { ...prev[k], flash: null };
            dirty = true;
          } else {
            out[k] = prev[k];
          }
        }
        return dirty ? out : prev;
      });
    }, 800);
    return () => clearInterval(id);
  }, []);

  return (
    <div className="divide-y divide-line-0">
      <div className="px-3 py-1.5 flex items-center justify-between mono text-[10px] text-ink-3 border-b border-line-0">
        <span className="uppercase tracking-[0.14em]">live ticker</span>
        <span className="inline-flex items-center gap-1">
          <span className={`w-1.5 h-1.5 ${connected ? 'bg-pos-0 pulse-dot' : 'bg-amb-0'}`}></span>
          {connected ? 'sse connected' : 'sse offline'}
        </span>
      </div>
      {symbols.length === 0 && (
        <div className="px-3 py-6 mono text-[10.5px] text-ink-3 text-center">no pairs configured</div>
      )}
      {symbols.map(sym => {
        const r = rowMap[sym] || { last: null, change: null, flash: null };
        const has = r.last != null;
        return (
          <div key={sym} className={`px-3 py-2 grid grid-cols-3 items-baseline gap-2 ${r.flash === 'up' ? 'ticker-flash-up' : r.flash === 'dn' ? 'ticker-flash-dn' : ''}`}>
            <div className="mono text-[11.5px] text-ink-1">{sym}</div>
            <div className="mono text-[11.5px] text-right text-ink-0 num">{has ? fmtNum(r.last, r.last < 1 ? 4 : r.last < 100 ? 3 : 2) : '—'}</div>
            <div className={`mono text-[10.5px] text-right ${r.change == null ? 'text-ink-3' : r.change >= 0 ? 'text-pos-0' : 'text-neg-0'}`}>{r.change == null ? '—' : fmtPct(r.change, 2)}</div>
          </div>
        );
      })}
    </div>
  );
}

// Compact trades table (also used non-compact on Trades page)
function TradesTable({ trades, compact, onRowClick }) {
  return (
    <div className="overflow-auto scrollbar">
      <table className="w-full text-[11.5px]">
        <thead className="text-ink-3 mono text-[10px] uppercase tracking-[0.1em]">
          <tr className="border-b border-line-0">
            <th className="text-left font-medium px-3 py-1.5">time</th>
            <th className="text-left font-medium px-3 py-1.5">from→to</th>
            <th className="text-right font-medium px-3 py-1.5">qty</th>
            {!compact && <th className="text-right font-medium px-3 py-1.5">price</th>}
            <th className="text-right font-medium px-3 py-1.5">fee</th>
            <th className="text-left font-medium px-3 py-1.5">state</th>
            <th className="text-left font-medium px-3 py-1.5">cid</th>
          </tr>
        </thead>
        <tbody className="mono">
          {trades.map((t, i) => (
            <tr key={t.id} onClick={() => onRowClick?.(t)}
              className={`border-b border-line-0 ${onRowClick ? 'cursor-pointer hover:bg-bg-2/60' : ''} ${i % 2 ? 'bg-bg-1' : 'bg-bg-1/50'}`}>
              <td className="px-3 py-1.5 text-ink-2">{fmtTs(t.ts, { short: true })}</td>
              <td className="px-3 py-1.5">
                <span className="inline-flex items-center gap-1.5">
                  <span className="text-ink-0">{t.from}</span>
                  <Icon.ArrowRight size={11} className="text-ink-3" />
                  <span className="text-ink-0">{t.to}</span>
                </span>
              </td>
              <td className="px-3 py-1.5 text-right text-ink-0 num">{fmtNum(t.qty, 4)}</td>
              {!compact && <td className="px-3 py-1.5 text-right text-ink-0 num">{fmtNum(t.price, 2)}</td>}
              <td className="px-3 py-1.5 text-right text-ink-2 num">{fmtNum(t.fee, 4)}</td>
              <td className="px-3 py-1.5">
                <Pill tone={
                  t.state === 'FILLED' ? 'pos' :
                  t.state === 'PARTIALLY_FILLED' ? 'amb' :
                  t.state === 'REJECTED' ? 'neg' : 'ink'
                }>{t.state.replace('_',' ')}</Pill>
              </td>
              <td className="px-3 py-1.5"><Copy text={t.client_order_id} display={t.client_order_id.slice(0, 14) + '…'} /></td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function OutboxMini({ stats }) {
  // No-data state vs. data state. While stats is null/undefined the
  // dashboard endpoint is still loading or unreachable; show a faint
  // empty layout rather than a row of zeros that looks like real "all
  // CONFIRMED" data.
  const has = stats != null;
  const lanes = [
    { state: 'PENDING',     count: has ? stats.pending     : null, tone: 'ink' },
    { state: 'DISPATCHING', count: has ? stats.dispatching : null, tone: 'inf' },
    { state: 'SENT',        count: has ? stats.sent        : null, tone: 'inf' },
    { state: 'CONFIRMED',   count: has ? stats.confirmed   : null, tone: 'pos' },
    { state: 'FAILED',      count: has ? stats.failed      : null, tone: 'amb' },
    { state: 'DEAD',        count: has ? stats.dead        : null, tone: 'neg' },
  ];
  return (
    <div className="divide-y divide-line-0">
      {lanes.map(l => (
        <div key={l.state} className="px-3 py-2 flex items-center justify-between">
          <div className="flex items-center gap-2">
            <span className={`w-1.5 h-1.5 ${ {ink:'bg-ink-3',pos:'bg-pos-0',neg:'bg-neg-0',amb:'bg-amb-0',inf:'bg-inf-0'}[l.tone]}`}></span>
            <span className="mono text-[11px] text-ink-1">{l.state}</span>
          </div>
          <span className={`mono text-[12px] num ${l.count > 0 ? 'text-ink-0' : 'text-ink-4'}`}>{l.count == null ? '—' : l.count}</span>
        </div>
      ))}
    </div>
  );
}

window.PageDashboard = PageDashboard;
window.TradesTable = TradesTable;
