"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  createChart,
  type CandlestickData,
  type HistogramData,
  type IChartApi,
  type IPriceLine,
  type ISeriesApi,
  type LineData,
  type LogicalRange,
  type MouseEventParams,
  type UTCTimestamp,
} from "lightweight-charts";
import type { ChartPayload, LatestPredictionResponse } from "../lib/api";
import { csvFilename, downloadCsv, ohlcvToCsv } from "../lib/csv";
import type { Targets } from "../lib/targets";

type Props = {
  chart: ChartPayload;
  prediction?: LatestPredictionResponse | null;
  // 사용자가 저장한 목표가/손절가 — 메인 캔들 위 가로 라인 오버레이.
  targets?: Targets | null;
};

// KST naive ISO ('YYYY-MM-DD' 또는 'YYYY-MM-DDTHH:MM:SS') 를 UTCTimestamp 로.
// lightweight-charts 는 UTC 라고 가정하지만 KST wall-clock 을 UTC 처럼 넣어
// timeScale 표시가 한국 사용자에게 자연스럽게 KST 그대로 나오도록 함.
function isoToUtcTs(s: string): UTCTimestamp {
  if (s.length <= 10) {
    return (Date.UTC(
      Number(s.slice(0, 4)),
      Number(s.slice(5, 7)) - 1,
      Number(s.slice(8, 10)),
    ) / 1000) as UTCTimestamp;
  }
  return (Date.UTC(
    Number(s.slice(0, 4)),
    Number(s.slice(5, 7)) - 1,
    Number(s.slice(8, 10)),
    Number(s.slice(11, 13)),
    Number(s.slice(14, 16)),
    Number(s.slice(17, 19) || "0"),
  ) / 1000) as UTCTimestamp;
}

// 한국 거래소 관행: 상승=빨강, 하락=파랑.
const COLOR_UP = "#ef4444";
const COLOR_DOWN = "#3b82f6";

// crosshair overlay 용 — 토스 차트는 마우스 hover 시 좌상단에 한국어 정보 박스를 띄움.
// UTCTimestamp(초) → 'YYYY-MM-DD(요일)' / intraday 면 시:분 까지.
const WEEKDAYS = ["일", "월", "화", "수", "목", "금", "토"];
function tsToKLabel(ts: UTCTimestamp, intraday: boolean): string {
  const d = new Date((ts as number) * 1000);
  const y = d.getUTCFullYear();
  const m = String(d.getUTCMonth() + 1).padStart(2, "0");
  const day = String(d.getUTCDate()).padStart(2, "0");
  const wd = WEEKDAYS[d.getUTCDay()];
  const base = `${y}-${m}-${day}(${wd})`;
  if (!intraday) return base;
  const hh = String(d.getUTCHours()).padStart(2, "0");
  const mm = String(d.getUTCMinutes()).padStart(2, "0");
  return `${base} ${hh}:${mm}`;
}

// 거래량 한국식 단위 (억/만). 토스 사이드바 fmtCompact 와 동일.
function fmtKVolume(n: number | null | undefined): string {
  if (n == null || !Number.isFinite(n)) return "-";
  if (n >= 1e8) return `${(n / 1e8).toFixed(1)}억`;
  if (n >= 1e4) return `${(n / 1e4).toFixed(1)}만`;
  return n.toLocaleString();
}

function fmtKPrice(n: number | null | undefined): string {
  if (n == null || !Number.isFinite(n)) return "-";
  return n.toLocaleString(undefined, { maximumFractionDigits: 0 });
}

type CrosshairInfo = {
  label: string;       // '2025-11-28(금)' 또는 '2025-11-28(금) 14:30'
  open: number | null;
  high: number | null;
  low: number | null;
  close: number | null;
  volume: number | null;
  // 시가 → 종가 등락률 (%). null 이면 안 그림.
  pct: number | null;
};
// 예측 캔들은 같은 up/down 색 옅은 톤. "어우러지되 미래 데이터임이 명확" 한 시각 신호.
const PRED_UP = "#fda4af";
const PRED_DOWN = "#93c5fd";

// 단순 이동평균. 윈도우 미만은 null (lightweight-charts 는 whitespace 로 처리됨).
function sma(values: (number | null)[], window: number): (number | null)[] {
  const out: (number | null)[] = new Array(values.length).fill(null);
  let sum = 0;
  let count = 0;
  const buf: (number | null)[] = [];
  for (let i = 0; i < values.length; i++) {
    const v = values[i];
    buf.push(v);
    if (v != null) {
      sum += v;
      count += 1;
    }
    if (buf.length > window) {
      const drop = buf.shift();
      if (drop != null) {
        sum -= drop;
        count -= 1;
      }
    }
    if (buf.length === window && count === window) {
      out[i] = sum / window;
    }
  }
  return out;
}

type SmaWindow = 5 | 20 | 60;

const SMA_PRESETS: { window: SmaWindow; color: string; label: string }[] = [
  { window: 5, color: "#f59e0b", label: "MA5" },
  { window: 20, color: "#22d3ee", label: "MA20" },
  { window: 60, color: "#a78bfa", label: "MA60" },
];

