'use client';

import { useEffect, useState, useCallback } from 'react';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import type { PlotParams } from 'react-plotly.js';
import type { ForecastsResponse, ForecastData, Model } from '@/src/types/challenge';
import { getChallengeSeries, getSeriesData, getSeriesForecasts, getRoundModels } from '@/src/services/roundService';
import humanizeDuration from 'humanize-duration';
import { parse, toSeconds } from 'iso8601-duration';
import wrap from 'word-wrap';


// Dynamically import Plotly to avoid SSR issues
const Plot = dynamic(() => import('react-plotly.js'), { ssr: false });

// Wrap a legend label: replace underscores with spaces and break at ~25 chars
function wrapLegendLabel(label: string | undefined, width = 25): string {
  if (!label) return '';
  const spaced = label.replace(/_/g, ' ');

  return wrap(spaced, { width, indent: '', trim: true, cut: false }).replace(/\n/g, '<br>');
}

// Convert a hex color to an rgba() string
function hexToRgba(hex: string, alpha: number): string {
  const r = parseInt(hex.slice(1, 3), 16);
  const g = parseInt(hex.slice(3, 5), 16);
  const b = parseInt(hex.slice(5, 7), 16);
  return `rgba(${r},${g},${b},${alpha})`;
}

// CI bands to render: wider interval → lower alpha (more transparent)
const CI_BANDS = [
  { lower: 'q_0.2', upper: 'q_0.8', alpha: 0.12 },
  { lower: 'q_0.3', upper: 'q_0.7', alpha: 0.22 },
] as const;

// Convert ISO 8601 duration to milliseconds
function durationToMs(isoStr: string | undefined): number | null {
  if (!isoStr) return null;
  try {
    return toSeconds(parse(isoStr)) * 1000;
  } catch {
    return null;
  }
}

// Convert ISO 8601 duration to human-readable format
function formatFrequency(freq: string | undefined): string {
  if (!freq) return 'N/A';
  
  try {
    // Parse ISO 8601 duration string (e.g., "PT3M" for 3 minutes)
    const duration = parse(freq);
    
    // Convert to seconds, then to milliseconds
    const seconds = toSeconds(duration);
    const milliseconds = seconds * 1000;
    
    // Use humanize-duration to format
    return humanizeDuration(milliseconds, { 
      largest: 2, 
      round: true,
      conjunction: ' and ',
      serialComma: false
    });
  } catch (error) {
    // If parsing fails, return the original string
    console.warn(`Failed to parse frequency "${freq}":`, error);
    return freq;
  }
}

interface SeriesDataItem {
  contextData: any[];
  testData: any[];
  series_id: number;
  series_name: string;
  unit?: string | null;
  forecasts?: ForecastsResponse;
  forecastsLoading?: boolean;
  forecastsError?: string;
}

interface TimeSeriesChartProps {
  challengeId: number;
  challengeName: string;
  challengeDescription?: string;
  registrationStart?: string;
  registrationEnd?: string;
  evaluationStart?: string;
  frequency?: string;
  horizon?: string;
  seriesId?: number;
  on_title_page?: boolean;
  definitionId?: number;
  status?: string;
}

