"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  CandlestickSeries,
  ColorType,
  HistogramSeries,
  createChart,
  type IChartApi,
  type ISeriesApi,
  type Time,
} from "lightweight-charts";
import { ChevronDown } from "lucide-react";
import { useTheme } from "next-themes";
import { toast } from "sonner";
import { fetchStockChart } from "@/features/trade/apis/kis-stock.api";
import type { KisRuntimeCredentials } from "@/features/settings/store/use-kis-runtime-store";
import type {
  DashboardChartTimeframe,
  DashboardRealtimeTradeTick,
  StockCandlePoint,
} from "@/features/trade/types/trade.types";
import { cn } from "@/lib/utils";
import {
  type ChartBar,
  formatKstCrosshairTime,
  formatKstTickMark,
  formatPrice,
  formatSignedPercent,
  isMinuteTimeframe,
  mergeBars,
  normalizeCandles,
  toRealtimeTickBar,
  upsertRealtimeBar,
} from "./chart-utils";
import {
  areBarsEqual,
  type ChartPalette,
  CHART_MIN_HEIGHT,
  DEFAULT_CHART_PALETTE,
  getChartPaletteFromCssVars,
  MINUTE_SYNC_INTERVAL_MS,
  MINUTE_TIMEFRAMES,
  PERIOD_TIMEFRAMES,
  REALTIME_STALE_THRESHOLD_MS,
  UP_COLOR,
} from "./stock-line-chart-meta";

interface StockLineChartProps {
  symbol?: string;
  candles: StockCandlePoint[];
  credentials?: KisRuntimeCredentials | null;
  latestTick?: DashboardRealtimeTradeTick | null;
}

/**
 * @description TradingView 스타일 캔들 차트를 렌더링하고, timeframe별 KIS 차트 API를 조회합니다.
 * @see features/trade/apis/kis-stock.api.ts fetchStockChart
 * @see lib/kis/domestic.ts getDomesticChart
 */