// Wilder's smoothing RSI. period=14 가 표준. 워밍업 구간은 null.
// 첫 평균은 SMA, 이후는 ((prev*(n-1)) + cur) / n 점화식.
function rsi(closes: (number | null)[], period = 14): (number | null)[] {
  const out: (number | null)[] = new Array(closes.length).fill(null);
  if (closes.length <= period) return out;
  let gainSum = 0;
  let lossSum = 0;
  // 첫 period 구간으로 초기 평균. null 이 끼면 직전 값 기준으로 변화량 산출하되,
  // 둘 중 하나라도 null 이면 그 step 은 0 으로 취급 (보수적).
  for (let i = 1; i <= period; i++) {
    const a = closes[i];
    const b = closes[i - 1];
    if (a == null || b == null) continue;
    const ch = a - b;
    if (ch >= 0) gainSum += ch;
    else lossSum -= ch;
  }
  let avgGain = gainSum / period;
  let avgLoss = lossSum / period;
  out[period] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);
  for (let i = period + 1; i < closes.length; i++) {
    const a = closes[i];
    const b = closes[i - 1];
    let ch = 0;
    if (a != null && b != null) ch = a - b;
    const g = ch > 0 ? ch : 0;
    const l = ch < 0 ? -ch : 0;
    avgGain = (avgGain * (period - 1) + g) / period;
    avgLoss = (avgLoss * (period - 1) + l) / period;
    out[i] = avgLoss === 0 ? 100 : 100 - 100 / (1 + avgGain / avgLoss);
  }
  return out;
}

// 지수이동평균. 첫 유효 값에서 시드 시작, 이후 alpha=2/(n+1) 점화식.
// null 은 그대로 통과 (해당 시점 계산 안 함, 직전 ema 유지).
function ema(values: (number | null)[], period: number): (number | null)[] {
  const out: (number | null)[] = new Array(values.length).fill(null);
  const alpha = 2 / (period + 1);
  let cur: number | null = null;
  for (let i = 0; i < values.length; i++) {
    const v = values[i];
    if (v == null) {
      out[i] = cur;
      continue;
    }
    if (cur == null) cur = v;
    else cur = alpha * v + (1 - alpha) * cur;
    out[i] = cur;
  }
  return out;
}

// MACD = EMA12 - EMA26, signal = EMA9(MACD), hist = MACD - signal.
function macd(
  closes: (number | null)[],
  fast = 12,
  slow = 26,
  signal = 9,
): { macd: (number | null)[]; signal: (number | null)[]; hist: (number | null)[] } {
  const ef = ema(closes, fast);
  const es = ema(closes, slow);
  const m: (number | null)[] = ef.map((a, i) => {
    const b = es[i];
    if (a == null || b == null) return null;
    return a - b;
  });
  const sig = ema(m, signal);
  const hist: (number | null)[] = m.map((a, i) => {
    const b = sig[i];
    if (a == null || b == null) return null;
    return a - b;
  });
  return { macd: m, signal: sig, hist };
}

