'use client';

import { useState, useRef, useEffect, useCallback } from 'react';
import Toolbox, { Tool } from '@/components/Toolbox';
import FileUpload from '@/components/FileUpload';
import CandleChart, { CandleChartHandle } from '@/components/CandleChart';
import ChartSelector from '@/components/ChartSelector';
import PredictionPanel from '@/components/PredictionPanel';
import SpanAnnotationList from '@/components/SpanAnnotationList';
import TalibPatternPanel from '@/components/TalibPatternPanel';
import TrainingPanel from '@/components/TrainingPanel';
import { ThemeToggle } from '@/components/ThemeToggle';
import KeyboardShortcutsModal from '@/components/KeyboardShortcutsModal';
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogClose,
} from '@/components/ui/dialog';
import { useTheme } from 'next-themes';
import { Settings, Tag, Layers } from 'lucide-react';
import type { PredictionState, PredictionSpan, PerCandlePrediction, ModelInfoResponse, Disagreement, DisagreementType, PredictionSummary } from '@/types/predictions';
import type { Candle, SpanAnnotation, SpanLabelType } from '@/types';
import type { Geometry } from '@/types/annotations';

/**
 * Calculate overlap between two time ranges
 * Returns overlap ratio (0-1) relative to the smaller span
 */
function calculateOverlap(
  start1: number,
  end1: number,
  start2: number,
  end2: number
): number {
  const overlapStart = Math.max(start1, start2);
  const overlapEnd = Math.min(end1, end2);
  const overlap = Math.max(0, overlapEnd - overlapStart);
  const minLength = Math.min(end1 - start1, end2 - start2);
  return minLength > 0 ? overlap / minLength : 0;
}

/**
 * Detect disagreements between human annotations and model predictions
 * Returns summary with agreement/disagreement counts and detailed disagreement list
 */
function detectDisagreements(
  humanSpans: SpanAnnotation[],
  predictionSpans: PredictionSpan[],
  overlapThreshold: number = 0.5
): PredictionSummary {
  const disagreements: Disagreement[] = [];
  const matchedHumanIds = new Set<number>();
  const matchedPredictionIndices = new Set<number>();
  

  // Find matches and label mismatches
  humanSpans.forEach((humanSpan) => {
    predictionSpans.forEach((predSpan, predIdx) => {
      const overlap = calculateOverlap(
        humanSpan.start_time,
        humanSpan.end_time,
        predSpan.start_time,
        predSpan.end_time
      );

      if (overlap >= overlapThreshold) {
        matchedHumanIds.add(humanSpan.id);
        matchedPredictionIndices.add(predIdx);

        // Check for label mismatch
        if (humanSpan.label !== predSpan.label) {
          disagreements.push({
            type: 'label_mismatch',
            humanSpan: {
              id: humanSpan.id,
              label: humanSpan.label,
              start_time: humanSpan.start_time,
              end_time: humanSpan.end_time,
            },
            predictionSpan: predSpan,
            overlap_ratio: overlap,
          });
        }
      }
    });
  });

  // Find spans missed by model (human annotation but no prediction)
  humanSpans.forEach((humanSpan) => {
    if (!matchedHumanIds.has(humanSpan.id)) {
      disagreements.push({
        type: 'missed_by_model',
        humanSpan: {
          id: humanSpan.id,
          label: humanSpan.label,
          start_time: humanSpan.start_time,
          end_time: humanSpan.end_time,
        },
      });
    }
  });

  // Find spans missed by human (prediction but no human annotation)
  predictionSpans.forEach((predSpan, idx) => {
    if (!matchedPredictionIndices.has(idx)) {
      disagreements.push({
        type: 'missed_by_human',
        predictionSpan: predSpan,
      });
    }
  });

  // Calculate agreements (matched spans with same label)
  const agreements = matchedHumanIds.size - disagreements.filter(d => d.type === 'label_mismatch').length;

  return {
    total_predictions: predictionSpans.length,
    total_human_annotations: humanSpans.length,
    agreements,
    disagreements,
  };
}

// SpanAnnotation, SpanLabelType are imported from @/types above.
// Chart: kept local because shared Chart.created_at is Date|number but local usage expects number.
interface Chart {
  id: number;
  name: string;
  created_at: number;
}

// Annotation: kept local because shared Annotation has geometry: Geometry|null but local usage passes geometry: any.
interface Annotation {
  id: number;
  chart_id: number;
  timestamp: number;
  label_type: string;
  color: string | null;
  geometry: Geometry | null;
  created_at: number;
}

