// =============================================================================
// a-detail.jsx — Screen 02 仓位详情 + TradingView K线
// =============================================================================
// Top: symbol header / price + funding. Mid: timeframe tabs + indicator picker
// + standalone TradingView widget + TP/SL/LIQ overlay. Bottom: position info.
// Props: { t, sym, nav(id), onBack }
// =============================================================================
//
// Position Detail — read-only, big chart + indicators, TP/SL strip, position info below.

// TradingView Lightweight Charts — fed by Hyperliquid candleSnapshot REST +
// candle WS channel. Real HL prices, accurate price-line overlays for
// LIQ / TP / SL via createPriceLine. Indicators kept minimal: MA overlay
// and a Volume histogram pane (TODO: EMA/BOLL/RSI/MACD).
const HL_INTERVAL_MAP = { '60':'1h', '240':'4h', 'D':'1d', 'W':'1w', 'M':'1M' };
const HL_INTERVAL_MS = {
  '1m': 60000, '3m': 180000, '5m': 300000, '15m': 900000, '30m': 1800000,
  '1h': 3600000, '2h': 7200000, '4h': 14400000, '8h': 28800000, '12h': 43200000,
  '1d': 86400000, '3d': 259200000, '1w': 604800000, '1M': 2592000000,
};

// Decimal precision picker — keeps display digits sensible across the wide
// price range (BTC 100k+ vs kPEPE 0.005).
function pricePrecision(p) {
  const a = Math.abs(p || 0);
  if (a < 0.001) return 6;
  if (a < 0.01)  return 5;
  if (a < 1)     return 4;
  if (a < 10)    return 3;
  if (a < 1000)  return 2;
  return 1;
}

function computeSMA(closes, length) {
  const out = new Array(closes.length).fill(null);
  if (!Array.isArray(closes) || length < 2) return out;
  let sum = 0;
  for (let i = 0; i < closes.length; i++) {
    sum += closes[i];
    if (i >= length) sum -= closes[i - length];
    if (i >= length - 1) out[i] = sum / length;
  }
  return out;
}
function computeEMA(closes, length) {
  const out = new Array(closes.length).fill(null);
  if (!Array.isArray(closes) || length < 2) return out;
  const k = 2 / (length + 1);
  let prev = null;
  for (let i = 0; i < closes.length; i++) {
    const c = closes[i];
    if (prev == null) {
      // Seed with SMA of first `length` closes once we have them.
      if (i === length - 1) {
        let s = 0; for (let j = 0; j <= i; j++) s += closes[j];
        prev = s / length; out[i] = prev;
      }
    } else {
      prev = c * k + prev * (1 - k);
      out[i] = prev;
    }
  }
  return out;
}
function computeRSI(closes, length) {
  const out = new Array(closes.length).fill(null);
  if (!Array.isArray(closes) || closes.length <= length) return out;
  let avgGain = 0, avgLoss = 0;
  // Initial averages from first `length` deltas (simple average per Wilder's seed).
  for (let i = 1; i <= length; i++) {
    const d = closes[i] - closes[i - 1];
    if (d >= 0) avgGain += d; else avgLoss -= d;
  }
  avgGain /= length;
  avgLoss /= length;
  out[length] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);
  // Wilder smoothing thereafter.
  for (let i = length + 1; i < closes.length; i++) {
    const d = closes[i] - closes[i - 1];
    const gain = d > 0 ? d : 0;
    const loss = d < 0 ? -d : 0;
    avgGain = (avgGain * (length - 1) + gain) / length;
    avgLoss = (avgLoss * (length - 1) + loss) / length;
    out[i] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);
  }
  return out;
}
function computeMACD(closes, fast, slow, signal) {
  const fastEMA = computeEMA(closes, fast);
  const slowEMA = computeEMA(closes, slow);
  const macd = closes.map((_, i) =>
    (fastEMA[i] != null && slowEMA[i] != null) ? fastEMA[i] - slowEMA[i] : null
  );
  // Signal = EMA(macd, signal). EMA helper expects a continuous numeric input
  // so we strip leading nulls and re-align indices.
  const startIdx = macd.findIndex(v => v != null);
  const signalArr = new Array(closes.length).fill(null);
  if (startIdx >= 0) {
    const trimmed = macd.slice(startIdx);
    const sig = computeEMA(trimmed, signal);
    for (let i = 0; i < sig.length; i++) {
      if (sig[i] != null) signalArr[startIdx + i] = sig[i];
    }
  }
  const hist = macd.map((v, i) => (v != null && signalArr[i] != null) ? v - signalArr[i] : null);
  return { macd, signal: signalArr, hist };
}
function computeBoll(closes, length, mult) {
  // Returns [middle, upper, lower] arrays (each same length as closes).
  const mid = computeSMA(closes, length);
  const upper = new Array(closes.length).fill(null);
  const lower = new Array(closes.length).fill(null);
  for (let i = length - 1; i < closes.length; i++) {
    let sumSq = 0;
    for (let j = i - length + 1; j <= i; j++) {
      const d = closes[j] - mid[i];
      sumSq += d * d;
    }
    const sd = Math.sqrt(sumSq / length);
    upper[i] = mid[i] + mult * sd;
    lower[i] = mid[i] - mult * sd;
  }
  return { mid, upper, lower };
}

// Last-good candle cache (keyed by coin+interval). Lets the chart paint
// instantly on revisit and survives offline reloads.
function _candleCacheKey(coin, hlInterval) { return 'hl.candles.' + coin + '.' + hlInterval; }
function _candleCacheRead(coin, hlInterval) {
  try {
    const raw = localStorage.getItem(_candleCacheKey(coin, hlInterval));
    if (!raw) return null;
    const j = JSON.parse(raw);
    if (!Array.isArray(j) || j.length === 0) return null;
    return j;
  } catch (e) { return null; }
}
function _candleCacheWrite(coin, hlInterval, arr) {
  try {
    if (!Array.isArray(arr) || arr.length === 0) return;
    // Keep a slim copy (last 300) to stay within the localStorage budget.
    const slim = arr.slice(-300);
    localStorage.setItem(_candleCacheKey(coin, hlInterval), JSON.stringify(slim));
  } catch (e) {}
}

