"use client";

import {
  ChartDrawingLayer,
  type ChartDrawingLayerHandle,
} from "@/components/chart/ChartDrawingLayer";
import {
  IndicatorAnnotationLayer,
  type IndicatorAnnotationLayerHandle,
} from "@/components/chart/IndicatorAnnotationLayer";
import { ChartAppearancePanel } from "@/components/chart/ChartAppearancePanel";
import { ChartIndicatorPanel } from "@/components/chart/ChartIndicatorPanel";
import { DrawingToolbar, isInteractiveDrawingTool } from "@/components/chart/DrawingToolbar";
import {
  CHART_LOAD_OLDER_THRESHOLD_BARS,
  CHART_WAREHOUSE_CHUNK_BARS,
  CHART_WAREHOUSE_INITIAL_BARS,
} from "@/lib/chart/constants";
import { MAX_CANDLE_COUNT } from "@/lib/market/limits";
import { oldestBarTimeSec, prependOlderCandles } from "@/lib/chart/candle-window";
import {
  applyChartAppearanceToRuntime,
  applyVolumePaneVisibility,
} from "@/lib/chart/apply-chart-appearance";
import { AlertDrawer } from "@/components/alerts/AlertDrawer";
import { DrawingStylePanel } from "@/components/chart/DrawingStylePanel";
import { IndicatorCatalogModal } from "@/components/indicators/IndicatorCatalogModal";
import {
  CHART_LAYOUTS,
  loadActiveChartLayoutId,
  saveActiveChartLayoutId,
  type ChartLayoutId,
} from "@/lib/chart/layout-profile";
import { loadChartSession, saveChartSession } from "@/lib/chart/chart-session";
import { loadIndicatorInstances } from "@/lib/indicators/instances/storage";
import { createChartRuntime } from "@/lib/chart/create-chart-runtime";
import { applyIncrementalBarToRuntime } from "@/lib/chart/incremental-update";
import { useChartAppearance } from "@/components/chart/useChartAppearance";
import { toOhlcSeriesData, toVolumeSeriesData } from "@/lib/chart/series-data";
import type { ChartRuntime } from "@/lib/chart/lwc-types";
import { useChartLiveTail } from "@/components/chart/useChartLiveTail";
import {
  getInstrument,
  INSTRUMENTS,
  isCanonicalSymbol,
  type CanonicalSymbol,
} from "@/lib/market/instruments";
import type { ChartInteractionTool } from "@/lib/chart/drawings/types";
import { chartResponseMatchesRequest } from "@/lib/market/chart-guard";
import type { CandlesResponseBody, ChartCandle } from "@/lib/market/types";
import {
  formatCanonicalLabel,
  isWarehouseChartSymbol,
  WAREHOUSE_CHART_SYMBOLS,
  type WarehouseChartSymbol,
} from "@/lib/market/instruments";
import {
  DEFAULT_SYMBOL,
  formatSymbolLabel,
  isSupportedSymbol,
  type SupportedSymbol,
} from "@/lib/market/symbols";
import {
  DEFAULT_TIMEFRAME,
  SUPPORTED_TIMEFRAMES,
  type SupportedTimeframe,
} from "@/lib/market/timeframes";
import { useIndicatorInstances } from "@/components/chart/useIndicatorInstances";
import { ALERTS_CHANGED_EVENT } from "@/lib/alerts/events";
import { loadAlertRules } from "@/lib/alerts/storage";
import {
  clearIndicatorChartOutputs,
  ensureBuiltinIndicatorsRegistered,
  LEGACY_SMA_INSTANCE_ID,
  LEGACY_SR_FINDER_INSTANCE_ID,
  runIndicatorInstances,
  syncIndicatorChartOutputs,
  type ChartWithSeries,
  type OverlayAnnotationState,
} from "@/lib/indicators";
import { createClientMtfCandleProvider } from "@/lib/indicators/mtf/client-api-provider";
import { resolveMtfCandlesForInstances } from "@/lib/indicators/mtf/resolve-for-instances";
import { CreateAlertModal } from "@/components/alerts/CreateAlertModal";
import type { ChartDrawing } from "@/lib/chart/drawings/types";
import Link from "next/link";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

ensureBuiltinIndicatorsRegistered();

const CONFIG_MESSAGE =
  "OANDA API is not configured. Add your practice token to .env as OANDA_API_TOKEN, then restart npm run dev.";

type ChartBar = ChartCandle;

type OverlayLineSeries = {
  setData: (data: { time: number; value: number }[]) => void;
};

type WarehouseMeta = {
  firstTimestampMs: number | null;
  lastTimestampMs: number | null;
  rowCount: number;
};

type LoadState =
  | { status: "loading"; symbol: ChartSymbol; timeframe: SupportedTimeframe }
  | {
      status: "error";
      title: string;
      message: string;
      symbol: ChartSymbol;
      timeframe: SupportedTimeframe;
    }
  | {
      status: "ready";
      data: CandlesResponseBody;
      symbol: ChartSymbol;
      timeframe: SupportedTimeframe;
      feedLabel: string;
      warehouseMeta?: WarehouseMeta;
    };

type PriceFeed = "live" | "warehouse";

