/* global React, Panel */
// Arbitrage page — cash/futures + calendar + option permutations with tax-aware P&L

const { useState, useMemo, useEffect } = React;

// ============ INDIAN TAX & BROKERAGE ENGINE ============
// Approximations of current Indian market charges (FY2025-26)
const CHARGES = {
  equity_delivery: {
    brokerage: { type: "max", rate: 0.0003, cap: 20 },  // 0.03% or ₹20 whichever lower
    stt: { buy: 0.001, sell: 0.001 },                    // 0.1% both sides
    exchange: 0.0000325,                                 // NSE
    sebi: 0.000001,                                      // 10/cr
    stamp: { buy: 0.00015, sell: 0 },                    // 0.015% buy side only
    gst: 0.18,                                           // on brokerage + exchange + sebi
  },
  equity_intraday: {
    brokerage: { type: "max", rate: 0.0003, cap: 20 },
    stt: { buy: 0, sell: 0.00025 },                      // 0.025% sell side
    exchange: 0.0000325,
    sebi: 0.000001,
    stamp: { buy: 0.00003, sell: 0 },                    // 0.003%
    gst: 0.18,
  },
  futures: {
    brokerage: { type: "max", rate: 0.0003, cap: 20 },
    stt: { buy: 0, sell: 0.0002 },                       // 0.02% sell side (notional)
    exchange: 0.0000173,                                 // 1.73/lakh
    sebi: 0.000001,
    stamp: { buy: 0.00002, sell: 0 },                    // 0.002%
    gst: 0.18,
  },
  options: {
    brokerage: { type: "flat", amount: 20 },             // ₹20 per executed order
    stt: { buy: 0, sell: 0.001 },                        // 0.1% on premium sell side
    exchange: 0.0003503,                                 // ~3.503/lakh on premium
    sebi: 0.000001,
    stamp: { buy: 0.00003, sell: 0 },
    gst: 0.18,
  },
};

function calcCharges(segment, side, notional, premium) {
  const c = CHARGES[segment];
  const base = segment === "options" ? premium : notional;
  const brokerage = c.brokerage.type === "flat"
    ? c.brokerage.amount
    : Math.min(c.brokerage.cap, base * c.brokerage.rate);
  const stt = base * (side === "buy" ? c.stt.buy : c.stt.sell);
  const exchange = base * c.exchange;
  const sebi = base * c.sebi;
  const stamp = side === "buy" ? base * c.stamp.buy : 0;
  const gst = (brokerage + exchange + sebi) * c.gst;
  return {
    brokerage: round2(brokerage),
    stt: round2(stt),
    exchange: round2(exchange),
    sebi: round2(sebi),
    stamp: round2(stamp),
    gst: round2(gst),
    total: round2(brokerage + stt + exchange + sebi + stamp + gst),
  };
}

function round2(n) { return Math.round(n * 100) / 100; }

// ============ ARB OPPORTUNITY GENERATION ============
const SYMBOLS = ["RELIANCE","TCS","INFY","HDFCBANK","ICICIBANK","SBIN","ADANIENT","BAJFINANCE","KOTAKBANK","ITC","LT","AXISBANK","MARUTI","HINDUNILVR","WIPRO","TECHM","SUNPHARMA","ASIANPAINT","TITAN","BHARTIARTL","NIFTY","BANKNIFTY"];

// F&O ban list — scrips that crossed MWPL (Market-Wide Position Limit) 95% or are otherwise
// restricted for fresh position-taking. Only squaring-off existing positions is permitted.
// This list is NSE-published, refreshed daily. Existing positions can be closed but not opened.
const FNO_BAN = {
  ADANIENT:    { reason: "MWPL > 95%",              severity: "BAN",       asOf: "29 APR 2026" },
  BAJFINANCE:  { reason: "MWPL 92% · approaching",  severity: "WARN",      asOf: "29 APR 2026" },
  SBIN:        { reason: "Circuit filter triggered",severity: "HALT",      asOf: "29 APR 2026" },
};

// Illiquid / not tradable for arb purposes (low volumes, wide spreads)
const ILLIQUID = new Set(["ASIANPAINT"]);

function scripStatus(sym) {
  if (FNO_BAN[sym]) return FNO_BAN[sym];
  if (ILLIQUID.has(sym)) return { reason: "Low liquidity · wide spreads", severity: "ILLIQUID", asOf: "live" };
  return null;
}

// Arb subtypes the backend emits — see arbitrage.py.  Each opp's `subtype`
// (or `type`) maps directly to one of these chips on the Category panel.
const ARB_SUBTYPES = [
  { id: "CASH_FUTURES",     label: "Cash ⇄ Futures",       color: "#4ec9f0" },
  { id: "REVERSE_CF",       label: "Reverse Cash-Future",  color: "#4ec9f0" },
  { id: "CALENDAR",         label: "Calendar Spread",      color: "#4ec9f0" },
  { id: "BOX",              label: "Box Spread",           color: "#22d66f" },
  { id: "SUB_INTRINSIC",    label: "Sub-Intrinsic",        color: "#ffb020" },
  { id: "OVER_INTRINSIC",   label: "Over-Intrinsic",       color: "#b18cff" },
];
const ARB_SUBTYPE_IDS = ARB_SUBTYPES.map(t => t.id);

// Mock-only metadata used by generateOpportunities() when localStorage
// `arb_dev_mock=1` is set.  These strategies (straddle/condor/etc.) are NOT
// risk-free arb and never appear in production scans — kept for offline UI
// preview only.
const MOCK_TYPES = {
  cash_futures:    { label: "Cash⇄Futures",       cat: "CASH_FUTURES",   risk: "LOW" },
  reverse_cf:      { label: "Reverse C-F",        cat: "REVERSE_CF",     risk: "LOW" },
  calendar:        { label: "Calendar",           cat: "CALENDAR",       risk: "LOW" },
  long_straddle:   { label: "Long Straddle",      cat: "VOL_PREVIEW",    risk: "HIGH" },
  short_straddle:  { label: "Short Straddle",     cat: "VOL_PREVIEW",    risk: "UNLIMIT" },
  long_strangle:   { label: "Long Strangle",      cat: "VOL_PREVIEW",    risk: "MED" },
  iron_condor:     { label: "Iron Condor",        cat: "VOL_PREVIEW",    risk: "LOW" },
  iron_butterfly:  { label: "Iron Butterfly",     cat: "VOL_PREVIEW",    risk: "LOW" },
  bull_call:       { label: "Bull Call Spread",   cat: "DIR_PREVIEW",    risk: "LOW" },
  bear_put:        { label: "Bear Put Spread",    cat: "DIR_PREVIEW",    risk: "LOW" },
};

function hashStr(s) { let h = 0; for (let i = 0; i < s.length; i++) h = ((h << 5) - h) + s.charCodeAt(i) | 0; return Math.abs(h); }
function seededRand(seed) { let s = seed; return () => { s = (s * 9301 + 49297) % 233280; return s / 233280; }; }

function generateOpportunities(seed = 1) {
  const out = [];
  let id = 0;
  const r = seededRand(seed);

  SYMBOLS.forEach((sym) => {
    // Cash-Futures spread
    if (r() > 0.3) {
      const spot = 500 + hashStr(sym) % 4000 + (r() - 0.5) * 20;
      const basis = (r() - 0.4) * 0.015;                // futures premium/discount
      const futPrice = spot * (1 + basis);
      const absGross = Math.abs(futPrice - spot);
      const lot = sym === "NIFTY" ? 75 : sym === "BANKNIFTY" ? 35 : Math.round(50000 / spot / 5) * 5 || 50;
      const notional = spot * lot;

      const buyChg  = calcCharges(basis > 0 ? "equity_delivery" : "futures", "buy",  notional);
      const sellChg = calcCharges(basis > 0 ? "futures" : "equity_delivery", "sell", notional);
      const grossPL = absGross * lot;
      const netPL   = grossPL - buyChg.total - sellChg.total;

      out.push({
        id: "A" + (++id),
        type: basis > 0 ? "cash_futures" : "reverse_cf",
        typeLabel: basis > 0 ? "Cash⇄Futures" : "Reverse C-F",
        cat: "CLASSIC",
        symbol: sym,
        legs: [
          { action: basis > 0 ? "BUY"  : "SELL", instrument: `${sym} · CASH`,    qty: lot, price: spot,     segment: basis > 0 ? "equity_delivery" : "equity_intraday" },
          { action: basis > 0 ? "SELL" : "BUY",  instrument: `${sym} · FUT APR`, qty: lot, price: futPrice, segment: "futures" },
        ],
        notional: round2(notional),
        grossPL: round2(grossPL),
        charges: round2(buyChg.total + sellChg.total),
        chargeBreakdown: { buy: buyChg, sell: sellChg },
        netPL: round2(netPL),
        netPct: round2((netPL / notional) * 100 * 365 / 30), // annualized
        holdingDays: 22,
        confidence: Math.round(75 + r() * 20),
        risk: "LOW",
        live: r() > 0.3,
      });
    }

    // Calendar spread
    if (r() > 0.55) {
      const spot = 500 + hashStr(sym + "c") % 4000;
      const lot = sym === "NIFTY" ? 75 : sym === "BANKNIFTY" ? 35 : 50;
      const nearPrem = spot * 0.003;
      const farPrem = spot * (0.011 + (r() - 0.5) * 0.005);
      const spread = farPrem - nearPrem;
      const gross = spread * lot;
      const buyChg  = calcCharges("futures", "buy", spot * lot);
      const sellChg = calcCharges("futures", "sell", spot * lot);
      const net = gross - buyChg.total - sellChg.total;
      if (net > 0) {
        out.push({
          id: "A" + (++id),
          type: "calendar",
          typeLabel: "Calendar",
          cat: "CLASSIC",
          symbol: sym,
          legs: [
            { action: "SELL", instrument: `${sym} FUT APR`, qty: lot, price: spot + nearPrem, segment: "futures" },
            { action: "BUY",  instrument: `${sym} FUT MAY`, qty: lot, price: spot + farPrem,  segment: "futures" },
          ],
          notional: round2(spot * lot),
          grossPL: round2(gross),
          charges: round2(buyChg.total + sellChg.total),
          chargeBreakdown: { buy: buyChg, sell: sellChg },
          netPL: round2(net),
          netPct: round2(net / (spot * lot) * 100),
          holdingDays: 28,
          confidence: Math.round(60 + r() * 25),
          risk: "LOW",
          live: r() > 0.4,
        });
      }
    }

    // Option combos (only for liquid names)
    if (["NIFTY","BANKNIFTY","RELIANCE","HDFCBANK","TCS","INFY","ICICIBANK"].includes(sym)) {
      ["long_straddle","short_straddle","iron_condor","iron_butterfly","bull_call","bear_put","long_strangle"].forEach(combo => {
        if (r() > 0.55) return;
        const spot = 500 + hashStr(sym + combo) % 4000;
        const lot = sym === "NIFTY" ? 75 : sym === "BANKNIFTY" ? 35 : 50;
        const atm = Math.round(spot / 50) * 50;
        const legs = buildOptionLegs(combo, sym, atm, lot, r);
        if (!legs) return;
        const { grossPL, buyCharges, sellCharges, legObjs, maxLoss } = legs;
        const totalChg = buyCharges + sellCharges;
        const net = grossPL - totalChg;

        out.push({
          id: "A" + (++id),
          type: combo,
          typeLabel: (MOCK_TYPES[combo] || {}).label,
          cat: (MOCK_TYPES[combo] || {}).cat,
          symbol: sym,
          legs: legObjs,
          notional: round2(atm * lot),
          grossPL: round2(grossPL),
          charges: round2(totalChg),
          chargeBreakdown: { buyTotal: buyCharges, sellTotal: sellCharges },
          netPL: round2(net),
          maxLoss: round2(maxLoss),
          netPct: round2(net / (atm * lot) * 100),
          holdingDays: combo.includes("straddle") ? 7 : combo === "iron_condor" ? 14 : 21,
          confidence: Math.round(55 + r() * 30),
          risk: (MOCK_TYPES[combo] || {}).risk,
          live: r() > 0.35,
        });
      });
    }
  });

  return out.sort((a, b) => b.netPL - a.netPL);
}