export function StockLineChart({
  symbol,
  candles,
  credentials,
  latestTick,
}: StockLineChartProps) {
  const { resolvedTheme } = useTheme();
  const containerRef = useRef<HTMLDivElement | null>(null);
  const chartRef = useRef<IChartApi | null>(null);
  const candleSeriesRef = useRef<ISeriesApi<"Candlestick", Time> | null>(null);
  const volumeSeriesRef = useRef<ISeriesApi<"Histogram", Time> | null>(null);

  const [timeframe, setTimeframe] = useState<DashboardChartTimeframe>("1d");
  const [isMinuteDropdownOpen, setIsMinuteDropdownOpen] = useState(false);
  const [bars, setBars] = useState<ChartBar[]>([]);
  const [nextCursor, setNextCursor] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [isLoadingMore, setIsLoadingMore] = useState(false);
  const [isChartReady, setIsChartReady] = useState(false);
  const lastRealtimeKeyRef = useRef<string>("");
  const lastRealtimeAppliedAtRef = useRef(0);
  const chartPaletteRef = useRef<ChartPalette>(DEFAULT_CHART_PALETTE);
  const renderableBarsRef = useRef<ChartBar[]>([]);
  const initialThemeModeRef = useRef<"light" | "dark">("light");

  const activeThemeMode: "light" | "dark" =
    resolvedTheme === "dark"
      ? "dark"
      : resolvedTheme === "light"
        ? "light"
        : typeof document !== "undefined" &&
            document.documentElement.classList.contains("dark")
          ? "dark"
          : "light";

  useEffect(() => {
    initialThemeModeRef.current = activeThemeMode;
  }, [activeThemeMode]);

  // 복수 이벤트에서 중복 로드를 막기 위한 ref 상태
  const loadingMoreRef = useRef(false);
  const loadMoreHandlerRef = useRef<() => Promise<void>>(async () => {});
  const initialLoadCompleteRef = useRef(false);

  // API 오류 시 fallback 용도로 유지
  const latestCandlesRef = useRef(candles);
  useEffect(() => {
    latestCandlesRef.current = candles;
  }, [candles]);

  const latest = bars.at(-1);
  const prevClose = bars.length > 1 ? (bars.at(-2)?.close ?? 0) : 0;
  const change = latest ? latest.close - prevClose : 0;
  const changeRate = prevClose > 0 ? (change / prevClose) * 100 : 0;

  const renderableBars = useMemo(() => {
    const dedup = new Map<number, ChartBar>();

    for (const bar of bars) {
      if (
        !Number.isFinite(bar.time) ||
        !Number.isFinite(bar.open) ||
        !Number.isFinite(bar.high) ||
        !Number.isFinite(bar.low) ||
        !Number.isFinite(bar.close) ||
        bar.close <= 0
      ) {
        continue;
      }

      dedup.set(bar.time, bar);
    }

    return [...dedup.values()].sort((a, b) => a.time - b.time);
  }, [bars]);

  useEffect(() => {
    renderableBarsRef.current = renderableBars;
  }, [renderableBars]);

  /**
   * @description lightweight-charts 시리즈 데이터에 안전한 OHLCV 값을 주입합니다.
   * @see features/trade/components/chart/StockLineChart.tsx renderableBars useMemo
   */
  const setSeriesData = useCallback((nextBars: ChartBar[]) => {
    const candleSeries = candleSeriesRef.current;
    const volumeSeries = volumeSeriesRef.current;
    if (!candleSeries || !volumeSeries) return;

    try {
      candleSeries.setData(
        nextBars.map((bar) => ({
          time: bar.time,
          open: bar.open,
          high: bar.high,
          low: bar.low,
          close: bar.close,
        })),
      );

      volumeSeries.setData(
        nextBars.map((bar) => ({
          time: bar.time,
          value: Number.isFinite(bar.volume) ? bar.volume : 0,
          color:
            bar.close >= bar.open
              ? "rgba(239,68,68,0.45)"
              : chartPaletteRef.current.volumeDownColor,
        })),
      );
    } catch (error) {
      if (process.env.NODE_ENV !== "production") {
        console.error("Failed to render chart series data:", error);
      }
    }
  }, []);

  /**
   * @description 좌측 스크롤 시 cursor 기반 과거 캔들을 추가 로드합니다.
   * @see lib/kis/domestic.ts getDomesticChart cursor
   */
  const handleLoadMore = useCallback(async () => {
    if (!symbol || !credentials || !nextCursor || loadingMoreRef.current)
      return;

    loadingMoreRef.current = true;
    setIsLoadingMore(true);

    try {
      const response = await fetchStockChart(
        symbol,
        timeframe,
        credentials,
        nextCursor,
      );

      const olderBars = normalizeCandles(response.candles, timeframe);
      setBars((prev) => mergeBars(olderBars, prev));
      setNextCursor(response.hasMore ? response.nextCursor : null);
    } catch (error) {
      const message =
        error instanceof Error
          ? error.message
          : "과거 차트 데이터를 불러오지 못했습니다.";
      toast.error(message);
    } finally {
      loadingMoreRef.current = false;
      setIsLoadingMore(false);
    }
  }, [credentials, nextCursor, symbol, timeframe]);

  useEffect(() => {
    loadMoreHandlerRef.current = handleLoadMore;
  }, [handleLoadMore]);

  useEffect(() => {
    lastRealtimeKeyRef.current = "";
    lastRealtimeAppliedAtRef.current = 0;
  }, [symbol, timeframe]);

  useEffect(() => {
    const container = containerRef.current;
    if (!container || chartRef.current) return;

    // 브랜드 색상은 globals.css의 CSS 변수에서 읽어 차트(canvas)에도 동일하게 적용합니다.
    const palette = getChartPaletteFromCssVars(initialThemeModeRef.current);
    chartPaletteRef.current = palette;

    const chart = createChart(container, {
      width: Math.max(container.clientWidth, 320),
      height: Math.max(container.clientHeight, CHART_MIN_HEIGHT),
      layout: {
        background: { type: ColorType.Solid, color: palette.backgroundColor },
        textColor: palette.textColor,
        attributionLogo: true,
      },
      localization: {
        locale: "ko-KR",
        timeFormatter: formatKstCrosshairTime,
      },
      rightPriceScale: {
        borderColor: palette.borderColor,
        scaleMargins: {
          top: 0.08,
          bottom: 0.2,
        },
      },
      grid: {
        vertLines: { color: palette.gridColor },
        horzLines: { color: palette.gridColor },
      },
      crosshair: {
        vertLine: { color: palette.crosshairColor, width: 1, style: 2 },
        horzLine: { color: palette.crosshairColor, width: 1, style: 2 },
      },
      timeScale: {
        borderColor: palette.borderColor,
        timeVisible: true,
        secondsVisible: false,
        rightOffset: 2,
        tickMarkFormatter: formatKstTickMark,
      },
      handleScroll: {
        mouseWheel: true,
        pressedMouseMove: true,
      },
      handleScale: {
        mouseWheel: true,
        pinch: true,
        axisPressedMouseMove: true,
      },
    });

    const candleSeries = chart.addSeries(CandlestickSeries, {
      upColor: UP_COLOR,
      downColor: palette.downColor,
      wickUpColor: UP_COLOR,
      wickDownColor: palette.downColor,
      borderUpColor: UP_COLOR,
      borderDownColor: palette.downColor,
      priceLineVisible: true,
      lastValueVisible: true,
    });

    const volumeSeries = chart.addSeries(HistogramSeries, {
      priceScaleId: "volume",
      priceLineVisible: false,
      lastValueVisible: false,
      base: 0,
    });

    chart.priceScale("volume").applyOptions({
      scaleMargins: {
        top: 0.78,
        bottom: 0,
      },
      borderVisible: false,
    });

    let scrollTimeout: number | undefined;
    chart.timeScale().subscribeVisibleLogicalRangeChange((range) => {
      if (!range || !initialLoadCompleteRef.current) return;
      if (range.from >= 10) return;

      if (scrollTimeout !== undefined) window.clearTimeout(scrollTimeout);
      scrollTimeout = window.setTimeout(() => {
        void loadMoreHandlerRef.current();
      }, 250);
    });

    chartRef.current = chart;
    candleSeriesRef.current = candleSeries;
    volumeSeriesRef.current = volumeSeries;
    setIsChartReady(true);

    const resizeObserver = new ResizeObserver(() => {
      chart.resize(
        Math.max(container.clientWidth, 320),
        Math.max(container.clientHeight, CHART_MIN_HEIGHT),
      );
    });
    resizeObserver.observe(container);

    const rafId = window.requestAnimationFrame(() => {
      chart.resize(
        Math.max(container.clientWidth, 320),
        Math.max(container.clientHeight, CHART_MIN_HEIGHT),
      );
    });

    return () => {
      if (scrollTimeout !== undefined) window.clearTimeout(scrollTimeout);
      window.cancelAnimationFrame(rafId);
      resizeObserver.disconnect();
      chart.remove();
      chartRef.current = null;
      candleSeriesRef.current = null;
      volumeSeriesRef.current = null;
      setIsChartReady(false);
    };
  }, []);

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

    const palette = getChartPaletteFromCssVars(activeThemeMode);
    chartPaletteRef.current = palette;

    chart.applyOptions({
      layout: {
        background: { type: ColorType.Solid, color: palette.backgroundColor },
        textColor: palette.textColor,
      },
      rightPriceScale: { borderColor: palette.borderColor },
      grid: {
        vertLines: { color: palette.gridColor },
        horzLines: { color: palette.gridColor },
      },
      crosshair: {
        vertLine: { color: palette.crosshairColor, width: 1, style: 2 },
        horzLine: { color: palette.crosshairColor, width: 1, style: 2 },
      },
      timeScale: { borderColor: palette.borderColor },
    });

    candleSeries.applyOptions({
      downColor: palette.downColor,
      wickDownColor: palette.downColor,
      borderDownColor: palette.downColor,
    });

    setSeriesData(renderableBarsRef.current);
  }, [activeThemeMode, setSeriesData]);

  useEffect(() => {
    if (symbol && credentials) return;

    // 인증 전/종목 미선택 상태는 overview 캔들로 fallback
    setBars(normalizeCandles(candles, "1d"));
    setNextCursor(null);
  }, [candles, credentials, symbol]);

  useEffect(() => {
    if (!symbol || !credentials) return;

    initialLoadCompleteRef.current = false;
    let disposed = false;
    let initialLoadTimer: number | null = null;

    const load = async () => {
      setIsLoading(true);

      try {
        const firstPage = await fetchStockChart(symbol, timeframe, credentials);
        if (disposed) return;

        let mergedBars = normalizeCandles(firstPage.candles, timeframe);
        let resolvedNextCursor = firstPage.hasMore
          ? firstPage.nextCursor
          : null;

        // 분봉은 기본 3페이지까지 순차 조회해 오전 구간 누락을 줄입니다.
        if (
          isMinuteTimeframe(timeframe) &&
          firstPage.hasMore &&
          firstPage.nextCursor
        ) {
          let minuteCursor: string | null = firstPage.nextCursor;
          let extraPageCount = 0;

          while (minuteCursor && extraPageCount < 2) {
            try {
              const olderPage = await fetchStockChart(
                symbol,
                timeframe,
                credentials,
                minuteCursor,
              );

              const olderBars = normalizeCandles(olderPage.candles, timeframe);
              mergedBars = mergeBars(olderBars, mergedBars);
              resolvedNextCursor = olderPage.hasMore
                ? olderPage.nextCursor
                : null;
              minuteCursor = olderPage.hasMore ? olderPage.nextCursor : null;
              extraPageCount += 1;
            } catch {
              // 추가 페이지 실패는 치명적이지 않으므로 현재 데이터는 유지합니다.
              minuteCursor = null;
            }
          }
        }

        setBars(mergedBars);
        setNextCursor(resolvedNextCursor);

        initialLoadTimer = window.setTimeout(() => {
          if (!disposed) initialLoadCompleteRef.current = true;
        }, 350);
      } catch (error) {
        if (disposed) return;

        const message =
          error instanceof Error
            ? error.message
            : "차트 조회 중 오류가 발생했습니다.";
        toast.error(message);

        setBars(normalizeCandles(latestCandlesRef.current, timeframe));
        setNextCursor(null);
      } finally {
        if (!disposed) setIsLoading(false);
      }
    };

    void load();

    return () => {
      disposed = true;
      if (initialLoadTimer !== null) {
        window.clearTimeout(initialLoadTimer);
      }
    };
  }, [credentials, symbol, timeframe]);

  useEffect(() => {
    if (!isChartReady) return;

    setSeriesData(renderableBars);
    if (!initialLoadCompleteRef.current && renderableBars.length > 0) {
      chartRef.current?.timeScale().fitContent();
    }
  }, [isChartReady, renderableBars, setSeriesData]);

  /**
   * @description WebSocket 체결 틱을 현재 timeframe 캔들에 반영합니다.
   * @see features/trade/hooks/useKisTradeWebSocket.ts latestTick
   * @see features/trade/components/chart/chart-utils.ts toRealtimeTickBar
   */
  useEffect(() => {
    if (!latestTick) return;
    if (renderableBarsRef.current.length === 0) return;

    const dedupeKey = `${timeframe}:${latestTick.tickTime}:${latestTick.price}:${latestTick.tradeVolume}`;
    if (lastRealtimeKeyRef.current === dedupeKey) return;

    const realtimeBar = toRealtimeTickBar(latestTick, timeframe);
    if (!realtimeBar) return;

    lastRealtimeKeyRef.current = dedupeKey;
    lastRealtimeAppliedAtRef.current = Date.now();
    setBars((prev) => upsertRealtimeBar(prev, realtimeBar));
  }, [latestTick, timeframe]);

  /**
   * @description 분봉(1m/30m/1h)에서는 WS 공백 상황을 대비해 최신 페이지를 주기적으로 동기화합니다.
   * @see features/trade/apis/kis-stock.api.ts fetchStockChart
   * @see lib/kis/domestic.ts getDomesticChart
   */
  useEffect(() => {
    if (!symbol || !credentials) return;
    if (!isMinuteTimeframe(timeframe)) return;

    let disposed = false;

    const syncLatestMinuteBars = async () => {
      const now = Date.now();
      const isRealtimeFresh =
        now - lastRealtimeAppliedAtRef.current < REALTIME_STALE_THRESHOLD_MS;
      if (isRealtimeFresh) return;

      try {
        const response = await fetchStockChart(symbol, timeframe, credentials);
        if (disposed) return;

        const latestPageBars = normalizeCandles(response.candles, timeframe);
        const recentBars = latestPageBars.slice(-10);
        if (recentBars.length === 0) return;

        setBars((prev) => {
          const merged = mergeBars(prev, recentBars);
          return areBarsEqual(prev, merged) ? prev : merged;
        });
      } catch {
        // 폴링 실패는 치명적이지 않으므로 조용히 다음 주기에서 재시도합니다.
      }
    };

    const intervalId = window.setInterval(() => {
      void syncLatestMinuteBars();
    }, MINUTE_SYNC_INTERVAL_MS);

    return () => {
      disposed = true;
      window.clearInterval(intervalId);
    };
  }, [credentials, symbol, timeframe]);

  const statusMessage = (() => {
    if (isLoading && bars.length === 0) {
      return "차트 데이터를 불러오는 중입니다.";
    }
    if (bars.length === 0) {
      return "차트 데이터가 없습니다.";
    }
    if (renderableBars.length === 0) {
      return "차트 데이터 형식이 올바르지 않습니다.";
    }
    return null;
  })();

  return (
    <div className="flex h-full min-h-[280px] flex-col bg-white dark:bg-brand-900/10 xl:min-h-0">
      {/* ========== CHART TOOLBAR ========== */}
      <div className="flex flex-wrap items-center justify-between gap-2 border-b border-brand-100 bg-muted/20 px-2 py-2 sm:px-3 dark:border-brand-800/45 dark:bg-brand-900/35">
        <div className="flex flex-wrap items-center gap-1 text-xs sm:text-sm">
          <div className="relative">
            <button
              type="button"
              onClick={() => setIsMinuteDropdownOpen((prev) => !prev)}
              onBlur={() =>
                window.setTimeout(() => setIsMinuteDropdownOpen(false), 200)
              }
              className={cn(
                "relative flex items-center gap-1 border-b-2 border-transparent px-2 py-1.5 text-muted-foreground transition-colors hover:text-foreground dark:text-brand-100/72 dark:hover:text-brand-50",
                MINUTE_TIMEFRAMES.some((item) => item.value === timeframe) &&
                  "border-brand-400 font-semibold text-foreground dark:text-brand-50",
              )}
            >
              {MINUTE_TIMEFRAMES.find((item) => item.value === timeframe)
                ?.label ?? "분봉"}
              <ChevronDown className="h-3 w-3" />
            </button>

            {isMinuteDropdownOpen && (
              <div className="absolute left-0 top-full z-10 mt-1 rounded border border-brand-100 bg-white shadow-lg dark:border-brand-700/45 dark:bg-[#1a1624]">
                {MINUTE_TIMEFRAMES.map((item) => (
                  <button
                    key={item.value}
                    type="button"
                    onClick={() => {
                      setTimeframe(item.value);
                      setIsMinuteDropdownOpen(false);
                    }}
                    className={cn(
                      "block w-full whitespace-nowrap px-3 py-1.5 text-left hover:bg-brand-50 dark:text-brand-100 dark:hover:bg-brand-800/35",
                      timeframe === item.value &&
                        "bg-brand-50 font-semibold text-brand-700 dark:bg-brand-700/30 dark:text-brand-100",
                    )}
                  >
                    {item.label}
                  </button>
                ))}
              </div>
            )}
          </div>

          {PERIOD_TIMEFRAMES.map((item) => (
            <button
              key={item.value}
              type="button"
              onClick={() => setTimeframe(item.value)}
              className={cn(
                "relative border-b-2 border-transparent px-2 py-1.5 text-muted-foreground transition-colors hover:text-foreground dark:text-brand-100/72 dark:hover:text-brand-50",
                timeframe === item.value &&
                  "border-brand-400 font-semibold text-foreground dark:text-brand-50",
              )}
            >
              {item.label}
            </button>
          ))}

          {isLoadingMore && (
            <span className="ml-2 text-[11px] text-muted-foreground dark:text-brand-200/70">
              과거 데이터 로딩 중...
            </span>
          )}
        </div>

        <div className="text-[11px] text-muted-foreground dark:text-brand-100/85 sm:text-xs">
          O {formatPrice(latest?.open ?? 0)} H {formatPrice(latest?.high ?? 0)}{" "}
          L {formatPrice(latest?.low ?? 0)} C{" "}
          <span
            className={cn(
              change >= 0 ? "text-red-600" : "text-blue-600 dark:text-blue-400",
            )}
          >
            {formatPrice(latest?.close ?? 0)} ({formatSignedPercent(changeRate)}
            )
          </span>
        </div>
      </div>

      {/* ========== CHART BODY ========== */}
      <div className="relative min-h-0 flex-1 overflow-hidden">
        <div ref={containerRef} className="h-full w-full" />

        {statusMessage && (
          <div className="absolute inset-0 flex items-center justify-center bg-white/80 text-sm text-muted-foreground dark:bg-background/90 dark:text-brand-100/80">
            {statusMessage}
          </div>
        )}
      </div>
    </div>
  );
}