// PriceLineLabels — small chips placed at the LEFT edge of the candle pane,
// vertically tracking each price line's screen y. Updates on each animation
// frame while visible so panning / zooming keep them aligned. Stays out of
// the right-axis tick numbers.
function PriceLineLabels({ chartRef, candleRef, items, loaded }) {
  const [ticks, setTicks] = React.useState(0);
  // rAF loop syncs label position with the chart's actual redraw frame —
  // setInterval(250ms) lagged visibly while the user dragged the axis.
  // Cost is one tiny re-render per frame; cheap for the 1–3 chips we render.
  React.useEffect(() => {
    if (!loaded) return;
    let alive = true;
    let raf = 0;
    const loop = () => {
      if (!alive) return;
      setTicks(t => (t + 1) | 0);
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => { alive = false; cancelAnimationFrame(raf); };
  }, [loaded]);

  // Force re-render on resize.
  React.useEffect(() => {
    const onResize = () => setTicks(t => (t + 1) | 0);
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  const candle = candleRef.current;
  const chart  = chartRef.current;
  if (!candle || !chart || !loaded) return null;

  // Pane 0 height + right price-scale width. Position the chip flush against
  // the inner edge of the right axis so it sits in the scale gutter without
  // covering the tick numbers (1.40, 1.20…).
  let paneHeight = 0, scaleWidth = 50;
  try {
    const panes = chart.panes && chart.panes();
    if (panes && panes[0] && panes[0].getHeight) paneHeight = panes[0].getHeight();
  } catch (e) {}
  try {
    const ps = chart.priceScale('right');
    if (ps && ps.width) scaleWidth = ps.width() || scaleWidth;
  } catch (e) {}

  return (
    <>{items.filter(it => it.show).map(it => {
      let y = null;
      try { y = candle.priceToCoordinate(it.price); } catch (e) {}
      if (y == null) return null;
      const priceText = it.price.toFixed(pricePrecision(it.price));
      // Clamp to pane edges with directional arrow when off-range so the
      // user always sees roughly where the level sits.
      const min = 4, max = (paneHeight || 999) - 18;
      let displayY = y, arrow = '';
      if (y < min) { displayY = min; arrow = '↑ '; }
      else if (paneHeight && y > max) { displayY = max; arrow = '↓ '; }
      return (
        <div key={it.key + ticks} style={{
          position:'absolute',
          right: scaleWidth + 2, top: displayY - 9,
          background: it.color, color: '#fff',
          fontFamily: 'JetBrains Mono, ui-monospace, monospace',
          fontSize: 11, fontWeight: 700, letterSpacing: '0.04em',
          padding: '1px 6px', borderRadius: 3,
          pointerEvents: 'none', zIndex: 5,
          boxShadow: '0 1px 4px rgba(0,0,0,0.18)',
          whiteSpace: 'nowrap',
        }}>{arrow}{it.label} {priceText}</div>
      );
    })}</>
  );
}

function LWChart({ coin='BTC', interval='60', height=400, theme='dark', bg, position, lines={}, indicators=[], indParams={} }) {
  const containerRef = React.useRef(null);
  const chartRef     = React.useRef(null);
  const candleRef    = React.useRef(null);
  const volumeRef    = React.useRef(null);
  const maRef        = React.useRef(null);
  const emaRef       = React.useRef(null);
  const bollMidRef   = React.useRef(null);
  const bollUpRef    = React.useRef(null);
  const bollLoRef    = React.useRef(null);
  const rsiRef       = React.useRef(null);
  const rsiPaneRef   = React.useRef(null); // assigned pane index, or null
  const macdLineRef  = React.useRef(null);
  const macdSigRef   = React.useRef(null);
  const macdHistRef  = React.useRef(null);
  const macdPaneRef  = React.useRef(null);
  // TP / SL / LIQ rendered as series.createPriceLine on the candle series.
  // This keeps candle autoscale based on candles only (so the candles don't
  // get squashed when LIQ is far away). The PriceLineLabels overlay on the
  // left clamps to the chart edge with an arrow when the price is off-range.
  const priceLinesRef = React.useRef([]);
  const dataRef      = React.useRef([]); // raw candles, full series
  const [loading, setLoading] = React.useState(true);
  // Bumped every time candle data is replaced (interval / coin switch).
  // Indicator effects depend on this so they recompute against fresh data.
  const [dataEpoch, setDataEpoch] = React.useState(0);
  // OHLC of the candle under the crosshair (or last bar when not hovering),
  // rendered as a compact panel in the chart's top-left.
  const [ohlc, setOhlc] = React.useState(null);

  const isDark = theme === 'dark';

  // Build / rebuild chart whenever theme or container size changes.
  React.useEffect(() => {
    if (!containerRef.current || !window.LightweightCharts) return;
    const LWC = window.LightweightCharts;
    const chart = LWC.createChart(containerRef.current, {
      width:  containerRef.current.clientWidth,
      height: height,
      layout: {
        background: { type: 'solid', color: bg || (isDark ? '#0b0e11' : '#ffffff') },
        textColor:  isDark ? 'rgba(234,236,239,0.62)' : 'rgba(23,24,31,0.62)',
        fontFamily: 'JetBrains Mono, ui-monospace, monospace',
        // Smaller axis labels → LWC's tick auto-density picks more ticks per
        // axis. fontSize 9 squeezes ~14 horizontal lines onto a ~200px pane,
        // enough for 0.10 step instead of 0.20.
        fontSize:   10,
      },
      grid: {
        vertLines: { color: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.05)' },
        horzLines: { color: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.05)' },
      },
      timeScale: {
        timeVisible: true, secondsVisible: false,
        borderColor: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)',
        // ~6px per bar matches the user's preferred density (fig55: ~60 bars
        // in viewport). User can pinch / wheel to zoom further.
        barSpacing: 6,
        // Small right pad so the latest candle has a touch of breathing room
        // without floating far from the price axis.
        rightOffset: 2,
        // Beijing time + 中文 date in axis tick marks (5月8日 / 10:00).
        tickMarkFormatter: (time, tickMarkType) => {
          const d = new Date((typeof time === 'number' ? time : 0) * 1000);
          // tickMarkType: Year=0 / Month=1 / DayOfMonth=2 / Time=3 / TimeWithSeconds=4
          if (tickMarkType <= 2) {
            const parts = new Intl.DateTimeFormat('zh-CN-u-ca-gregory', {
              timeZone: 'Asia/Shanghai', month: 'numeric', day: 'numeric',
            }).formatToParts(d);
            const m = parts.find(p => p.type === 'month')?.value || '';
            const dd = parts.find(p => p.type === 'day')?.value || '';
            return m + '月' + dd + '日';
          }
          const parts = new Intl.DateTimeFormat('zh-CN-u-ca-gregory', {
            timeZone: 'Asia/Shanghai', hour: '2-digit', minute: '2-digit', hour12: false,
          }).formatToParts(d);
          const hh = parts.find(p => p.type === 'hour')?.value || '';
          const mm = parts.find(p => p.type === 'minute')?.value || '';
          return hh + ':' + mm;
        },
      },
      // Crosshair tooltip / right-axis label localization → Beijing time.
      localization: {
        locale: 'zh-CN',
        timeFormatter: (time) => {
          const d = new Date((typeof time === 'number' ? time : 0) * 1000);
          const parts = new Intl.DateTimeFormat('zh-CN-u-ca-gregory', {
            timeZone: 'Asia/Shanghai',
            month: 'numeric', day: 'numeric',
            hour: '2-digit', minute: '2-digit', hour12: false,
          }).formatToParts(d);
          const get = (t) => parts.find(p => p.type === t)?.value || '';
          return `${get('month')}月${get('day')}日 ${get('hour')}:${get('minute')}`;
        },
      },
      rightPriceScale: {
        borderColor: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)',
        autoScale: true,
        // Generous top/bottom padding so the candle range occupies ~65% of
        // the pane vertical, matching the looser visual the user wants
        // (fig 61). Volume histogram lives in the bottom 22% via its own
        // overlay scale margins.
        scaleMargins: { top: 0.15, bottom: 0.28 },
        // Allow more tick density when there's room for the smaller font.
        entireTextOnly: false,
      },
      // Crosshair shows during touch / hover so the user can read price+time
      // at a specific point. We clear it on touchend below so it doesn't
      // linger after lift-off (LWC's default leaves it stuck on iOS).
      crosshair: {
        mode: 1, // Magnet — snaps to candles, more useful than free-roam
        vertLine: {
          visible: true, labelVisible: true,
          color: isDark ? 'rgba(234,236,239,0.35)' : 'rgba(23,24,31,0.35)',
          width: 1, style: 2,
        },
        horzLine: {
          visible: true, labelVisible: true,
          color: isDark ? 'rgba(234,236,239,0.35)' : 'rgba(23,24,31,0.35)',
          width: 1, style: 2,
        },
      },
      // Both axes draggable for zoom (desktop + touch). Double-click any
      // axis resets it back to autoScale; pinch on chart body zooms time.
      handleScroll: {
        mouseWheel: true,
        pressedMouseMove: true,
        horzTouchDrag: true,
        vertTouchDrag: true,
      },
      handleScale: {
        axisPressedMouseMove: { time: true, price: true },
        axisDoubleClickReset: { time: true, price: true },
        mouseWheel: true,
        pinch: true,
      },
    });
    chartRef.current = chart;
    // Hyperliquid-aligned candle palette: teal up / clean red down.
    // v5 API: addSeries(SeriesType, options, paneIndex=0)
    candleRef.current = chart.addSeries(LWC.CandlestickSeries, {
      upColor:    'rgb(37,166,154)', downColor:    '#ef5350',
      wickUpColor:'rgb(37,166,154)', wickDownColor:'#ef5350',
      borderVisible: false,
    });
    // OHLC overlay subscription. When crosshair is active, show the bar
    // under the cursor; otherwise show the latest bar from buffer.
    const onCross = (param) => {
      const series = candleRef.current;
      if (!series) return;
      if (!param || !param.time || !param.seriesData) {
        const arr = dataRef.current;
        if (arr.length) {
          const last = arr[arr.length - 1];
          setOhlc({ time: last.time, open: last.open, high: last.high, low: last.low, close: last.close });
        } else {
          setOhlc(null);
        }
        return;
      }
      const d = param.seriesData.get(series);
      if (d && d.open != null) {
        setOhlc({ time: param.time, open: d.open, high: d.high, low: d.low, close: d.close });
      }
    };
    chart.subscribeCrosshairMove(onCross);
    // Ensure no crosshair on initial paint — LWC sometimes leaves a default
    // position which would render the dashed cross on first frame.
    try { chart.clearCrosshairPosition(); } catch (e) {}
    return () => {
      try { chart.unsubscribeCrosshairMove(onCross); } catch (e) {}
      try { chart.remove(); } catch (e) {}
      chartRef.current = null;
      candleRef.current = null;
      volumeRef.current = null;
      maRef.current = null;
      priceLinesRef.current = [];
    };
  }, [theme, height, bg]);

  // Aggressive crosshair cleanup. LWC keeps the last hover position painted
  // even after the user lifts off, which is jarring on touch devices. We
  // listen on every pointer-end variant + clear on a short timer after mount
  // to wipe any default initial position.
  React.useEffect(() => {
    const el = containerRef.current;
    if (!el) return;
    const clear = () => {
      try { chartRef.current && chartRef.current.clearCrosshairPosition(); } catch (e) {}
    };
    // Pointer events cover both mouse and touch on modern Safari.
    el.addEventListener('pointerup',     clear);
    el.addEventListener('pointercancel', clear);
    el.addEventListener('pointerleave',  clear);
    // Legacy touch events as a belt-and-suspenders fallback.
    el.addEventListener('touchend',      clear, { passive: true });
    el.addEventListener('touchcancel',   clear, { passive: true });
    el.addEventListener('mouseleave',    clear);
    // Tap anywhere outside the chart also clears (e.g., user scrolls list).
    const onDocPointerDown = (ev) => {
      if (!el.contains(ev.target)) clear();
    };
    document.addEventListener('pointerdown', onDocPointerDown);
    // Wipe any default initial crosshair set by LWC during first frames.
    const t1 = setTimeout(clear, 50);
    const t2 = setTimeout(clear, 250);
    const t3 = setTimeout(clear, 800);
    return () => {
      el.removeEventListener('pointerup',     clear);
      el.removeEventListener('pointercancel', clear);
      el.removeEventListener('pointerleave',  clear);
      el.removeEventListener('touchend',      clear);
      el.removeEventListener('touchcancel',   clear);
      el.removeEventListener('mouseleave',    clear);
      document.removeEventListener('pointerdown', onDocPointerDown);
      clearTimeout(t1); clearTimeout(t2); clearTimeout(t3);
    };
  }, []);

  // Window resize → resize chart to container width.
  React.useEffect(() => {
    if (!containerRef.current) return;
    const ro = new ResizeObserver(() => {
      const c = chartRef.current;
      if (!c) return;
      try { c.applyOptions({ width: containerRef.current.clientWidth }); } catch (e) {}
    });
    ro.observe(containerRef.current);
    return () => ro.disconnect();
  }, []);

  // Load candles + subscribe to live updates whenever coin / interval change.
  React.useEffect(() => {
    if (!candleRef.current) return;
    let cancelled = false;
    const hlInterval = HL_INTERVAL_MAP[interval] || '1h';
    const ms = HL_INTERVAL_MS[hlInterval] || 3600000;
    // Aim for ~200 candles, but cap absolute lookback so 1M doesn't ask HL
    // for 16 years of history (which it doesn't have).
    const MAX_LOOKBACK = 5 * 365 * 86400000; // 5 years
    const startTime = Date.now() - Math.min(ms * 200, MAX_LOOKBACK);

    // Paint cached data instantly so the user never sees an empty chart on
    // revisit; the network response will overwrite this within a beat.
    const cached = _candleCacheRead(coin, hlInterval);
    if (cached) {
      dataRef.current = cached;
      try { candleRef.current.setData(cached); } catch (e) {}
      setDataEpoch(e => e + 1);
      if (cached.length) {
        const last = cached[cached.length - 1];
        setOhlc({ time: last.time, open: last.open, high: last.high, low: last.low, close: last.close });
      }
      const lastClose = cached.length ? cached[cached.length - 1].close : null;
      if (lastClose != null) {
        const precision = pricePrecision(lastClose);
        try {
          candleRef.current.applyOptions({
            priceFormat: { type: 'price', precision, minMove: Math.pow(10, -precision) },
          });
        } catch (e) {}
      }
      if (volumeRef.current) {
        try {
          volumeRef.current.setData(cached.map(d => ({
            time: d.time, value: d.volume,
            color: d.close >= d.open ? 'rgba(37,166,154,0.5)' : 'rgba(239,83,80,0.5)',
          })));
        } catch (e) {}
      }
      if (maRef.current) {
        const len = (indParams.MA && indParams.MA.length) || 20;
        const sma = computeSMA(cached.map(d => d.close), len);
        try { maRef.current.setData(cached.map((d, i) => sma[i] != null ? { time: d.time, value: sma[i] } : null).filter(Boolean)); } catch (e) {}
      }
      setLoading(false);
    } else {
      setLoading(true);
    }

    window.hlPost('candleSnapshot', {
      req: { coin: coin, interval: hlInterval, startTime: startTime, endTime: Date.now() },
    }).then(rows => {
      if (cancelled || !candleRef.current) return;
      const arr = (Array.isArray(rows) ? rows : []).map(r => ({
        time: Math.floor(r.t / 1000),
        open: parseFloat(r.o),
        high: parseFloat(r.h),
        low:  parseFloat(r.l),
        close: parseFloat(r.c),
        volume: parseFloat(r.v),
      })).filter(d => isFinite(d.close));
      dataRef.current = arr;
      candleRef.current.setData(arr);
      setDataEpoch(e => e + 1);
      if (arr.length) {
        const last = arr[arr.length - 1];
        setOhlc({ time: last.time, open: last.open, high: last.high, low: last.low, close: last.close });
      }
      // Match the right-axis price chip to the same decimal precision the
      // header uses (e.g. TON 2.581, BTC 102315.7) instead of LWC's default 2.
      const lastClose = arr.length ? arr[arr.length - 1].close : null;
      if (lastClose != null) {
        const precision = pricePrecision(lastClose);
        try {
          candleRef.current.applyOptions({
            priceFormat: { type: 'price', precision, minMove: Math.pow(10, -precision) },
          });
        } catch (e) {}
      }
      _candleCacheWrite(coin, hlInterval, arr);
      if (volumeRef.current) {
        volumeRef.current.setData(arr.map(d => ({
          time: d.time, value: d.volume,
          color: d.close >= d.open ? 'rgba(37,166,154,0.5)' : 'rgba(239,83,80,0.5)',
        })));
      }
      if (maRef.current) {
        const len = (indParams.MA && indParams.MA.length) || 20;
        const sma = computeSMA(arr.map(d => d.close), len);
        maRef.current.setData(arr.map((d, i) => sma[i] != null ? { time: d.time, value: sma[i] } : null).filter(Boolean));
      }
      setLoading(false);
    }).catch(() => { setLoading(false); });

    const sock = window.getHLSocket();
    const off = sock.subscribe({ type: 'candle', coin: coin, interval: hlInterval }, (msg) => {
      if (cancelled || !candleRef.current) return;
      const r = msg && msg.t != null ? msg : (msg && msg[0]) || null;
      if (!r) return;
      const point = {
        time: Math.floor(r.t / 1000),
        open: parseFloat(r.o),
        high: parseFloat(r.h),
        low:  parseFloat(r.l),
        close: parseFloat(r.c),
        volume: parseFloat(r.v),
      };
      if (!isFinite(point.close)) return;
      try { candleRef.current.update(point); } catch (e) {}
      // Maintain rolling buffer for MA/Volume recompute on close.
      const arr = dataRef.current;
      if (arr.length && arr[arr.length - 1].time === point.time) {
        arr[arr.length - 1] = point;
      } else {
        arr.push(point);
      }
      if (volumeRef.current) {
        try {
          volumeRef.current.update({
            time: point.time, value: point.volume,
            color: point.close >= point.open ? 'rgba(37,166,154,0.5)' : 'rgba(239,83,80,0.5)',
          });
        } catch (e) {}
      }
      // Live indicator recompute. The fast path (MA) updates incrementally;
      // EMA / BOLL / RSI / MACD recompute over the rolling buffer — costs
      // a few hundred μs per tick which is fine at WS cadence.
      const closes = arr.map(d => d.close);
      if (maRef.current) {
        const len = (indParams.MA && indParams.MA.length) || 20;
        if (arr.length >= len) {
          let s = 0;
          for (let i = arr.length - len; i < arr.length; i++) s += arr[i].close;
          try { maRef.current.update({ time: point.time, value: s / len }); } catch (e) {}
        }
      }
      if (emaRef.current) {
        const len = (indParams.EMA && indParams.EMA.length) || 21;
        const ema = computeEMA(closes, len);
        const last = ema[ema.length - 1];
        if (last != null) try { emaRef.current.update({ time: point.time, value: last }); } catch (e) {}
      }
      if (bollMidRef.current) {
        const len = (indParams.BOLL && indParams.BOLL.length) || 20;
        const m   = (indParams.BOLL && indParams.BOLL.stdDev) || 2;
        const { mid, upper, lower } = computeBoll(closes, len, m);
        const i = closes.length - 1;
        try {
          if (mid[i]   != null) bollMidRef.current.update({ time: point.time, value: mid[i] });
          if (upper[i] != null) bollUpRef.current .update({ time: point.time, value: upper[i] });
          if (lower[i] != null) bollLoRef.current .update({ time: point.time, value: lower[i] });
        } catch (e) {}
      }
      if (rsiRef.current) {
        const len = (indParams.RSI && indParams.RSI.length) || 14;
        const r = computeRSI(closes, len);
        const last = r[r.length - 1];
        if (last != null) try { rsiRef.current.update({ time: point.time, value: last }); } catch (e) {}
      }
      if (macdLineRef.current) {
        const fast   = (indParams.MACD && indParams.MACD.fast)   || 12;
        const slow   = (indParams.MACD && indParams.MACD.slow)   || 26;
        const signal = (indParams.MACD && indParams.MACD.signal) || 9;
        const m = computeMACD(closes, fast, slow, signal);
        const i = closes.length - 1;
        try {
          if (m.macd[i]   != null) macdLineRef.current.update({ time: point.time, value: m.macd[i] });
          if (m.signal[i] != null) macdSigRef.current .update({ time: point.time, value: m.signal[i] });
          if (m.hist[i]   != null) macdHistRef.current.update({
            time: point.time, value: m.hist[i],
            color: m.hist[i] >= 0 ? 'rgba(37,166,154,0.55)' : 'rgba(239,83,80,0.55)',
          });
        } catch (e) {}
      }
    });

    return () => { cancelled = true; try { off(); } catch (e) {} };
  }, [coin, interval, theme]);

  // Volume histogram pane (toggle via 'VOL' indicator).
  React.useEffect(() => {
    const chart = chartRef.current; if (!chart) return;
    const want = indicators.includes('VOL');
    if (want && !volumeRef.current) {
      const vol = chart.addSeries(window.LightweightCharts.HistogramSeries, {
        priceFormat: { type: 'volume' },
        priceScaleId: '',
        color: 'rgba(37,166,154,0.5)',
      });
      vol.priceScale().applyOptions({ scaleMargins: { top: 0.78, bottom: 0 } });
      volumeRef.current = vol;
      // Seed from existing candle buffer.
      if (dataRef.current.length) {
        vol.setData(dataRef.current.map(d => ({
          time: d.time, value: d.volume,
          color: d.close >= d.open ? 'rgba(37,166,154,0.5)' : 'rgba(239,83,80,0.5)',
        })));
      }
    } else if (!want && volumeRef.current) {
      try { chart.removeSeries(volumeRef.current); } catch (e) {}
      volumeRef.current = null;
    }
  }, [indicators.includes('VOL'), dataEpoch]);

  // MA overlay (toggle via 'MA' + indParams.MA.length).
  React.useEffect(() => {
    const chart = chartRef.current; if (!chart) return;
    const want = indicators.includes('MA');
    if (want && !maRef.current) {
      maRef.current = chart.addSeries(window.LightweightCharts.LineSeries, {
        color: '#5cdcc9', lineWidth: 1.5, priceLineVisible: false, lastValueVisible: false,
      });
    } else if (!want && maRef.current) {
      try { chart.removeSeries(maRef.current); } catch (e) {}
      maRef.current = null;
    }
    if (maRef.current && dataRef.current.length) {
      const len = (indParams.MA && indParams.MA.length) || 20;
      const sma = computeSMA(dataRef.current.map(d => d.close), len);
      maRef.current.setData(dataRef.current.map((d, i) => sma[i] != null ? { time: d.time, value: sma[i] } : null).filter(Boolean));
    }
  }, [indicators.includes('MA'), indParams.MA && indParams.MA.length, dataEpoch]);

  // EMA overlay (toggle via 'EMA' + indParams.EMA.length).
  React.useEffect(() => {
    const chart = chartRef.current; if (!chart) return;
    const want = indicators.includes('EMA');
    if (want && !emaRef.current) {
      emaRef.current = chart.addSeries(window.LightweightCharts.LineSeries, {
        color: '#fbbf24', lineWidth: 1.5, priceLineVisible: false, lastValueVisible: false,
      });
    } else if (!want && emaRef.current) {
      try { chart.removeSeries(emaRef.current); } catch (e) {}
      emaRef.current = null;
    }
    if (emaRef.current && dataRef.current.length) {
      const len = (indParams.EMA && indParams.EMA.length) || 21;
      const ema = computeEMA(dataRef.current.map(d => d.close), len);
      emaRef.current.setData(dataRef.current.map((d, i) => ema[i] != null ? { time: d.time, value: ema[i] } : null).filter(Boolean));
    }
  }, [indicators.includes('EMA'), indParams.EMA && indParams.EMA.length, dataEpoch]);

  // Bollinger Bands overlay (3 lines: mid + upper + lower).
  React.useEffect(() => {
    const chart = chartRef.current; if (!chart) return;
    const want = indicators.includes('BOLL');
    const ensure = (refKey, color, dashed) => {
      if (refKey.current) return;
      refKey.current = chart.addSeries(window.LightweightCharts.LineSeries, {
        color: color, lineWidth: 1, priceLineVisible: false, lastValueVisible: false,
        lineStyle: dashed ? 2 : 0,
      });
    };
    const drop = (refKey) => {
      if (refKey.current) { try { chart.removeSeries(refKey.current); } catch (e) {} refKey.current = null; }
    };
    if (want) {
      ensure(bollMidRef, 'rgba(168,168,168,0.65)', false);
      ensure(bollUpRef,  'rgba(120,180,255,0.65)', true);
      ensure(bollLoRef,  'rgba(120,180,255,0.65)', true);
      if (dataRef.current.length) {
        const len = (indParams.BOLL && indParams.BOLL.length) || 20;
        const m   = (indParams.BOLL && indParams.BOLL.stdDev) || 2;
        const { mid, upper, lower } = computeBoll(dataRef.current.map(d => d.close), len, m);
        const seq = (arr) => dataRef.current.map((d, i) => arr[i] != null ? { time: d.time, value: arr[i] } : null).filter(Boolean);
        bollMidRef.current.setData(seq(mid));
        bollUpRef.current .setData(seq(upper));
        bollLoRef.current .setData(seq(lower));
      }
    } else {
      drop(bollMidRef); drop(bollUpRef); drop(bollLoRef);
    }
  }, [indicators.includes('BOLL'), indParams.BOLL && indParams.BOLL.length, indParams.BOLL && indParams.BOLL.stdDev, dataEpoch]);

  // ── RSI sub-pane (own price scale 0–100, with 30 / 70 reference levels).
  React.useEffect(() => {
    const chart = chartRef.current; if (!chart) return;
    const LWC = window.LightweightCharts;
    const want = indicators.includes('RSI');
    if (want && !rsiRef.current) {
      const idx = chart.panes ? chart.panes().length : 1;
      rsiPaneRef.current = idx;
      rsiRef.current = chart.addSeries(LWC.LineSeries, {
        color: '#a78bfa', lineWidth: 1.5,
        priceLineVisible: false, lastValueVisible: true,
        priceFormat: { type: 'custom', formatter: (v) => v.toFixed(1) },
      }, idx);
      try {
        rsiRef.current.createPriceLine({ price: 70, color: 'rgba(239,83,80,0.5)',  lineWidth: 1, lineStyle: 2 });
        rsiRef.current.createPriceLine({ price: 30, color: 'rgba(37,166,154,0.5)', lineWidth: 1, lineStyle: 2 });
      } catch (e) {}
      try {
        const panes = chart.panes && chart.panes();
        if (panes && panes[idx] && panes[idx].setHeight) panes[idx].setHeight(110);
      } catch (e) {}
    } else if (!want && rsiRef.current) {
      try { chart.removeSeries(rsiRef.current); } catch (e) {}
      rsiRef.current = null;
      // pane index stays around (v5 allows empty panes); will be reused if
      // the user re-enables RSI.
    }
    if (rsiRef.current && dataRef.current.length) {
      const len = (indParams.RSI && indParams.RSI.length) || 14;
      const rsi = computeRSI(dataRef.current.map(d => d.close), len);
      rsiRef.current.setData(dataRef.current.map((d, i) => rsi[i] != null ? { time: d.time, value: rsi[i] } : null).filter(Boolean));
    }
  }, [indicators.includes('RSI'), indParams.RSI && indParams.RSI.length, dataEpoch]);

  // ── MACD sub-pane (line + signal + histogram, all on one pane).
  React.useEffect(() => {
    const chart = chartRef.current; if (!chart) return;
    const LWC = window.LightweightCharts;
    const want = indicators.includes('MACD');
    if (want && !macdLineRef.current) {
      const idx = chart.panes ? chart.panes().length : (rsiRef.current ? 2 : 1);
      macdPaneRef.current = idx;
      macdHistRef.current = chart.addSeries(LWC.HistogramSeries, {
        priceFormat: { type: 'price', precision: 4, minMove: 0.0001 },
        priceScaleId: 'macd',
      }, idx);
      macdLineRef.current = chart.addSeries(LWC.LineSeries, {
        color: '#3b82f6', lineWidth: 1.5,
        priceLineVisible: false, lastValueVisible: false,
        priceScaleId: 'macd',
      }, idx);
      macdSigRef.current  = chart.addSeries(LWC.LineSeries, {
        color: '#f59e0b', lineWidth: 1.5,
        priceLineVisible: false, lastValueVisible: false,
        priceScaleId: 'macd',
      }, idx);
      try {
        const panes = chart.panes && chart.panes();
        if (panes && panes[idx] && panes[idx].setHeight) panes[idx].setHeight(110);
      } catch (e) {}
    } else if (!want && macdLineRef.current) {
      try { chart.removeSeries(macdLineRef.current); } catch (e) {}
      try { chart.removeSeries(macdSigRef.current);  } catch (e) {}
      try { chart.removeSeries(macdHistRef.current); } catch (e) {}
      macdLineRef.current = macdSigRef.current = macdHistRef.current = null;
    }
    if (macdLineRef.current && dataRef.current.length) {
      const fast   = (indParams.MACD && indParams.MACD.fast)   || 12;
      const slow   = (indParams.MACD && indParams.MACD.slow)   || 26;
      const signal = (indParams.MACD && indParams.MACD.signal) || 9;
      const closes = dataRef.current.map(d => d.close);
      const m = computeMACD(closes, fast, slow, signal);
      const seq = (arr) => dataRef.current.map((d, i) => arr[i] != null ? { time: d.time, value: arr[i] } : null).filter(Boolean);
      macdLineRef.current.setData(seq(m.macd));
      macdSigRef.current .setData(seq(m.signal));
      macdHistRef.current.setData(dataRef.current.map((d, i) => m.hist[i] != null ? {
        time: d.time, value: m.hist[i],
        color: m.hist[i] >= 0 ? 'rgba(37,166,154,0.55)' : 'rgba(239,83,80,0.55)',
      } : null).filter(Boolean));
    }
  }, [
    indicators.includes('MACD'),
    indParams.MACD && indParams.MACD.fast,
    indParams.MACD && indParams.MACD.slow,
    indParams.MACD && indParams.MACD.signal,
    dataEpoch,
  ]);

  // Live mid → last candle close. The candle WS channel only pushes when the
  // current bar's OHLC actually shifts; the header price (driven by allMids /
  // activeAssetCtx ticks) updates every second. Without this hook the chart's
  // built-in last-price marker (dashed teal line) drifts noticeably behind.
  React.useEffect(() => {
    if (!candleRef.current) return;
    const sock = window.getHLSocket && window.getHLSocket();
    if (!sock) return;
    const off = sock.subscribe({ type: 'allMids' }, (data) => {
      const mids = (data && data.mids) || data;
      if (!mids || typeof mids !== 'object') return;
      const raw = mids[coin];
      if (raw == null) return;
      const v = parseFloat(raw);
      if (!isFinite(v)) return;
      const arr = dataRef.current;
      if (!arr.length || !candleRef.current) return;
      const last = arr[arr.length - 1];
      if (last.close === v) return;
      const merged = {
        time: last.time,
        open: last.open,
        high: Math.max(last.high, v),
        low:  Math.min(last.low,  v),
        close: v,
        volume: last.volume,
      };
      arr[arr.length - 1] = merged;
      try { candleRef.current.update(merged); } catch (e) {}
    });
    return () => { try { off(); } catch (e) {} };
  }, [coin]);

  // TP / SL / LIQ as series.createPriceLine — visible only within the
  // candle's visible price range. The custom PriceLineLabels overlay on
  // the left side renders a chip that's clamped to the chart edge with an
  // arrow when the line is off-range, so the user always sees where each
  // level sits relative to current price.
  React.useEffect(() => {
    const candle = candleRef.current; if (!candle) return;
    priceLinesRef.current.forEach(pl => { try { candle.removePriceLine(pl); } catch (e) {} });
    priceLinesRef.current = [];
    if (!position) return;
    const add = (price, color) => {
      if (price == null || !isFinite(price)) return;
      try {
        const pl = candle.createPriceLine({
          price: price, color: color, lineWidth: 1, lineStyle: 2,
          axisLabelVisible: false, // we draw our own label on the left
        });
        priceLinesRef.current.push(pl);
      } catch (e) {}
    };
    if (lines.tp)  add(position.tp,  '#22c55e');
    if (lines.sl)  add(position.sl,  '#ef4444');
    if (lines.liq) add(position.liq, isDark?'#fb923c':'#c2410c');
  }, [lines.tp, lines.sl, lines.liq, position && position.tp, position && position.sl, position && position.liq, theme]);

  return (
    <div style={{ position:'relative', width:'100%', height: height,
                  background: bg || (isDark ? '#0b0e11' : '#ffffff'),
                  overflow:'hidden' }}>
      <div ref={containerRef} style={{
        width:'100%', height:'100%',
        opacity: loading ? 0 : 1,
        transition: 'opacity 220ms ease-out',
      }}/>
      {!loading && ohlc && (() => {
        const up = ohlc.close >= ohlc.open;
        const upC = isDark ? 'rgb(37,166,154)' : 'rgb(37,166,154)';
        const dnC = '#ef5350';
        const c   = up ? upC : dnC;
        const fmt = (v) => v.toFixed(pricePrecision(v));
        const cellLabel = isDark ? 'rgba(234,236,239,0.55)' : 'rgba(23,24,31,0.55)';
        const cellValue = isDark ? '#eaecef' : '#17181f';
        const cell = (k, v, color) => (
          <span key={k} style={{
            display:'inline-flex', alignItems:'baseline', gap:3, marginRight:10, whiteSpace:'nowrap',
          }}>
            <span style={{ color: cellLabel, fontWeight:500 }}>{k}</span>
            <span style={{ color: color || cellValue, fontWeight:600, fontVariantNumeric:'tabular-nums' }}>{v}</span>
          </span>
        );
        return (
          <div style={{
            position:'absolute', top:6, left:8,
            fontFamily: 'JetBrains Mono, ui-monospace, monospace',
            fontSize: 10, letterSpacing:'0.02em',
            pointerEvents:'none', zIndex: 4,
            color: cellLabel,
            display:'flex', flexWrap:'wrap', alignItems:'baseline',
          }}>
            {cell('O', fmt(ohlc.open))}
            {cell('H', fmt(ohlc.high), upC)}
            {cell('L', fmt(ohlc.low),  dnC)}
            {cell('C', fmt(ohlc.close), c)}
          </div>
        );
      })()}
      <PriceLineLabels
        candleRef={candleRef}
        chartRef={chartRef}
        loaded={!loading}
        items={[
          { key:'tp',  show: lines.tp  && position && position.tp  != null && isFinite(position.tp),  price: position && position.tp,  color: '#22c55e', label: 'TP' },
          { key:'sl',  show: lines.sl  && position && position.sl  != null && isFinite(position.sl),  price: position && position.sl,  color: '#ef4444', label: 'SL' },
          { key:'liq', show: lines.liq && position && position.liq != null && isFinite(position.liq), price: position && position.liq, color: isDark?'#fb923c':'#c2410c', label: 'LIQ' },
        ]}
      />
      <div style={{
        position:'absolute', inset:0, pointerEvents:'none',
        display:'flex', alignItems:'center', justifyContent:'center',
        background: bg || (isDark ? '#0b0e11' : '#ffffff'),
        opacity: loading ? 1 : 0,
        transition: 'opacity 220ms ease-out',
      }}>
        <div style={{
          position:'absolute', left:0, right:0, top:'50%',
          borderTop: `1px dashed ${isDark?'rgba(234,236,239,0.10)':'rgba(23,24,31,0.10)'}`,
        }}/>
        <div style={{
          display:'inline-flex', alignItems:'center', gap:8,
          color: isDark?'rgba(234,236,239,0.55)':'rgba(23,24,31,0.55)',
          fontFamily:'JetBrains Mono, ui-monospace, monospace',
          fontSize:12, letterSpacing:'0.08em',
          padding:'8px 12px', borderRadius:999,
          background: isDark?'rgba(255,255,255,0.04)':'rgba(0,0,0,0.04)',
          backdropFilter:'blur(4px)',
        }}>
          <span style={{
            width:10, height:10, borderRadius:'50%',
            border:`2px solid ${isDark?'rgba(92,220,201,0.25)':'rgba(13,138,122,0.25)'}`,
            borderTopColor: isDark?'#5cdcc9':'#0d8a7a',
            animation: 'hl-spin 0.8s linear infinite',
          }}/>
          载入 K 线
        </div>
        <style>{`@keyframes hl-spin{to{transform:rotate(360deg)}}`}</style>
      </div>
    </div>
  );
}

// Legacy ATvChart wrapper kept so the call site stays simple.
function ATvChart({ symbol, coin='BTC', height=380, theme='dark', interval='60', studies=[], params={}, bg, position, lines }) {
  return (
    <LWChart
      coin={coin || (symbol||'').replace(/^.*:/,'').replace(/USDT.*$/,'') || 'BTC'}
      height={height} theme={theme} interval={interval}
      bg={bg} position={position} lines={lines}
      indicators={studies} indParams={params}
    />
  );
}
// Hyperliquid funding settles every hour at the top of the hour (UTC).
// Renders the live mm:ss countdown, ticking once per second.
function NextFundingCountdown() {
  const [now, setNow] = React.useState(() => Date.now());
  React.useEffect(() => {
    const id = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(id);
  }, []);
  const next = Math.ceil(now / 3600000) * 3600000;
  const ms = Math.max(0, next - now);
  const m = Math.floor(ms / 60000);
  const s = Math.floor((ms % 60000) / 1000);
  return <span>{`00:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`}</span>;
}

function ADetail({ t, sym='BTC', nav, onBack }) {
  const th = aTheme(t);
  const state = HL.useStore();
  const market = (state.markets || []).find(x=>x.sym===sym);
  const realPos = (state.positions || []).find(x=>x.sym===sym) || null;
  const hasPos = !!realPos;

  // Subscribe to the per-coin detailed ctx for as long as we're on this screen.
  React.useEffect(() => {
    if (!sym) return;
    HL.subscribeDetail(sym);
    return () => HL.unsubscribeDetail(sym);
  }, [sym]);

  // For the chart + price block we always render against a "view" object.
  // When the user has no position we synth one from market data so the upper
  // half (chart, price, funding) keeps working — the lower position card is
  // gated by hasPos and replaced with a "未持仓" placeholder.
  // Single source of truth for "live price" — the mid coming from allMids.
  // The chart's last-bar close is also driven by this stream, so the header
  // and the chart's right-axis chip stay in lockstep. Falls back to
  // activeAssetCtx markPx and market.price for the moment before allMids
  // arrives.
  const liveMid = state.mids && state.mids[sym] != null ? parseFloat(state.mids[sym]) : null;
  const ctxMark = state.assetCtx[sym] ? parseFloat(state.assetCtx[sym].markPx) : null;
  const livePrice = (liveMid != null && isFinite(liveMid)) ? liveMid
                  : (ctxMark != null && isFinite(ctxMark)) ? ctxMark
                  : (market ? market.price : 0);
  const p = realPos
    ? Object.assign({}, realPos, { mark: livePrice || realPos.mark })
    : (market
        ? { sym, side:'LONG', size:'—', entry: livePrice, mark: livePrice,
            pnl: 0, pnlPct: market.ch24, lev: 1, liq: null, value: 0, tp:null, sl:null }
        : { sym, side:'LONG', size:'—', entry:0, mark:0, pnl:0, pnlPct:0, lev:1, liq:null, value:0, tp:null, sl:null });
  // Chart UI state — persisted across reloads via localStorage.
  const _readJSON = (k, fb) => { try { const v = localStorage.getItem(k); return v ? JSON.parse(v) : fb; } catch (e) { return fb; } };
  const _writeJSON = (k, v) => { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {} };
  const [tf, setTf] = React.useState(() => _readJSON('hl.chart.tf.v1', '60'));
  const tfs = [['60','1h'],['240','4h'],['D','1D'],['W','1W'],['M','1M']];
  // RSI / MACD render as separate panes below the candles via LWC v5 multi-pane.
  const allInds = [['MA','MA'],['EMA','EMA'],['BOLL','BOLL'],['RSI','RSI'],['MACD','MACD'],['VOL','VOL']];
  const [inds, setInds] = React.useState(() => _readJSON('hl.chart.inds.v1', []));
  const [indOpen, setIndOpen] = React.useState(false);
  const [editing, setEditing] = React.useState(null); // which indicator's params are open
  const _DEFAULT_PARAMS = {
    MA:   { length: 20 },
    EMA:  { length: 21 },
    BOLL: { length: 20, stdDev: 2 },
    RSI:  { length: 14 },
    MACD: { fast: 12, slow: 26, signal: 9 },
    VOL:  {},
  };
  const [indParams, setIndParams] = React.useState(() => {
    const stored = _readJSON('hl.chart.indParams.v1', null) || {};
    return Object.assign({}, _DEFAULT_PARAMS, stored);
  });
  React.useEffect(() => _writeJSON('hl.chart.tf.v1', tf), [tf]);
  React.useEffect(() => _writeJSON('hl.chart.inds.v1', inds), [inds]);
  React.useEffect(() => _writeJSON('hl.chart.indParams.v1', indParams), [indParams]);
  const setParam = (k, field, v) => setIndParams(s => ({ ...s, [k]: { ...s[k], [field]: v } }));
  const paramSchema = {
    MA:   [['length','周期', 1, 200]],
    EMA:  [['length','周期', 1, 200]],
    BOLL: [['length','周期', 1, 200], ['stdDev','倍数', 1, 5]],
    RSI:  [['length','周期', 2, 100]],
    MACD: [['fast','快线', 1, 50], ['slow','慢线', 1, 100], ['signal','信号', 1, 50]],
    VOL:  [],
  };
  const fundingRate = state.funding[p.sym] || 0;
  const isDark = th.mode === 'dark';
  const pnlC = window.pnlColor(p.pnl, th.pnlMode);
  const [lines, setLines] = React.useState(() => _readJSON('hl.chart.lines.v1', { tp: true, sl: true, liq: true }));
  React.useEffect(() => _writeJSON('hl.chart.lines.v1', lines), [lines]);
  const [linesOpen, setLinesOpen] = React.useState(false);
  const linesActive = (lines.tp && p.tp!=null) || (lines.sl && p.sl!=null) || (lines.liq && p.liq!=null);
  const linesCount = (lines.tp && p.tp!=null?1:0) + (lines.sl && p.sl!=null?1:0) + (lines.liq && p.liq!=null?1:0);
  const allOn = lines.tp && lines.sl && lines.liq;
  const toggleLine = (k) => setLines(s => ({ ...s, [k]: !s[k] }));
  const toggleAll = () => {
    const next = !allOn;
    setLines({ tp: next, sl: next, liq: next });
  };
  const toggleInd = (k) => setInds(xs => xs.includes(k) ? xs.filter(x=>x!==k) : [...xs, k]);

  return (
    <>
      {/* Compact top bar */}
      <div style={{
        padding:'4px 16px 10px', display:'flex', alignItems:'center', justifyContent:'space-between',
        flexShrink: 0,
      }}>
        <button onClick={onBack} style={{
          background:'transparent', border:0, padding:6, color:th.ink, cursor:'pointer',
          display:'flex', alignItems:'center', gap:4, fontSize:14,
        }}>
          {Icon.back('currentColor', 18)}
        </button>
        <div style={{ display:'flex', alignItems:'center', gap:8 }}>
          <TokenGlyph sym={p.sym} size={22} radius={6}/>
          <div style={{ fontWeight:600, fontSize:15, letterSpacing:'-0.01em' }}>
            {p.sym}-USD
          </div>
          {hasPos && (
            <ATag tone={p.side==='LONG'?'long':'short'} t={t}>
              {p.side==='LONG'?'Long':'Short'} · {p.lev}×
            </ATag>
          )}
        </div>
        <div style={{ width: 30 }}/>
      </div>

      {/* Price block */}
      <div style={{ padding:'0 18px 10px', flexShrink: 0 }}>
        <div style={{ display:'flex', alignItems:'baseline', gap:10 }}>
          <span style={{
            fontFamily: th.numFont, fontSize: 30, fontWeight: 600, letterSpacing:'-0.02em',
            fontVariantNumeric:'tabular-nums',
          }}>
            <NumFlow value={p.mark} format={(v)=>window.fmt.num(v, p.mark<10?3:1)}/>
          </span>
          <span style={{ color: pnlC, fontFamily: th.numFont, fontSize: 14, fontWeight: 500 }}>
            {p.pnlPct>=0?'▲':'▼'} <NumFlow value={p.pnlPct} format={window.fmt.pct}/>
          </span>
        </div>
        <div style={{
          marginTop: 6, display:'flex', flexWrap:'wrap',
          columnGap: 14, rowGap: 4, fontSize: 11.5,
          fontFamily: th.numFont, letterSpacing:'0.04em',
        }}>
          <span style={{
            color: th.faint, whiteSpace:'nowrap',
            display:'inline-flex', alignItems:'baseline', gap:6,
          }}>
            资金费率
            <span style={{
              color: fundingRate >= 0 ? (isDark?'#7ee2a8':'#15803d') : (isDark?'#fca5a5':'#b91c1c'),
              fontWeight: 600,
            }}><NumFlow value={fundingRate*100} format={(v)=>(v>=0?'+':'')+v.toFixed(4)+'%'}/></span>
            <span style={{ color: th.dim, fontVariantNumeric:'tabular-nums' }}>
              <NextFundingCountdown/>
            </span>
          </span>
          {hasPos && (() => {
            // Estimated funding paid (-) / received (+) at the next settlement
            // = positionNotional × hourlyFundingRate, signed by side.
            // HL convention: long pays funding when rate>0, short receives.
            const sideMul = p.side === 'LONG' ? 1 : -1;
            const settle = -p.value * fundingRate * sideMul;
            const positive = settle >= 0;
            const c = positive ? (isDark?'#7ee2a8':'#15803d') : (isDark?'#fca5a5':'#b91c1c');
            return (
              <span style={{ color: th.faint, whiteSpace:'nowrap' }}>
                下次结算 <span style={{ color: c, marginLeft: 4, fontWeight: 600 }}>
                  <NumFlow value={settle} format={(v)=>(v>=0?'+':'−')+window.fmt.usd(Math.abs(v))}/>
                </span>
              </span>
            );
          })()}
          {market && (
            <span style={{ color: th.faint, whiteSpace:'nowrap' }}>
              24h量 <span style={{ color: th.dim, marginLeft: 4 }}>${(market.vol24/1e6).toFixed(1)}M</span>
            </span>
          )}
          {market && (
            <span style={{ color: th.faint, whiteSpace:'nowrap' }}>
              24h涨跌 <span style={{ color: window.pnlColor(market.ch24, th.pnlMode), marginLeft:4, fontWeight:600 }}>
                {window.fmt.pct(market.ch24)}
              </span>
            </span>
          )}
        </div>
      </div>

      {/* TF tabs + indicator dropdown trigger */}
      <div style={{ padding:'0 0 4px', flexShrink: 0 }}>
        <div style={{
          display:'flex', gap:2, padding:'0 16px 8px', alignItems:'center',
        }}>
          {tfs.map(([k, lbl])=>(
            <button key={k} onClick={()=>setTf(k)} style={{
              background:'transparent',
              color: tf===k ? th.ink : th.faint,
              border:0, padding:'6px 10px', fontSize:13, fontWeight: tf===k?600:500, cursor:'pointer',
              fontFamily: th.numFont, letterSpacing:'0.04em',
              borderBottom: `2px solid ${tf===k ? th.accent : 'transparent'}`,
              transition: 'all 0.15s',
            }}>{lbl}</button>
          ))}
          <div style={{ flex:1 }}/>
          <button onClick={()=>{ setLinesOpen(false); setIndOpen(o=>!o); }} style={{
            background:'transparent', border:0, color: inds.length>0 ? th.accent : th.dim,
            cursor:'pointer', fontSize:12, fontWeight:600, fontFamily: th.numFont,
            letterSpacing:'0.04em', padding:'6px 4px', display:'inline-flex', alignItems:'center', gap:4,
          }}>
            ƒ 指标{inds.length>0?` · ${inds.length}`:''}
          </button>
          <button onClick={()=>{ setIndOpen(false); setLinesOpen(o=>!o); }} title="标线设置" style={{
            background:'transparent', border:0, color: linesActive ? th.accent : th.dim,
            cursor:'pointer', fontSize:12, fontWeight:600, fontFamily: th.numFont,
            letterSpacing:'0.04em', padding:'6px 8px 6px 4px', display:'inline-flex', alignItems:'center', gap:4,
          }}>
            <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
              <path d="M2 4h12" strokeDasharray="2 2"/><path d="M2 8h12"/><path d="M2 12h12" strokeDasharray="2 2"/>
            </svg>
            标线{linesActive?` · ${linesCount}`:''}
          </button>
        </div>

        {/* Indicator chip row + per-indicator params */}
        {indOpen && (
          <div style={{ padding:'2px 16px 10px' }}>
            <div style={{ display:'flex', flexWrap:'wrap', gap: 6 }}>
              {allInds.map(([k, lbl])=>{
                const on = inds.includes(k);
                const hasParams = (paramSchema[k]||[]).length > 0;
                return (
                  <span key={k} style={{ display:'inline-flex', alignItems:'stretch' }}>
                    <button onClick={()=>toggleInd(k)} style={{
                      background: on ? (isDark?'rgba(80,210,193,0.14)':'rgba(13,138,122,0.10)') : 'transparent',
                      color: on ? th.accent : th.dim,
                      border: `1px solid ${on ? th.accent : th.hair}`,
                      borderRight: hasParams && on ? 'none' : `1px solid ${on ? th.accent : th.hair}`,
                      borderRadius: hasParams && on ? '14px 0 0 14px' : 14,
                      padding:'4px 11px', fontSize: 12, fontWeight: 600,
                      fontFamily: th.numFont, letterSpacing:'0.04em', cursor:'pointer',
                    }}>{lbl}</button>
                    {hasParams && on && (
                      <button onClick={()=>setEditing(e=>e===k?null:k)} title="参数" style={{
                        background: editing===k ? (isDark?'rgba(80,210,193,0.22)':'rgba(13,138,122,0.16)') : (isDark?'rgba(80,210,193,0.14)':'rgba(13,138,122,0.10)'),
                        color: th.accent,
                        border: `1px solid ${th.accent}`, borderLeft:'none',
                        borderRadius:'0 14px 14px 0', padding:'4px 8px', fontSize: 12, cursor:'pointer',
                        fontFamily: th.numFont,
                      }}>⚙</button>
                    )}
                  </span>
                );
              })}
            </div>
            {editing && inds.includes(editing) && paramSchema[editing].length > 0 && (
              <div style={{
                marginTop: 8, padding: '8px 10px',
                background: isDark?'rgba(255,255,255,0.03)':'rgba(0,0,0,0.03)',
                border: `1px solid ${th.hair}`, borderRadius: 8,
                display:'flex', flexWrap:'wrap', gap: 12, alignItems:'center',
              }}>
                <span style={{ fontSize: 11.5, color: th.dim, letterSpacing:'0.14em' }}>{editing} 参数</span>
                {paramSchema[editing].map(([field, lbl, mn, mx])=>{
                  const v = indParams[editing][field];
                  return (
                    <span key={field} style={{ display:'inline-flex', alignItems:'center', gap: 6 }}>
                      <span style={{ fontSize: 11.5, color: th.faint, fontFamily: th.numFont }}>{lbl}</span>
                      <button onClick={()=>setParam(editing, field, Math.max(mn, v-1))} style={{
                        width: 22, height: 22, background:'transparent', border:`1px solid ${th.hair}`,
                        color: th.dim, borderRadius: 4, cursor:'pointer', fontSize: 13, lineHeight: 1,
                      }}>−</button>
                      <input type="number" min={mn} max={mx} value={v}
                        onChange={(e)=>{
                          const n = parseInt(e.target.value, 10);
                          if (!isNaN(n)) setParam(editing, field, Math.max(mn, Math.min(mx, n)));
                        }}
                        style={{
                          width: 44, height: 22, textAlign:'center',
                          background:'transparent', border:`1px solid ${th.hair}`, color: th.ink,
                          fontFamily: th.numFont, fontSize: 12.5, borderRadius: 4,
                        }}/>
                      <button onClick={()=>setParam(editing, field, Math.min(mx, v+1))} style={{
                        width: 22, height: 22, background:'transparent', border:`1px solid ${th.hair}`,
                        color: th.dim, borderRadius: 4, cursor:'pointer', fontSize: 13, lineHeight: 1,
                      }}>+</button>
                    </span>
                  );
                })}
              </div>
            )}
          </div>
        )}

        {/* Lines settings popover (anchored above chart) */}
        {linesOpen && (
          <div style={{ padding:'2px 16px 10px' }}>
            <div style={{
              border:`1px solid ${th.hair}`, borderRadius: 10,
              background: isDark?'rgba(255,255,255,0.025)':'rgba(0,0,0,0.025)',
              overflow:'hidden',
            }}>
              <div style={{
                display:'flex', alignItems:'center', justifyContent:'space-between',
                padding:'8px 12px', borderBottom:`1px solid ${th.hair}`,
              }}>
                <span style={{ fontSize:11.5, color:th.dim, letterSpacing:'0.16em', fontWeight:600 }}>标线显示设置</span>
                <button onClick={toggleAll} style={{
                  background:'transparent', border:`1px solid ${th.hair}`, color: th.dim,
                  padding:'2px 8px', borderRadius: 10, fontSize: 11.5, fontWeight: 600,
                  fontFamily: th.numFont, cursor:'pointer', letterSpacing:'0.04em',
                }}>{allOn ? '全部关闭' : '全部打开'}</button>
              </div>
              {[
                ['tp',  '止盈 TP',  p.tp,  '#22c55e'],
                ['sl',  '止损 SL',  p.sl,  '#ef4444'],
                ['liq', '强平 LIQ', p.liq, isDark?'#fb923c':'#c2410c'],
              ].map(([k, lbl, price, color], i, arr)=>{
                const has = price != null;
                const on = lines[k] && has;
                const pct = has ? (k==='tp'
                  ? (p.side==='LONG'?'+':'−') + window.fmt.num(Math.abs((price-p.entry)/p.entry*100), 1) + '%'
                  : window.fmt.num((price-p.entry)/p.entry*100, 1) + '%') : '—';
                return (
                  <div key={k} onClick={()=>has && toggleLine(k)} style={{
                    display:'flex', alignItems:'center', gap: 12,
                    padding:'10px 12px',
                    borderBottom: i<arr.length-1 ? `1px solid ${th.hair}` : 'none',
                    cursor: has?'pointer':'default', opacity: has?1:0.5,
                  }}>
                    {/* checkbox */}
                    <span style={{
                      width: 18, height: 18, borderRadius: 5, flexShrink:0,
                      border: `1.5px solid ${on ? color : th.hair}`,
                      background: on ? color : 'transparent',
                      display:'inline-flex', alignItems:'center', justifyContent:'center',
                    }}>
                      {on && (
                        <svg width="11" height="11" viewBox="0 0 12 12" fill="none">
                          <path d="M2.5 6.2 L5 8.5 L9.5 3.5" stroke={isDark?'#04100d':'#fafaf7'} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
                        </svg>
                      )}
                    </span>
                    {/* color line indicator */}
                    <span style={{
                      width: 22, height: 0, borderTop:`1.5px dashed ${color}`, flexShrink:0,
                    }}/>
                    {/* label */}
                    <span style={{ fontSize: 13.5, color: th.ink, fontWeight: 500, flexShrink:0 }}>{lbl}</span>
                    <div style={{ flex:1 }}/>
                    {/* price + pct */}
                    <span style={{ fontSize: 12.5, color: has?th.dim:th.faint, fontFamily: th.numFont }}>
                      {has ? window.fmt.num(price, p.mark<10?3:1) : '未设置'}
                    </span>
                    {has && (
                      <span style={{ fontSize: 11.5, color: th.faint, fontFamily: th.numFont, minWidth: 48, textAlign:'right' }}>
                        {pct}
                      </span>
                    )}
                  </div>
                );
              })}
            </div>
          </div>
        )}

        {/* TradingView Widget — embed doesn't expose its price scale, so we
            don't draw TP/SL/LIQ lines on top (would be misaligned). The 标线
            popover above lists the precise prices instead. */}
        <div style={{
          borderTop: `1px solid ${th.hair}`, borderBottom: `1px solid ${th.hair}`,
          background: th.bg, position:'relative',
        }}>
          <ATvChart
            coin={p.sym}
            height={350}
            theme={isDark?'dark':'light'}
            interval={tf}
            studies={inds} params={indParams}
            bg={th.bg}
            position={hasPos ? p : null}
            lines={lines}
          />
        </div>
      </div>

      {/* Position info OR no-position placeholder */}
      {!hasPos ? (
        <div style={{ flex:1, overflow:'auto', padding:'14px 18px 8px' }}>
          {market && (
            <div style={{ marginTop:14, display:'grid', gridTemplateColumns:'1fr 1fr', rowGap:7, columnGap:24 }}>
              {[
                ['标记价格', window.fmt.num(p.mark, p.mark<10?3:1)],
                ['24h成交',  '$' + (market.vol24/1e9).toFixed(2) + 'B'],
                ['持仓量(OI)','$' + ((market.oi||0)/1e6).toFixed(1) + 'M'],
                ['资金费率',  window.fmt.pct(fundingRate*100)],
              ].map(([k,v])=>(
                <div key={k} style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline' }}>
                  <span style={{ fontSize:12.5, color:th.dim }}>{k}</span>
                  <span style={{ fontFamily:th.numFont, fontSize:13, color:th.ink }}>{v}</span>
                </div>
              ))}
            </div>
          )}
        </div>
      ) : (
      <div style={{ flex:1, overflow:'auto', padding:'12px 18px 8px' }}>
        {(() => {
          // 资金费 使用与盈亏 / 回报率 完全一致的 pnlColor 调色板
          const fundingColor = p.funding == null
            ? th.ink
            : window.pnlColor(p.funding, th.pnlMode);
          const cell = (label, valueNode, color) => (
            <div style={{ textAlign:'center' }}>
              <div style={{
                fontSize:11.5, color:th.faint, letterSpacing:'0.12em',
                marginBottom: 4, fontWeight:500,
              }}>{label}</div>
              <div style={{
                fontFamily:th.numFont, fontSize:17, fontWeight:600,
                color: color, fontVariantNumeric:'tabular-nums', lineHeight:1.1,
                whiteSpace:'nowrap',
              }}>{valueNode}</div>
            </div>
          );
          return (
            <div style={{
              display:'grid', gridTemplateColumns:'repeat(3, 1fr)', columnGap: 12,
              paddingBottom: 12, borderBottom: `1px solid ${th.hair}`, marginBottom: 12,
            }}>
              {cell('未实现盈亏',
                <NumFlow value={p.pnl} format={(v)=>(v>=0?'+':'−')+window.fmt.usd(Math.abs(v))}/>,
                pnlC)}
              {cell('回报率',
                <NumFlow value={p.pnlPct * p.lev} format={window.fmt.pct}/>,
                pnlC)}
              {cell('资金费',
                p.funding != null
                  ? <NumFlow value={p.funding} format={(v)=>(v>=0?'+':'−')+window.fmt.usd(Math.abs(v))}/>
                  : <span style={{ color:th.faint }}>—</span>,
                fundingColor)}
            </div>
          );
        })()}

        <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', rowGap: 7, columnGap: 24 }}>
          {[
            { k:'持仓价值', v:<NumFlow.usd value={p.value} d={0}/> },
            { k:'持仓量',   v:`${p.size} ${p.sym}` },
            { k:'持仓均价', v:<NumFlow value={p.entry} format={(v)=>window.fmt.num(v, p.entry<10?3:1)}/> },
            { k:'标记价格', v:<NumFlow value={p.mark}  format={(v)=>window.fmt.num(v, p.mark<10?3:1)}/> },
            { k:'保证金',   v:<NumFlow.usd value={p.value/p.lev} d={0}/> },
            { k:'预估强平', v: p.liq != null
                ? <NumFlow value={p.liq} format={(v)=>window.fmt.num(v, p.liq<10?3:1)}/>
                : '—', kind:'liq' },
          ].map(({k, v, kind})=>(
            <div key={k} style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline' }}>
              <span style={{ fontSize:12.5, color:th.dim }}>{k}</span>
              <span style={{
                fontFamily:th.numFont, fontSize:13, fontVariantNumeric:'tabular-nums',
                color: kind==='liq' ? (isDark?'#fca5a5':'#b91c1c') : th.ink,
              }}>{v}</span>
            </div>
          ))}
        </div>

        {p.liq != null && (
          <div style={{ marginTop: 12, paddingTop: 10, borderTop: `1px solid ${th.hair}` }}>
            {(() => {
              const distPct = ((p.mark - p.liq) / p.mark) * 100 * (p.side === 'LONG' ? 1 : -1);
              const safePct = Math.max(0, Math.min(100, distPct));
              const tone = safePct > 30 ? '安全' : safePct > 10 ? '关注' : '危险';
              return (
                <>
                  <div style={{ display:'flex', justifyContent:'space-between', fontSize:11.5, color:th.faint, marginBottom: 6, fontFamily:th.numFont, letterSpacing:'0.04em' }}>
                    <span>距强平 <NumFlow value={Math.abs(distPct)} format={(v)=>v.toFixed(1)+'%'}/></span>
                    <span>风险 · {tone}</span>
                  </div>
                  <div style={{ position:'relative', height: 4, borderRadius: 2, background: isDark?'rgba(255,255,255,0.06)':'rgba(0,0,0,0.06)', overflow:'hidden' }}>
                    <div style={{
                      position:'absolute', left: 0, width: `${100-safePct}%`, top: 0, bottom: 0,
                      background: `linear-gradient(90deg, ${isDark?'#ef4444':'#dc2626'}, ${th.accent})`,
                      transition: 'width 350ms ease-out',
                    }}/>
                    <div style={{ position:'absolute', left:`${100-safePct}%`, top:-2, bottom:-2, width:2, background: th.accent, transition:'left 350ms ease-out' }}/>
                  </div>
                </>
              );
            })()}
          </div>
        )}
      </div>

      )}

      <ATabBar active={hasPos ? 'home' : 'markets'} onNav={nav || onBack} t={t}/>
    </>
  );
}

window.ATvChart = ATvChart;
window.ADetail = ADetail;