function buildOptionLegs(combo, sym, atm, lot, r) {
  const ceAtm = atm * 0.018 + r() * 20;
  const peAtm = atm * 0.018 + r() * 20;
  const ceOtm = ceAtm * 0.55;
  const peOtm = peAtm * 0.55;
  const strikeGap = sym === "NIFTY" ? 50 : sym === "BANKNIFTY" ? 100 : 50;

  let legs = [], gross = 0, buyPrem = 0, sellPrem = 0, maxLoss = 0;

  if (combo === "long_straddle") {
    legs = [
      { action: "BUY", instrument: `${sym} ${atm} CE`, qty: lot, price: round2(ceAtm), segment: "options" },
      { action: "BUY", instrument: `${sym} ${atm} PE`, qty: lot, price: round2(peAtm), segment: "options" },
    ];
    buyPrem = (ceAtm + peAtm) * lot;
    const moveNeeded = ceAtm + peAtm;
    const expectedMove = moveNeeded * (1.1 + r() * 0.4);
    gross = (expectedMove - moveNeeded) * lot;
    maxLoss = buyPrem;
  } else if (combo === "short_straddle") {
    legs = [
      { action: "SELL", instrument: `${sym} ${atm} CE`, qty: lot, price: round2(ceAtm), segment: "options" },
      { action: "SELL", instrument: `${sym} ${atm} PE`, qty: lot, price: round2(peAtm), segment: "options" },
    ];
    sellPrem = (ceAtm + peAtm) * lot;
    gross = sellPrem * (0.6 + r() * 0.3); // theta decay capture
    maxLoss = Infinity; // unlimited — cap visually
  } else if (combo === "long_strangle") {
    legs = [
      { action: "BUY", instrument: `${sym} ${atm + strikeGap} CE`, qty: lot, price: round2(ceOtm), segment: "options" },
      { action: "BUY", instrument: `${sym} ${atm - strikeGap} PE`, qty: lot, price: round2(peOtm), segment: "options" },
    ];
    buyPrem = (ceOtm + peOtm) * lot;
    gross = (ceAtm + peAtm - ceOtm - peOtm) * lot * (0.8 + r() * 0.4);
    maxLoss = buyPrem;
  } else if (combo === "iron_condor") {
    legs = [
      { action: "SELL", instrument: `${sym} ${atm + strikeGap} CE`,    qty: lot, price: round2(ceOtm),        segment: "options" },
      { action: "BUY",  instrument: `${sym} ${atm + strikeGap*2} CE`,  qty: lot, price: round2(ceOtm * 0.5),  segment: "options" },
      { action: "SELL", instrument: `${sym} ${atm - strikeGap} PE`,    qty: lot, price: round2(peOtm),        segment: "options" },
      { action: "BUY",  instrument: `${sym} ${atm - strikeGap*2} PE`,  qty: lot, price: round2(peOtm * 0.5),  segment: "options" },
    ];
    const netCredit = (ceOtm - ceOtm * 0.5 + peOtm - peOtm * 0.5) * lot;
    sellPrem = (ceOtm + peOtm) * lot;
    buyPrem = (ceOtm * 0.5 + peOtm * 0.5) * lot;
    gross = netCredit * (0.55 + r() * 0.3);
    maxLoss = strikeGap * lot - netCredit;
  } else if (combo === "iron_butterfly") {
    legs = [
      { action: "SELL", instrument: `${sym} ${atm} CE`,             qty: lot, price: round2(ceAtm),       segment: "options" },
      { action: "SELL", instrument: `${sym} ${atm} PE`,             qty: lot, price: round2(peAtm),       segment: "options" },
      { action: "BUY",  instrument: `${sym} ${atm + strikeGap} CE`, qty: lot, price: round2(ceOtm),       segment: "options" },
      { action: "BUY",  instrument: `${sym} ${atm - strikeGap} PE`, qty: lot, price: round2(peOtm),       segment: "options" },
    ];
    sellPrem = (ceAtm + peAtm) * lot;
    buyPrem = (ceOtm + peOtm) * lot;
    gross = (sellPrem - buyPrem) * (0.5 + r() * 0.3);
    maxLoss = strikeGap * lot - (sellPrem - buyPrem);
  } else if (combo === "bull_call") {
    legs = [
      { action: "BUY",  instrument: `${sym} ${atm} CE`,             qty: lot, price: round2(ceAtm), segment: "options" },
      { action: "SELL", instrument: `${sym} ${atm + strikeGap} CE`, qty: lot, price: round2(ceOtm), segment: "options" },
    ];
    buyPrem = ceAtm * lot;
    sellPrem = ceOtm * lot;
    gross = (strikeGap - (ceAtm - ceOtm)) * lot * (0.4 + r() * 0.3);
    maxLoss = (ceAtm - ceOtm) * lot;
  } else if (combo === "bear_put") {
    legs = [
      { action: "BUY",  instrument: `${sym} ${atm} PE`,             qty: lot, price: round2(peAtm), segment: "options" },
      { action: "SELL", instrument: `${sym} ${atm - strikeGap} PE`, qty: lot, price: round2(peOtm), segment: "options" },
    ];
    buyPrem = peAtm * lot;
    sellPrem = peOtm * lot;
    gross = (strikeGap - (peAtm - peOtm)) * lot * (0.4 + r() * 0.3);
    maxLoss = (peAtm - peOtm) * lot;
  }

  if (!legs.length) return null;
  const buyChg = calcCharges("options", "buy", 0, buyPrem);
  const sellChg = calcCharges("options", "sell", 0, sellPrem);

  return {
    grossPL: gross,
    buyCharges: buyChg.total,
    sellCharges: sellChg.total,
    legObjs: legs,
    maxLoss: maxLoss === Infinity ? 999999 : maxLoss,
  };
}

// Dev-only offline mock toggle.  Set `localStorage.arb_dev_mock = "1"` in
// DevTools to render seeded mock opportunities without a backend call.
// Never set in production builds; check is purely runtime.
const ARB_DEV_MOCK = (() => {
  try { return typeof window !== "undefined" && window.localStorage && localStorage.getItem("arb_dev_mock") === "1"; }
  catch (e) { return false; }
})();

function mapBackendOp(op, i, stale) {
  const subtype = op.subtype || op.type;
  return {
    id: subtype + "_" + i,
    strategy: op.strategy,
    typeLabel: op.strategy,
    symbol: op.symbol,
    cat: subtype,
    legs: (op.legs || []).map(l => ({
      action:     l.side,
      instrument: l.symbol,
      price:      l.price,
      qty:        l.qty,
      kind:       l.kind,
    })),
    legsCount: op.legs_count,
    notional: op.notional,
    grossPL: op.gross,
    charges: op.charges,
    chargeBreakdown: op.charges_breakdown || null,
    netPL: op.net,
    netPct: op.yield_pct,
    yieldPct: op.yield_pct,
    annualisedYield: op.annualised_yield,
    // Depth-walked executable yield (Pass B) — null when status != "OK".
    execYieldPct:           typeof op.executable_yield_pct === "number" ? op.executable_yield_pct : null,
    execAnnualisedYield:    typeof op.executable_annualised_yield === "number" ? op.executable_annualised_yield : null,
    execNet:                typeof op.executable_net === "number" ? op.executable_net : null,
    execGross:              typeof op.executable_gross === "number" ? op.executable_gross : null,
    execNotional:           typeof op.executable_notional === "number" ? op.executable_notional : null,
    execCharges:            typeof op.executable_charges === "number" ? op.executable_charges : null,
    execStatus:             op.executable_status || null,
    execStatusDetail:       op.executable_status_detail || "",
    execLegs:               op.executable_legs || [],
    // Multi-size sweep: depth-walked at lots × {1, 5, 10}. Each entry
    // {lots, status, status_detail, yield_pct, annualised_yield, net, notional}.
    execAtLots:             Array.isArray(op.executable_at_lots) ? op.executable_at_lots : [],
    depthAgeSec:            typeof op.depth_age_sec === "number" ? op.depth_age_sec : null,
    confidence: op.confidence != null ? op.confidence * 100 : 0,
    risk: op.risk,
    live: !stale,
    detail: op.detail,
    expiry: op.expiry,
    holdingDays: op.holding_days || op.days_to_expiry,
    // Liquidity sanity — only emitted by option-based scanners.
    strikeDistancePct: typeof op.strike_distance_pct === "number" ? op.strike_distance_pct : null,
    liquidityWarning: !!op.liquidity_warning,
    // Council verdict (only present for top-N reviewed ops).
    councilVerdict:    op.council_verdict || null,
    councilConfidence: typeof op.council_confidence === "number" ? op.council_confidence : null,
    councilReasoning:  op.council_reasoning || "",
    councilRisks:      op.council_risks || [],
    councilCached:     !!op.council_cached,
    councilCostInr:    typeof op.council_cost_inr === "number" ? op.council_cost_inr : 0,
    councilLatencyMs:  op.council_latency_ms || 0,
  };
}