export default function TimeSeriesChart({ challengeId, challengeName, challengeDescription, registrationStart, registrationEnd, evaluationStart, frequency, horizon, seriesId, on_title_page = false, definitionId, status }: TimeSeriesChartProps) {
  const [seriesData, setSeriesData] = useState<SeriesDataItem[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [expandedSeries, setExpandedSeries] = useState<Set<number>>(new Set());

  // Forecast filter state
  const [maxSizeFilter, setMaxSizeFilter] = useState<string>('');
  const [architectureFilter, setArchitectureFilter] = useState<string>('');
  const [modelSearchFilter, setModelSearchFilter] = useState<string>('');
  
  // Models data
  const [models, setModels] = useState<Model[]>([]);
  const [modelsLoading, setModelsLoading] = useState(true);

  // Function to load data and forecasts for a specific series
  const loadForecastsForSeries = useCallback(async (seriesId: number) => {
    // Check if data is already loaded or loading
    setSeriesData(prev => {
      const series = prev.find(s => s.series_id === seriesId);
      if (!series || (series.forecasts && series.contextData?.length > 0)) {
        return prev;
      }
      
      // Mark as loading
      return prev.map(s => 
        s.series_id === seriesId 
          ? { ...s, forecastsLoading: true, forecastsError: undefined }
          : s
      );
    });

    try {
      // Get the series metadata to fetch context and test data
      const series = await getChallengeSeries(challengeId);
      const currentSeries = series.find(s => s.series_id === seriesId);
      
      if (!currentSeries) {
        throw new Error(`Series ${seriesId} not found`);
      }

      const startTime = currentSeries.context_start_time;
      const endTimeContext = currentSeries.context_end_time;
      const endTimeTest = currentSeries.end_time;

      if (!startTime || !endTimeContext || !endTimeTest) {
        throw new Error(`Missing time range for series ${seriesId}`);
      }

      // Load context data, test data, and forecasts in parallel
      const [dataContext, dataTest, forecasts] = await Promise.all([
        getSeriesData(challengeId, seriesId, startTime, endTimeContext),
        getSeriesData(challengeId, seriesId, endTimeContext, endTimeTest),
        getSeriesForecasts(challengeId, seriesId)
      ]);

      const modelCount = forecasts?.forecasts ? Object.keys(forecasts.forecasts).length : 0;
      console.log(`✓ Loaded ${dataContext.data?.length || 0} context data points for series ${seriesId}`);
      console.log(`✓ Loaded ${dataTest.data?.length || 0} test data points for series ${seriesId}`);
      console.log(`✓ Loaded ${modelCount} forecast models for series ${seriesId}`);
      
      // Update with loaded data and forecasts
      setSeriesData(prev => prev.map(s =>
        s.series_id === seriesId
          ? {
              ...s,
              contextData: dataContext.data,
              testData: dataTest.data,
              unit: currentSeries.unit,
              forecasts,
              forecastsLoading: false
            }
          : s
      ));
    } catch (err) {
      console.error(`Failed to fetch data for series ${seriesId}:`, err);
      setSeriesData(prev => prev.map(s => 
        s.series_id === seriesId 
          ? { ...s, forecastsLoading: false, forecastsError: 'Failed to load data' }
          : s
      ));
    }
  }, [challengeId]);

  const toggleSeries = (seriesId: number) => {
    setExpandedSeries(prev => {
      const newSet = new Set(prev);
      if (newSet.has(seriesId)) {
        newSet.delete(seriesId);
      } else {
        newSet.add(seriesId);
        // Load forecasts when expanding
        loadForecastsForSeries(seriesId);
      }
      return newSet;
    });
  };

  // Fetch models for the challenge
  useEffect(() => {
    async function fetchModels() {
      try {
        setModelsLoading(true);
        const modelsData = await getRoundModels(challengeId);
        
        // API returns models as a direct array
        const modelsList = Array.isArray(modelsData) ? modelsData : [];
        setModels(modelsList);
      } catch (err) {
        console.error('Failed to fetch models:', err);
        setModels([]);
      } finally {
        setModelsLoading(false);
      }
    }
    fetchModels();
  }, [challengeId]);

  useEffect(() => {
    async function fetchData() {
      try {
        setLoading(true);
        setError(null);

        // Fetch all series for this challenge
        const series = await getChallengeSeries(challengeId);
        console.log(`Fetched ${series.length} series for challenge ${challengeId}`);
        
        // Filter by seriesId if provided
        const filteredSeries = seriesId 
          ? series.filter(s => s.series_id === seriesId)
          : series;
        
        if (seriesId && filteredSeries.length === 0) {
          console.warn(`Series ${seriesId} not found in challenge ${challengeId}`);
        }
        
        // Create series items with metadata only (data will be loaded on demand)
        const validData = filteredSeries.map((s) => ({
          contextData: [],
          testData: [],
          series_id: s.series_id,
          series_name: s.name,
          forecasts: undefined,
          forecastsLoading: false,
          forecastsError: undefined
        } as SeriesDataItem));
        
        setSeriesData(validData);
        
        // Expand the first series by default and load its data
        if (validData.length > 0) {
          const firstSeriesId = validData[0].series_id;
          setExpandedSeries(new Set([firstSeriesId]));
          // Load data and forecasts for the first series
          loadForecastsForSeries(firstSeriesId);
        }
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Failed to load time series data');
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, [challengeId, loadForecastsForSeries, seriesId]);

  if (loading) {
    return (
      <div className="flex items-center justify-center p-12">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="bg-red-50 border border-red-200 text-red-700 p-4 rounded-lg">
        <p className="font-semibold">Error loading time series data</p>
        <p className="text-sm">{error}</p>
      </div>
    );
  }

  if (seriesData.length === 0) {
    return (
      <div className="text-center py-8 text-gray-500">
        No time series data available for this challenge.
      </div>
    );
  }

  // Filter forecasts based on user input by matching with models
  const filterForecasts = (forecasts: Record<string, ForecastData>) => {
    
    const filtered = Object.entries(forecasts).filter(([modelName, forecastData]) => {
      // Find the matching model from the models list
      const model = models.find(m => {
        const match1 = m.name === modelName;
        const match2 = m.readable_id === forecastData.model_id;
        const match3 = m.readable_id === modelName;
        return match1 || match2 || match3;
      });
      
      if (!model) {
        console.log('  ❌ NO MODEL FOUND');
        return false;
      }
      
      // Filter by max_size - use model data if available
      if (maxSizeFilter) {
        const filterValue = parseFloat(maxSizeFilter);
        if (!isNaN(filterValue)) {
          if (model.model_size !== undefined && model.model_size > filterValue) {
            return false;
          }
        }
      }
      
      // Filter by architecture - use model data if available
      if (architectureFilter) {
        if (model.architecture) {
          if (!model.architecture.toLowerCase().includes(architectureFilter.toLowerCase())) {
            return false;
          }
        } else {
          return false;
        }
      }
      
      // Filter by model name/ID search
      if (modelSearchFilter) {
        const searchLower = modelSearchFilter.toLowerCase();
        const modelIdMatch = model.readable_id?.toLowerCase() === searchLower;
        const modelNameMatch = model.name?.toLowerCase().includes(searchLower);
        if (!modelIdMatch && !modelNameMatch) {
          return false;
        }
      }
      
      return true;
    }).reduce((acc, [key, value]) => {
      acc[key] = value;
      return acc;
    }, {} as Record<string, ForecastData>);
    return filtered;
  };

  // Get unique architectures and max sizes for filter options from models data
  const getFilterOptions = () => {
    const architectures = new Set<string>();
    const maxSizes = new Set<number>();
    
    models.forEach(model => {
      if (model.architecture) architectures.add(model.architecture);
      if (model.model_size !== undefined) maxSizes.add(model.model_size);
    });
    
    return {
      architectures: Array.from(architectures).sort(),
      maxSizes: Array.from(maxSizes).sort((a, b) => a - b)
    };
  };

  const filterOptions = getFilterOptions();

  return (
    <div className="bg-white rounded-lg shadow p-6">
      <div className="mb-6">
        {on_title_page && definitionId ? (
          <div className="mb-4">
            <div className="flex items-center gap-3 mb-2">
              <span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold bg-green-100 text-green-800 animate-pulse">
                <span className="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
                Now Live!
              </span>
            </div>
            <h2 className="text-xl font-semibold text-gray-900">
              Currently competing in challenge:{' '}
              <Link href={`/challenges/${definitionId}/${challengeId}`}>
                <span className="text-blue-600 hover:text-blue-700 underline decoration-2 underline-offset-2 transition-colors">
                  {challengeName}
                </span>
              </Link>
            </h2>
          </div>
        ) : (
          <h2 className="text-xl font-semibold mb-4">{challengeName}</h2>
        )}
        {!on_title_page && (
          <>
            {challengeDescription && (
              <p className="text-sm text-gray-600 mb-4 italic">{challengeDescription}</p>
            )}
            <div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
          <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-5 2xl:grid-cols-6 gap-4 auto-rows-auto">
            <div>
              <p className="text-xs text-gray-500 uppercase tracking-wide">Status</p>
              <p className={`text-sm font-semibold uppercase ${
                status === 'active'
                  ? 'text-green-600'
                  : status === 'completed'
                  ? 'text-blue-600'
                  : status === 'registration'
                  ? 'text-yellow-600'
                  : 'text-gray-600'
              }`}>
                {status || 'N/A'}
              </p>
            </div>
            <div>
              <p className="text-xs text-gray-500 uppercase tracking-wide">ID</p>
              <p className="text-sm font-semibold text-gray-900">{challengeId}</p>
            </div>
            <div>
              <p className="text-xs text-gray-500 uppercase tracking-wide">Series</p>
              <p className="text-sm font-semibold text-gray-900">{seriesData.length}</p>
            </div>
            <div>
              <p className="text-xs text-gray-500 uppercase tracking-wide">Models</p>
              <p className="text-sm font-semibold text-gray-900">
                {seriesData.length > 0 && seriesData[0].forecasts?.forecasts 
                  ? Object.keys(seriesData[0].forecasts.forecasts).length 
                  : 0}
              </p>
            </div>
            <div>
              <p className="text-xs text-gray-500 uppercase tracking-wide">Context</p>
              <p className="text-sm font-semibold text-gray-900">
                {seriesData.length > 0 && seriesData[0].contextData?.length 
                  ? seriesData[0].contextData.length 
                  : 'N/A'}
              </p>
            </div>
            <div>
              <p className="text-xs text-gray-500 uppercase tracking-wide">Frequency</p>
              <p className="text-sm font-semibold text-gray-900">
                {formatFrequency(frequency)}
              </p>
            </div>
            <div>
              <p className="text-xs text-gray-500 uppercase tracking-wide">Horizon</p>
              <p className="text-sm font-semibold text-gray-900">
                {horizon || 'N/A'}
              </p>
            </div>
            <div>
              <p className="text-xs text-gray-500 uppercase tracking-wide">Registration Start</p>
              <p className="text-sm font-semibold text-gray-900">
                {registrationStart ? new Date(registrationStart).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }) : 'N/A'}
              </p>
            </div>
            <div>
              <p className="text-xs text-gray-500 uppercase tracking-wide">Registration End</p>
              <p className="text-sm font-semibold text-gray-900">
                {registrationEnd ? new Date(registrationEnd).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }) : 'N/A'}
              </p>
            </div>
            <div>
              <p className="text-xs text-gray-500 uppercase tracking-wide">Start of Evaluation</p>
              <p className="text-sm font-semibold text-gray-900">
                {evaluationStart ? new Date(evaluationStart).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }) : 'N/A'}
              </p>
            </div>
          </div>
        </div>
          </>
        )}
        
        {/* Forecast Filters */}
        {!on_title_page && (
        <div className="mt-6">
          <h3 className="text-sm font-semibold text-gray-700 mb-3">Filter Forecasts</h3>
          <div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
            <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
              {/* Model Search */}
              <div>
                <label htmlFor="model-search" className="block text-xs font-medium text-gray-700 mb-1">
                  Model Name/ID Search
                </label>
                <input
                  id="model-search"
                  type="text"
                  placeholder="Search by name or ID..."
                  value={modelSearchFilter}
                  onChange={(e) => setModelSearchFilter(e.target.value)}
                  className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                />
              </div>
              
              {/* Max Size Filter */}
              <div>
                <label htmlFor="max-size" className="block text-xs font-medium text-gray-700 mb-1">
                  Max Size (parameters)
                </label>
                <input
                  id="max-size"
                  type="number"
                  placeholder="Max parameters..."
                  value={maxSizeFilter}
                  onChange={(e) => setMaxSizeFilter(e.target.value)}
                  className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                />
                {filterOptions.maxSizes.length > 0 && (
                  <p className="text-xs text-gray-500 mt-1">
                    Available: {filterOptions.maxSizes.join(', ')}
                  </p>
                )}
              </div>
              
              {/* Architecture Filter */}
              <div>
                <label htmlFor="architecture" className="block text-xs font-medium text-gray-700 mb-1">
                  Architecture
                </label>
                <select
                  id="architecture"
                  value={architectureFilter}
                  onChange={(e) => setArchitectureFilter(e.target.value)}
                  className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
                >
                  <option value="">All architectures</option>
                  {filterOptions.architectures.map((arch) => (
                    <option key={arch} value={arch}>{arch}</option>
                  ))}
                </select>
              </div>
            </div>
            
            {/* Clear Filters Button */}
            {(maxSizeFilter || architectureFilter || modelSearchFilter) && (
              <div className="mt-3 flex justify-end">
                <button
                  onClick={() => {
                    setMaxSizeFilter('');
                    setArchitectureFilter('');
                    setModelSearchFilter('');
                  }}
                  className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
                >
                  Clear Filters
                </button>
              </div>
            )}
          </div>
        </div>
        )}
      </div>
      <div className="space-y-6">{seriesData.map((series: any) => {
          // Prepare traces for context and test data
          const traces: any[] = [];
          
          // Ground truth trace (solid black line) — pushed first so it renders behind all other traces
          if (series.testData && series.testData.length > 0) {
            traces.push({
              x: series.testData.map((d: any) => d.ts),
              y: series.testData.map((d: any) => d.value),
              type: 'scatter',
              mode: 'lines',
              name: wrapLegendLabel(`Live: ${series.series_name || series.series_id}`),
              line: { width: 2, color: '#000000', dash: 'solid' },
              legendgroup: 'actual',
              legendgrouptitle: { text: 'Observations' },
              hovertemplate: '%{x|%Y-%m-%d %H:%M} UTC<br>Value: %{y:.4g}<extra>%{fullData.name}</extra>',
            });
          }

          // Context data trace (solid blue line)
          if (series.contextData && series.contextData.length > 0) {
            traces.push({
              x: series.contextData.map((d: any) => d.ts),
              y: series.contextData.map((d: any) => d.value),
              type: 'scatter',
              mode: 'lines',
              name: 'Historical Data',
              line: { width: 2, color: '#2563eb' },
              marker: { size: 4 },
              legendgroup: 'actual',
              hovertemplate: '%{x|%Y-%m-%d %H:%M} UTC<br>Value: %{y:.4g}<extra>%{fullData.name}</extra>',
            });
          }

          // Add forecast traces for each model
          if (series.forecasts?.forecasts) {
            const colors = ['#dc2626', '#16a34a', '#9333ea', '#ea580c', '#0891b2', '#ca8a04'];
            
            // Get the last context data point to connect forecasts
            const lastActualPoint = series.contextData && series.contextData.length > 0 
              ? series.contextData[series.contextData.length - 1] 
              : null;
            
            // Apply filters to forecasts
            const filteredForecasts = filterForecasts(series.forecasts.forecasts);
            
            // Sort models by MASE (ascending - best first)
            const modelEntries = Object.entries(filteredForecasts)
              .map(([modelName, forecastData]: [string, ForecastData]) => ({
                modelName,
                forecastData,
                label: forecastData?.label,
                mase: forecastData?.current_mase
              }))
              .sort((a, b) => {
                // Handle undefined/null MASE values - put them at the end
                if (a.mase === undefined || a.mase === null) return 1;
                if (b.mase === undefined || b.mase === null) return -1;
                return a.mase - b.mase;
              });
            
            let visibleForecastCount = 0;
            
            modelEntries.forEach(({ modelName, forecastData, label, mase }, idx) => {
              const dataArray = forecastData?.data;
              if (Array.isArray(dataArray) && dataArray.length > 0 && lastActualPoint) {
                const displayName = wrapLegendLabel(
                  mase !== undefined && mase !== null
                    ? `${label} (MASE: ${typeof mase === 'number' ? mase.toFixed(3) : mase})`
                    : label
                );

                // Prepend the last actual point to connect the forecast line
                const connectedX = [lastActualPoint.ts, ...dataArray.map((d: any) => d.ts)];
                const connectedY = [lastActualPoint.value, ...dataArray.map((d: any) => d.y)];

                // Show only the best 2 forecasts by default
                const isVisible = visibleForecastCount < 2;
                visibleForecastCount++;

                const color = colors[idx % colors.length];

                console.log(`CI bounds for model "${modelName}":`, dataArray[0]?.ci ?? 'none');

                // CI bands: push upper + lower (fill tonexty) pairs BEFORE the forecast
                // line so the line renders on top. Each pair must be consecutive for
                // fill: 'tonexty' to fill between them correctly.
                CI_BANDS.forEach(({ lower, upper, alpha }) => {
                  const hasCI = dataArray.some(
                    (d: any) => d.ci?.[lower] !== undefined && d.ci?.[upper] !== undefined
                  );
                  if (!hasCI) return;

                  const ciUpperY = [
                    lastActualPoint.value,
                    ...dataArray.map((d: any) => d.ci?.[upper] ?? d.y),
                  ];
                  const ciLowerY = [
                    lastActualPoint.value,
                    ...dataArray.map((d: any) => d.ci?.[lower] ?? d.y),
                  ];

                  // Upper bound (invisible line, no legend entry)
                  traces.push({
                    x: connectedX,
                    y: ciUpperY,
                    type: 'scatter',
                    mode: 'lines',
                    line: { width: 0, color: 'transparent' },
                    showlegend: false,
                    legendgroup: `forecast-${idx}`,
                    visible: isVisible ? true : 'legendonly',
                    hoverinfo: 'skip',
                  });

                  // Lower bound – fills to the upper bound trace above it
                  traces.push({
                    x: connectedX,
                    y: ciLowerY,
                    type: 'scatter',
                    mode: 'lines',
                    fill: 'tonexty',
                    fillcolor: hexToRgba(color, alpha),
                    line: { width: 0, color: 'transparent' },
                    showlegend: false,
                    legendgroup: `forecast-${idx}`,
                    visible: isVisible ? true : 'legendonly',
                    hoverinfo: 'skip',
                  });
                });

                // Forecast line rendered on top of CI bands
                traces.push({
                  x: connectedX,
                  y: connectedY,
                  type: 'scatter',
                  mode: 'lines',
                  name: displayName,
                  line: { width: 2, dash: 'dash', color },
                  visible: isVisible ? true : 'legendonly',
                  legendgroup: `forecast-${idx}`,
                  legendgrouptitle: idx === 0 ? { text: 'Forecasts' } : undefined,
                  hovertemplate: '%{x|%Y-%m-%d %H:%M} UTC<br>Value: %{y:.4g}<extra>%{fullData.name}</extra>',
                });
              }
            });
          }

          const plotData: PlotParams['data'] = traces;

          // Compute default zoom: show last 3× horizon of context + full forecast window.
          const defaultXRange: [string, string] | undefined = (() => {
            const horizonMs = durationToMs(horizon);

            if (!horizonMs || !series.contextData?.length) {
              return undefined;
            }

            const contextEnd = new Date(
              series.contextData[series.contextData.length - 1].ts
            );

            const rangeStart = new Date(contextEnd.getTime() - 3 * horizonMs);
            const rangeEnd = new Date(contextEnd.getTime() + 1.1 * horizonMs);

            return [rangeStart.toISOString(), rangeEnd.toISOString()];
          })();

          const layout: PlotParams['layout'] = {
            xaxis: {
              title: { text: '' },
              showgrid: true,
              type: 'date',
              domain: [0, 0.82],
              range: defaultXRange,
              rangeslider: { visible: true },
            },
            yaxis: {
              title: { text: series.unit ?? '' },
              autorange: true,
              showgrid: true,
            },
            hovermode: 'closest',
            showlegend: true,
            legend: {
              orientation: 'v',
              yanchor: 'top',
              y: 1,
              xanchor: 'left',
              x: 0.85,
              font: { size: 10 },
              bgcolor: 'rgba(255, 255, 255, 0.95)',
              bordercolor: '#E5E7EB',
              borderwidth: 1,
            },
            autosize: true,
            margin: { l: 60, r: 20, t: 60, b: 50 },
            uirevision: "static"
          };

          const isExpanded = expandedSeries.has(series.series_id);

          return (
            <div key={series.series_id} className="w-full border border-gray-200 rounded-lg overflow-hidden">
              <button
                onClick={() => toggleSeries(series.series_id)}
                className="w-full px-4 py-3 flex items-center justify-between bg-gray-50 hover:bg-gray-100 transition-colors"
              >
                <div className="flex items-center gap-3">
                  <svg
                    className={`w-5 h-5 transition-transform ${isExpanded ? 'rotate-90' : ''}`}
                    fill="none"
                    stroke="currentColor"
                    viewBox="0 0 24 24"
                  >
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
                  </svg>
                  <div className="text-left">
                    <h3 className="font-semibold text-gray-900">
                      {series.series_name || `Series ${series.series_id}`}
                    </h3>
                  </div>
                </div>
                <span className="text-sm text-gray-500">
                  {isExpanded ? 'Click to collapse' : 'Click to expand'}
                </span>
              </button>
              {isExpanded && (
                <div className="p-4 relative">
                  {series.forecastsLoading && (
                    <div className="absolute top-0 left-0 right-0 bg-blue-50 border border-blue-200 rounded-lg p-3 mx-4 mt-4 z-10 flex items-center gap-3">
                      <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-500"></div>
                      <p className="text-sm text-blue-700">Loading forecasts...</p>
                    </div>
                  )}
                  {series.forecastsError && !series.forecastsLoading && (
                    <div className="bg-yellow-50 border border-yellow-200 text-yellow-700 p-4 rounded-lg mb-4">
                      <p className="font-semibold">Warning</p>
                      <p className="text-sm">{series.forecastsError}</p>
                      <button
                        onClick={() => loadForecastsForSeries(series.series_id)}
                        className="mt-2 text-sm underline hover:no-underline"
                      >
                        Retry loading forecasts
                      </button>
                    </div>
                  )}
                  {series.contextData.length > 0 && (
                    <Plot
                      data={plotData}
                      layout={layout}
                      style={{ width: '100%', height: '400px' }}
                      useResizeHandler
                    />
                  )}
                </div>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}
