// ============================================================================
// SymbolPriceChartLW — TradingView-like interactive candlestick chart
// Uses LightweightCharts (TradingView's open-source library) for pan/zoom/crosshair
// ============================================================================

const SymbolPriceChartLW = ({
  symbol,
  bars = [],
  loading = false,
  loadError = false,
  openPositions = [],
  closedTrades = [],
  pendingOrders = [],
  timeframe = "5m",
  height = 420,
  focusTicket = null,
  hasMoreHistory = false,
  loadingHistory = false,
  onLoadMoreHistory,
  viewOptions = null,
  onHoverTicketChange,
  onTicketClick,
}) => {
  const containerRef = React.useRef(null);
  const chartRef = React.useRef(null);
  const candleSeriesRef = React.useRef(null);
  const markerSeriesRef = React.useRef(null);
  const [hovered, setHovered] = React.useState(null);
  const hasBars = Boolean(bars?.length);
  const showEntry = viewOptions?.entry !== false;
  const showSl = viewOptions?.sl !== false;
  const showTp = viewOptions?.tp !== false;
  const showPending = viewOptions?.pending !== false;
  const showFilled = viewOptions?.filled !== false;
  const showClosed = viewOptions?.closed !== false;
  const viewKeyRef = React.useRef("");
  const rangeStoreRef = React.useRef(window.V3OPS_SYMBOL_CHART_RANGES || (window.V3OPS_SYMBOL_CHART_RANGES = {}));
  const dataBoundsRef = React.useRef(null);
  const loadMoreHistoryRef = React.useRef(null);
  const historyStateRef = React.useRef({ hasMoreHistory: false, loadingHistory: false });
  const historyInteractionRef = React.useRef({ key: "", expiresAt: 0 });
  viewKeyRef.current = `${symbol || ""}|${timeframe || "5m"}`;
  const RIGHT_GUTTER_RATIO = 0.30;
  const MIN_RIGHT_GUTTER_BARS = 8;
  const HISTORY_INTERACTION_WINDOW_MS = 3500;
  const rightGutterBarsFor = (visibleDataBars) => {
    const safeBars = Math.max(1, Number(visibleDataBars) || 1);
    return Math.max(
      MIN_RIGHT_GUTTER_BARS,
      Math.ceil((safeBars * RIGHT_GUTTER_RATIO) / (1 - RIGHT_GUTTER_RATIO))
    );
  };
  const logicalRangeWithRightGutter = (range, totalBars) => {
    if (!range || !Number.isFinite(range.from) || !Number.isFinite(range.to) || !totalBars) return range;
    const dataEnd = totalBars - 1;
    if (range.to < dataEnd - 1) return range;
    const currentGutterBars = Math.max(0, range.to - dataEnd);
    const visibleDataBars = Math.max(1, Math.min(totalBars, dataEnd - range.from + 1));
    const targetGutterBars = rightGutterBarsFor(visibleDataBars);
    if (currentGutterBars >= targetGutterBars) return range;
    return { from: range.from, to: dataEnd + targetGutterBars };
  };
  const fitContentWithRightGutter = (timeScale, totalBars) => {
    timeScale.fitContent();
    if (!totalBars) return;
    timeScale.setVisibleLogicalRange({
      from: 0,
      to: totalBars - 1 + rightGutterBarsFor(totalBars),
    });
  };

  React.useEffect(() => {
    loadMoreHistoryRef.current = onLoadMoreHistory;
    historyStateRef.current = { hasMoreHistory, loadingHistory };
  }, [hasMoreHistory, loadingHistory, onLoadMoreHistory]);

  // Init chart once
  React.useEffect(() => {
    if (!hasBars || !containerRef.current || !window.LightweightCharts) return;
    const chart = LightweightCharts.createChart(containerRef.current, {
      width: containerRef.current.clientWidth,
      height: height,
      layout: {
        background: { type: "solid", color: "transparent" },
        textColor: "#9ba3b4",
        fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
      },
      grid: {
        vertLines: { color: "rgba(255,255,255,0.04)" },
        horzLines: { color: "rgba(255,255,255,0.04)" },
      },
      timeScale: {
        timeVisible: true,
        secondsVisible: false,
        borderColor: "rgba(255,255,255,0.10)",
      },
      rightPriceScale: { borderColor: "rgba(255,255,255,0.10)" },
      crosshair: {
        mode: 1, // Normal mode
        vertLine: { color: "rgba(120, 200, 255, 0.5)", width: 1, style: 2 },
        horzLine: { color: "rgba(120, 200, 255, 0.5)", width: 1, style: 2 },
      },
    });
    chartRef.current = chart;
    const candleSeries = chart.addCandlestickSeries({
      upColor: "#26a69a",
      downColor: "#ef5350",
      borderUpColor: "#26a69a",
      borderDownColor: "#ef5350",
      wickUpColor: "#26a69a",
      wickDownColor: "#ef5350",
    });
    candleSeriesRef.current = candleSeries;

    chart.subscribeCrosshairMove((param) => {
      if (!param.time || !param.point) {
        setHovered(null);
        return;
      }
      const data = param.seriesData?.get(candleSeries);
      if (data) {
        setHovered({
          ts: param.time,
          o: data.open, h: data.high, l: data.low, c: data.close,
          x: param.point.x, y: param.point.y,
        });
      }
    });

    const markUserChartInteraction = () => {
      historyInteractionRef.current = {
        key: viewKeyRef.current,
        expiresAt: Date.now() + HISTORY_INTERACTION_WINDOW_MS,
      };
    };
    const canLoadHistoryFromUserInteraction = () => {
      const interaction = historyInteractionRef.current || {};
      return (
        interaction.key === viewKeyRef.current
        && Number(interaction.expiresAt || 0) >= Date.now()
      );
    };
    const consumeHistoryInteraction = () => {
      historyInteractionRef.current = { key: viewKeyRef.current, expiresAt: 0 };
    };

    const onVisibleLogicalRangeChange = (range) => {
      const key = viewKeyRef.current;
      if (!key || !range) return;
      let visibleRange = null;
      try { visibleRange = chart.timeScale().getVisibleRange(); } catch (_) {}
      rangeStoreRef.current[key] = {
        logicalRange: range,
        visibleRange: visibleRange || rangeStoreRef.current[key]?.visibleRange || null,
      };
      const historyState = historyStateRef.current || {};
      if (
        range.from <= 12
        && canLoadHistoryFromUserInteraction()
        && historyState.hasMoreHistory
        && !historyState.loadingHistory
      ) {
        consumeHistoryInteraction();
        try { loadMoreHistoryRef.current?.(); } catch (_) {}
      }
    };
    try {
      chart.timeScale().subscribeVisibleLogicalRangeChange(onVisibleLogicalRangeChange);
    } catch (_) {}
    const interactionEvents = ["wheel", "pointerdown", "touchstart", "mousedown"];
    const interactionTarget = containerRef.current;
    interactionEvents.forEach((eventName) => {
      try { interactionTarget?.addEventListener(eventName, markUserChartInteraction, { passive: true }); } catch (_) {}
    });

    const ro = new ResizeObserver(() => {
      if (chartRef.current && containerRef.current) {
        chartRef.current.applyOptions({
          width: containerRef.current.clientWidth,
          height: height,
        });
      }
    });
    ro.observe(containerRef.current);

    return () => {
      ro.disconnect();
      interactionEvents.forEach((eventName) => {
        try { interactionTarget?.removeEventListener(eventName, markUserChartInteraction); } catch (_) {}
      });
      try { chart.timeScale().unsubscribeVisibleLogicalRangeChange(onVisibleLogicalRangeChange); } catch (_) {}
      chart.remove();
      chartRef.current = null;
      candleSeriesRef.current = null;
    };
  }, [height, hasBars]);

  // Update bars — preserve user's pan/zoom view across polling updates
  const prevViewKeyRef = React.useRef(viewKeyRef.current);
  React.useEffect(() => {
    if (!candleSeriesRef.current || !chartRef.current || !bars?.length) return;
    const chart = chartRef.current;
    const timeScale = chart.timeScale();
    const viewKey = viewKeyRef.current;
    const data = bars
      .filter((b) => b && Number.isFinite(Number(b.close)))
      .map((b) => ({
        time: Math.floor(new Date(b.ts).getTime() / 1000),
        open: Number(b.open) || Number(b.close),
        high: Number(b.high) || Number(b.close),
        low: Number(b.low) || Number(b.close),
        close: Number(b.close),
      }))
      .sort((a, b) => a.time - b.time);

    const captureRange = () => {
      let logicalRange = null;
      try { logicalRange = timeScale.getVisibleLogicalRange(); } catch (_) {}
      let visibleRange = null;
      try { visibleRange = timeScale.getVisibleRange(); } catch (_) {}
      return visibleRange || logicalRange ? { visibleRange, logicalRange } : null;
    };
    const restoreRange = (range, preferVisibleRange = false) => {
      if (!range) return;
      const apply = () => {
        if (!chartRef.current) return;
        const ts = chartRef.current.timeScale();
        if (preferVisibleRange && range.visibleRange) {
          try {
            ts.setVisibleRange(range.visibleRange);
            return;
          } catch (_) {}
        }
        if (range.logicalRange) {
          try {
            ts.setVisibleLogicalRange(logicalRangeWithRightGutter(range.logicalRange, data.length));
            return;
          } catch (_) {}
        }
        if (range.visibleRange) {
          try {
            ts.setVisibleRange(range.visibleRange);
            const logicalRange = ts.getVisibleLogicalRange();
            if (logicalRange) {
              ts.setVisibleLogicalRange(logicalRangeWithRightGutter(logicalRange, data.length));
            }
          } catch (_) {}
        }
      };
      if (window.requestAnimationFrame) {
        window.requestAnimationFrame(apply);
      } else {
        window.setTimeout(apply, 0);
      }
    };

    // Save user's current visible range BEFORE setData; setData can snap to the right edge.
    const viewChanged = prevViewKeyRef.current !== viewKey;
    let savedRange = null;
    if (!viewChanged) {
      savedRange = captureRange() || rangeStoreRef.current[viewKey] || null;
    }
    const nextBounds = data.length
      ? { first: data[0].time, last: data[data.length - 1].time, length: data.length }
      : null;
    const previousBounds = dataBoundsRef.current;
    const prependedHistory = Boolean(
      !viewChanged
      && previousBounds
      && nextBounds
      && nextBounds.first < previousBounds.first
      && nextBounds.last >= previousBounds.last
    );

    candleSeriesRef.current.setData(data);
    dataBoundsRef.current = nextBounds;

    if (viewChanged) {
      // Reset auto-scale + fit only when changing symbol/timeframe.
      // fitContent() snaps to data range, so we re-anchor the logical range to
      // keep about 30% empty space to the right of the latest candle.
      try {
        chart.priceScale("right").applyOptions({ autoScale: true });
        fitContentWithRightGutter(timeScale, data.length);
      } catch (_) {}
      prevViewKeyRef.current = viewKey;
    } else if (savedRange) {
      // Restore user's view (pan/zoom) on the next frame so polling refresh doesn't snap back.
      restoreRange(savedRange, prependedHistory);
    } else {
      // No saved view but data just arrived — keep the right-edge gap visible.
      try {
        fitContentWithRightGutter(timeScale, data.length);
      } catch (_) {}
    }
  }, [bars, symbol, timeframe]);

  // Update trade/order markers + price lines.
  // For each open position, draw:
  //   - Entry / SL / TP price lines (with axis labels)
  //   - Markers for open entries
  // Pending orders get only an order-price line; closed trades get entry/exit markers.
  const priceLinesRef = React.useRef([]);
  const zoneSeriesRef = React.useRef([]);  // tracks AreaSeries for TP/SL zones

  React.useEffect(() => {
    if (!candleSeriesRef.current || !chartRef.current) return;
    const series = candleSeriesRef.current;
    const chart = chartRef.current;

    // Remove old price lines + zone series before adding new ones
    priceLinesRef.current.forEach((pl) => {
      try { series.removePriceLine(pl); } catch (_) {}
    });
    priceLinesRef.current = [];
    zoneSeriesRef.current.forEach((s) => {
      try { chart.removeSeries(s); } catch (_) {}
    });
    zoneSeriesRef.current = [];

    // TradeBox (TP/SL shaded zones) disabled per UX request — keep only price lines
    const drawTradeBox = (_p, _tsOpen, _tsClose, _pnl, _isOpen) => { /* zones disabled */ };

    const markers = [];
    const focusKey = focusTicket != null ? String(focusTicket) : null;
    const toTime = (value) => {
      const ms = value ? new Date(value).getTime() : NaN;
      return Number.isFinite(ms) ? Math.floor(ms / 1000) : null;
    };
    const ticketTail = (ticket) => String(ticket ?? "").slice(-4) || "----";
    const positivePrice = (value) => {
      const price = Number(value);
      return Number.isFinite(price) && price > 0 ? price : null;
    };
    const pendingTypeLabel = (type) => ({
      buy_limit: "BUY LIMIT",
      sell_limit: "SELL LIMIT",
      buy_stop: "BUY STOP",
      sell_stop: "SELL STOP",
      buy_stop_limit: "BUY STOP LIMIT",
      sell_stop_limit: "SELL STOP LIMIT",
    }[String(type || "").toLowerCase()] || String(type || "PENDING").toUpperCase());

    (openPositions || []).forEach((p) => {
      const ts = toTime(p.openedAt);
      if (!ts) return;
      const isFocused = focusKey != null && String(p.ticket) === focusKey;
      const isOtherFocused = focusKey != null && !isFocused;

      // Show TP/SL zones + price lines ONLY for focused ticket (declutter)
      // OR show all if no focus selected (for overview)
      const showZone = focusKey == null || isFocused;
      if (showZone) {
        drawTradeBox(p, ts, null, 0, true);
      }

      const dirColor = p.direction === "short" ? "#ef5350" : "#26a69a";
      if (showEntry) {
        markers.push({
          time: ts,
          position: p.direction === "short" ? "aboveBar" : "belowBar",
          color: isOtherFocused ? "rgba(120,120,120,0.5)" : dirColor,
          shape: p.direction === "short" ? "arrowDown" : "arrowUp",
          text: `${p.direction === "short" ? "SHORT" : "LONG"} ${Number(p.lots || 0).toFixed(2)} #${ticketTail(p.ticket)}`,
          size: isFocused ? 1.7 : 1.2,
          id: `open-${p.ticket}`,
        });
      }

      if (showZone) {
        if (showEntry && Number.isFinite(Number(p.entry)) && Number(p.entry) > 0) {
          priceLinesRef.current.push(series.createPriceLine({
            price: Number(p.entry),
            color: dirColor,
            lineWidth: 1,
            lineStyle: 2, // dashed
            axisLabelVisible: true,
            title: `#${ticketTail(p.ticket)} entry`,
          }));
        }
        if (showSl && Number.isFinite(Number(p.sl)) && Number(p.sl) > 0) {
          priceLinesRef.current.push(series.createPriceLine({
            price: Number(p.sl),
            color: "#ff5252",
            lineWidth: isFocused ? 2 : 1,
            lineStyle: 1,
            axisLabelVisible: true,
            title: `SL #${ticketTail(p.ticket)}`,
          }));
        }
        if (showTp && Number.isFinite(Number(p.tp)) && Number(p.tp) > 0) {
          priceLinesRef.current.push(series.createPriceLine({
            price: Number(p.tp),
            color: "#69f0ae",
            lineWidth: isFocused ? 2 : 1,
            lineStyle: 1,
            axisLabelVisible: true,
            title: `TP #${ticketTail(p.ticket)}`,
          }));
        }
      }
    });

    if (showPending) (pendingOrders || []).forEach((o) => {
      const orderPrice = positivePrice(o.price);
      const direction = String(o.direction || "").toLowerCase();
      const isShort = direction === "short" || String(o.type || "").toLowerCase().startsWith("sell");
      const isFocused = focusKey != null && String(o.ticket) === focusKey;
      const isOtherFocused = focusKey != null && !isFocused;
      const color = isShort ? "#ffb86c" : "#f6c85f";
      const dimmedColor = isShort ? "rgba(255,184,108,0.35)" : "rgba(246,200,95,0.35)";
      const label = pendingTypeLabel(o.type);

      if (orderPrice) {
        priceLinesRef.current.push(series.createPriceLine({
          price: orderPrice,
          color: isOtherFocused ? dimmedColor : color,
          lineWidth: isFocused ? 2 : 1,
          lineStyle: 2,
          axisLabelVisible: true,
          title: `${label} #${ticketTail(o.ticket)}`,
        }));
      }
    });

    (closedTrades || []).forEach((p) => {
      const openedTs = toTime(p.openedAt);
      const closedTs = toTime(p.closedAt);
      const direction = String(p.direction || "").toLowerCase();
      const isShort = direction === "short";
      const isFocused = focusKey != null && String(p.ticket) === focusKey;
      const isOtherFocused = focusKey != null && !isFocused;
      const entryColor = isShort ? "#ef5350" : "#26a69a";
      const pnl = Number(p.pnl) || 0;
      const exitColor = pnl >= 0 ? "#69f0ae" : "#ff5252";
      const signedPnl = `${pnl >= 0 ? "+" : ""}${pnl.toFixed(2)}`;

      if (showFilled && openedTs) {
        markers.push({
          time: openedTs,
          position: isShort ? "aboveBar" : "belowBar",
          color: isOtherFocused ? "rgba(120,120,120,0.35)" : entryColor,
          shape: "circle",
          text: `완료진입 #${ticketTail(p.ticket)}`,
          size: isFocused ? 1.2 : 0.75,
          id: `closed-entry-${p.ticket}`,
        });
      }
      if (showClosed && closedTs) {
        markers.push({
          time: closedTs,
          position: isShort ? "belowBar" : "aboveBar",
          color: isOtherFocused ? "rgba(120,120,120,0.35)" : exitColor,
          shape: "square",
          text: `청산 ${signedPnl} #${ticketTail(p.ticket)}`,
          size: isFocused ? 1.3 : 0.9,
          id: `closed-exit-${p.ticket}`,
        });
      }
    });

    // LightweightCharts requires markers sorted by time
    markers.sort((a, b) => a.time - b.time);
    series.setMarkers(markers);
  }, [openPositions, closedTrades, pendingOrders, focusTicket, bars?.length, showEntry, showSl, showTp, showPending, showFilled, showClosed]);

  // Auto-fit only on symbol/timeframe change (not on every polling refresh).
  // Re-apply the right-edge gap after fitContent so the latest candle doesn't
  // crash into the right-axis price labels.
  React.useEffect(() => {
    if (chartRef.current && bars?.length) {
      const ts = chartRef.current.timeScale();
      try { fitContentWithRightGutter(ts, bars.length); } catch (_) {}
    }
  }, [symbol, timeframe]);

  if (!window.LightweightCharts) {
    return (
      <div className="tb-chart tb-empty-chart" style={{ height }}>
        <div>
          <div className="label-caps">LightweightCharts 라이브러리 로드 실패</div>
          <div className="tb-meta">CDN을 확인해 주세요.</div>
        </div>
      </div>
    );
  }

  if (!bars?.length) {
    // loadError: first-page fetch 가 non-Abort 로 실패(빈 심볼과 구분). loading 보다 우선 표시.
    const emptyTitle = loadError ? "로딩 실패(재시도 중)" : loading ? "차트 로딩 중" : "차트 데이터 없음";
    const emptyDetail = loadError
      ? `${symbol || "선택 종목"} ${timeframe || "5m"} 차트를 못 받았습니다. 다음 갱신에서 자동 재시도합니다.`
      : loading
        ? `${symbol || "선택 종목"} ${timeframe || "5m"} 캔들을 불러오는 중…`
        : `DB market_bars_${timeframe || "5m"}에 ${symbol || "선택 종목"} 데이터가 아직 없습니다.`;
    return (
      <div className="tb-chart tb-empty-chart" style={{ height }}>
          <div>
            <div className="label-caps">{emptyTitle}</div>
            <div className="tb-meta">{emptyDetail}</div>
          </div>
        </div>
      );
  }

  return (
    <div style={{ position: "relative" }}>
      <div ref={containerRef} style={{ height, width: "100%" }} />
      {hovered && (
        <div
          className="tb-chart-hover"
          style={{
            position: "absolute",
            top: 8,
            left: 8,
            background: "rgba(20, 25, 40, 0.85)",
            color: "#cfd6e2",
            padding: "6px 10px",
            borderRadius: 4,
            fontSize: 11,
            fontFamily: "ui-monospace, monospace",
            border: "1px solid rgba(255,255,255,0.10)",
          }}
        >
          <div>O {hovered.o?.toFixed(2)} H {hovered.h?.toFixed(2)}</div>
          <div>L {hovered.l?.toFixed(2)} C {hovered.c?.toFixed(2)}</div>
        </div>
      )}
      {loadingHistory && (
        <div className="tb-chart-history-loading">과거 봉 조회 중</div>
      )}
    </div>
  );
};

Object.assign(window, { SymbolPriceChartLW });