export default function Home() {
  const { theme, setTheme } = useTheme();
  const [settingsOpen, setSettingsOpen] = useState(false);
  const [shortcutsOpen, setShortcutsOpen] = useState(false);
  const [activeTool, setActiveTool] = useState<Tool | 'span'>(null);
  const [selectedColor, setSelectedColor] = useState('#3b82f6');
  const [selectedLabelId, setSelectedLabelId] = useState<number | null>(null);
  const [annotations, setAnnotations] = useState<Annotation[]>([]);
  const [charts, setCharts] = useState<Chart[]>([]);
  const [activeChartId, setActiveChartId] = useState<number | null>(null);
  const chartRef = useRef<CandleChartHandle>(null);

  // Span annotation state
  const [spanAnnotations, setSpanAnnotations] = useState<SpanAnnotation[]>([]);
  const [selectedSpanId, setSelectedSpanId] = useState<number | null>(null);
  const [spanLabelTypes, setSpanLabelTypes] = useState<SpanLabelType[]>([]);

  // Prediction state
  const [predictionState, setPredictionState] = useState<PredictionState>({
    spans: [],
    perCandlePredictions: [],
    isLoading: false,
    error: null,
    modelInfo: null,
    visible: false,
    confidenceThreshold: 0.5,
    selectedLabels: new Set<string>(),
    autoPredict: false,
    cacheKey: null,
  });

  // Prediction cache: Map<cacheKey, { spans, predictions, modelVersion }>
  const predictionCacheRef = useRef<Map<string, {
    spans: PredictionSpan[];
    predictions: PerCandlePrediction[];
    modelVersion: string;
  }>>(new Map());

  // Ref to avoid stale closure over modelInfo in fetchPredictions/generateCacheKey
  const modelInfoRef = useRef(predictionState.modelInfo);
  const fetchPredictionsAbortRef = useRef<AbortController | null>(null);
  const fetchBatchAbortRef = useRef<AbortController | null>(null);
  useEffect(() => {
    modelInfoRef.current = predictionState.modelInfo;
  }, [predictionState.modelInfo]);

  // Model health state
  const [isModelOnline, setIsModelOnline] = useState(true);

  // Prediction summary state
  const [predictionSummary, setPredictionSummary] = useState<PredictionSummary | null>(null);
  
  // Disagreement filter state
  const [showOnlyDisagreements, setShowOnlyDisagreements] = useState(false);
  const [deleteAllDialogOpen, setDeleteAllDialogOpen] = useState(false);

  // Fetch charts list
  const fetchCharts = useCallback(async () => {
    try {
      const response = await fetch('/api/charts');
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
      const data = await response.json();
      setCharts(data);
      return data as Chart[];
    } catch (error) {
      console.error('Failed to fetch charts:', error);
      return [];
    }
  }, []);

  // Fetch annotations for active chart
  const fetchAnnotations = useCallback(async (chartId: number | null) => {
    if (!chartId) {
      setAnnotations([]);
      return;
    }
    try {
      const response = await fetch(`/api/annotations?chartId=${chartId}`);
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
      const data = await response.json();
      setAnnotations(data);
    } catch (error) {
      console.error('Failed to fetch annotations:', error);
    }
  }, []);

  // Fetch span annotations for active chart
  const fetchSpanAnnotations = useCallback(async (chartId: number | null) => {
    if (!chartId) {
      setSpanAnnotations([]);
      return;
    }
    try {
      const response = await fetch(`/api/span-annotations?chartId=${chartId}`);
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
      const data = await response.json();
      setSpanAnnotations(data);
    } catch (error) {
      console.error('Failed to fetch span annotations:', error);
    }
  }, []);

  // Fetch span label types
  const fetchSpanLabelTypes = useCallback(async () => {
    try {
      const response = await fetch('/api/span-label-types');
      if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
      const data = await response.json();
      setSpanLabelTypes(data);
    } catch (error) {
      console.error('Failed to fetch span label types:', error);
    }
  }, []);

  // Fetch charts and span label types on mount, auto-select the most recent chart
  useEffect(() => {
    const init = async () => {
      const chartList = await fetchCharts();
      await fetchSpanLabelTypes();
      if (chartList.length > 0) {
        setActiveChartId(chartList[0].id); // sorted by created_at desc
      }
    };
    init();
  }, [fetchCharts, fetchSpanLabelTypes]);

  // When activeChartId changes, refetch data
  useEffect(() => {
    if (activeChartId !== null) {
      chartRef.current?.refreshData();
      fetchAnnotations(activeChartId);
      fetchSpanAnnotations(activeChartId);
      setSelectedLabelId(null);
      setSelectedSpanId(null);
    }
  }, [activeChartId, fetchAnnotations, fetchSpanAnnotations]);

  const handleExport = () => {
    if (activeChartId) {
      window.location.href = `/api/export?chartId=${activeChartId}`;
    } else {
      window.location.href = '/api/export';
    }
  };

  const handleUploadSuccess = (chart: { id: number; name: string }) => {
    // Add new chart to list and select it
    const newChart: Chart = {
      id: chart.id,
      name: chart.name,
      created_at: Math.floor(Date.now() / 1000),
    };
    setCharts((prev) => [newChart, ...prev]);
    setActiveChartId(chart.id);
  };

  const handleAnnotationChange = async () => {
    await chartRef.current?.refreshData();
    await fetchAnnotations(activeChartId);
  };

  const handleSpanAnnotationsChange = async () => {
    await fetchSpanAnnotations(activeChartId);
  };

  const handleSelectedSpanChange = (spanId: number | null) => {
    setSelectedSpanId(spanId);
  };

  // Handle prediction click to convert to annotation
  const handlePredictionClick = useCallback(async (span: PredictionSpan, disagreementType: string | null) => {
    if (!activeChartId) return;

    // Find the span label type that matches the prediction label
    const matchingLabelType = spanLabelTypes.find((lt) => lt.name === span.label);
    
    if (!matchingLabelType) {
      console.warn(`No span label type found for prediction label: ${span.label}`);
      return;
    }

    // Create span annotation from prediction
    try {
      const response = await fetch('/api/span-annotations', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          chart_id: activeChartId,
          start_time: span.start_time,
          end_time: span.end_time,
          label: span.label,
          confidence: 3, // Default confidence for model-confirmed annotations
          source: disagreementType === 'label_mismatch' ? 'model_corrected' : 'model_confirmed',
          model_prediction: {
            label: span.label,
            confidence: span.avg_confidence,
          },
        }),
      });

      if (response.ok) {
        await fetchSpanAnnotations(activeChartId);
        // Show a brief notification (you could add a toast notification here)
        console.log(`Created span annotation from prediction: ${span.label}`);
      } else {
        console.error('Failed to create span annotation from prediction');
      }
    } catch (error) {
      console.error('Error creating span annotation from prediction:', error);
    }
  }, [activeChartId, spanLabelTypes, fetchSpanAnnotations]);

  // Handle prediction dismiss (save as negative annotation with label "O")
  const handlePredictionDismiss = useCallback(async (span: PredictionSpan, disagreementType: string | null) => {
    if (!activeChartId) return;

    // Create negative annotation with label "O"
    try {
      const response = await fetch('/api/span-annotations', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          chart_id: activeChartId,
          start_time: span.start_time,
          end_time: span.end_time,
          label: 'O', // "O" means "not a pattern"
          confidence: 5, // High confidence for explicit user correction
          source: 'human_correction',
          model_prediction: {
            label: span.label,
            confidence: span.avg_confidence,
          },
        }),
      });

      if (response.ok) {
        await fetchSpanAnnotations(activeChartId);
        console.log(`Dismissed prediction as "not a pattern": ${span.label}`);
      } else {
        console.error('Failed to save negative annotation');
      }
    } catch (error) {
      console.error('Error saving negative annotation:', error);
    }
  }, [activeChartId, fetchSpanAnnotations]);

  const handleDeleteSpan = async (spanId: number) => {
    try {
      const response = await fetch(`/api/span-annotations/${spanId}`, {
        method: 'DELETE',
      });
      if (response.ok) {
        await fetchSpanAnnotations(activeChartId);
        if (selectedSpanId === spanId) {
          setSelectedSpanId(null);
        }
      }
    } catch (error) {
      console.error('Failed to delete span annotation:', error);
    }
  };

  const handleDeleteAllAnnotations = async () => {
    if (!activeChartId) return;
    await Promise.all([
      fetch(`/api/span-annotations?chartId=${activeChartId}`, { method: 'DELETE' }),
      fetch(`/api/annotations?all=true&chartId=${activeChartId}`, { method: 'DELETE' }),
    ]);
    await Promise.all([
      fetchSpanAnnotations(activeChartId),
      fetchAnnotations(activeChartId),
      chartRef.current?.refreshData(),
    ]);
    setSelectedSpanId(null);
    setSelectedLabelId(null);
  };

  const handleLabelDelete = async (id: number) => {
    setAnnotations(annotations.filter((a) => a.id !== id));
    if (selectedLabelId === id) {
      setSelectedLabelId(null);
    }
  };

  const handleLabelSelect = (id: number) => {
    setSelectedLabelId(id === -1 ? null : id);
  };

  const handleSelectChart = (chartId: number) => {
    setActiveChartId(chartId);
  };

  const handleDeleteChart = async (chartId: number) => {
    try {
      const response = await fetch(`/api/charts/${chartId}`, { method: 'DELETE' });
      if (response.ok) {
        const remaining = charts.filter((c) => c.id !== chartId);
        setCharts(remaining);
        if (activeChartId === chartId) {
          setActiveChartId(remaining.length > 0 ? remaining[0].id : null);
        }
      }
    } catch (error) {
      console.error('Failed to delete chart:', error);
    }
  };

  // Fetch model info and initialize selected labels
  const fetchModelInfo = useCallback(async () => {
    try {
      const response = await fetch('/api/model/info');
      let data: any = null;
      try {
        data = await response.json();
      } catch {
        data = null;
      }

      if (!response.ok) {
        const message = data?.error || data?.detail || 'Model info unavailable';

        // Treat "no model" as online-but-unloaded so the selector still works.
        if (response.status === 503 && /no model/i.test(message)) {
          setIsModelOnline(true);
          setPredictionState((prev) => ({
            ...prev,
            modelInfo: null,
            selectedLabels: new Set(),
            error: null,
          }));
          return null;
        }

        setIsModelOnline(false);
        setPredictionState((prev) => ({
          ...prev,
          modelInfo: null,
          error: message,
        }));
        return null;
      }

      const modelInfo: ModelInfoResponse = data;
      setIsModelOnline(true);
      setPredictionState((prev) => ({
        ...prev,
        modelInfo,
        selectedLabels: new Set(modelInfo.labels),
        error: null,
      }));
      return modelInfo;
    } catch (error) {
      console.error('Failed to fetch model info:', error);
      setIsModelOnline(false);
      setPredictionState((prev) => ({
        ...prev,
        modelInfo: null,
        error: error instanceof Error ? error.message : 'Failed to fetch model info',
      }));
      return null;
    }
  }, []);

  // Generate cache key from chart, timerange, and model version
  const generateCacheKey = useCallback((chartId: number | null, modelVersion?: string | null) => {
    if (!chartId) return null;
    const version = modelVersion || modelInfoRef.current?.model_version || 'unknown';
    return `${chartId}_${version}`;
  }, []);

  // Fetch predictions for visible candles
  const fetchPredictions = useCallback(async (candles: Candle[]) => {
    if (!activeChartId || candles.length === 0) return;

    const currentModelInfo = modelInfoRef.current;
    const cacheKey = generateCacheKey(activeChartId, currentModelInfo?.model_version);

    // Check cache first
    if (cacheKey && predictionCacheRef.current.has(cacheKey)) {
      const cached = predictionCacheRef.current.get(cacheKey)!;
      if (cached.modelVersion === currentModelInfo?.model_version) {
        setPredictionState((prev) => ({
          ...prev,
          spans: cached.spans,
          perCandlePredictions: cached.predictions,
          cacheKey,
        }));
        return;
      }
    }

    // Abort any in-flight prediction request
    fetchPredictionsAbortRef.current?.abort();
    const controller = new AbortController();
    fetchPredictionsAbortRef.current = controller;

    setPredictionState((prev) => ({ ...prev, isLoading: true, error: null }));

    try {
      const response = await fetch('/api/predict', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ candles }),
        signal: controller.signal,
      });

      if (!response.ok) {
        let message = response.statusText || 'Prediction failed';
        try {
          const errorBody = await response.json();
          message = errorBody?.error || errorBody?.detail || message;
        } catch {
          // ignore parse errors
        }
        throw new Error(`Prediction failed: ${message}`);
      }

      const data = await response.json();

      // Cache the results (bounded: max 100 entries, FIFO eviction)
      if (cacheKey) {
        const cache = predictionCacheRef.current;
        if (cache.size >= 100) {
          const firstKey = cache.keys().next().value;
          if (firstKey !== undefined) cache.delete(firstKey);
        }
        cache.set(cacheKey, {
          spans: data.spans,
          predictions: data.predictions,
          modelVersion: data.model_info.model_version,
        });
      }

      setPredictionState((prev) => ({
        ...prev,
        spans: data.spans,
        perCandlePredictions: data.predictions,
        isLoading: false,
        cacheKey,
      }));
    } catch (error) {
      if (error instanceof Error && error.name === 'AbortError') return;
      console.error('Failed to fetch predictions:', error);
      setPredictionState((prev) => ({
        ...prev,
        isLoading: false,
        error: error instanceof Error ? error.message : 'Failed to fetch predictions',
      }));
    }
  }, [activeChartId, generateCacheKey]);

  // Toggle prediction visibility
  const togglePredictionVisibility = useCallback(() => {
    setPredictionState((prev) => ({ ...prev, visible: !prev.visible }));
  }, []);

  // Update confidence threshold
  const setConfidenceThreshold = useCallback((threshold: number) => {
    setPredictionState((prev) => ({ ...prev, confidenceThreshold: threshold }));
  }, []);

  // Toggle label selection
  const toggleLabelSelection = useCallback((label: string) => {
    setPredictionState((prev) => {
      const newSelected = new Set(prev.selectedLabels);
      if (newSelected.has(label)) {
        newSelected.delete(label);
      } else {
        newSelected.add(label);
      }
      return { ...prev, selectedLabels: newSelected };
    });
  }, []);

  // Toggle show only disagreements
  const toggleShowOnlyDisagreements = useCallback(() => {
    setShowOnlyDisagreements((prev) => !prev);
  }, []);

  // Handle on-demand prediction for visible candles
  const handleFetchVisiblePredictions = useCallback(() => {
    // This will be called by the PredictionPanel
    // The actual candles data will be fetched from the chart ref
    const candles = chartRef.current?.getVisibleCandles();
    if (candles && candles.length > 0) {
      fetchPredictions(candles);
    }
  }, [fetchPredictions]);

  // Handle model loaded via ModelSelector: refresh model info and clear prediction cache
  const handleModelLoaded = useCallback(async () => {
    predictionCacheRef.current = new Map();
    setPredictionState((prev) => ({ ...prev, spans: [], perCandlePredictions: [], visible: false }));
    await fetchModelInfo();
  }, [fetchModelInfo]);

  // Handle batch prediction for all candles
  const handleFetchBatchPredictions = useCallback(async () => {
    if (!activeChartId) return;

    // Abort any in-flight batch prediction request
    fetchBatchAbortRef.current?.abort();
    const controller = new AbortController();
    fetchBatchAbortRef.current = controller;

    setPredictionState((prev) => ({ ...prev, isLoading: true, error: null }));

    try {
      // Fetch chart data to get pair/timeframe info
      const chartResponse = await fetch(`/api/charts/${activeChartId}`, {
        signal: controller.signal,
      });
      if (!chartResponse.ok) {
        throw new Error('Failed to fetch chart info');
      }
      const chartData = await chartResponse.json();

      // Fetch candles for the chart
      const candlesResponse = await fetch(`/api/candles?chartId=${activeChartId}`, {
        signal: controller.signal,
      });
      if (!candlesResponse.ok) {
        throw new Error('Failed to fetch candles');
      }
      const candlesData = await candlesResponse.json();

      if (candlesData.length === 0) {
        throw new Error('No candles found for this chart');
      }

      // Use regular predict endpoint with all candles from the database
      const response = await fetch('/api/predict', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          candles: candlesData,
        }),
        signal: controller.signal,
      });

      if (!response.ok) {
        let message = response.statusText || 'Batch prediction failed';
        try {
          const errorBody = await response.json();
          message = errorBody?.error || errorBody?.detail || message;
        } catch {
          // ignore parse errors
        }
        throw new Error(`Batch prediction failed: ${message}`);
      }

      const data = await response.json();

      const cacheKey = generateCacheKey(activeChartId, data.model_info.model_version);
      if (cacheKey) {
        const cache = predictionCacheRef.current;
        if (cache.size >= 100) {
          const firstKey = cache.keys().next().value;
          if (firstKey !== undefined) cache.delete(firstKey);
        }
        cache.set(cacheKey, {
          spans: data.spans,
          predictions: data.predictions,
          modelVersion: data.model_info.model_version,
        });
      }

      setPredictionState((prev) => ({
        ...prev,
        spans: data.spans,
        perCandlePredictions: data.predictions,
        isLoading: false,
        cacheKey,
      }));
    } catch (error) {
      if (error instanceof Error && error.name === 'AbortError') return;
      console.error('Failed to fetch batch predictions:', error);
      setPredictionState((prev) => ({
        ...prev,
        isLoading: false,
        error: error instanceof Error ? error.message : 'Failed to fetch batch predictions',
      }));
    }
  }, [activeChartId, generateCacheKey]);

  // Clear prediction cache when model version changes
  useEffect(() => {
    if (predictionState.modelInfo) {
      const currentVersion = predictionState.modelInfo.model_version;
      // Clear cache entries with different model versions
      const newCache = new Map();
      for (const [key, value] of predictionCacheRef.current.entries()) {
        if (value.modelVersion === currentVersion) {
          newCache.set(key, value);
        }
      }
      predictionCacheRef.current = newCache;
    }
  }, [predictionState.modelInfo?.model_version]);

  // Health polling - check model status every 30 seconds when offline
  useEffect(() => {
    if (!isModelOnline) {
      const interval = setInterval(() => {
        fetchModelInfo();
      }, 30000);
      return () => clearInterval(interval);
    }
  }, [isModelOnline, fetchModelInfo]);

  // Initialize model info on mount
  useEffect(() => {
    fetchModelInfo();
  }, [fetchModelInfo]);

  // Compute prediction summary when predictions or span annotations change
  useEffect(() => {
    if (predictionState.visible && predictionState.spans.length > 0) {
      const summary = detectDisagreements(spanAnnotations, predictionState.spans);
      setPredictionSummary(summary);
    } else {
      setPredictionSummary(null);
    }
  }, [predictionState.visible, predictionState.spans, spanAnnotations]);

  // Keyboard handler for Delete/Backspace key and tool shortcuts
  useEffect(() => {
    const handleKeyDown = async (e: KeyboardEvent) => {
      // Ignore shortcuts when typing in an input/textarea/select
      const tag = (e.target as HTMLElement).tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;

      // Delete selected marker annotation
      if ((e.key === 'Delete' || e.key === 'Backspace') && selectedLabelId !== null) {
        try {
          const response = await fetch(`/api/annotations/${selectedLabelId}`, {
            method: 'DELETE',
          });
          if (response.ok) {
            setSelectedLabelId(null);
            chartRef.current?.refreshData();
          }
        } catch (error) {
          console.error('Failed to delete label:', error);
        }
        return;
      }

      // Tool shortcuts (only when not in a modal)
      switch (e.key.toLowerCase()) {
        case 'r':
          setActiveTool((prev) => (prev === 'rectangle' ? null : 'rectangle'));
          break;
        case 's':
          setActiveTool((prev) => (prev === 'span' ? null : 'span'));
          break;
        case 'l':
          setActiveTool((prev) => (prev === 'line' ? null : 'line'));
          break;
        case 'd':
          setActiveTool((prev) => (prev === 'delete' ? null : 'delete'));
          break;
        case 't':
          setTheme(theme === 'dark' ? 'light' : 'dark');
          break;
        case '?':
          setShortcutsOpen((prev) => !prev);
          break;
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [selectedLabelId, theme, setTheme]);

  const activeChart = charts.find((c) => c.id === activeChartId);

  return (
    <div className="flex h-screen w-full overflow-hidden bg-background">
      <KeyboardShortcutsModal open={shortcutsOpen} onClose={() => setShortcutsOpen(false)} />
      {/* Sidebar */}
      <div className="flex flex-col bg-sidebar border-r border-sidebar-border w-60 flex-shrink-0 animate-fade-in">
        {/* Sidebar Header */}
        <div className="flex items-center justify-between px-4 py-3 border-b border-sidebar-border flex-shrink-0">
          <div>
            <h1 className="text-sm font-semibold text-foreground tracking-tight">Candle Annotator</h1>
            <p className="text-[10px] text-muted-foreground">Chart annotation tool</p>
          </div>
          <div className="flex items-center gap-1">
            <ThemeToggle />
          </div>
        </div>

        {/* Chart Selector */}
        <div className="px-3 py-2 border-b border-sidebar-border flex-shrink-0">
          <ChartSelector
            charts={charts}
            activeChartId={activeChartId}
            onSelectChart={handleSelectChart}
            onDeleteChart={handleDeleteChart}
          />
        </div>

        {/* File Upload */}
        <div className="px-3 py-2 border-b border-sidebar-border flex-shrink-0">
          <FileUpload onUploadSuccess={handleUploadSuccess} />
        </div>

        {/* Toolbox */}
        <div className="px-3 py-2 border-b border-sidebar-border flex-shrink-0">
          <Toolbox
            activeTool={activeTool}
            onToolChange={setActiveTool}
            onExport={handleExport}
            selectedColor={selectedColor}
            onColorChange={setSelectedColor}
            annotations={annotations}
            selectedLabelId={selectedLabelId}
            onLabelSelect={handleLabelSelect}
            onLabelDelete={handleLabelDelete}
            activeChartId={activeChartId}
            spanAnnotations={spanAnnotations}
            spanLabelTypes={spanLabelTypes}
            selectedSpanId={selectedSpanId}
            onSelectSpan={handleSelectedSpanChange}
            onDeleteSpan={handleDeleteSpan}
          />
        </div>

        {/* Scrollable middle: Annotations + TA-Lib + Training + Predictions */}
        <div className="flex-1 overflow-y-auto scrollbar-thin min-h-0">
          {/* Annotations List */}
          <div className="px-3 py-2 border-b border-sidebar-border">
            <SpanAnnotationList
              spanAnnotations={spanAnnotations}
              spanLabelTypes={spanLabelTypes}
              selectedSpanId={selectedSpanId}
              onSelectSpan={handleSelectedSpanChange}
              onDeleteSpan={handleDeleteSpan}
            />
          </div>

          {/* TA-Lib Pattern Panel */}
          <div className="px-3 py-2 border-b border-sidebar-border">
            <TalibPatternPanel
              activeChartId={activeChartId}
              onAnnotationsChanged={() => fetchSpanAnnotations(activeChartId)}
              getCandles={() => chartRef.current?.getVisibleCandles()}
            />
          </div>

          {/* Training Panel */}
          <div className="px-3 py-2 border-b border-sidebar-border">
            <TrainingPanel activeChartId={activeChartId} />
          </div>

          {/* Predictions */}
          <div className="px-3 py-2">
            <PredictionPanel
              predictionState={predictionState}
              onToggleVisibility={togglePredictionVisibility}
              onFetchPredictions={handleFetchVisiblePredictions}
              onFetchBatchPredictions={handleFetchBatchPredictions}
              onConfidenceChange={setConfidenceThreshold}
              onToggleLabelSelection={toggleLabelSelection}
              predictionSummary={predictionSummary}
              isModelOnline={isModelOnline}
              showOnlyDisagreements={showOnlyDisagreements}
              onToggleShowOnlyDisagreements={toggleShowOnlyDisagreements}
              onModelLoaded={handleModelLoaded}
            />
          </div>
        </div>

        {/* Fixed bottom actions */}
        {/* Delete all annotations */}
        <div className="px-3 py-2 border-t border-sidebar-border flex-shrink-0">
          <button
            onClick={() => setDeleteAllDialogOpen(true)}
            disabled={!activeChartId}
            className="w-full px-2 py-1 text-[10px] text-destructive border border-destructive/30 rounded hover:bg-destructive/10 transition-colors disabled:opacity-50"
          >
            Delete All Annotations
          </button>
        </div>

        {/* Confirmation dialog for delete-all annotations */}
        <Dialog open={deleteAllDialogOpen} onOpenChange={setDeleteAllDialogOpen}>
          <DialogContent>
            <DialogHeader>
              <DialogTitle>Delete All Annotations</DialogTitle>
              <DialogDescription>
                This will permanently delete all span and label annotations for the current chart.
                This action cannot be undone.
              </DialogDescription>
            </DialogHeader>
            <DialogFooter>
              <DialogClose asChild>
                <button className="px-4 py-2 text-sm border rounded hover:bg-muted transition-colors">
                  Cancel
                </button>
              </DialogClose>
              <button
                onClick={() => {
                  setDeleteAllDialogOpen(false);
                  handleDeleteAllAnnotations();
                }}
                className="px-4 py-2 text-sm bg-destructive text-destructive-foreground rounded hover:bg-destructive/90 transition-colors"
              >
                Confirm Delete
              </button>
            </DialogFooter>
          </DialogContent>
        </Dialog>

        {/* Export */}
        <div className="px-3 py-2 border-t border-sidebar-border flex-shrink-0">
          <button
            onClick={handleExport}
            className="w-full flex items-center justify-center gap-1.5 px-2 py-1.5 text-xs rounded bg-primary/10 hover:bg-primary/20 text-primary transition-colors"
          >
            <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" x2="12" y1="15" y2="3"/></svg>
            Export JSON
          </button>
        </div>

        {/* Settings */}
        <div className="relative px-3 py-2 border-t border-sidebar-border flex-shrink-0">
          <button
            onClick={() => setSettingsOpen((o) => !o)}
            className="flex items-center gap-1.5 px-2 py-1.5 text-xs rounded text-muted-foreground hover:text-foreground hover:bg-secondary/50 transition-colors"
          >
            <Settings className="w-3.5 h-3.5" />
            Settings
          </button>
          {settingsOpen && (
            <>
              {/* backdrop */}
              <div className="fixed inset-0 z-10" onClick={() => setSettingsOpen(false)} />
              {/* menu */}
              <div className="absolute bottom-full left-3 mb-1 z-20 w-48 rounded-md border border-border bg-popover shadow-md py-1">
                <a
                  href="/annotation-types"
                  className="flex items-center gap-2 px-3 py-2 text-xs text-foreground hover:bg-secondary/50 transition-colors"
                >
                  <Tag className="w-3.5 h-3.5 text-muted-foreground" />
                  Annotation Types
                </a>
                <a
                  href="/span-label-types"
                  className="flex items-center gap-2 px-3 py-2 text-xs text-foreground hover:bg-secondary/50 transition-colors"
                >
                  <Layers className="w-3.5 h-3.5 text-muted-foreground" />
                  Span Label Types
                </a>
              </div>
            </>
          )}
        </div>
      </div>

      {/* Main chart area */}
      <div className="flex-1 flex flex-col min-w-0">
        {/* Chart top bar */}
        <div className="flex items-center justify-between px-3 py-1.5 border-b border-border bg-card/50">
          <div className="flex items-center gap-3">
            <span className="font-mono text-sm font-semibold text-foreground">
              {activeChart?.name || 'No chart'}
            </span>
          </div>
          <div className="flex items-center gap-2 text-[10px] text-muted-foreground font-mono">
            <span className="px-1.5 py-0.5 rounded bg-secondary/50">R Rect</span>
            <span className="px-1.5 py-0.5 rounded bg-secondary/50">S Span</span>
            <span className="px-1.5 py-0.5 rounded bg-secondary/50">L Line</span>
            <span className="px-1.5 py-0.5 rounded bg-secondary/50">D Del</span>
            <span className="px-1.5 py-0.5 rounded bg-secondary/50">T Theme</span>
          </div>
        </div>

        {/* Chart */}
        <div className="flex-1 min-h-0 relative">
          {/* Loading overlay for predictions */}
          {predictionState.isLoading && (
            <div className="absolute inset-0 bg-background/50 backdrop-blur-sm z-50 flex items-center justify-center">
              <div className="bg-card border border-border rounded-lg p-6 shadow-lg">
                <div className="flex items-center gap-3">
                  <div className="animate-spin rounded-full h-5 w-5 border-2 border-primary border-t-transparent" />
                  <span className="text-sm text-foreground">Loading predictions...</span>
                </div>
              </div>
            </div>
          )}
          <CandleChart
            ref={chartRef}
            activeTool={activeTool}
            onAnnotationChange={handleAnnotationChange}
            selectedColor={selectedColor}
            selectedLabelId={selectedLabelId}
            onLabelSelect={handleLabelSelect}
            activeChartId={activeChartId}
            spanAnnotations={spanAnnotations}
            spanLabelTypes={spanLabelTypes}
            selectedSpanId={selectedSpanId}
            onSpanAnnotationsChange={handleSpanAnnotationsChange}
            onSelectedSpanChange={handleSelectedSpanChange}
            predictionVisible={predictionState.visible}
            perCandlePredictions={predictionState.perCandlePredictions}
            predictionSpans={predictionState.spans}
            confidenceThreshold={predictionState.confidenceThreshold}
            selectedLabels={predictionState.selectedLabels}
            modelInfo={predictionState.modelInfo}
            predictionSummary={predictionSummary}
            showOnlyDisagreements={showOnlyDisagreements}
            onPredictionClick={handlePredictionClick}
            onPredictionDismiss={handlePredictionDismiss}
          />
        </div>
      </div>
    </div>
  );
}