// ============ MAIN PAGE ============
function ArbitragePage({ app }) {
  const initialOpps = ARB_DEV_MOCK ? generateOpportunities(1) : [];
  const [opportunities, setOpps] = useState(initialOpps);
  const [backendStatus, setBackendStatus] = useState({ ok: ARB_DEV_MOCK, error: ARB_DEV_MOCK ? null : "initial", count: initialOpps.length, duration_ms: 0 });
  const [scanMeta, setScanMeta] = useState({ stale: false, staleSince: null, marketStatus: null, council: null, mode: "live" });
  // User-selected scan mode.  "live" subscribes to the SSE stream (30 s ticks).
  // "last_close" disables SSE and does one-shot fetches against /api/arbitrage/scan
  // — last-session prices only change once a day, so polling would be wasteful.
  // No auto-switch between modes; the user's pick is what runs.
  const [mode, setMode] = useState("live");
  const [lastCloseTick, setLastCloseTick] = useState(0);   // bump → re-fetch
  const [lastCloseLoading, setLastCloseLoading] = useState(false);
  const [councilTop, setCouncilTop] = useState(3);
  const [filters, setFilters] = useState({
    cats: new Set(ARB_SUBTYPE_IDS),
    minYield:     0,   // % — gate on LTP-based yield (op.yield_pct)
    minExecYield: 0,   // % — gate on depth-walked yield (op.executable_yield_pct)
    onlyProfitable: true,
    onlyLive: false,
    hideLiquiditySuspects: false,   // backend flags strike Δ>10% AND yield>1%
  });
  const [sortBy, setSortBy] = useState("netPL");
  const [sortDir, setSortDir] = useState("desc");
  const [selected, setSelected] = useState(null);
  const [executing, setExecuting] = useState(null);
  const [capital, setCapital] = useState(500000);
  const [maxDays, setMaxDays] = useState("");
  const [scanTick, setScanTick] = useState(0);
  const [detailWidth, setDetailWidth] = useState(380);

  // REAL SCANNER — subscribes to /api/arbitrage/stream (SSE) ONLY in live
  // mode.  In last_close mode we drop the stream entirely (last-session prices
  // change once a day; polling at 30 s is wasteful) and do one-shot fetches.
  // Dev-mock skips both and lets the seeded data stand.
  const arbStream = useStream(
    (ARB_DEV_MOCK || mode !== "live") ? null : "/api/arbitrage/stream",
    { bufferSize: 5 }
  );

  const _applyScanPayload = React.useCallback((d) => {
    if (!d || typeof d !== "object") return;
    if (d.error && !d.ops) {
      setBackendStatus({ ok: false, error: d.error, count: 0, duration_ms: 0 });
      setScanMeta({ stale: false, staleSince: null,
                    marketStatus: d.market_status || null, council: null, mode: "live" });
      return;
    }
    const stale = !!d.stale;
    const mode = d.mode || "live";
    const mapped = (d.ops || []).map((op, i) => mapBackendOp(op, i, stale));
    setOpps(mapped);
    setBackendStatus({ ok: true, error: null,
                       count: d.count || 0, duration_ms: d.duration_ms || 0 });
    setScanMeta({ stale, staleSince: d.stale_since || null,
                  marketStatus: d.market_status || null, council: d.council || null, mode });
  }, []);

  useEffect(() => { _applyScanPayload(arbStream.snapshot); },
            [arbStream.snapshot, _applyScanPayload]);
  useEffect(() => {
    const ev = arbStream.lastEvent;
    if (!ev) return;
    if (ev.type === "arbitrage_tick" && ev.data) {
      _applyScanPayload(ev.data);
      setScanTick(x => x + 1);
    }
  }, [arbStream.lastEvent, _applyScanPayload]);
  useEffect(() => {
    if (arbStream.error) {
      setBackendStatus(s => ({ ...s, ok: false, error: arbStream.error }));
    }
  }, [arbStream.error]);

  // One-shot fetch for LAST CLOSE mode — fires on entering the mode and on
  // every refresh-button click (lastCloseTick bumps).  No retry, no fallback:
  // if the backend errors, we surface the error and let the user decide.
  useEffect(() => {
    if (ARB_DEV_MOCK || mode !== "last_close") return;
    let cancelled = false;
    setLastCloseLoading(true);
    fetch("/api/arbitrage/scan?mode=last_close&council_top=0")
      .then(r => r.ok ? r.json() : r.text().then(t => { throw new Error(`${r.status} ${t}`); }))
      .then(data => { if (!cancelled) _applyScanPayload(data); })
      .catch(err => {
        if (cancelled) return;
        setBackendStatus({ ok: false, error: String(err.message || err), count: 0, duration_ms: 0 });
        setOpps([]);
      })
      .finally(() => { if (!cancelled) setLastCloseLoading(false); });
    return () => { cancelled = true; };
  }, [mode, lastCloseTick, _applyScanPayload]);

  // Reset state when switching modes so a stale view from one mode doesn't
  // briefly leak into the other while the new fetch is in flight.  Keep
  // `error: null` — the BACKEND ERROR banner must only show real errors,
  // not the brief in-between state during a mode transition.
  useEffect(() => {
    setOpps([]);
    setBackendStatus({ ok: false, error: null, count: 0, duration_ms: 0 });
    setScanMeta(s => ({ ...s, stale: false, staleSince: null, mode }));
  }, [mode]);

  const filtered = useMemo(() => {
    let r = opportunities.filter(o => filters.cats.has(o.cat));
    if (filters.onlyProfitable) r = r.filter(o => o.netPL > 0);
    if (filters.onlyLive) r = r.filter(o => o.live);
    if (filters.hideLiquiditySuspects) r = r.filter(o => !o.liquidityWarning);
    if (filters.minYield > 0) r = r.filter(o => (o.netPct ?? -Infinity) >= filters.minYield);
    if (filters.minExecYield > 0) r = r.filter(o => (o.execYieldPct ?? -Infinity) >= filters.minExecYield);
    r = [...r].sort((a, b) => {
      const av = a[sortBy], bv = b[sortBy];
      return sortDir === "desc" ? (bv - av) : (av - bv);
    });
    return r;
  }, [opportunities, filters, sortBy, sortDir]);

  const stats = useMemo(() => {
    const profitable = filtered.filter(o => o.netPL > 0);
    const totalGross = profitable.reduce((s, o) => s + o.grossPL, 0);
    const totalCharges = profitable.reduce((s, o) => s + o.charges, 0);
    const totalNet = profitable.reduce((s, o) => s + o.netPL, 0);
    const totalNotional = profitable.reduce((s, o) => s + (o.notional || 0), 0);
    const chargeRatio = totalGross > 0 ? (totalCharges / totalGross) * 100 : 0;
    const byCategory = {};
    ARB_SUBTYPE_IDS.forEach(c => { byCategory[c] = profitable.filter(o => o.cat === c).length; });
    return { count: filtered.length, profitable: profitable.length, totalGross, totalCharges, totalNet, totalNotional, chargeRatio, byCategory, backendOk: backendStatus.ok, backendError: backendStatus.error, durationMs: backendStatus.duration_ms };
  }, [filtered, backendStatus]);

  const toggleCat = (cat) => {
    setFilters(f => {
      const s = new Set(f.cats);
      if (s.has(cat)) s.delete(cat); else s.add(cat);
      return { ...f, cats: s };
    });
  };

  const toggleSort = (col) => {
    if (sortBy === col) setSortDir(d => d === "desc" ? "asc" : "desc");
    else { setSortBy(col); setSortDir("desc"); }
  };

  const executeArb = (opp) => {
    setExecuting({ opp, step: "review" });
  };

  // Single-leg execute (per-leg button inside the OrderDepthPanel). Wraps the
  // chosen leg into a synthetic single-leg "opp" so the existing review modal
  // can surface it. Real order placement is intentionally not wired here —
  // upstream paper-trade demo flow remains the source of truth.
  const executeLeg = (opp, leg, i) => {
    const wrapped = {
      ...opp,
      id:        opp.id + "_L" + (i + 1),
      typeLabel: opp.typeLabel + " · Leg " + (i + 1),
      symbol:    leg.symbol,
      legs:      [{
        action:     leg.side,
        instrument: leg.symbol,
        price:      leg.avg_fill_price != null ? leg.avg_fill_price : 0,
        qty:        leg.qty,
        kind:       leg.kind || "",
      }],
      legsCount: 1,
      // P&L summary for the single leg = its notional only; charges/net are
      // unavailable in isolation so we surface the cashflow instead.
      grossPL:   (leg.avg_fill_price || 0) * leg.qty,
      netPL:     (leg.avg_fill_price || 0) * leg.qty,
      notional:  (leg.avg_fill_price || 0) * leg.qty,
      charges:   0,
    };
    setExecuting({ opp: wrapped, step: "review" });
  };

  const confirmExecute = () => {
    const opp = executing.opp;
    app.pushNews({
      headline: `Arb executed · ${opp.typeLabel} on ${opp.symbol} · net ₹${opp.netPL.toFixed(0)} after charges`,
      severity: "LOW",
      category: "CORPORATE",
      source: "ArbEngine",
      symbols: [opp.symbol],
    });
    setExecuting(null);
  };

  const onDetailResizeStart = (e) => {
    e.preventDefault();
    const startX = e.clientX;
    const startW = detailWidth;
    const onMove = (mv) => {
      // Panel sits on the right — dragging the left edge LEFT widens it.
      const next = Math.max(320, Math.min(window.innerWidth - 360, startW + (startX - mv.clientX)));
      setDetailWidth(next);
    };
    const onUp = () => {
      document.removeEventListener("mousemove", onMove);
      document.removeEventListener("mouseup", onUp);
      document.body.style.cursor = "";
      document.body.style.userSelect = "";
    };
    document.addEventListener("mousemove", onMove);
    document.addEventListener("mouseup", onUp);
    document.body.style.cursor = "col-resize";
    document.body.style.userSelect = "none";
  };

  return (
    <div style={{ display: "grid", gridTemplateColumns: selected ? `240px 1fr ${detailWidth}px` : "240px 1fr", gap: 8, height: "100%", padding: 8, overflow: "hidden" }}>
      {/* LEFT FILTERS */}
      <div className="scroll-stack" style={{ display: "flex", flexDirection: "column", gap: 8, minHeight: 0, overflow: "auto" }}>
        <Panel title="Scanner Status">
          {/* Mode toggle — user picks LIVE (SSE 30s) or LAST CLOSE (one-shot prev-close). */}
          <div style={{ display: "flex", gap: 4, marginBottom: 8 }}>
            <button
              onClick={() => setMode("live")}
              style={{
                flex: 1, padding: "4px 6px", fontSize: 10, letterSpacing: "0.05em",
                background: mode === "live" ? "rgba(76,217,100,0.10)" : "transparent",
                border: `1px solid ${mode === "live" ? "var(--up)" : "var(--border)"}`,
                color: mode === "live" ? "var(--up)" : "var(--fg-dim)",
                cursor: "pointer",
              }}
              title="Live Dhan ticks · 30s SSE refresh"
            >LIVE</button>
            <button
              onClick={() => setMode("last_close")}
              style={{
                flex: 1, padding: "4px 6px", fontSize: 10, letterSpacing: "0.05em",
                background: mode === "last_close" ? "rgba(110,200,255,0.10)" : "transparent",
                border: `1px solid ${mode === "last_close" ? "var(--cyan)" : "var(--border)"}`,
                color: mode === "last_close" ? "var(--cyan)" : "var(--fg-dim)",
                cursor: "pointer",
              }}
              title="Prior-session basis from Dhan ohlc.close · cash-fut + calendar only · informational"
            >LAST CLOSE</button>
            {mode === "last_close" ? (
              <button
                onClick={() => setLastCloseTick(t => t + 1)}
                disabled={lastCloseLoading}
                style={{
                  padding: "4px 8px", fontSize: 11, lineHeight: 1,
                  background: "transparent", border: "1px solid var(--border)",
                  color: lastCloseLoading ? "var(--fg-faint)" : "var(--fg-dim)",
                  cursor: lastCloseLoading ? "wait" : "pointer",
                }}
                title="Refetch prior-session basis"
              >{lastCloseLoading ? "…" : "⟳"}</button>
            ) : null}
          </div>

          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <span className="dot dot-live"/>
            <div style={{ fontSize: 11 }}>
              <div className="bright">{ARB_DEV_MOCK ? "DEV MOCK"
                : scanMeta.mode === "last_close" ? "LAST CLOSE"
                : scanMeta.stale ? "STALE"
                : backendStatus.ok ? "SCANNING"
                : "OFFLINE"}</div>
              <div className="faint" style={{ fontSize: 9 }}>{
                ARB_DEV_MOCK ? "no backend poll"
                : mode === "last_close" ? `prev-close · scan #${lastCloseTick}`
                : `refresh every 30s · scan #${scanTick}`
              }</div>
            </div>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 4, marginTop: 10 }}>
            <KpiBox label="Found"       val={stats.count}/>
            <KpiBox label="Profitable"  val={stats.profitable} tone="up"/>
          </div>
          <div style={{ marginTop: 6 }}>
            <KpiBox label="Stack if ALL" val={"₹" + Math.round(stats.totalNet).toLocaleString("en-IN")} tone="up" big/>
          </div>
          <div style={{ marginTop: 6 }} title="Sum of notional across all profitable ops — capital required to execute every leg of every opportunity">
            <KpiBox label="Investment Needed" val={"₹" + Math.round(stats.totalNotional).toLocaleString("en-IN")} tone={stats.totalNotional > capital ? "down" : "dim"} big/>
          </div>
        </Panel>

        <Panel title="Charge Rate" bodyFlush>
          <div style={{ padding: 8 }}>
            <div className="faint" style={{ fontSize: 10, marginBottom: 4 }}>Charges as % of gross (all profitable opps)</div>
            <div style={{ fontSize: 22, fontWeight: 500, textAlign: "center", padding: "6px 0" }}
                 className={stats.chargeRatio < 15 ? "up" : stats.chargeRatio < 30 ? "amber" : "down"}>
              {stats.chargeRatio.toFixed(1)}%
            </div>
            <div style={{ height: 4, background: "var(--bg-0)", border: "1px solid var(--border)" }}>
              <div style={{ height: "100%", width: `${Math.min(100, stats.chargeRatio * 2)}%`, background: stats.chargeRatio < 15 ? "var(--up)" : stats.chargeRatio < 30 ? "var(--amber)" : "var(--down)" }}/>
            </div>
            <div className="faint" style={{ fontSize: 9, marginTop: 6 }}>
              STT · exchange · SEBI · stamp · GST · brokerage all auto-deducted
            </div>
          </div>
        </Panel>

        <AutoExecutePanel/>

        <Panel title="Council Review" tag={scanMeta.council?.reviewed || 0}>
          <div className="label">Top-N (LLM verdict on top ops by net P&L)</div>
          <input className="input tnum" type="number" min={0} max={10} value={councilTop} onChange={e => setCouncilTop(Math.max(0, Math.min(10, +e.target.value || 0)))} step={1}/>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 4, marginTop: 8 }}>
            <KpiBox label="This scan" val={`₹${(scanMeta.council?.scan_cost_inr ?? 0).toFixed(2)}`} tone={(scanMeta.council?.scan_cost_inr ?? 0) > 0 ? "amber" : "dim"}/>
            <KpiBox label="Session" val={`₹${(scanMeta.council?.session_cost_inr ?? 0).toFixed(2)}`} tone="dim"/>
          </div>
          <div className="faint" style={{ fontSize: 9, marginTop: 6 }}>
            {scanMeta.council?.model || "claude-haiku-4-5"} · 5-min cache
          </div>
          {scanMeta.council?.errors?.length ? (
            <div className="down" style={{ fontSize: 10, marginTop: 4 }}>{scanMeta.council.errors[0]}</div>
          ) : null}
        </Panel>

        <Panel title="Category" tag={filters.cats.size}>
          <div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
            {ARB_SUBTYPES.map(({ id, label, color }) => {
              const on = filters.cats.has(id);
              return (
                <label key={id} style={{ display: "flex", alignItems: "center", gap: 6, padding: "4px 6px", cursor: "pointer", background: on ? "var(--bg-0)" : "transparent", border: "1px solid", borderColor: on ? "var(--border)" : "transparent" }}>
                  <input type="checkbox" className="checkbox" checked={on} onChange={() => toggleCat(id)}/>
                  <span style={{ width: 6, height: 6, background: color, borderRadius: "50%" }}/>
                  <span style={{ fontSize: 11, flex: 1 }} className={on ? "bright" : "dim"}>{label}</span>
                  <span className="faint tnum" style={{ fontSize: 9 }}>{stats.byCategory[id] || 0}</span>
                </label>
              );
            })}
          </div>
        </Panel>

      </div>

      {/* CENTER OPPORTUNITY TABLE */}
      <Panel
        title="Arbitrage Opportunities"
        tag={filtered.length}
        actions={
          <span className="faint" style={{ fontSize: 10, marginRight: 8 }}>click QUICK-HIT to execute all legs atomically · auto-refresh 30s</span>
        }
        bodyFlush
      >
        {/* Inline FILTERS strip — same controls as the sidebar Filters panel,
            placed above the table so common adjustments don't require a
            sidebar trip. State is shared (single `filters` object). */}
        <div style={{
          display: "flex", flexWrap: "wrap", alignItems: "center", gap: 12,
          padding: "6px 10px", borderBottom: "1px solid var(--border)",
          background: "var(--bg-0)", fontSize: 11,
        }}>
          <label style={{ display: "flex", alignItems: "center", gap: 4 }}>
            <input type="checkbox" className="checkbox" checked={filters.onlyProfitable} onChange={e => setFilters(f => ({...f, onlyProfitable: e.target.checked}))}/>
            net-positive
          </label>
          <label style={{ display: "flex", alignItems: "center", gap: 4 }}>
            <input type="checkbox" className="checkbox" checked={filters.onlyLive} onChange={e => setFilters(f => ({...f, onlyLive: e.target.checked}))}/>
            live only
          </label>
          <label style={{ display: "flex", alignItems: "center", gap: 4 }} title="Strike >10% from spot AND yield >1% — typical fingerprint of a stale-LTP ghost edge">
            <input type="checkbox" className="checkbox" checked={filters.hideLiquiditySuspects} onChange={e => setFilters(f => ({...f, hideLiquiditySuspects: e.target.checked}))}/>
            hide stale-tick
          </label>
          <span className="faint" style={{ paddingLeft: 8, borderLeft: "1px solid var(--grid)" }}>min yield %</span>
          <input
            className="input tnum"
            type="number"
            value={filters.minYield}
            step={0.1}
            style={{ width: 64, padding: "2px 4px" }}
            onChange={e => setFilters(f => ({...f, minYield: +e.target.value}))}
            title="Filter by LTP-based yield_pct (the 'Yield %' column)"
          />
          <span className="faint">min exec yield %</span>
          <input
            className="input tnum"
            type="number"
            value={filters.minExecYield}
            step={0.1}
            style={{ width: 64, padding: "2px 4px" }}
            onChange={e => setFilters(f => ({...f, minExecYield: +e.target.value}))}
            title="Filter by depth-walked executable yield (the 'Exec Yield %' column). Rows with no depth (—) are also hidden when this is non-zero."
          />
          <span className="faint" style={{ paddingLeft: 8, borderLeft: "1px solid var(--grid)" }}>capital ₹</span>
          <input
            className="input tnum"
            type="number"
            value={capital}
            step={10000}
            style={{ width: 96, padding: "2px 4px" }}
            onChange={e => setCapital(+e.target.value)}
            title="Available capital — opportunities needing more than this are greyed out (and disabled for QUICK-HIT)."
          />
          <span className="faint">max days</span>
          <input
            className="input tnum"
            type="number"
            value={maxDays}
            step={1}
            min={1}
            placeholder="no limit"
            style={{ width: 64, padding: "2px 4px" }}
            onChange={e => setMaxDays(e.target.value)}
            title="Holding period cap — opportunities with longer DTE are greyed out."
          />
        </div>
        {/* Status banner — LAST CLOSE / STALE / empty / error / dev-mock */}
        {ARB_DEV_MOCK ? (
          <div style={{ padding: "6px 10px", background: "rgba(177,140,255,0.10)", borderBottom: "1px solid var(--border)", fontSize: 11 }} className="bright">
            DEV MOCK · localStorage.arb_dev_mock=1 · backend not polled
          </div>
        ) : !backendStatus.ok && backendStatus.error && backendStatus.error !== "initial" ? (
          <div style={{ padding: "6px 10px", background: "rgba(255,77,77,0.12)", borderBottom: "1px solid var(--border)", fontSize: 11 }} className="down">
            BACKEND ERROR · {backendStatus.error}
          </div>
        ) : scanMeta.mode === "last_close" ? (
          <div style={{ padding: "6px 10px", background: "rgba(110,200,255,0.12)", borderBottom: "1px solid var(--border)", fontSize: 11 }} className="cyan">
            LAST CLOSE · option_chain last-traded prices · {scanMeta.marketStatus?.note || "market closed"} · cash-fut/calendar empty after-hours (no Dhan futures data); parity/box edges shown · informational, not executable
          </div>
        ) : scanMeta.stale ? (
          <div style={{ padding: "6px 10px", background: "rgba(255,176,32,0.12)", borderBottom: "1px solid var(--border)", fontSize: 11 }} className="amber">
            STALE · last live scan {scanMeta.staleSince || "—"} · {scanMeta.marketStatus?.note || "market closed"}
          </div>
        ) : backendStatus.ok && backendStatus.count === 0 ? (
          <div style={{ padding: "6px 10px", background: "var(--bg-0)", borderBottom: "1px solid var(--border)", fontSize: 11 }} className="faint">
            Scanner active · no edges currently exceed charges threshold
            {scanMeta.marketStatus?.note ? ` · ${scanMeta.marketStatus.note}` : ""}
          </div>
        ) : null}
        <div style={{ overflow: "auto", height: "100%" }}>
          <table className="tbl tbl-compact">
            <thead>
              <tr>
                <th>Strategy</th>
                <th>Symbol</th>
                <th className="num" onClick={() => toggleSort("strikeDistancePct")} style={{ cursor: "pointer" }} title="Strike distance from spot (%) — large values flag illiquid stale-tick edges">Δ {sortBy === "strikeDistancePct" && (sortDir === "desc" ? "▼" : "▲")}</th>
                <th>Legs</th>
                <th className="num" onClick={() => toggleSort("holdingDays")} style={{ cursor: "pointer" }} title="Days to expiry — capital is locked until then">DTE {sortBy === "holdingDays" && (sortDir === "desc" ? "▼" : "▲")}</th>
                <th className="num" onClick={() => toggleSort("notional")} style={{ cursor: "pointer" }}>Notional {sortBy === "notional" && (sortDir === "desc" ? "▼" : "▲")}</th>
                <th className="num" onClick={() => toggleSort("grossPL")} style={{ cursor: "pointer" }}>Gross {sortBy === "grossPL" && (sortDir === "desc" ? "▼" : "▲")}</th>
                <th className="num">Charges</th>
                <th className="num" onClick={() => toggleSort("netPL")} style={{ cursor: "pointer" }}>Net ₹ {sortBy === "netPL" && (sortDir === "desc" ? "▼" : "▲")}</th>
                <th className="num" onClick={() => toggleSort("netPct")} style={{ cursor: "pointer" }}>Yield % {sortBy === "netPct" && (sortDir === "desc" ? "▼" : "▲")}</th>
                <th className="num" onClick={() => toggleSort("execYieldPct")} style={{ cursor: "pointer" }} title="Depth-walked executable yield at 1 lot. Walks 5-level book on every leg, recomputes charges on actual fills.">Exec @1L {sortBy === "execYieldPct" && (sortDir === "desc" ? "▼" : "▲")}</th>
                <th className="num" title="Same depth-walk at 5× lot size. PARTIAL = book too thin to absorb 5L.">Exec @5L</th>
                <th className="num" title="Same depth-walk at 10× lot size. PARTIAL = book too thin to absorb 10L.">Exec @10L</th>
                <th className="num">Conf</th>
                <th>Risk</th>
                <th style={{ textAlign: "right" }}>Action</th>
              </tr>
            </thead>
            <tbody>
              {filtered.map(o => {
                const affordable = o.notional <= capital;
                const withinDays = !maxDays || (o.holdingDays != null && o.holdingDays <= +maxDays);
                const usable = affordable && withinDays;
                const isSel = selected?.id === o.id;
                return (
                  <tr key={o.id} className={isSel ? "selected" : ""} onClick={() => setSelected(o)} style={{ cursor: "pointer", opacity: usable ? 1 : 0.5 }}>
                    <td>
                      <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
                        <CatDot cat={o.cat}/>
                        <div>
                          <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
                            <span className="bright" style={{ fontSize: 10, fontWeight: 500 }}>{o.typeLabel}</span>
                            {o.councilVerdict ? <CouncilChip verdict={o.councilVerdict} cached={o.councilCached}/> : null}
                          </div>
                          <div className="faint" style={{ fontSize: 9 }}>{o.cat}{o.councilConfidence != null ? ` · ⚖${o.councilConfidence}` : ""}</div>
                        </div>
                      </div>
                    </td>
                    <td className="bright" style={{ fontWeight: 500 }}>
                      <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
                        <span>{o.symbol}</span>
                        {o.liquidityWarning ? (
                          <span className="pill" style={{ fontSize: 8, background: "rgba(255,176,32,0.18)", color: "var(--amber)", border: "1px solid var(--amber)" }} title="Stale-tick suspect: deep OTM/ITM strike + implausible yield">⚠ STALE-TICK</span>
                        ) : null}
                      </div>
                    </td>
                    <td className="num tnum faint">{o.strikeDistancePct == null ? "—" : `${o.strikeDistancePct.toFixed(1)}%`}</td>
                    <td>
                      <span className="pill pill-dim" style={{ fontSize: 9 }}>{o.legs.length}-LEG</span>
                    </td>
                    <td className="num tnum faint">{o.holdingDays != null ? Math.round(o.holdingDays) : "—"}</td>
                    <td className="num tnum">₹{Math.round(o.notional).toLocaleString("en-IN")}</td>
                    <td className="num tnum bright">₹{o.grossPL.toFixed(0)}</td>
                    <td className="num tnum down">−₹{o.charges.toFixed(0)}</td>
                    <td className="num tnum" style={{ fontWeight: 600 }}>
                      <span className={o.netPL > 0 ? "up" : "down"}>{o.netPL > 0 ? "+" : ""}₹{o.netPL.toFixed(0)}</span>
                    </td>
                    <td className={`num tnum ${o.netPct > 0 ? "up" : "down"}`}>{o.netPct > 0 ? "+" : ""}{o.netPct.toFixed(2)}%</td>
                    <td className="num tnum" title={o.execStatus && o.execStatus !== "OK" ? `${o.execStatus}: ${o.execStatusDetail}` : (o.depthAgeSec != null ? `depth age ${o.depthAgeSec.toFixed(1)}s · exec net ₹${o.execNet?.toFixed(0)}` : "")}>
                      {o.execYieldPct == null ? (
                        <span className="faint">— {o.execStatus ? <span style={{fontSize:8, opacity:0.7}}>{o.execStatus}</span> : null}</span>
                      ) : (
                        <span className={o.execYieldPct > 0 ? "up" : "down"} style={{ fontWeight: 500 }}>
                          {o.execYieldPct > 0 ? "+" : ""}{o.execYieldPct.toFixed(2)}%
                        </span>
                      )}
                    </td>
                    <ExecCellAtSize entry={(o.execAtLots || []).find(e => e.lots === 5)}/>
                    <ExecCellAtSize entry={(o.execAtLots || []).find(e => e.lots === 10)}/>
                    <td className="num">
                      <ConfBar val={o.confidence}/>
                    </td>
                    <td>
                      <RiskPill r={o.risk}/>
                    </td>
                    <td style={{ textAlign: "right" }}>
                      {o.live ? (
                        <button
                          className="btn btn-xs btn-up"
                          disabled={!usable || o.netPL <= 0}
                          onClick={(e) => { e.stopPropagation(); executeArb(o); }}
                          style={{ fontWeight: 600 }}
                        >
                          ⚡ QUICK-HIT
                        </button>
                      ) : (
                        <span className="pill pill-dim" style={{ fontSize: 9 }}>STALE</span>
                      )}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      </Panel>

      {/* RIGHT DETAIL PANEL */}
      {selected ? (
        <ArbDetail opp={selected} capital={capital} onClose={() => setSelected(null)} onExecute={() => executeArb(selected)} onExecuteLeg={executeLeg} onResizeStart={onDetailResizeStart}/>
      ) : null}

      {/* EXECUTION MODAL */}
      {executing ? (
        <ExecuteModal executing={executing} setExecuting={setExecuting} onConfirm={confirmExecute}/>
      ) : null}
    </div>
  );
}

// ---------------------------------------------------------------------------
// LegDepthBook — one leg's 5-level book + per-leg trade button + viability.
// Bids on the left (descending price), asks on the right (ascending price).
// Highlights the row that contains the leg's required quantity ("fill line").
// ---------------------------------------------------------------------------
function LegDepthBook({ leg, idx, onTrade }) {
  const status = leg.leg_status || "OK";
  const ok     = status === "OK";
  const side   = (leg.side || "").toUpperCase();
  // Walk the relevant side to compute the row index where the lot fills.
  const fillSide = side === "BUY" ? (leg.asks || []) : (leg.bids || []);
  let cumQty = 0;
  let fillRow = -1;
  for (let i = 0; i < fillSide.length; i++) {
    cumQty += fillSide[i].qty || 0;
    if (cumQty >= (leg.qty || 0)) { fillRow = i; break; }
  }

  const cellPx = (lv, isFillLine) => ({
    padding: "2px 6px",
    background: isFillLine ? "rgba(255,176,32,0.18)" : "transparent",
    borderLeft: isFillLine ? "2px solid var(--amber)" : "2px solid transparent",
  });

  // Status badge colour.
  const badgeStyle = ok
    ? { background: "rgba(34,197,94,0.16)", color: "var(--up)", border: "1px solid var(--up)" }
    : { background: "rgba(239,68,68,0.16)", color: "var(--down)", border: "1px solid var(--down)" };

  // Render five rows: pad with empty cells if the book has fewer than 5 levels.
  const bids = [...(leg.bids || [])].slice(0, 5);
  const asks = [...(leg.asks || [])].slice(0, 5);
  while (bids.length < 5) bids.push(null);
  while (asks.length < 5) asks.push(null);

  return (
    <div style={{
      flex: "0 0 220px",
      border: "1px solid var(--border)",
      background: "var(--bg-1)",
      display: "flex", flexDirection: "column",
    }}>
      {/* Header row — leg label + side pill + status badge */}
      <div style={{ padding: "6px 8px", borderBottom: "1px solid var(--grid)", display: "flex", alignItems: "center", gap: 6 }}>
        <span className="faint" style={{ fontSize: 9 }}>L{idx + 1}</span>
        <span className={`pill ${side === "BUY" ? "pill-up" : "pill-down"}`} style={{ fontSize: 9 }}>{side}</span>
        <span className="bright" style={{ fontSize: 10, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={leg.symbol}>{leg.symbol}</span>
      </div>

      {/* Status row */}
      <div style={{ padding: "4px 8px", borderBottom: "1px solid var(--grid)", display: "flex", alignItems: "center", gap: 6, fontSize: 9 }}>
        <span className="pill" style={{ ...badgeStyle, fontSize: 8 }}>{status}</span>
        {leg.depth_age_sec != null ? <span className="faint">age {leg.depth_age_sec.toFixed(1)}s</span> : null}
        {leg.note ? <span className="faint" title={leg.note}>· {leg.note.length > 22 ? leg.note.slice(0, 22) + "…" : leg.note}</span> : null}
      </div>

      {/* Two-column book */}
      <table className="tbl tbl-compact" style={{ fontSize: 10, tableLayout: "fixed", width: "100%" }}>
        <thead>
          <tr>
            <th colSpan={2} style={{ textAlign: "center", color: "var(--up)" }}>BIDS</th>
            <th colSpan={2} style={{ textAlign: "center", color: "var(--down)" }}>ASKS</th>
          </tr>
          <tr>
            <th className="num" style={{ fontSize: 8 }}>Qty</th>
            <th className="num" style={{ fontSize: 8 }}>Px</th>
            <th className="num" style={{ fontSize: 8 }}>Px</th>
            <th className="num" style={{ fontSize: 8 }}>Qty</th>
          </tr>
        </thead>
        <tbody>
          {[0,1,2,3,4].map(i => {
            const b = bids[i], a = asks[i];
            const bidFill = side === "SELL" && i === fillRow;
            const askFill = side === "BUY"  && i === fillRow;
            return (
              <tr key={i}>
                <td className="num tnum" style={cellPx(b, bidFill)}>{b ? b.qty : "—"}</td>
                <td className="num tnum up" style={cellPx(b, bidFill)}>{b ? b.price.toFixed(2) : ""}</td>
                <td className="num tnum down" style={cellPx(a, askFill)}>{a ? a.price.toFixed(2) : ""}</td>
                <td className="num tnum" style={cellPx(a, askFill)}>{a ? a.qty : "—"}</td>
              </tr>
            );
          })}
        </tbody>
      </table>

      {/* Fill summary + per-leg trade button */}
      <div style={{ padding: "6px 8px", borderTop: "1px solid var(--grid)", fontSize: 10, display: "flex", flexDirection: "column", gap: 4 }}>
        <div style={{ display: "flex", justifyContent: "space-between" }}>
          <span className="faint">Need</span>
          <span className="tnum">{leg.qty}</span>
        </div>
        <div style={{ display: "flex", justifyContent: "space-between" }}>
          <span className="faint">Avg fill</span>
          <span className={`tnum ${ok ? "bright" : "faint"}`}>{leg.avg_fill_price != null ? "₹" + leg.avg_fill_price.toFixed(2) : "—"}</span>
        </div>
        <button
          className={`btn btn-xs ${side === "BUY" ? "btn-up" : "btn-down"}`}
          disabled={!ok}
          onClick={(e) => { e.stopPropagation(); onTrade && onTrade(leg, idx); }}
          style={{ marginTop: 2, fontWeight: 600 }}
          title={ok ? `${side} ${leg.qty} ${leg.symbol} @ ₹${leg.avg_fill_price?.toFixed(2)}` : `Cannot trade — ${status}`}
        >
          {ok ? `${side} ${leg.qty}` : `${side} (—)`}
        </button>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// OrderDepthPanel — side-by-side LegDepthBook for every leg of an opportunity.
// Surfaces the overall executable status + viability vs. LTP-yield gap.
// ---------------------------------------------------------------------------
function OrderDepthPanel({ opp, onTradeLeg }) {
  const legs = opp.execLegs || [];
  if (legs.length === 0) {
    // Two distinct causes — be explicit so the user knows whether to restart
    // the backend or just wait for the next tick.
    const hasExecField = opp.execStatus != null;
    return (
      <div className="faint" style={{ padding: 10, fontSize: 10, lineHeight: 1.5 }}>
        {hasExecField ? (
          <>Backend ran Pass B but no leg dicts came back (status: <b>{opp.execStatus}</b>). Detail: {opp.execStatusDetail || "—"}</>
        ) : (
          <>This opportunity has no <code>executable_legs</code> field — the backend is running an older build of <code>arbitrage.py</code> that predates the depth-walk pass. Restart the backend (<code>tv_signals.py</code>) to pick up the new code, then the next 30 s scan tick will populate depth.</>
        )}
      </div>
    );
  }
  const status = opp.execStatus || "UNKNOWN";
  const ok = status === "OK";
  const yieldGap = (ok && opp.execYieldPct != null && opp.netPct != null)
    ? opp.execYieldPct - opp.netPct : null;

  return (
    <div>
      {/* Summary strip */}
      <div style={{ padding: "8px 10px", display: "flex", flexWrap: "wrap", alignItems: "center", gap: 10, borderBottom: "1px solid var(--grid)", fontSize: 10 }}>
        <span className={`pill`} style={{
          fontSize: 9,
          background: ok ? "rgba(34,197,94,0.16)" : "rgba(239,68,68,0.16)",
          color: ok ? "var(--up)" : "var(--down)",
          border: "1px solid " + (ok ? "var(--up)" : "var(--down)"),
        }}>{ok ? "✓ VIABLE" : "✗ " + status}</span>
        {ok && opp.execYieldPct != null ? (
          <>
            <span className="faint">Exec yield</span>
            <span className={opp.execYieldPct > 0 ? "up tnum" : "down tnum"} style={{ fontWeight: 600 }}>
              {opp.execYieldPct > 0 ? "+" : ""}{opp.execYieldPct.toFixed(3)}%
            </span>
            {yieldGap != null ? (
              <span className={yieldGap >= 0 ? "faint" : "down"} title="Δ = exec yield − LTP yield. Negative means depth-walked execution yields LESS than the LTP screen suggests; large negative ⇒ the LTP edge was a stale-print mirage.">
                Δ vs LTP {yieldGap >= 0 ? "+" : ""}{yieldGap.toFixed(3)}%
              </span>
            ) : null}
            <span className="faint">·</span>
            <span className="faint">Net</span>
            <span className={(opp.execNet || 0) > 0 ? "up tnum" : "down tnum"} style={{ fontWeight: 600 }}>
              ₹{(opp.execNet || 0).toFixed(0)}
            </span>
            {opp.depthAgeSec != null ? (
              <span className="faint">· depth age {opp.depthAgeSec.toFixed(1)}s</span>
            ) : null}
          </>
        ) : (
          <span className="faint" title={opp.execStatusDetail}>{opp.execStatusDetail || "depth not viable for full execution"}</span>
        )}
      </div>

      {/* Side-by-side books */}
      <div style={{ display: "flex", gap: 6, padding: 8, overflowX: "auto" }}>
        {legs.map((leg, i) => (
          <LegDepthBook key={i} leg={leg} idx={i} onTrade={onTradeLeg}/>
        ))}
      </div>
    </div>
  );
}

function ArbDetail({ opp, capital, onClose, onExecute, onExecuteLeg, onResizeStart }) {
  // payoff diagrams only shown for legacy mock categories; risk-free arb
  // settles deterministically and has no payoff curve worth plotting.
  const showPayoff = opp.cat === "VOL_PREVIEW" || opp.cat === "DIR_PREVIEW";
  // Click on any leg row toggles the side-by-side depth panel. Reset whenever
  // the selected opportunity changes so a stale "open" state doesn't bleed
  // over into the next selection.
  const [depthOpen, setDepthOpen] = useState(false);
  useEffect(() => { setDepthOpen(false); }, [opp.id]);
  return (
    <div style={{ position: "relative", display: "flex", flexDirection: "column", minHeight: 0 }}>
      <div className="resize-handle-v" onMouseDown={onResizeStart} title="Drag to resize panel"/>
      <div className="scroll-stack" style={{ display: "flex", flexDirection: "column", gap: 8, minHeight: 0, overflow: "auto", flex: 1 }}>
      <Panel
        title={`${opp.typeLabel} · ${opp.symbol}`}
        actions={<button className="btn btn-xs btn-ghost" onClick={onClose}>✕</button>}
      >
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 4 }}>
          <KpiBox label="Gross P&L"   val={"₹" + opp.grossPL.toFixed(0)} tone="bright"/>
          <KpiBox label="All Charges" val={"−₹" + opp.charges.toFixed(0)} tone="down"/>
          <KpiBox label="Net P&L"     val={(opp.netPL > 0 ? "+" : "") + "₹" + opp.netPL.toFixed(0)} tone={opp.netPL > 0 ? "up" : "down"} big/>
          <KpiBox label="Yield"       val={(opp.netPct > 0 ? "+" : "") + opp.netPct.toFixed(2) + "%"} tone={opp.netPct > 0 ? "up" : "down"}/>
        </div>
        <dl className="kv" style={{ marginTop: 10, gridTemplateColumns: "auto 1fr", fontSize: 10 }}>
          <dt>Notional</dt><dd>₹{Math.round(opp.notional).toLocaleString("en-IN")}</dd>
          <dt>Holding</dt><dd>{opp.holdingDays} day{opp.holdingDays !== 1 ? "s" : ""}</dd>
          <dt>Max loss</dt><dd className="down">{opp.maxLoss && opp.maxLoss > 99999 ? "UNLIMITED" : opp.maxLoss ? "−₹" + Math.round(opp.maxLoss).toLocaleString("en-IN") : "—"}</dd>
          <dt>Affordable</dt><dd className={opp.notional <= capital ? "up" : "down"}>{opp.notional <= capital ? "✓ within capital" : "✗ exceeds capital"}</dd>
          <dt>Confidence</dt><dd>{opp.confidence}%</dd>
        </dl>
      </Panel>

      {opp.councilVerdict ? (
        <Panel title="Council Verdict" tag={opp.councilVerdict}>
          <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
            <CouncilChip verdict={opp.councilVerdict} cached={opp.councilCached}/>
            <span className="faint" style={{ fontSize: 10 }}>conf {opp.councilConfidence}% · {opp.councilLatencyMs}ms · ₹{(opp.councilCostInr || 0).toFixed(3)}</span>
          </div>
          <div style={{ fontSize: 11, lineHeight: 1.4, marginBottom: 6 }}>{opp.councilReasoning}</div>
          {opp.councilRisks?.length ? (
            <ul style={{ margin: 0, paddingLeft: 16, fontSize: 10 }} className="faint">
              {opp.councilRisks.map((r, i) => <li key={i} style={{ marginBottom: 2 }}>{r}</li>)}
            </ul>
          ) : null}
        </Panel>
      ) : null}

      <Panel
        title="Legs"
        tag={opp.legs.length}
        actions={
          <button
            className="btn btn-xs btn-ghost"
            onClick={() => setDepthOpen(v => !v)}
            title="Toggle side-by-side order depth for every leg"
          >
            {depthOpen ? "▾ Hide depth" : "▸ Show depth"}
          </button>
        }
        bodyFlush
      >
        <table className="tbl tbl-compact">
          <thead><tr><th>#</th><th>Side</th><th>Instrument</th><th className="num">Qty</th><th className="num">Price</th></tr></thead>
          <tbody>
            {opp.legs.map((leg, i) => (
              <tr
                key={i}
                onClick={() => setDepthOpen(true)}
                style={{ cursor: "pointer" }}
                title="Click to open side-by-side order depth"
              >
                <td className="faint">{i + 1}</td>
                <td><span className={`pill ${leg.action === "BUY" ? "pill-up" : "pill-down"}`} style={{ fontSize: 9 }}>{leg.action}</span></td>
                <td className="bright" style={{ fontSize: 10 }}>{leg.instrument}</td>
                <td className="num tnum">{leg.qty}</td>
                <td className="num tnum">₹{leg.price.toFixed(2)}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </Panel>

      {depthOpen ? (
        <Panel title="Order Depth · All Legs Side-by-Side" tag={opp.execStatus || "—"} bodyFlush>
          <OrderDepthPanel opp={opp} onTradeLeg={(leg, i) => onExecuteLeg && onExecuteLeg(opp, leg, i)}/>
        </Panel>
      ) : null}

      <Panel title="Charge Breakdown · Auto-computed">
        <ChargeBreakdown opp={opp}/>
      </Panel>

      <button className="btn btn-primary" disabled={!opp.live || opp.netPL <= 0 || opp.notional > capital} onClick={onExecute} style={{ padding: "10px 12px", fontSize: 12, letterSpacing: "0.12em" }}>
        ⚡ QUICK-HIT · EXECUTE ALL {opp.legs.length} LEGS
      </button>
      </div>
    </div>
  );
}

function ChargeBreakdown({ opp }) {
  const b = opp.chargeBreakdown;
  if (!b) {
    return (
      <div className="faint" style={{ fontSize: 10, padding: 8 }}>
        Backend did not return a charge breakdown for this opportunity.
      </div>
    );
  }
  const rows = [
    ["Brokerage",   b.brokerage],
    ["STT",         b.stt],
    ["Exchange tx", b.exchange],
    ["SEBI",        b.sebi],
    ["Stamp duty",  b.stamp],
    ["GST (18%)",   b.gst],
  ];
  if (b.council) rows.push(["Council (LLM)", b.council]);
  const sum = rows.reduce((s, r) => s + (r[1] || 0), 0);
  return (
    <div>
      <table className="tbl tbl-compact" style={{ marginTop: -4 }}>
        <tbody>
          {rows.map(([label, amt]) => (
            <tr key={label}>
              <td className="dim" style={{ fontSize: 10 }}>{label}</td>
              <td className="num tnum">₹{(amt || 0).toFixed(2)}</td>
              <td className="num faint tnum" style={{ width: 50, fontSize: 9 }}>
                {sum > 0 ? ((amt / sum) * 100).toFixed(1) + "%" : "—"}
              </td>
            </tr>
          ))}
          <tr style={{ background: "var(--bg-2)" }}>
            <td className="bright" style={{ fontSize: 10, fontWeight: 600 }}>TOTAL CHARGES</td>
            <td className="num tnum down" style={{ fontWeight: 600 }}>₹{opp.charges.toFixed(2)}</td>
            <td/>
          </tr>
        </tbody>
      </table>
      <div className="faint" style={{ fontSize: 9, padding: "6px 8px", lineHeight: 1.5 }}>
        Charges are <b>round-trip</b> — entry + exit unwind across all legs.
        Rates (FY 2026, post Oct-2024 STT hike): STT 0.1% options (sell on premium),
        0.02% futures (sell on turnover), 0.1% equity delivery (both sides), 0.025% equity intraday (sell).
        Brokerage ₹20/leg on F&O and equity intraday, ₹0 on equity delivery (Dhan). GST 18% on brokerage+exchange+SEBI.
        {(opp.cat === "SUB_INTRINSIC") ? (
          <div style={{ marginTop: 4 }}>
            <b>Note:</b> sub-intrinsic positions are <b>held to expiry</b> (no exit charges shown — option auto-settles, future expires/squares off).
          </div>
        ) : (opp.cat === "OVER_INTRINSIC") ? (
          <div style={{ marginTop: 4 }}>
            <b>Note:</b> over-intrinsic is a <b>mean-reversion bet</b>, not deterministic arb. Round-trip charges shown assume early exit when premium normalises. Risk: MED.
          </div>
        ) : null}
      </div>
    </div>
  );
}

function ExecuteModal({ executing, setExecuting, onConfirm }) {
  const [step, setStep] = useState(executing.step);
  const [executing_, setExecuting_] = useState(false);
  const [legStatus, setLegStatus] = useState(executing.opp.legs.map(() => "pending"));

  const runExecution = () => {
    setExecuting_(true);
    executing.opp.legs.forEach((leg, i) => {
      setTimeout(() => {
        setLegStatus(prev => { const n = [...prev]; n[i] = "sending"; return n; });
        setTimeout(() => {
          setLegStatus(prev => { const n = [...prev]; n[i] = "filled"; return n; });
          if (i === executing.opp.legs.length - 1) {
            setTimeout(() => onConfirm(), 400);
          }
        }, 300);
      }, i * 250);
    });
  };

  const opp = executing.opp;

  return (
    <div className="modal-backdrop" onClick={() => !executing_ && setExecuting(null)}>
      <div className="modal" onClick={e => e.stopPropagation()} style={{ minWidth: 560 }}>
        <div className="panel-header" style={{ padding: "10px 14px" }}>
          <span className="amber" style={{ fontSize: 14 }}>⚡</span>
          <span className="title">QUICK-HIT EXECUTION · {opp.typeLabel}</span>
          <span className="panel-tag">{opp.symbol}</span>
          {!executing_ && <button className="btn btn-xs btn-ghost" style={{ marginLeft: "auto" }} onClick={() => setExecuting(null)}>✕</button>}
        </div>
        <div style={{ padding: 14 }}>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 4, marginBottom: 12 }}>
            <KpiBox label="Net after charges" val={"+₹" + opp.netPL.toFixed(0)} tone="up" big/>
            <KpiBox label="Yield" val={"+" + opp.netPct.toFixed(2) + "%"} tone="up"/>
            <KpiBox label="Max loss" val={opp.maxLoss > 99999 ? "UNLIMITED" : "−₹" + Math.round(opp.maxLoss || 0)} tone="down"/>
          </div>

          <div className="h-xxs" style={{ marginBottom: 4 }}>EXECUTION LEGS ({opp.legs.length})</div>
          <div style={{ background: "var(--bg-0)", border: "1px solid var(--border)", marginBottom: 12 }}>
            {opp.legs.map((leg, i) => (
              <div key={i} style={{ display: "grid", gridTemplateColumns: "20px 50px 1fr 60px 70px 60px", gap: 8, padding: "6px 10px", borderBottom: i < opp.legs.length - 1 ? "1px solid var(--grid)" : "none", alignItems: "center" }}>
                <span className="faint tnum">{i + 1}</span>
                <span className={`pill ${leg.action === "BUY" ? "pill-up" : "pill-down"}`} style={{ fontSize: 9, justifySelf: "start" }}>{leg.action}</span>
                <span className="bright" style={{ fontSize: 10 }}>{leg.instrument}</span>
                <span className="tnum num">{leg.qty}</span>
                <span className="tnum num">₹{leg.price.toFixed(2)}</span>
                <span style={{ justifySelf: "end" }}>
                  {legStatus[i] === "pending" ? <span className="pill pill-dim" style={{ fontSize: 9 }}>READY</span>
                  : legStatus[i] === "sending" ? <span className="pill pill-amber" style={{ fontSize: 9 }}>◉ SEND…</span>
                  : <span className="pill pill-up" style={{ fontSize: 9 }}>✓ FILLED</span>}
                </span>
              </div>
            ))}
          </div>

          {!executing_ ? (
            <>
              <div style={{ background: "var(--bg-0)", border: "1px solid var(--amber)", borderLeft: "3px solid var(--amber)", padding: "8px 10px", fontSize: 10, marginBottom: 12 }}>
                <div className="amber" style={{ fontWeight: 600, marginBottom: 3 }}>ATOMIC EXECUTION</div>
                All legs will be sent simultaneously. If any leg fails, successful legs will be reversed to avoid legging risk. Slippage guard: ±0.15% per leg.
              </div>
              <div style={{ display: "flex", justifyContent: "flex-end", gap: 6 }}>
                <button className="btn" onClick={() => setExecuting(null)}>CANCEL</button>
                <button className="btn btn-primary" onClick={runExecution} style={{ padding: "6px 16px" }}>⚡ EXECUTE {opp.legs.length} LEGS</button>
              </div>
            </>
          ) : (
            <div style={{ textAlign: "center", padding: 10 }}>
              {legStatus.every(s => s === "filled") ? (
                <div className="up" style={{ fontSize: 14, fontWeight: 600 }}>✓ ALL LEGS FILLED</div>
              ) : (
                <div className="amber" style={{ fontSize: 12 }}>◉ EXECUTING…</div>
              )}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// ============ HELPERS ============
function KpiBox({ label, val, tone, big }) {
  const c = tone === "up" ? "up" : tone === "down" ? "down" : tone === "amber" ? "amber" : "bright";
  return (
    <div style={{ padding: big ? 8 : 5, background: "var(--bg-0)", border: "1px solid var(--border)" }}>
      <div className="h-xxs" style={{ fontSize: 8 }}>{label}</div>
      <div className={`tnum ${c}`} style={{ fontSize: big ? 18 : 12, marginTop: 2, fontWeight: big ? 600 : 400 }}>{val}</div>
    </div>
  );
}

function CatDot({ cat }) {
  const found = ARB_SUBTYPES.find(t => t.id === cat);
  const color = found ? found.color : "#666";
  return <span style={{ width: 8, height: 8, background: color, borderRadius: "50%" }}/>;
}

function CouncilChip({ verdict, cached }) {
  const color = verdict === "PROCEED" ? "var(--up)" : verdict === "REJECT" ? "var(--down)" : "var(--amber)";
  const glyph = verdict === "PROCEED" ? "✓" : verdict === "REJECT" ? "✗" : "?";
  return (
    <span
      title={`Council: ${verdict}${cached ? " (cached)" : " (fresh)"}`}
      className="pill"
      style={{ fontSize: 8, padding: "0 4px", background: "transparent", color, border: `1px solid ${color}`, opacity: cached ? 0.7 : 1 }}
    >
      {glyph} {verdict}
    </span>
  );
}

function ConfBar({ val }) {
  return (
    <div style={{ display: "flex", gap: 1, justifyContent: "flex-end" }}>
      {[0, 1, 2, 3, 4].map(i => (
        <div key={i} style={{ width: 4, height: 10, background: val >= (i + 1) * 20 ? (val > 80 ? "var(--up)" : val > 60 ? "var(--amber)" : "var(--fg-dim)") : "var(--bg-2)" }}/>
      ))}
    </div>
  );
}

function ExecCellAtSize({ entry }) {
  if (!entry) {
    return <td className="num tnum faint">—</td>;
  }
  if (entry.status !== "OK" || entry.yield_pct == null) {
    return (
      <td className="num tnum faint" title={`${entry.status}${entry.status_detail ? ': ' + entry.status_detail : ''}`}>
        — <span style={{ fontSize: 8, opacity: 0.7 }}>{entry.status}</span>
      </td>
    );
  }
  return (
    <td className="num tnum" title={`net ₹${entry.net?.toFixed?.(0) ?? entry.net} · notional ₹${(entry.notional || 0).toLocaleString("en-IN")}`}>
      <span className={entry.yield_pct > 0 ? "up" : "down"} style={{ fontWeight: 500 }}>
        {entry.yield_pct > 0 ? "+" : ""}{entry.yield_pct.toFixed(2)}%
      </span>
    </td>
  );
}

const PARTIAL_FILL_OPTIONS = [
  ["",                    "— select —"],
  ["ROLLBACK",            "ROLLBACK · auto-reverse filled legs"],
  ["HOLD_AND_ALERT",      "HOLD_AND_ALERT · pause, manual decision"],
  ["RETRY",               "RETRY · re-place failed leg N times"],
  ["RETRY_WITH_SLIPPAGE", "RETRY_WITH_SLIPPAGE · widen price on retry"],
  ["IGNORE",              "IGNORE · fire-and-forget (naked-leg risk)"],
  ["ABANDON_AND_NEXT",    "ABANDON_AND_NEXT · rollback + skip to next op"],
  ["HOLD_THEN_ROLLBACK",  "HOLD_THEN_ROLLBACK · wait N s, then rollback"],
];

const ARB_STRATEGY_OPTIONS = [
  ["CASH_FUTURES",   "Cash ⇄ Futures"],
  ["CALENDAR",       "Calendar Spread"],
  ["BOX",            "Box Spread"],
  ["SUB_INTRINSIC",  "Sub-Intrinsic"],
  ["OVER_INTRINSIC", "Over-Intrinsic"],
];

function AutoExecutePanel() {
  const [cfg, setCfg] = useState(null);
  const [status, setStatus] = useState(null);
  const [activity, setActivity] = useState([]);
  const [strategiesHelp, setStrategiesHelp] = useState({});
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");

  // Settings change rarely + via PUT, so still one-shot fetched.
  const reloadSettings = React.useCallback(async () => {
    try {
      const settings = await fetch("/api/arbitrage/auto/settings").then(r => r.json());
      if (settings.ok) {
        setCfg(settings.config);
        setStrategiesHelp(settings.strategies || {});
      }
    } catch (e) {
      setErr(String(e));
    }
  }, []);

  useEffect(() => { reloadSettings(); }, [reloadSettings]);

  // Status + activity come via SSE — pushed on every engine tick + on
  // start/stop/trip. The snapshot frame seeds initial state.
  const _absorbAutoFrame = React.useCallback((frame) => {
    if (!frame) return;
    if (frame.status) setStatus({ ok: true, ...frame.status });
    if (Array.isArray(frame.activity)) setActivity(frame.activity);
  }, []);
  const autoStream = useStream("/api/arbitrage/auto/stream", {
    onEvent: (ev) => _absorbAutoFrame(ev),
  });
  useEffect(() => { _absorbAutoFrame(autoStream.snapshot); }, [autoStream.snapshot, _absorbAutoFrame]);

  const patch = async (delta) => {
    setBusy(true);
    setErr("");
    try {
      const resp = await fetch("/api/arbitrage/auto/settings", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(delta),
      }).then(r => r.json());
      if (!resp.ok) setErr("save failed");
      else setCfg(resp.config);
    } catch (e) { setErr(String(e)); }
    finally { setBusy(false); }
  };

  const toggleStrategy = (id) => {
    if (!cfg) return;
    const cur = new Set(cfg.allowed_strategies || []);
    if (cur.has(id)) cur.delete(id); else cur.add(id);
    patch({ allowed_strategies: Array.from(cur) });
  };

  const start = async () => {
    setBusy(true); setErr("");
    try {
      const r = await fetch("/api/arbitrage/auto/start", { method: "POST" }).then(r => r.json());
      if (!r.ok) setErr((r.errors || ["start failed"]).join("; "));
      await reloadSettings();
    } finally { setBusy(false); }
  };
  const stop = async () => {
    setBusy(true); setErr("");
    try {
      await fetch("/api/arbitrage/auto/stop?flip_kill_switch=true", { method: "POST" });
      await reloadSettings();
    } finally { setBusy(false); }
  };
  const clearKillSwitch = () => patch({ kill_switch: false });

  if (!cfg) return <Panel title="Auto-Execute"><div className="faint" style={{ fontSize: 10 }}>loading…</div></Panel>;

  const validation = (status?.validation || []);
  const live = !!status?.live_trading;
  const running = !!status?.running;
  const tripped = !!status?.tripped;

  let chipBg, chipColor, chipLabel;
  if (tripped) { chipBg = "rgba(255,77,77,0.18)"; chipColor = "var(--down)"; chipLabel = "TRIPPED"; }
  else if (running) { chipBg = "rgba(34,214,111,0.18)"; chipColor = "var(--up)"; chipLabel = "RUNNING"; }
  else { chipBg = "rgba(138,144,155,0.18)"; chipColor = "var(--fg-dim)"; chipLabel = "STOPPED"; }

  return (
    <Panel title="Auto-Execute">
      <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 6 }}>
        <span className="pill" style={{ fontSize: 9, background: chipBg, color: chipColor, border: `1px solid ${chipColor}` }}>{chipLabel}</span>
        <span className="pill" style={{ fontSize: 9, background: live ? "rgba(255,77,77,0.18)" : "rgba(255,176,32,0.18)", color: live ? "var(--down)" : "var(--amber)", border: `1px solid ${live ? "var(--down)" : "var(--amber)"}` }}>
          {live ? "LIVE — REAL ₹" : "PAPER"}
        </span>
        <span className="faint" style={{ fontSize: 10, marginLeft: "auto" }}>
          {status?.daily_count ?? 0}{status?.config?.max_ops_per_day ? ` / ${status.config.max_ops_per_day}` : ""} today
        </span>
      </div>

      {err ? <div className="down" style={{ fontSize: 10, marginBottom: 4 }}>{err}</div> : null}
      {validation.length ? (
        <div className="amber" style={{ fontSize: 10, marginBottom: 6, lineHeight: 1.4 }}>
          {validation.slice(0, 3).map((v, i) => <div key={i}>· {v}</div>)}
        </div>
      ) : null}

      <div style={{ display: "flex", gap: 4, marginBottom: 8 }}>
        {tripped || cfg.kill_switch ? (
          <button className="btn btn-xs" onClick={clearKillSwitch} disabled={busy}>Clear kill-switch</button>
        ) : running ? (
          <button className="btn btn-xs btn-down" onClick={stop} disabled={busy} style={{ flex: 1 }}>STOP & FLIP KILL</button>
        ) : (
          <button className="btn btn-xs btn-up" onClick={start} disabled={busy || validation.length > 0} style={{ flex: 1 }}>START</button>
        )}
      </div>

      <div className="label" style={{ marginTop: 4 }}>Partial-fill strategy</div>
      <select className="input" value={cfg.partial_fill_strategy} onChange={e => patch({ partial_fill_strategy: e.target.value })} style={{ width: "100%", fontSize: 10 }}>
        {PARTIAL_FILL_OPTIONS.map(([v, label]) => <option key={v} value={v}>{label}</option>)}
      </select>
      {cfg.partial_fill_strategy && strategiesHelp[cfg.partial_fill_strategy] ? (
        <div className="faint" style={{ fontSize: 9, marginTop: 4, lineHeight: 1.3 }}>
          {strategiesHelp[cfg.partial_fill_strategy]}
        </div>
      ) : null}

      {(cfg.partial_fill_strategy === "RETRY" || cfg.partial_fill_strategy === "RETRY_WITH_SLIPPAGE") ? (
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 4, marginTop: 6 }}>
          <div>
            <div className="label">Retries</div>
            <input className="input tnum" type="number" min={1} max={10} value={cfg.retry_attempts} onChange={e => patch({ retry_attempts: +e.target.value || 0 })}/>
          </div>
          <div>
            <div className="label">{cfg.partial_fill_strategy === "RETRY_WITH_SLIPPAGE" ? "Slippage %" : "Backoff s"}</div>
            <input className="input tnum" type="number" step={0.1} value={cfg.partial_fill_strategy === "RETRY_WITH_SLIPPAGE" ? cfg.retry_slippage_pct : cfg.retry_backoff_s} onChange={e => patch(cfg.partial_fill_strategy === "RETRY_WITH_SLIPPAGE" ? { retry_slippage_pct: +e.target.value || 0 } : { retry_backoff_s: +e.target.value || 0 })}/>
          </div>
        </div>
      ) : null}
      {cfg.partial_fill_strategy === "HOLD_THEN_ROLLBACK" ? (
        <>
          <div className="label" style={{ marginTop: 4 }}>Hold timeout (s)</div>
          <input className="input tnum" type="number" min={1} max={300} value={cfg.hold_timeout_s} onChange={e => patch({ hold_timeout_s: +e.target.value || 0 })}/>
        </>
      ) : null}

      <div className="label" style={{ marginTop: 8 }}>Allowed strategies</div>
      <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
        {ARB_STRATEGY_OPTIONS.map(([id, label]) => {
          const on = (cfg.allowed_strategies || []).includes(id);
          return (
            <label key={id} style={{ display: "flex", alignItems: "center", gap: 4, cursor: "pointer", fontSize: 10 }}>
              <input type="checkbox" className="checkbox" checked={on} onChange={() => toggleStrategy(id)}/>
              <span className={on ? "bright" : "dim"}>{label}</span>
            </label>
          );
        })}
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 4, marginTop: 8 }}>
        <div>
          <div className="label">Min net ₹</div>
          <input className="input tnum" type="number" value={cfg.min_net_inr} onChange={e => patch({ min_net_inr: +e.target.value || 0 })}/>
        </div>
        <div>
          <div className="label">Min yield %</div>
          <input className="input tnum" type="number" step={0.01} value={cfg.min_yield_pct} onChange={e => patch({ min_yield_pct: +e.target.value || 0 })}/>
        </div>
        <div>
          <div className="label">Max notional ₹</div>
          <input className="input tnum" type="number" value={cfg.max_notional_per_op_inr} onChange={e => patch({ max_notional_per_op_inr: +e.target.value || 0 })}/>
        </div>
        <div>
          <div className="label">Max ops/day</div>
          <input className="input tnum" type="number" value={cfg.max_ops_per_day} onChange={e => patch({ max_ops_per_day: +e.target.value || 0 })}/>
        </div>
        <div>
          <div className="label">Max DTE (d)</div>
          <input className="input tnum" type="number" value={cfg.max_dte_days} onChange={e => patch({ max_dte_days: +e.target.value || 0 })}/>
        </div>
        <div>
          <div className="label">Top-N / tick</div>
          <input className="input tnum" type="number" min={1} max={10} value={cfg.execute_top_n_per_tick} onChange={e => patch({ execute_top_n_per_tick: +e.target.value || 1 })}/>
        </div>
      </div>

      <label style={{ display: "flex", alignItems: "center", gap: 4, fontSize: 10, marginTop: 8, cursor: "pointer" }}>
        <input type="checkbox" className="checkbox" checked={cfg.allow_liquidity_warnings} onChange={e => patch({ allow_liquidity_warnings: e.target.checked })}/>
        <span className={cfg.allow_liquidity_warnings ? "amber" : "dim"}>Allow stale-tick suspects</span>
      </label>

      {activity.length ? (
        <>
          <div className="label" style={{ marginTop: 10 }}>Activity (last {activity.length})</div>
          <div style={{ maxHeight: 140, overflow: "auto", fontSize: 9, lineHeight: 1.5 }}>
            {activity.map((a, i) => (
              <div key={i} style={{ display: "flex", gap: 4, alignItems: "center", borderBottom: "1px dashed var(--border)", padding: "2px 0" }}
                title={a.note || ""}>
                <span className="faint" style={{ width: 50, flexShrink: 0 }}>{(a.started_at || "").slice(11, 19)}</span>
                <span className={a.result === "ALL_FILLED" ? "up" : a.result === "ROLLED_BACK" ? "amber" : a.result === "FAILED" ? "down" : "dim"} style={{ width: 70, fontWeight: 500 }}>{a.result}</span>
                <span className="bright" style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{a.symbol}</span>
                {a.realized_edge_inr ? <span className="tnum">₹{Math.round(a.realized_edge_inr)}</span> : null}
                {a.paper ? <span className="pill" style={{ fontSize: 7, padding: "0 3px" }}>P</span> : null}
              </div>
            ))}
          </div>
        </>
      ) : null}

      <div className="down" style={{ fontSize: 9, marginTop: 8, lineHeight: 1.3, opacity: 0.85 }}>
        ⚠ Auto-executes real trades when LIVE. Test in PAPER first, set conservative gates, monitor activity feed. Kill-switch is sticky — survives restart.
      </div>
    </Panel>
  );
}

function RiskPill({ r }) {
  const map = { NONE: "pill-up", LOW: "pill-up", MED: "pill-amber", HIGH: "pill-down", UNLIMIT: "pill-red-solid" };
  return <span className={`pill ${map[r] || "pill-dim"}`} style={{ fontSize: 9 }}>{r}</span>;
}

Object.assign(window, { ArbitragePage });