export function StockChart({ chart, prediction, targets }: Props) {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const chartRef = useRef<IChartApi | null>(null);
  const candleRef = useRef<ISeriesApi<"Candlestick"> | null>(null);
  const volumeRef = useRef<ISeriesApi<"Histogram"> | null>(null);
  const predRef = useRef<ISeriesApi<"Candlestick"> | null>(null);
  const smaRefs = useRef<Map<SmaWindow, ISeriesApi<"Line">>>(new Map());
  // RSI 보조차트 — 별도 chart 인스턴스로 메인 아래에 붙여 timeScale 동기화.
  const rsiContainerRef = useRef<HTMLDivElement | null>(null);
  const rsiChartRef = useRef<IChartApi | null>(null);
  const rsiSeriesRef = useRef<ISeriesApi<"Line"> | null>(null);
  // MACD 보조차트 — RSI 와 같은 패턴 (별 인스턴스, 단방향 sync).
  const macdContainerRef = useRef<HTMLDivElement | null>(null);
  const macdChartRef = useRef<IChartApi | null>(null);
  const macdLineRef = useRef<ISeriesApi<"Line"> | null>(null);
  const macdSignalRef = useRef<ISeriesApi<"Line"> | null>(null);
  const macdHistRef = useRef<ISeriesApi<"Histogram"> | null>(null);
  // 목표가/손절가 가로 라인 — candleRef 의 priceLine 으로 부착.
  const targetLineRef = useRef<IPriceLine | null>(null);
  const stopLineRef = useRef<IPriceLine | null>(null);

  // 보조 차트 crosshair 동기화용 time→value lookup.
  // 메인 차트에서 hover 한 시점의 RSI/MACD 값을 O(1) 로 찾아 setCrosshairPosition 에
  // 넣어줘야 보조 차트의 가격 라벨이 정확히 뜬다.
  const rsiByTsRef = useRef<Map<number, number>>(new Map());
  const macdSignalByTsRef = useRef<Map<number, number>>(new Map());

  // 토글 상태 — 기본 MA20 만 ON. 사용자 컨트롤이 너무 시끄럽지 않게.
  const [active, setActive] = useState<Set<SmaWindow>>(() => new Set<SmaWindow>([20]));
  const [rsiOn, setRsiOn] = useState(false);
  const [macdOn, setMacdOn] = useState(false);
  // crosshair 위에 떠 있는 한국어 라벨 — null 이면 hover 안 함 (기본 마지막 봉 표시).
  const [hover, setHover] = useState<CrosshairInfo | null>(null);
  // 전체화면 모드 — Fullscreen API 가 동작하면 그게 우선, 안되면 fixed inset 오버레이 폴백.
  // 두 경로 모두 같은 state 로 통일 — UI(버튼 라벨/컨테이너 클래스) 분기 단순.
  const wrapperRef = useRef<HTMLDivElement | null>(null);
  const [fullscreen, setFullscreen] = useState(false);

  const isIntraday = chart.interval === "10m";
  const showSma = !isIntraday; // 10분봉에선 의미 적음 — UI 도 숨김.
  const showRsi = !isIntraday && rsiOn;
  const showMacd = !isIntraday && macdOn;

  // chart 생성 — interval 바뀌면 timeVisible 토글을 위해 재생성.
  useEffect(() => {
    if (!containerRef.current) return;
    const c = createChart(containerRef.current, {
      layout: {
        background: { color: "transparent" },
        textColor: "#cbd5e1",
      },
      grid: {
        vertLines: { color: "#1f2937" },
        horzLines: { color: "#1f2937" },
      },
      rightPriceScale: { borderColor: "#374151" },
      timeScale: {
        borderColor: "#374151",
        timeVisible: isIntraday,
        secondsVisible: false,
      },
      autoSize: true,
      crosshair: { mode: 1 },
    });
    const candle = c.addCandlestickSeries({
      upColor: COLOR_UP,
      downColor: COLOR_DOWN,
      borderUpColor: COLOR_UP,
      borderDownColor: COLOR_DOWN,
      wickUpColor: COLOR_UP,
      wickDownColor: COLOR_DOWN,
    });
    // 캔들 priceScale 을 위쪽 78% 로 좁혀서 거래량 막대를 아래쪽에 둘 공간 확보.
    c.priceScale("right").applyOptions({
      scaleMargins: { top: 0.05, bottom: 0.22 },
    });
    // 거래량 막대 — overlay priceScale("volume") 로 같은 chart 안에 띄움.
    // bottom 0/top 0.82 → 차트 영역의 아래 18% 만 사용. 가격축 라벨은 숨김.
    const volume = c.addHistogramSeries({
      priceFormat: { type: "volume" },
      priceScaleId: "volume",
      priceLineVisible: false,
      lastValueVisible: false,
    });
    c.priceScale("volume").applyOptions({
      scaleMargins: { top: 0.82, bottom: 0 },
    });
    chartRef.current = c;
    candleRef.current = candle;
    volumeRef.current = volume;
    const smaMap = smaRefs.current;
    return () => {
      c.remove();
      chartRef.current = null;
      candleRef.current = null;
      volumeRef.current = null;
      predRef.current = null;
      smaMap.clear();
    };
  }, [isIntraday]);

  // 실 데이터 push.
  useEffect(() => {
    if (!candleRef.current) return;
    const data: CandlestickData[] = chart.ohlcv
      .filter((p) => p.open !== null && p.high !== null && p.low !== null && p.close !== null)
      .map((p) => ({
        time: isoToUtcTs(p.date),
        open: p.open as number,
        high: p.high as number,
        low: p.low as number,
        close: p.close as number,
      }));
    candleRef.current.setData(data);
    chartRef.current?.timeScale().fitContent();
  }, [chart, isIntraday]);

  // 거래량 막대 — 종가 ≥ 시가면 한국 관행으로 빨강(상승), 아니면 파랑(하락).
  // null volume 은 그 시점 캔들 자체가 빠진 케이스(상장폐지/거래정지) 라 건너뛴다.
  useEffect(() => {
    if (!volumeRef.current) return;
    const data: HistogramData[] = chart.ohlcv
      .filter((p) => p.volume != null && p.volume > 0)
      .map((p) => {
        const up = p.close != null && p.open != null ? p.close >= p.open : true;
        return {
          time: isoToUtcTs(p.date),
          value: p.volume as number,
          color: up ? "#ef444466" : "#3b82f666",
        };
      });
    volumeRef.current.setData(data);
  }, [chart, isIntraday]);

  // SMA 시리즈 — chart.ohlcv 종가 기준. 토글 변화 또는 데이터 변화 시 갱신.
  // 비활성 윈도우는 제거하고, 활성은 재계산. 마지막에 fitContent 는 호출 안 함 — 사용자 줌 유지.
  const closes = useMemo(() => chart.ohlcv.map((p) => p.close), [chart]);
  useEffect(() => {
    const c = chartRef.current;
    if (!c) return;
    if (!showSma) {
      // intraday: 다 지움.
      for (const s of smaRefs.current.values()) c.removeSeries(s);
      smaRefs.current.clear();
      return;
    }
    // 비활성 윈도우 제거.
    for (const [w, s] of smaRefs.current) {
      if (!active.has(w)) {
        c.removeSeries(s);
        smaRefs.current.delete(w);
      }
    }
    // 활성 윈도우 생성/갱신.
    for (const preset of SMA_PRESETS) {
      if (!active.has(preset.window)) continue;
      const values = sma(closes, preset.window);
      const lineData: LineData[] = [];
      for (let i = 0; i < values.length; i++) {
        const v = values[i];
        if (v == null) continue;
        lineData.push({ time: isoToUtcTs(chart.ohlcv[i].date), value: v });
      }
      let s = smaRefs.current.get(preset.window);
      if (!s) {
        s = c.addLineSeries({
          color: preset.color,
          lineWidth: 1,
          priceLineVisible: false,
          lastValueVisible: false,
          title: preset.label,
        });
        smaRefs.current.set(preset.window, s);
      }
      s.setData(lineData);
    }
  }, [active, closes, chart, showSma]);

  // 예측 오버레이를 캔들 시리즈로 렌더.
  // 합성 OHLC:
  //   open[i]  = i==0 ? base_close : prev.point_close
  //   close[i] = point_close
  //   high[i]  = max(ci_high, open, close)
  //   low[i]   = min(ci_low,  open, close)
  // → 몸통 = 기간 사이 예상 변동, 위/아래 꼬리 = 신뢰구간 폭 (불확실성).
  // 10분봉(intraday) 에서는 예측 표시 안 함 — 모델은 일봉 기반.
  useEffect(() => {
    if (!chartRef.current) return;
    if (predRef.current) {
      chartRef.current.removeSeries(predRef.current);
      predRef.current = null;
    }
    if (isIntraday) return;
    if (!prediction?.found || !prediction.steps?.length) return;
    const baseDate = prediction.base_date;
    const baseClose = prediction.base_close;
    if (!baseDate || baseClose == null) return;

    const sorted = [...prediction.steps]
      .filter(
        (s) =>
          s.point_close != null && s.ci_low != null && s.ci_high != null && s.target_date,
      )
      .sort((a, b) => a.horizon - b.horizon);
    if (!sorted.length) return;

    const data: CandlestickData[] = [];
    let prevClose = baseClose;
    for (const s of sorted) {
      const open = prevClose;
      const close = s.point_close as number;
      const ciHi = s.ci_high as number;
      const ciLo = s.ci_low as number;
      data.push({
        time: isoToUtcTs(s.target_date),
        open,
        close,
        high: Math.max(ciHi, open, close),
        low: Math.min(ciLo, open, close),
      });
      prevClose = close;
    }

    const pred = chartRef.current.addCandlestickSeries({
      upColor: PRED_UP,
      downColor: PRED_DOWN,
      borderUpColor: PRED_UP,
      borderDownColor: PRED_DOWN,
      wickUpColor: PRED_UP,
      wickDownColor: PRED_DOWN,
      priceLineVisible: false,
      lastValueVisible: true,
      title: "예측",
    });
    pred.setData(data);
    predRef.current = pred;
    chartRef.current.timeScale().fitContent();
  }, [prediction, isIntraday]);

  // 목표가/손절가 가로 라인 — targets prop 또는 캔들 시리즈 재생성 시 동기화.
  // candleRef 가 바뀌면 이전 라인 ref 는 이미 해제된 상태라 그냥 새로 만들면 됨.
  useEffect(() => {
    const candle = candleRef.current;
    if (!candle) return;
    // 이전 라인 제거 후 새로 그림 — 입력 바뀔 때마다 가격값 변경하지 않고 그냥 재생성.
    if (targetLineRef.current) {
      try { candle.removePriceLine(targetLineRef.current); } catch { /* 이미 해제됨 */ }
      targetLineRef.current = null;
    }
    if (stopLineRef.current) {
      try { candle.removePriceLine(stopLineRef.current); } catch { /* 이미 해제됨 */ }
      stopLineRef.current = null;
    }
    if (!targets) return;
    if (targets.target != null && Number.isFinite(targets.target)) {
      targetLineRef.current = candle.createPriceLine({
        price: targets.target,
        color: "#fb7185",        // rose-400 (한국 상승색 톤)
        lineWidth: 1,
        lineStyle: 2,            // dashed
        axisLabelVisible: true,
        title: "목표",
      });
    }
    if (targets.stop != null && Number.isFinite(targets.stop)) {
      stopLineRef.current = candle.createPriceLine({
        price: targets.stop,
        color: "#60a5fa",        // sky-400 (한국 하락색 톤)
        lineWidth: 1,
        lineStyle: 2,
        axisLabelVisible: true,
        title: "손절",
      });
    }
  }, [targets, isIntraday]);

  // RSI 보조 차트 생성/제거 — toggle 또는 interval 변경 시. 메인 chart 와 별 인스턴스.
  // priceScale fix (0-100) + 30/70 가이드 라인.
  useEffect(() => {
    if (!showRsi || !rsiContainerRef.current) return;
    const c = createChart(rsiContainerRef.current, {
      layout: {
        background: { color: "transparent" },
        textColor: "#cbd5e1",
      },
      grid: {
        vertLines: { color: "#1f2937" },
        horzLines: { color: "#1f2937" },
      },
      rightPriceScale: {
        borderColor: "#374151",
        scaleMargins: { top: 0.1, bottom: 0.1 },
      },
      timeScale: {
        borderColor: "#374151",
        timeVisible: false,
        secondsVisible: false,
      },
      autoSize: true,
      crosshair: { mode: 1 },
      handleScroll: false,
      handleScale: false,
    });
    const s = c.addLineSeries({
      color: "#e879f9",
      lineWidth: 1,
      priceLineVisible: false,
      lastValueVisible: true,
      title: "RSI(14)",
      priceFormat: { type: "price", precision: 1, minMove: 0.1 },
    });
    s.createPriceLine({
      price: 70,
      color: "#ef4444",
      lineWidth: 1,
      lineStyle: 2,
      axisLabelVisible: true,
      title: "70",
    });
    s.createPriceLine({
      price: 30,
      color: "#3b82f6",
      lineWidth: 1,
      lineStyle: 2,
      axisLabelVisible: true,
      title: "30",
    });
    rsiChartRef.current = c;
    rsiSeriesRef.current = s;
    return () => {
      c.remove();
      rsiChartRef.current = null;
      rsiSeriesRef.current = null;
    };
  }, [showRsi]);

  // RSI 데이터 push + crosshair sync 용 ts→value map 채우기.
  useEffect(() => {
    if (!rsiSeriesRef.current) return;
    const values = rsi(closes, 14);
    const data: LineData[] = [];
    const lookup = new Map<number, number>();
    for (let i = 0; i < values.length; i++) {
      const v = values[i];
      if (v == null) continue;
      const t = isoToUtcTs(chart.ohlcv[i].date);
      data.push({ time: t, value: v });
      lookup.set(t as number, v);
    }
    rsiSeriesRef.current.setData(data);
    rsiByTsRef.current = lookup;
  }, [closes, chart, showRsi]);

  // MACD 보조 차트 생성/제거.
  useEffect(() => {
    if (!showMacd || !macdContainerRef.current) return;
    const c = createChart(macdContainerRef.current, {
      layout: { background: { color: "transparent" }, textColor: "#cbd5e1" },
      grid: {
        vertLines: { color: "#1f2937" },
        horzLines: { color: "#1f2937" },
      },
      rightPriceScale: {
        borderColor: "#374151",
        scaleMargins: { top: 0.1, bottom: 0.1 },
      },
      timeScale: {
        borderColor: "#374151",
        timeVisible: false,
        secondsVisible: false,
      },
      autoSize: true,
      crosshair: { mode: 1 },
      handleScroll: false,
      handleScale: false,
    });
    const hist = c.addHistogramSeries({
      priceLineVisible: false,
      lastValueVisible: false,
      priceFormat: { type: "price", precision: 2, minMove: 0.01 },
    });
    const macdLine = c.addLineSeries({
      color: "#60a5fa",
      lineWidth: 1,
      priceLineVisible: false,
      lastValueVisible: true,
      title: "MACD",
      priceFormat: { type: "price", precision: 2, minMove: 0.01 },
    });
    const signalLine = c.addLineSeries({
      color: "#fb923c",
      lineWidth: 1,
      priceLineVisible: false,
      lastValueVisible: true,
      title: "Signal",
      priceFormat: { type: "price", precision: 2, minMove: 0.01 },
    });
    macdChartRef.current = c;
    macdHistRef.current = hist;
    macdLineRef.current = macdLine;
    macdSignalRef.current = signalLine;
    return () => {
      c.remove();
      macdChartRef.current = null;
      macdHistRef.current = null;
      macdLineRef.current = null;
      macdSignalRef.current = null;
    };
  }, [showMacd]);

  // MACD 데이터 push + crosshair sync 용 signal ts→value map 채우기.
  // signal 라인 기준으로 crosshair 가격 라벨이 뜨도록 — MACD 라인보다 안정적으로 보임.
  useEffect(() => {
    const hSeries = macdHistRef.current;
    const mSeries = macdLineRef.current;
    const sSeries = macdSignalRef.current;
    if (!hSeries || !mSeries || !sSeries) return;
    const { macd: m, signal: sig, hist } = macd(closes, 12, 26, 9);
    const mLine: LineData[] = [];
    const sLine: LineData[] = [];
    const hBars: HistogramData[] = [];
    const signalLookup = new Map<number, number>();
    for (let i = 0; i < closes.length; i++) {
      const t = isoToUtcTs(chart.ohlcv[i].date);
      if (m[i] != null) mLine.push({ time: t, value: m[i] as number });
      if (sig[i] != null) {
        sLine.push({ time: t, value: sig[i] as number });
        signalLookup.set(t as number, sig[i] as number);
      }
      if (hist[i] != null) {
        const h = hist[i] as number;
        // 한국 관행: 양수 막대=상승=빨강, 음수=하락=파랑. 컬러 옅게 (배경 톤).
        hBars.push({ time: t, value: h, color: h >= 0 ? "#ef444488" : "#3b82f688" });
      }
    }
    hSeries.setData(hBars);
    mSeries.setData(mLine);
    sSeries.setData(sLine);
    macdSignalByTsRef.current = signalLookup;
  }, [closes, chart, showMacd]);

  // crosshair → 한국어 오버레이.
  //  - 같은 time(UTCTimestamp) 의 ohlcv 포인트를 O(1) 조회용 Map 으로 미리 만든다.
  //  - hover 안 했을 때는 마지막 봉을 기본으로 표시 (토스 패턴 — 차트에 들어오자마자 정보 보임).
  const ohlcvByTs = useMemo(() => {
    const m = new Map<number, ChartPayload["ohlcv"][number]>();
    for (const p of chart.ohlcv) m.set(isoToUtcTs(p.date) as number, p);
    return m;
  }, [chart]);

  const lastSummary = useMemo<CrosshairInfo | null>(() => {
    const pts = chart.ohlcv.filter((p) => p.close != null);
    if (!pts.length) return null;
    const last = pts[pts.length - 1];
    const pct =
      last.open != null && last.close != null && last.open > 0
        ? ((last.close - last.open) / last.open) * 100
        : null;
    return {
      label: tsToKLabel(isoToUtcTs(last.date), isIntraday),
      open: last.open,
      high: last.high,
      low: last.low,
      close: last.close,
      volume: last.volume,
      pct,
    };
  }, [chart, isIntraday]);

  useEffect(() => {
    const c = chartRef.current;
    if (!c) return;
    const handler = (param: MouseEventParams) => {
      // 차트 영역 밖이면 hover 해제 → lastSummary 가 자동 표시 + 보조 차트 crosshair clear.
      if (!param.time || !param.point) {
        setHover(null);
        rsiChartRef.current?.clearCrosshairPosition();
        macdChartRef.current?.clearCrosshairPosition();
        return;
      }
      const t = param.time as UTCTimestamp;
      const p = ohlcvByTs.get(t as number);
      if (!p) {
        setHover(null);
        rsiChartRef.current?.clearCrosshairPosition();
        macdChartRef.current?.clearCrosshairPosition();
        return;
      }
      const pct =
        p.open != null && p.close != null && p.open > 0
          ? ((p.close - p.open) / p.open) * 100
          : null;
      setHover({
        label: tsToKLabel(t, isIntraday),
        open: p.open,
        high: p.high,
        low: p.low,
        close: p.close,
        volume: p.volume,
        pct,
      });
      // 보조 차트 crosshair 같은 시점으로 이동 — 그 시점 데이터가 있을 때만.
      // setCrosshairPosition 은 series 의 price 좌표를 받음.
      const rsiChart = rsiChartRef.current;
      const rsiSeries = rsiSeriesRef.current;
      if (rsiChart && rsiSeries) {
        const v = rsiByTsRef.current.get(t as number);
        if (v != null) rsiChart.setCrosshairPosition(v, t, rsiSeries);
        else rsiChart.clearCrosshairPosition();
      }
      const macdChart = macdChartRef.current;
      const macdSig = macdSignalRef.current;
      if (macdChart && macdSig) {
        const v = macdSignalByTsRef.current.get(t as number);
        if (v != null) macdChart.setCrosshairPosition(v, t, macdSig);
        else macdChart.clearCrosshairPosition();
      }
    };
    c.subscribeCrosshairMove(handler);
    return () => {
      c.unsubscribeCrosshairMove(handler);
    };
  }, [ohlcvByTs, isIntraday]);

  // 메인 ↔ 보조패널 timeScale 단방향 동기화. RSI/MACD 둘 다 처리.
  useEffect(() => {
    const main = chartRef.current;
    if (!main) return;
    const followers: IChartApi[] = [];
    if (showRsi && rsiChartRef.current) followers.push(rsiChartRef.current);
    if (showMacd && macdChartRef.current) followers.push(macdChartRef.current);
    if (!followers.length) return;
    const handler = (range: LogicalRange | null) => {
      if (!range) return;
      for (const f of followers) f.timeScale().setVisibleLogicalRange(range);
    };
    const initial = main.timeScale().getVisibleLogicalRange();
    if (initial) {
      for (const f of followers) f.timeScale().setVisibleLogicalRange(initial);
    }
    main.timeScale().subscribeVisibleLogicalRangeChange(handler);
    return () => {
      main.timeScale().unsubscribeVisibleLogicalRangeChange(handler);
    };
  }, [showRsi, showMacd, chart]);

  // 전체화면 토글 — Fullscreen API 가능하면 native, 아니면 오버레이 폴백.
  // iOS Safari 등 element.requestFullscreen 미지원 환경은 자연히 폴백 경로로.
  const toggleFullscreen = useCallback(async () => {
    const el = wrapperRef.current;
    if (!el || typeof document === "undefined") return;
    const docAny = document as Document & { fullscreenEnabled?: boolean };
    if (docAny.fullscreenEnabled && typeof el.requestFullscreen === "function") {
      try {
        if (document.fullscreenElement) {
          await document.exitFullscreen();
        } else {
          await el.requestFullscreen();
        }
        return;
      } catch {
        // 권한 거부/실패 → 오버레이 폴백.
      }
    }
    setFullscreen((v) => !v);
  }, []);

  // Native fullscreen 상태 → state 동기화. ESC 로 빠져나오면 여기로 들어옴.
  useEffect(() => {
    if (typeof document === "undefined") return;
    const onChange = () => setFullscreen(!!document.fullscreenElement);
    document.addEventListener("fullscreenchange", onChange);
    return () => document.removeEventListener("fullscreenchange", onChange);
  }, []);

  // F 키 = 토글, ESC = 폴백 오버레이 해제 (native 는 브라우저가 알아서).
  // 입력 필드에 포커스가 있으면 무시 — 메모 작성 등 방해 금지.
  useEffect(() => {
    if (typeof window === "undefined") return;
    const onKey = (e: KeyboardEvent) => {
      const t = e.target as HTMLElement | null;
      if (
        t &&
        (t.tagName === "INPUT" ||
          t.tagName === "TEXTAREA" ||
          t.tagName === "SELECT" ||
          t.isContentEditable)
      ) {
        return;
      }
      // Ctrl+F / Cmd+F (브라우저 찾기) / Alt+F (브라우저 메뉴) / Shift+F 는 가로채지 않음.
      // 단순 F 만 차트 전체화면 토글로 사용.
      if (
        !e.ctrlKey &&
        !e.metaKey &&
        !e.altKey &&
        !e.shiftKey &&
        e.key.toLowerCase() === "f"
      ) {
        e.preventDefault();
        void toggleFullscreen();
      } else if (e.key === "Escape" && fullscreen && !document.fullscreenElement) {
        setFullscreen(false);
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [toggleFullscreen, fullscreen]);

  const toggle = (w: SmaWindow) => {
    setActive((cur) => {
      const next = new Set(cur);
      if (next.has(w)) next.delete(w);
      else next.add(w);
      return next;
    });
  };

  // 전체화면(native or 폴백) 일 때는 viewport 를 꽉 채워서 차트 면적 최대화.
  // 일반 모드는 기존 카드 스타일 유지.
  const wrapperCls = fullscreen
    ? "fixed inset-0 z-50 flex w-full flex-col overflow-auto bg-zinc-950 p-3"
    : "w-full rounded-lg border border-zinc-800 bg-zinc-900/30 p-2";
  // 차트 본체 높이 — 전체화면이면 가용 공간을 차지하도록 flex-1.
  const chartBoxCls = fullscreen
    ? "relative min-h-[300px] flex-1 w-full"
    : "relative h-[460px] w-full";

  return (
    <div ref={wrapperRef} className={wrapperCls}>
      <div className="mb-1 flex flex-wrap items-center gap-1.5 px-1">
        {showSma && (
          <>
            {SMA_PRESETS.map((p) => {
              const on = active.has(p.window);
              return (
                <button
                  key={p.window}
                  type="button"
                  onClick={() => toggle(p.window)}
                  className={`flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] transition ${
                    on
                      ? "border-zinc-600 bg-zinc-800/80 text-zinc-100"
                      : "border-zinc-800 bg-transparent text-zinc-500 hover:text-zinc-300"
                  }`}
                  aria-pressed={on}
                >
                  <span
                    className="inline-block h-2 w-2 rounded-full"
                    style={{ backgroundColor: on ? p.color : "transparent", border: `1px solid ${p.color}` }}
                  />
                  {p.label}
                </button>
              );
            })}
            <span className="mx-1 text-zinc-700">|</span>
            <button
              type="button"
              onClick={() => setRsiOn((v) => !v)}
              className={`flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] transition ${
                rsiOn
                  ? "border-zinc-600 bg-zinc-800/80 text-zinc-100"
                  : "border-zinc-800 bg-transparent text-zinc-500 hover:text-zinc-300"
              }`}
              aria-pressed={rsiOn}
              title="상대강도지수 (Relative Strength Index, 14일)"
            >
              <span
                className="inline-block h-2 w-2 rounded-full"
                style={{ backgroundColor: rsiOn ? "#e879f9" : "transparent", border: "1px solid #e879f9" }}
              />
              RSI
            </button>
            <button
              type="button"
              onClick={() => setMacdOn((v) => !v)}
              className={`flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] transition ${
                macdOn
                  ? "border-zinc-600 bg-zinc-800/80 text-zinc-100"
                  : "border-zinc-800 bg-transparent text-zinc-500 hover:text-zinc-300"
              }`}
              aria-pressed={macdOn}
              title="MACD (12/26/9) — 추세 전환 신호"
            >
              <span
                className="inline-block h-2 w-2 rounded-full"
                style={{ backgroundColor: macdOn ? "#60a5fa" : "transparent", border: "1px solid #60a5fa" }}
              />
              MACD
            </button>
          </>
        )}
        <button
          type="button"
          onClick={() => {
            const filename = csvFilename(
              chart.name,
              chart.code,
              chart.interval,
              chart.range.from,
              chart.range.to,
            );
            downloadCsv(filename, ohlcvToCsv(chart.ohlcv));
          }}
          disabled={!chart.ohlcv.length}
          className="ml-auto flex items-center gap-1 rounded-full border border-zinc-800 px-2 py-0.5 text-[10px] text-zinc-400 transition hover:border-zinc-600 hover:text-zinc-200 disabled:cursor-not-allowed disabled:opacity-40"
          title={`현재 로드된 기간(${chart.range.from} ~ ${chart.range.to}) 의 봉 데이터를 CSV 로 내보내기 (UTF-8 BOM, Excel 호환)`}
        >
          ⬇ CSV
        </button>
        <button
          type="button"
          onClick={() => void toggleFullscreen()}
          className="flex items-center gap-1 rounded-full border border-zinc-800 px-2 py-0.5 text-[10px] text-zinc-400 transition hover:border-zinc-600 hover:text-zinc-200"
          title={fullscreen ? "전체화면 해제 (ESC / F)" : "전체화면 (F)"}
          aria-pressed={fullscreen}
        >
          {fullscreen ? "⛶ 해제" : "⛶ 전체화면"}
        </button>
      </div>
      <div className={chartBoxCls}>
        <div ref={containerRef} className="h-full w-full" />
        <CrosshairLabel info={hover ?? lastSummary} isHover={hover != null} />
      </div>
      {showRsi && (
        <div className="mt-2 h-[110px] w-full border-t border-zinc-800 pt-1">
          <div ref={rsiContainerRef} className="h-full w-full" />
        </div>
      )}
      {showMacd && (
        <div className="mt-2 h-[110px] w-full border-t border-zinc-800 pt-1">
          <div ref={macdContainerRef} className="h-full w-full" />
        </div>
      )}
      {prediction?.found && !isIntraday && (
        <div className="mt-1 flex items-center justify-end gap-2 px-2 text-[11px] text-zinc-500">
          <span className="inline-block h-2 w-2 rounded-sm bg-rose-300" />
          <span className="inline-block h-2 w-2 rounded-sm bg-sky-300" />
          <span>예측 (꼬리 = q10~q90 신뢰구간)</span>
        </div>
      )}
    </div>
  );
}

// Toss 차트의 hover 정보 박스. 좌상단 absolute 로 띄우고 pointer-events-none
// 으로 마우스 이벤트는 그대로 차트가 받음. info=null 이면 표시 안 함.
function CrosshairLabel({
  info,
  isHover,
}: {
  info: CrosshairInfo | null;
  isHover: boolean;
}) {
  if (!info) return null;
  const { label, open, high, low, close, volume, pct } = info;
  // 등락률 색 — 한국 관행. 0 또는 null 은 회색.
  const pctCls =
    pct == null || pct === 0
      ? "text-zinc-400"
      : pct > 0
        ? "text-rose-300"
        : "text-sky-300";
  const pctText =
    pct == null ? "" : `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
  return (
    <div
      className={`pointer-events-none absolute left-2 top-2 z-10 rounded-md border border-zinc-700/70 bg-zinc-950/80 px-2.5 py-1.5 text-[11px] tabular-nums shadow backdrop-blur-sm ${
        isHover ? "opacity-100" : "opacity-80"
      }`}
    >
      <div className="mb-0.5 flex items-center gap-1.5 text-zinc-400">
        <span>{label}</span>
        {pctText && <span className={pctCls}>{pctText}</span>}
      </div>
      <div className="flex flex-wrap gap-x-2 gap-y-0.5 text-zinc-200">
        <CrosshairCell k="시" v={fmtKPrice(open)} />
        <CrosshairCell k="고" v={fmtKPrice(high)} tone="up" />
        <CrosshairCell k="저" v={fmtKPrice(low)} tone="down" />
        <CrosshairCell k="종" v={fmtKPrice(close)} strong />
        <CrosshairCell k="거래량" v={fmtKVolume(volume)} />
      </div>
    </div>
  );
}

function CrosshairCell({
  k,
  v,
  tone,
  strong,
}: {
  k: string;
  v: string;
  tone?: "up" | "down";
  strong?: boolean;
}) {
  const tcls =
    tone === "up"
      ? "text-rose-300"
      : tone === "down"
        ? "text-sky-300"
        : strong
          ? "text-zinc-100"
          : "text-zinc-300";
  return (
    <span className="flex items-baseline gap-1">
      <span className="text-zinc-500">{k}</span>
      <span className={tcls}>{v}</span>
    </span>
  );
}