type ChartSymbol = SupportedSymbol | WarehouseChartSymbol;

function symbolOptions(feed: PriceFeed): ChartSymbol[] {
  if (feed === "warehouse") {
    return [...WAREHOUSE_CHART_SYMBOLS];
  }
  return INSTRUMENTS.filter(
    (i) => i.liveOandaChart || i.historicalSource === "binance",
  ).map((i) => i.canonicalSymbol as ChartSymbol);
}

function usesWarehouseInitialLoad(feed: PriceFeed): boolean {
  return feed === "warehouse";
}

function defaultChartHeight(): number {
  if (typeof window === "undefined") {
    return 520;
  }
  return Math.max(480, Math.min(780, Math.floor(window.innerHeight * 0.62)));
}

function formatChartSymbolLabel(symbol: ChartSymbol): string {
  if (isSupportedSymbol(symbol)) {
    return formatSymbolLabel(symbol);
  }
  return formatCanonicalLabel(symbol);
}

function toSeriesData(candles: ChartCandle[]): ChartBar[] {
  return toOhlcSeriesData(candles);
}

function errorTitle(code: string): string {
  if (code === "CONFIG_MISSING" || code === "CONFIG_INVALID") {
    return "Configuration";
  }
  if (code === "AUTH_FAILED") {
    return "Authentication";
  }
  if (code === "PROVIDER_ERROR") {
    return "Market data provider";
  }
  return "Application";
}

type TradingChartProps = {
  oandaConfigured: boolean;
  initialCatalogOpen?: boolean;
};

export function TradingChart({
  oandaConfigured,
  initialCatalogOpen = false,
}: TradingChartProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const runtimeRef = useRef<ChartRuntime | null>(null);
  const runtimePromiseRef = useRef<Promise<ChartRuntime | null> | null>(null);
  const chartInitGenerationRef = useRef(0);
  const requestSeq = useRef(0);
  const candleBufferRef = useRef<ChartCandle[]>([]);
  const warehouseMetaRef = useRef<WarehouseMeta | null>(null);
  const loadingOlderRef = useRef(false);
  const drawingLayerRef = useRef<ChartDrawingLayerHandle>(null);
  const chartWrapRef = useRef<HTMLDivElement>(null);
  const workspaceRef = useRef<HTMLDivElement>(null);
  const [loadingOlderUi, setLoadingOlderUi] = useState(false);
  const [chartFullscreen, setChartFullscreen] = useState(false);
  const [selectedDrawing, setSelectedDrawing] = useState<ChartDrawing | null>(null);
  const [createAlertOpen, setCreateAlertOpen] = useState(false);

  const [layoutId, setLayoutId] = useState<ChartLayoutId>(() => loadActiveChartLayoutId());
  const [symbol, setSymbol] = useState<ChartSymbol>(() => {
    const saved = loadChartSession(loadActiveChartLayoutId())?.symbol;
    if (saved && (isSupportedSymbol(saved) || isCanonicalSymbol(saved))) {
      return saved as ChartSymbol;
    }
    return DEFAULT_SYMBOL;
  });
  const [timeframe, setTimeframe] = useState<SupportedTimeframe>(() => {
    return loadChartSession(loadActiveChartLayoutId())?.timeframe ?? DEFAULT_TIMEFRAME;
  });
  const [refreshToken, setRefreshToken] = useState(0);
  const [priceFeed, setPriceFeed] = useState<PriceFeed>(
    () => loadChartSession(loadActiveChartLayoutId())?.priceFeed ?? "live",
  );
  const indicatorScope = useMemo(
    () => ({
      symbol: (isCanonicalSymbol(symbol) ? symbol : DEFAULT_SYMBOL) as CanonicalSymbol,
      timeframe,
      layoutId,
    }),
    [symbol, timeframe, layoutId],
  );
  const { instances, replaceInstances, patchInstance } =
    useIndicatorInstances(indicatorScope);
  const instancesRef = useRef(instances);
  instancesRef.current = instances;
  const smaInstance = instances.find((i) => i.instanceId === LEGACY_SMA_INSTANCE_ID);
  const srInstance = instances.find((i) => i.instanceId === LEGACY_SR_FINDER_INSTANCE_ID);
  const smaEnabled = Boolean(smaInstance?.enabled && smaInstance.visible);
  const srEnabled = Boolean(srInstance?.enabled && srInstance.visible);
  const smaPeriod =
    typeof smaInstance?.parameters.period === "number" ? smaInstance.parameters.period : 20;
  const [drawingIdsWithAlerts, setDrawingIdsWithAlerts] = useState<ReadonlySet<string>>(
    () => new Set(),
  );

  const [indicatorError, setIndicatorError] = useState<string | null>(null);
  const overlaySeriesRef = useRef<Map<string, OverlayLineSeries>>(new Map());
  const panelSeriesRef = useRef<Map<string, OverlayLineSeries>>(new Map());
  const annotationStateRef = useRef<OverlayAnnotationState>({ labels: [], boxes: [] });
  const annotationLayerRef = useRef<IndicatorAnnotationLayerHandle>(null);
  const mtfProviderRef = useRef(createClientMtfCandleProvider("live"));
  const indicatorLiveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const [chartModuleError, setChartModuleError] = useState<string | null>(null);
  const [activeTool, setActiveTool] = useState<ChartInteractionTool>("select");
  const [chartSize, setChartSize] = useState({ width: 0, height: defaultChartHeight() });
  const [drawingsVisible, setDrawingsVisible] = useState(true);
  const [runtimeTick, setRuntimeTick] = useState(0);
  const { appearance, themeId, preference, setThemeId, setCustomColors, replacePreference } =
    useChartAppearance();
  const [alertDrawerOpen, setAlertDrawerOpen] = useState(false);
  const [catalogOpen, setCatalogOpen] = useState(initialCatalogOpen);
  const [indicatorPanelOpen, setIndicatorPanelOpen] = useState(true);
  const appearanceRef = useRef(appearance);
  appearanceRef.current = appearance;

  useEffect(() => {
    if (initialCatalogOpen) {
      setCatalogOpen(true);
    }
  }, [initialCatalogOpen]);

  useEffect(() => {
    saveChartSession(layoutId, {
      symbol,
      timeframe,
      priceFeed,
    });
  }, [layoutId, symbol, timeframe, priceFeed]);
  const [loadState, setLoadState] = useState<LoadState>(() =>
    oandaConfigured
      ? {
          status: "loading",
          symbol: DEFAULT_SYMBOL,
          timeframe: DEFAULT_TIMEFRAME,
        }
      : {
          status: "error",
          title: "Configuration",
          message: CONFIG_MESSAGE,
          symbol: DEFAULT_SYMBOL,
          timeframe: DEFAULT_TIMEFRAME,
        },
  );

  const ensureChartRuntime = useCallback(async (): Promise<ChartRuntime | null> => {
    if (runtimeRef.current) {
      return runtimeRef.current;
    }
    if (!containerRef.current) {
      return null;
    }
    if (!runtimePromiseRef.current) {
      const initGeneration = chartInitGenerationRef.current;
      runtimePromiseRef.current = createChartRuntime(containerRef.current, {
        width: containerRef.current.clientWidth,
        height: defaultChartHeight(),
        appearance: appearanceRef.current,
      })
        .then((runtime) => {
          if (initGeneration !== chartInitGenerationRef.current) {
            if (runtimeRef.current) {
              return runtimeRef.current;
            }
            return null;
          }
          if (runtimeRef.current) {
            return runtimeRef.current;
          }
          runtimeRef.current = runtime;
          applyChartAppearanceToRuntime(runtime, appearanceRef.current);
          setChartModuleError(null);
          setChartSize({
            width: containerRef.current!.clientWidth,
            height: defaultChartHeight(),
          });
          setRuntimeTick((n) => n + 1);
          return runtime;
        })
        .catch((err: unknown) => {
          runtimePromiseRef.current = null;
          const message =
            err instanceof Error ? err.message : "Failed to load chart module.";
          setChartModuleError(message);
          throw err;
        });
    }
    return runtimePromiseRef.current;
  }, []);

  const fitContent = useCallback(() => {
    runtimeRef.current?.chart.timeScale().fitContent();
  }, []);

  const handleRefresh = useCallback(() => {
    setRefreshToken((n) => n + 1);
  }, []);

  useEffect(() => {
    const refreshAlertBadges = () => {
      const ids = new Set<string>();
      for (const rule of loadAlertRules(layoutId)) {
        if (!rule.enabled) {
          continue;
        }
        for (const cond of rule.conditions) {
          const ref = cond.reference;
          if (ref.kind === "drawing" && ref.drawingId) {
            ids.add(ref.drawingId);
          }
        }
      }
      setDrawingIdsWithAlerts(ids);
    };
    refreshAlertBadges();
    window.addEventListener(ALERTS_CHANGED_EVENT, refreshAlertBadges);
    return () => window.removeEventListener(ALERTS_CHANGED_EVENT, refreshAlertBadges);
  }, [layoutId]);

  useEffect(() => {
    if (indicatorLiveTimerRef.current) {
      clearTimeout(indicatorLiveTimerRef.current);
      indicatorLiveTimerRef.current = null;
    }
  }, [instances]);

  useEffect(() => {
    mtfProviderRef.current = createClientMtfCandleProvider(
      priceFeed === "warehouse" ? "warehouse" : "live",
    );
  }, [priceFeed]);

  const syncIndicatorOverlays = useCallback(
    async (runtime: ChartRuntime, candles: ChartCandle[]) => {
      const mtf = await resolveMtfCandlesForInstances(
        mtfProviderRef.current,
        indicatorScope.symbol,
        indicatorScope.timeframe,
        candles,
        instancesRef.current,
      );
      const { outputs, errors } = runIndicatorInstances(candles, instancesRef.current, {
        mtfCandlesByTimeframe: mtf.byTimeframe,
      });

      const messages = [...errors, ...mtf.errors];
      setIndicatorError(messages.length > 0 ? messages.join(" ") : null);
      const lc = await import("lightweight-charts");
      await syncIndicatorChartOutputs(
        runtime.chart as ChartWithSeries,
        runtime.series,
        outputs,
        overlaySeriesRef.current,
        panelSeriesRef.current,
        annotationStateRef.current,
        lc,
      );
      annotationLayerRef.current?.notifyAnnotationsChanged();
    },
    [indicatorScope.symbol, indicatorScope.timeframe],
  );

  const applyCandlesToChart = useCallback(
    async (candles: ChartCandle[], opts?: { fit?: boolean; preserveRange?: boolean }) => {
      const runtime = runtimeRef.current;
      if (!runtime) {
        return;
      }
      const previousRange = opts?.preserveRange
        ? runtime.chart.timeScale().getVisibleLogicalRange()
        : null;
      const previousLen = candleBufferRef.current.length;
      runtime.series.setData(toSeriesData(candles));
      runtime.volumeSeries.setData(toVolumeSeriesData(candles, appearanceRef.current));
      await syncIndicatorOverlays(runtime, candles);
      if (opts?.preserveRange && previousRange) {
        const added = candles.length - previousLen;
        if (added > 0) {
          runtime.chart.timeScale().setVisibleLogicalRange({
            from: previousRange.from + added,
            to: previousRange.to + added,
          });
        }
      } else if (opts?.fit !== false) {
        runtime.chart.timeScale().fitContent();
      }
    },
    [syncIndicatorOverlays],
  );

  useEffect(() => {
    const runtime = runtimeRef.current;
    if (!runtime) {
      return;
    }
    const settings = appearanceRef.current;
    applyChartAppearanceToRuntime(runtime, settings);
    applyVolumePaneVisibility(runtime, preference.showVolumePane !== false);
    const candles = candleBufferRef.current;
    if (candles.length > 0) {
      runtime.volumeSeries.setData(toVolumeSeriesData(candles, settings));
    }
  }, [themeId, runtimeTick, appearance, preference.showVolumePane]);

  const scheduleIndicatorLiveSync = useCallback(
    (runtime: ChartRuntime, candles: ChartCandle[], immediate = false) => {
      if (indicatorLiveTimerRef.current) {
        clearTimeout(indicatorLiveTimerRef.current);
      }
      const run = () => {
        void syncIndicatorOverlays(runtime, candles);
      };
      if (immediate) {
        run();
        return;
      }
      indicatorLiveTimerRef.current = setTimeout(run, 450);
    },
    [syncIndicatorOverlays],
  );

  const applyLiveHandoff = useCallback(
    (result: {
      buffer: ChartCandle[];
      kind: "none" | "forming" | "completed" | "append";
    }) => {
      if (result.kind === "none") {
        return;
      }
      const runtime = runtimeRef.current;
      if (!runtime) {
        return;
      }
      candleBufferRef.current = result.buffer;
      const last = result.buffer[result.buffer.length - 1];
      if (!last) {
        return;
      }
      applyIncrementalBarToRuntime(runtime, last, appearanceRef.current);
      if (result.kind === "completed" || result.kind === "append") {
        scheduleIndicatorLiveSync(runtime, result.buffer, true);
      } else {
        scheduleIndicatorLiveSync(runtime, result.buffer, false);
      }
      setLoadState((prev) => {
        if (prev.status !== "ready") {
          return prev;
        }
        return {
          ...prev,
          data: { ...prev.data, candles: result.buffer, count: result.buffer.length },
        };
      });
    },
    [scheduleIndicatorLiveSync],
  );

  const liveTailEnabled =
    loadState.status === "ready" &&
    (priceFeed === "live" || priceFeed === "warehouse") &&
    isCanonicalSymbol(symbol);

  const { liveState } = useChartLiveTail({
    enabled: liveTailEnabled,
    symbol: symbol as CanonicalSymbol,
    timeframe,
    getBuffer: () => candleBufferRef.current,
    onHandoff: applyLiveHandoff,
  });

  const loadOlderWarehouseChunk = useCallback(async () => {
    if (loadingOlderRef.current || !usesWarehouseInitialLoad(priceFeed)) {
      return;
    }
    const meta = warehouseMetaRef.current;
    const oldestSec = oldestBarTimeSec(candleBufferRef.current);
    if (!meta || oldestSec === null) {
      return;
    }
    const oldestMs = oldestSec * 1000;
    if (meta.firstTimestampMs !== null && oldestMs <= meta.firstTimestampMs) {
      return;
    }
    loadingOlderRef.current = true;
    setLoadingOlderUi(true);
    try {
      const params = new URLSearchParams({
        symbol,
        timeframe,
        beforeMs: String(oldestMs),
        count: String(CHART_WAREHOUSE_CHUNK_BARS),
      });
      const res = await fetch(`/api/warehouse/candles?${params.toString()}`);
      const json: unknown = await res.json();
      if (!res.ok) {
        return;
      }
      const body = json as { candles: ChartCandle[] };
      if (body.candles.length === 0) {
        return;
      }
      const merged = prependOlderCandles(candleBufferRef.current, body.candles);
      candleBufferRef.current = merged;
      await applyCandlesToChart(merged, { fit: false, preserveRange: true });
      setLoadState((prev) => {
        if (prev.status !== "ready") {
          return prev;
        }
        return {
          ...prev,
          data: { ...prev.data, candles: merged, count: merged.length },
        };
      });
    } finally {
      loadingOlderRef.current = false;
      setLoadingOlderUi(false);
    }
  }, [applyCandlesToChart, priceFeed, symbol, timeframe]);

  useEffect(() => {
    const onResize = () => {
      const container = containerRef.current;
      if (container && runtimeRef.current) {
        const height = chartFullscreen
          ? Math.max(420, window.innerHeight - 220)
          : defaultChartHeight();
        runtimeRef.current.chart.applyOptions({
          width: container.clientWidth,
          height,
        });
        setChartSize({ width: container.clientWidth, height });
      }
    };
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, [chartFullscreen, chartSize.height]);

  useEffect(() => {
    if (!runtimeRef.current) {
      return;
    }
    runtimeRef.current.chart.applyOptions({
      width: chartSize.width,
      height: chartSize.height,
    });
  }, [chartSize.width, chartSize.height]);

  useEffect(() => {
    const overlays = overlaySeriesRef.current;
    const panels = panelSeriesRef.current;
    const container = containerRef.current;
    return () => {
      chartInitGenerationRef.current += 1;
      const runtime = runtimeRef.current;
      if (runtime) {
        clearIndicatorChartOutputs(
          runtime.chart as ChartWithSeries,
          overlays,
          panels,
          annotationStateRef.current,
          runtime.series,
        );
      }
      runtime?.chart.remove();
      runtimeRef.current = null;
      runtimePromiseRef.current = null;
      if (container) {
        container.replaceChildren();
      }
    };
  }, []);

  useEffect(() => {
    const runtime = runtimeRef.current;
    if (
      !runtime ||
      loadState.status !== "ready" ||
      !usesWarehouseInitialLoad(priceFeed)
    ) {
      return;
    }
    const handler = () => {
      const range = runtime.chart.timeScale().getVisibleLogicalRange();
      if (!range) {
        return;
      }
      if (range.from <= CHART_LOAD_OLDER_THRESHOLD_BARS) {
        void loadOlderWarehouseChunk();
      }
    };
    runtime.chart.timeScale().subscribeVisibleLogicalRangeChange(handler);
    return () => {
      runtime.chart.timeScale().unsubscribeVisibleLogicalRangeChange(handler);
    };
  }, [loadState.status, priceFeed, symbol, loadOlderWarehouseChunk]);

  useEffect(() => {
    if (loadState.status !== "ready") {
      return;
    }
    void ensureChartRuntime().then((runtime) => {
      if (!runtime) {
        return;
      }
      void syncIndicatorOverlays(runtime, candleBufferRef.current);
    });
  }, [loadState, instances, ensureChartRuntime, syncIndicatorOverlays]);

  useEffect(() => {
    if (!oandaConfigured && priceFeed === "live") {
      setLoadState({
        status: "error",
        title: "Configuration",
        message: CONFIG_MESSAGE,
        symbol,
        timeframe,
      });
      return;
    }

    const seq = ++requestSeq.current;
    const controller = new AbortController();
    const timeoutId = window.setTimeout(() => controller.abort(), 25_000);

    setLoadState({ status: "loading", symbol, timeframe });
    candleBufferRef.current = [];
    warehouseMetaRef.current = null;
    void ensureChartRuntime().then((runtime) => {
      if (runtime) {
        runtime.series.setData([]);
        clearIndicatorChartOutputs(
          runtime.chart as ChartWithSeries,
          overlaySeriesRef.current,
          panelSeriesRef.current,
          annotationStateRef.current,
          runtime.series,
        );
      }
    });

    async function load() {
      try {
        const warehouseInitial = usesWarehouseInitialLoad(priceFeed);
        if (warehouseInitial && !isWarehouseChartSymbol(symbol)) {
          setLoadState({
            status: "error",
            title: "Application",
            message:
              "This symbol is not enabled for warehouse chart preview. Import data first or use OANDA Live.",
            symbol,
            timeframe,
          });
          return;
        }
        if (
          priceFeed === "live" &&
          !isSupportedSymbol(symbol) &&
          !getInstrument(symbol)?.liveOandaChart &&
          getInstrument(symbol)?.historicalSource !== "binance"
        ) {
          setLoadState({
            status: "error",
            title: "Application",
            message: "Live chart is not available for this symbol with configured providers.",
            symbol,
            timeframe,
          });
          return;
        }

        const params = new URLSearchParams({ symbol, timeframe });
        if (warehouseInitial) {
          params.set("count", String(CHART_WAREHOUSE_INITIAL_BARS));
        } else {
          params.set("count", String(MAX_CANDLE_COUNT));
        }
        const url = warehouseInitial
          ? `/api/warehouse/candles?${params.toString()}`
          : `/api/candles?${params.toString()}`;
        const res = await fetch(url, {
          signal: controller.signal,
        });
        const json: unknown = await res.json();
        if (seq !== requestSeq.current) {
          return;
        }
        if (!res.ok) {
          const err = json as { error?: { code?: string; message?: string } };
          const code = err.error?.code ?? "APP_ERROR";
          setLoadState({
            status: "error",
            title: errorTitle(code),
            message: err.error?.message ?? "Unable to load chart data.",
            symbol,
            timeframe,
          });
          runtimeRef.current?.series.setData([]);
          return;
        }
        const wh = json as {
          dataSource?: string;
          symbol: SupportedSymbol;
          timeframe: SupportedTimeframe;
          source: string;
          volumeType: CandlesResponseBody["volumeType"];
          count?: number;
          candles: ChartCandle[];
          metadata?: WarehouseMeta;
        };
        const candles = wh.candles;
        candleBufferRef.current = candles;
        if (warehouseInitial && wh.metadata) {
          warehouseMetaRef.current = wh.metadata;
        }
        const data: CandlesResponseBody = {
          symbol: wh.symbol,
          timeframe: wh.timeframe,
          source: wh.source,
          volumeType: wh.volumeType,
          count: wh.count ?? candles.length,
          candles,
        };
        const feedLabel = warehouseInitial
          ? wh.dataSource === "TRADINGBITE_WAREHOUSE"
            ? "TRADINGBITE WAREHOUSE"
            : "WAREHOUSE"
          : "OANDA LIVE/API";
        if (
          !chartResponseMatchesRequest(
            { symbol, timeframe },
            { symbol: data.symbol, timeframe: data.timeframe },
          )
        ) {
          return;
        }
        const runtime = await ensureChartRuntime();
        if (!runtime || seq !== requestSeq.current) {
          return;
        }
        await applyCandlesToChart(candles, { fit: true });
        setLoadState({
          status: "ready",
          data,
          symbol,
          timeframe,
          feedLabel,
          warehouseMeta: wh.metadata,
        });
      } catch {
        if (seq !== requestSeq.current) {
          return;
        }
        setLoadState({
          status: "error",
          title: "Application",
          message: controller.signal.aborted
            ? "Chart data request timed out."
            : "Failed to load chart data.",
          symbol,
          timeframe,
        });
        runtimeRef.current?.series.setData([]);
      } finally {
        window.clearTimeout(timeoutId);
      }
    }

    void load();
    return () => {
      controller.abort();
      window.clearTimeout(timeoutId);
    };
  }, [
    oandaConfigured,
    symbol,
    timeframe,
    refreshToken,
    priceFeed,
    ensureChartRuntime,
    applyCandlesToChart,
  ]);

  const chartReady = loadState.status === "ready";
  const drawingScope = useMemo(
    () => ({ symbol, timeframe, layoutId }),
    [symbol, timeframe, layoutId],
  );

  const localPointFromWrap = (e: React.PointerEvent | PointerEvent) => {
    const el = chartWrapRef.current;
    if (!el) {
      return null;
    }
    const rect = el.getBoundingClientRect();
    return { x: e.clientX - rect.left, y: e.clientY - rect.top };
  };

  const onWrapPointerDownCapture = (e: React.PointerEvent) => {
    if (!chartReady || drawingLayerRef.current?.isInteractionSuspended()) {
      return;
    }
    const pt = localPointFromWrap(e);
    if (!pt) {
      return;
    }
    const consumed = drawingLayerRef.current?.pointerDown(pt.x, pt.y, activeTool) ?? false;
    if (consumed) {
      e.preventDefault();
      e.stopPropagation();
      chartWrapRef.current?.setPointerCapture(e.pointerId);
    }
  };

  const onWrapPointerMoveCapture = (e: React.PointerEvent) => {
    if (drawingLayerRef.current?.isInteractionSuspended()) {
      return;
    }
    const pt = localPointFromWrap(e);
    if (!pt) {
      return;
    }
    drawingLayerRef.current?.pointerMove(pt.x, pt.y, activeTool);
  };

  const onWrapPointerUpCapture = (e: React.PointerEvent) => {
    drawingLayerRef.current?.pointerUp();
    if (chartWrapRef.current?.hasPointerCapture(e.pointerId)) {
      chartWrapRef.current.releasePointerCapture(e.pointerId);
    }
  };

  const toggleChartFullscreen = async () => {
    const el = workspaceRef.current ?? chartWrapRef.current;
    if (!el) {
      return;
    }
    try {
      if (!document.fullscreenElement) {
        await el.requestFullscreen();
        setChartFullscreen(true);
      } else {
        await document.exitFullscreen();
        setChartFullscreen(false);
      }
    } catch {
      setChartFullscreen((v) => !v);
    }
  };

  useEffect(() => {
    const onFs = () => {
      const fs =
        document.fullscreenElement === workspaceRef.current ||
        document.fullscreenElement === chartWrapRef.current;
      setChartFullscreen(fs);
      const container = containerRef.current;
      if (container && runtimeRef.current) {
        const height = fs
          ? Math.max(320, window.innerHeight - (indicatorPanelOpen ? 140 : 100))
          : defaultChartHeight();
        runtimeRef.current.chart.applyOptions({
          width: container.clientWidth,
          height,
        });
        setChartSize({ width: container.clientWidth, height });
      }
    };
    document.addEventListener("fullscreenchange", onFs);
    return () => document.removeEventListener("fullscreenchange", onFs);
  }, []);

  return (
    <div
      ref={workspaceRef}
      className={`chart-workspace${chartFullscreen ? " chart-workspace-fs" : ""}${
        themeId === "light" ? " chart-theme-light" : ""
      }`}
    >
      <div className="chart-toolbar" role="toolbar" aria-label="Chart controls">
        <label className="toolbar-field">
          <span className="toolbar-label">Symbol</span>
          <select
            value={symbol}
            onChange={(e) => setSymbol(e.target.value as ChartSymbol)}
            aria-label="Symbol"
          >
            {symbolOptions(priceFeed).map((s) => (
              <option key={s} value={s}>
                {formatChartSymbolLabel(s)}
              </option>
            ))}
          </select>
        </label>
        <label className="toolbar-field">
          <span className="toolbar-label">Timeframe</span>
          <select
            value={timeframe}
            onChange={(e) => setTimeframe(e.target.value as SupportedTimeframe)}
            aria-label="Timeframe"
          >
            {SUPPORTED_TIMEFRAMES.map((tf) => (
              <option key={tf} value={tf}>
                {tf}
              </option>
            ))}
          </select>
        </label>
        <label className="toolbar-field">
          <span className="toolbar-label">Theme</span>
          <select
            value={themeId}
            onChange={(e) => setThemeId(e.target.value === "light" ? "light" : "dark")}
            aria-label="Chart theme"
          >
            <option value="dark">Dark</option>
            <option value="light">Light</option>
          </select>
        </label>
        <label className="toolbar-field">
          <span className="toolbar-label">Layout</span>
          <select
            value={layoutId}
            onChange={(e) => {
              const next = e.target.value as ChartLayoutId;
              saveChartSession(layoutId, { symbol, timeframe, priceFeed });
              setLayoutId(next);
              saveActiveChartLayoutId(next);
              const saved = loadChartSession(next);
              if (saved?.symbol && (isSupportedSymbol(saved.symbol) || isCanonicalSymbol(saved.symbol))) {
                setSymbol(saved.symbol as ChartSymbol);
              } else {
                setSymbol(DEFAULT_SYMBOL);
              }
              setTimeframe(saved?.timeframe ?? DEFAULT_TIMEFRAME);
              setPriceFeed(saved?.priceFeed ?? "live");
              setSelectedDrawing(null);
              drawingLayerRef.current?.clearSelection();
            }}
            aria-label="Chart layout profile"
          >
            {CHART_LAYOUTS.map((l) => (
              <option key={l.id} value={l.id}>
                {l.label}
              </option>
            ))}
          </select>
        </label>
        <label className="toolbar-field">
          <span className="toolbar-label">Data</span>
          <select
            value={priceFeed}
            onChange={(e) => {
              const next = e.target.value as PriceFeed;
              setPriceFeed(next);
              const allowed = symbolOptions(next);
              if (!allowed.includes(symbol)) {
                setSymbol(DEFAULT_SYMBOL);
              }
            }}
            aria-label="Price data source"
          >
            <option value="live">OANDA Live</option>
            <option value="warehouse">Warehouse</option>
          </select>
        </label>
        <button type="button" className="toolbar-btn" onClick={handleRefresh}>
          Refresh
        </button>
        <button type="button" className="toolbar-btn" onClick={fitContent}>
          Fit
        </button>
        <button type="button" className="toolbar-btn" onClick={() => void toggleChartFullscreen()}>
          {chartFullscreen ? "Exit full chart" : "Full chart"}
        </button>
        <button
          type="button"
          className="toolbar-btn"
          onClick={() => setAlertDrawerOpen(true)}
        >
          Alerts
        </button>
        <button type="button" className="toolbar-btn" onClick={() => setCatalogOpen(true)}>
          Indicators
        </button>
        {chartFullscreen ? (
          <button
            type="button"
            className="toolbar-btn toolbar-btn-sm"
            onClick={() => setIndicatorPanelOpen((v) => !v)}
          >
            {indicatorPanelOpen ? "Hide panel" : "Show panel"}
          </button>
        ) : null}
        <label className="toolbar-field toolbar-inline">
          <span className="toolbar-check-row">
            <input
              type="checkbox"
              checked={preference.showVolumePane !== false}
              onChange={(e) =>
                replacePreference({ ...preference, showVolumePane: e.target.checked })
              }
              aria-label="Show volume pane"
            />
            <span className="toolbar-label">Volume</span>
          </span>
        </label>
        <ChartAppearancePanel
          preference={preference}
          onCustomColorsChange={(colors) => setCustomColors(colors)}
          onResetColors={() => setCustomColors(undefined)}
          onPreferenceChange={replacePreference}
        />
        <label className="toolbar-field toolbar-inline">
          <span className="toolbar-check-row">
            <input
              type="checkbox"
              checked={drawingsVisible}
              onChange={(e) => setDrawingsVisible(e.target.checked)}
              aria-label="Show drawings on chart"
            />
            <span className="toolbar-label">Drawings</span>
          </span>
        </label>
      </div>

      <div className="chart-body-row">
        <div className="chart-main-column">
      <DrawingToolbar
        activeTool={activeTool}
        onToolChange={setActiveTool}
        selectedDrawing={selectedDrawing}
      />

      {indicatorError && (
        <p className="status banner indicator-error" role="alert">
          {indicatorError}
        </p>
      )}

      {chartModuleError && (
        <div className="error-panel" role="alert">
          <strong>Application problem</strong>
          <p>{chartModuleError}</p>
        </div>
      )}

      {loadState.status === "loading" && (
        <p className="status banner" aria-live="polite">
          Loading {formatChartSymbolLabel(loadState.symbol)} {loadState.timeframe}…
        </p>
      )}

      {loadState.status === "error" && (
        <div className="error-panel" role="alert">
          <strong>{loadState.title} problem</strong>
          <p>{loadState.message}</p>
        </div>
      )}

      <div
        ref={chartWrapRef}
        className={`chart-wrap${loadState.status === "loading" ? " chart-dimmed" : ""}${
          isInteractiveDrawingTool(activeTool) ? " chart-wrap-drawing" : ""
        }${chartFullscreen ? " chart-wrap-fullscreen" : ""}`}
        onPointerDownCapture={onWrapPointerDownCapture}
        onPointerMoveCapture={onWrapPointerMoveCapture}
        onPointerUpCapture={onWrapPointerUpCapture}
        onPointerCancelCapture={onWrapPointerUpCapture}
      >
        {selectedDrawing ? (
          <div className="drawing-style-float" role="region" aria-label="Selected drawing">
            <DrawingStylePanel
              drawing={selectedDrawing}
              onPatch={(stylePatch) =>
                drawingLayerRef.current?.patchDrawingStyle(selectedDrawing.id, stylePatch)
              }
            />
            <button
              type="button"
              className="toolbar-btn toolbar-btn-sm"
              onClick={() => setCreateAlertOpen(true)}
            >
              Alert
            </button>
            <button
              type="button"
              className="toolbar-btn toolbar-btn-sm"
              onClick={() => drawingLayerRef.current?.clearSelection()}
              aria-label="Clear drawing selection"
            >
              ×
            </button>
          </div>
        ) : null}
        <div ref={containerRef} className="chart-container" data-testid="chart-container" />
        {runtimeRef.current && runtimeTick >= 0 && loadState.status !== "error" && (
          <>
            <IndicatorAnnotationLayer
              ref={annotationLayerRef}
              chart={runtimeRef.current.chart}
              series={runtimeRef.current.series}
              annotationsRef={annotationStateRef}
              appearance={appearance}
              width={chartSize.width}
              height={chartSize.height}
            />
            <ChartDrawingLayer
              ref={drawingLayerRef}
              scope={drawingScope}
              chart={runtimeRef.current.chart}
              series={runtimeRef.current.series}
              activeTool={activeTool}
              width={chartSize.width}
              height={chartSize.height}
              visible={drawingsVisible}
              drawingIdsWithAlerts={drawingIdsWithAlerts}
              ohlcSnapCandles={
                loadState.status === "ready" ? loadState.data.candles : candleBufferRef.current
              }
              onSelectionChange={setSelectedDrawing}
            />
          </>
        )}
      </div>

      {loadState.status === "ready" && (
        <p className="chart-meta">
          {formatChartSymbolLabel(loadState.data.symbol)} · {loadState.data.timeframe} ·{" "}
          {loadState.data.candles.length} bars loaded
          {loadState.warehouseMeta
            ? ` (${loadState.warehouseMeta.rowCount.toLocaleString()} in warehouse)`
            : ""}{" "}
          · {loadState.feedLabel} · provider {loadState.data.source} · volume type{" "}
          {loadState.data.volumeType}
          {usesWarehouseInitialLoad(priceFeed)
            ? ` · pan left to load older history${loadingOlderUi ? " · loading older…" : ""}`
            : ""}
          {smaEnabled ? ` · SMA(${smaPeriod}) overlay` : ""}
          {srEnabled ? " · S/R Finder 2.1 overlay" : ""}
          {liveTailEnabled ? ` · live ${liveState}` : ""}
        </p>
      )}
        </div>
        {indicatorPanelOpen ? (
          <ChartIndicatorPanel
            scope={indicatorScope}
            instances={instances}
            patchInstance={patchInstance}
            replaceInstances={replaceInstances}
            onIndicatorError={setIndicatorError}
            onOpenCatalog={() => setCatalogOpen(true)}
          />
        ) : null}
      </div>

      <IndicatorCatalogModal
        open={catalogOpen}
        onClose={() => setCatalogOpen(false)}
        scope={indicatorScope}
        onAdded={() => replaceInstances(loadIndicatorInstances(indicatorScope))}
      />

      <AlertDrawer
        open={alertDrawerOpen}
        onClose={() => setAlertDrawerOpen(false)}
        layoutId={layoutId}
        defaultSymbol={
          isCanonicalSymbol(symbol)
            ? symbol
            : DEFAULT_SYMBOL
        }
        defaultTimeframe={timeframe}
      />

      <CreateAlertModal
        open={createAlertOpen}
        onClose={() => setCreateAlertOpen(false)}
        layoutId={layoutId}
        symbol={
          isSupportedSymbol(symbol)
            ? symbol
            : isCanonicalSymbol(symbol)
              ? symbol
              : DEFAULT_SYMBOL
        }
        timeframe={timeframe}
        drawing={selectedDrawing}
      />
    </div>
  );
}
