import React, { useEffect, useMemo, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { CandlestickSeries, ColorType, CrosshairMode, LineSeries, createChart } from 'lightweight-charts';
import './styles.css';

const quadrantColors = {
  Improving: '#3b82f6',
  Leading: '#22c55e',
  Lagging: '#ef4444',
  Weakening: '#eab308',
};

const quadrantLabels = {
  Improving: { x: 0.02, y: 0.06 },
  Leading: { x: 0.94, y: 0.06 },
  Lagging: { x: 0.03, y: 0.94 },
  Weakening: { x: 0.88, y: 0.94 },
};

const mainSectorProxyTickers = {
  'Basic Materials': 'XLB',
  'Communication Services': 'XLC',
  'Consumer Cyclical': 'XLY',
  'Consumer Defensive': 'XLP',
  Energy: 'XLE',
  'Financial Services': 'XLF',
  Healthcare: 'XLV',
  Industrials: 'XLI',
  'Real Estate': 'XLRE',
  Technology: 'XLK',
  Utilities: 'XLU',
};

function representativeWeight(row) {
  const groupWeight =
    row.group === 'Industry Proxy' ? 100 : row.group === 'US Sector' ? 8 : row.group === 'Industry' ? 6 : row.group === 'Thematic' ? 4 : 0;
  return groupWeight + Math.hypot(row.rsRatio - 100, row.rsMomentum - 100) / 10;
}

function representativeRow(rows) {
  return [...rows].sort((a, b) => representativeWeight(b) - representativeWeight(a))[0];
}

function industryProxyRow(rows) {
  return rows.find((row) => row.group === 'Industry Proxy') || representativeRow(rows);
}

function rrgRowId(row) {
  return row.id || `${row.ticker}-${row.mainSector}-${row.subSector}`;
}

function rrgRowLabel(row) {
  return row.chartGroupLabel || (row.chartLevel === 'mainSector' ? row.mainSector : row.subSector || row.mainSector || row.sector);
}

function selectMainSectorRows(rows, mainSectors) {
  const usedTickers = new Set();
  return mainSectors.flatMap((mainSector) => {
    const proxyTicker = mainSectorProxyTickers[mainSector];
    const sectorRows = rows.filter((row) => row.mainSector === mainSector);
    const selectedRow =
      rows.find((row) => row.ticker === proxyTicker && row.group === 'US Sector') ||
      rows.find((row) => row.ticker === proxyTicker) ||
      representativeRow(sectorRows);
    if (!selectedRow || usedTickers.has(selectedRow.ticker)) {
      return [];
    }
    usedTickers.add(selectedRow.ticker);
    return [{ ...selectedRow, chartLevel: 'mainSector' }];
  });
}

function selectSubSectorRows(rows, mainSector) {
  const rowsBySubSector = new Map();
  rows
    .filter((row) => row.mainSector === mainSector)
    .forEach((row) => {
      const key = row.subSector || row.sector;
      rowsBySubSector.set(key, [...(rowsBySubSector.get(key) || []), row]);
    });
  return [...rowsBySubSector.entries()]
    .map(([subSector, subRows]) => ({ ...industryProxyRow(subRows), chartLevel: 'subSector', chartGroupLabel: subSector }))
    .sort((a, b) => a.subSector.localeCompare(b.subSector));
}

function formatNumber(value, digits = 2) {
  if (value === null || value === undefined || Number.isNaN(Number(value))) {
    return '-';
  }
  return Number(value).toLocaleString(undefined, {
    minimumFractionDigits: 0,
    maximumFractionDigits: digits,
  });
}

function SelectControl({ label, value, onChange, options }) {
  return (
    <label className="control">
      <span>{label}</span>
      <select value={value} onChange={(event) => onChange(event.target.value)}>
        {options.map((option) => (
          <option key={option.value} value={option.value}>
            {option.label}
          </option>
        ))}
      </select>
    </label>
  );
}

function LookbackControl({ value, onChange, options }) {
  return (
    <fieldset className="lookback-control">
      <legend>RS-Momentum lookback</legend>
      <div className="radio-row">
        {options.map((option) => (
          <label key={option} className="radio-option">
            <input
              checked={String(value) === String(option)}
              name="lookback"
              onChange={() => onChange(String(option))}
              type="radio"
              value={option}
            />
            <span>{option}</span>
          </label>
        ))}
      </div>
    </fieldset>
  );
}

function SectorList({ title, items, selectedValue, onSelect }) {
  const rowsRef = useRef(null);

  useEffect(() => {
    rowsRef.current?.querySelector('.sector-row.active')?.scrollIntoView({ block: 'nearest' });
  }, [selectedValue]);

  return (
    <section className="sector-list" aria-label={title}>
      <h3>{title}</h3>
      <div className="sector-list-rows" ref={rowsRef}>
        {items.map((item) => (
          <button
            className={item.value === selectedValue ? 'sector-row active' : 'sector-row'}
            key={item.value}
            onClick={() => onSelect(item.value)}
            title={item.label}
            type="button"
          >
            <span>{item.label}</span>
          </button>
        ))}
      </div>
    </section>
  );
}

function SectorDrilldown({
  mainSectors,
  selectedMainSector,
  selectedSubSector,
  subSectors,
  onMainSectorChange,
  onSubSectorChange,
}) {
  const mainItems = mainSectors.map((sector) => ({ value: sector, label: sector }));
  const subItems = [
    { value: 'ALL', label: `All ${selectedMainSector}` },
    ...subSectors.map((sector) => ({ value: sector, label: sector })),
  ];

  return (
    <div className="sector-drilldown">
      <SectorList title="Main sectors" items={mainItems} selectedValue={selectedMainSector} onSelect={onMainSectorChange} />
      <SectorList title={`${selectedMainSector} subsectors`} items={subItems} selectedValue={selectedSubSector} onSelect={onSubSectorChange} />
    </div>
  );
}

function scaleFor(values, fallback) {
  const finite = values.filter((value) => Number.isFinite(value));
  if (!finite.length) {
    return fallback;
  }
  const min = Math.min(...finite, fallback[0]);
  const max = Math.max(...finite, fallback[1]);
  const padding = Math.max(5, (max - min) * 0.08);
  return [
    Math.floor((min - padding) / 5) * 5,
    Math.ceil((max + padding) / 5) * 5,
  ];
}

function ticksFor(min, max, targetCount = 8) {
  const range = Math.max(max - min, 1);
  const rawStep = range / targetCount;
  const magnitude = 10 ** Math.floor(Math.log10(rawStep));
  const normalized = rawStep / magnitude;
  const step = (normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10) * magnitude;
  const first = Math.ceil(min / step) * step;
  const ticks = [];
  for (let tick = first; tick <= max + step * 0.25; tick += step) {
    ticks.push(Number(tick.toFixed(2)));
  }
  return ticks;
}

function clampNumber(value, min, max) {
  return Math.min(Math.max(value, min), max);
}

function truncateLabel(value, maxLength = 34) {
  const label = String(value ?? '');
  return label.length > maxLength ? `${label.slice(0, maxLength - 3)}...` : label;
}

function pointInRect(point, rect, padding = 0) {
  return (
    point.x >= rect.x - padding &&
    point.x <= rect.x + rect.width + padding &&
    point.y >= rect.y - padding &&
    point.y <= rect.y + rect.height + padding
  );
}

function segmentOrientation(a, b, c) {
  const value = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y);
  if (Math.abs(value) < 0.001) {
    return 0;
  }
  return value > 0 ? 1 : 2;
}

function pointOnSegment(a, b, c) {
  return (
    b.x <= Math.max(a.x, c.x) + 0.001 &&
    b.x >= Math.min(a.x, c.x) - 0.001 &&
    b.y <= Math.max(a.y, c.y) + 0.001 &&
    b.y >= Math.min(a.y, c.y) - 0.001
  );
}

function segmentsIntersect(a, b, c, d) {
  const orientationA = segmentOrientation(a, b, c);
  const orientationB = segmentOrientation(a, b, d);
  const orientationC = segmentOrientation(c, d, a);
  const orientationD = segmentOrientation(c, d, b);
  if (orientationA !== orientationB && orientationC !== orientationD) {
    return true;
  }
  if (orientationA === 0 && pointOnSegment(a, c, b)) {
    return true;
  }
  if (orientationB === 0 && pointOnSegment(a, d, b)) {
    return true;
  }
  if (orientationC === 0 && pointOnSegment(c, a, d)) {
    return true;
  }
  return orientationD === 0 && pointOnSegment(c, b, d);
}

function segmentIntersectsRect(start, end, rect, padding = 8) {
  const expanded = {
    x: rect.x - padding,
    y: rect.y - padding,
    width: rect.width + padding * 2,
    height: rect.height + padding * 2,
  };
  if (pointInRect(start, expanded) || pointInRect(end, expanded)) {
    return true;
  }
  const topLeft = { x: expanded.x, y: expanded.y };
  const topRight = { x: expanded.x + expanded.width, y: expanded.y };
  const bottomRight = { x: expanded.x + expanded.width, y: expanded.y + expanded.height };
  const bottomLeft = { x: expanded.x, y: expanded.y + expanded.height };
  return (
    segmentsIntersect(start, end, topLeft, topRight) ||
    segmentsIntersect(start, end, topRight, bottomRight) ||
    segmentsIntersect(start, end, bottomRight, bottomLeft) ||
    segmentsIntersect(start, end, bottomLeft, topLeft)
  );
}

function chooseTooltipPlacement(point, trailPoints, tooltipSize, bounds) {
  const gap = 18;
  const candidates = [
    { x: point.x + gap, y: point.y - tooltipSize.height - gap },
    { x: point.x + gap, y: point.y + gap },
    { x: point.x - tooltipSize.width - gap, y: point.y - tooltipSize.height - gap },
    { x: point.x - tooltipSize.width - gap, y: point.y + gap },
    { x: point.x - tooltipSize.width / 2, y: point.y - tooltipSize.height - gap * 1.5 },
    { x: point.x - tooltipSize.width / 2, y: point.y + gap * 1.5 },
    { x: bounds.right - tooltipSize.width, y: bounds.top },
    { x: bounds.right - tooltipSize.width, y: bounds.bottom - tooltipSize.height },
    { x: bounds.left, y: bounds.top },
    { x: bounds.left, y: bounds.bottom - tooltipSize.height },
  ];
  const scored = candidates.map((candidate) => {
    const rect = {
      x: clampNumber(candidate.x, bounds.left, bounds.right - tooltipSize.width),
      y: clampNumber(candidate.y, bounds.top, bounds.bottom - tooltipSize.height),
      width: tooltipSize.width,
      height: tooltipSize.height,
    };
    let score = Math.hypot(rect.x - candidate.x, rect.y - candidate.y);
    if (pointInRect(point, rect, 16)) {
      score += 50000;
    }
    for (let index = 1; index < trailPoints.length; index += 1) {
      if (segmentIntersectsRect(trailPoints[index - 1], trailPoints[index], rect)) {
        score += 10000;
      }
    }
    score += Math.hypot(rect.x + rect.width / 2 - point.x, rect.y + rect.height / 2 - point.y) / 100;
    return { rect, score };
  });
  return scored.sort((a, b) => a.score - b.score)[0].rect;
}

function RrgChart({ activeRowId, rows, onHoverRow, onRowClick }) {
  const width = 920;
  const height = 540;
  const margin = { top: 34, right: 36, bottom: 58, left: 72 };
  const innerWidth = width - margin.left - margin.right;
  const innerHeight = height - margin.top - margin.bottom;
  const allPoints = rows.flatMap((row) => row.trail || []);
  const [xMin, xMax] = scaleFor(
    allPoints.map((point) => point.rsRatio),
    [70, 125],
  );
  const [yMin, yMax] = scaleFor(
    allPoints.map((point) => point.rsMomentum),
    [65, 145],
  );
  const xTicks = ticksFor(xMin, xMax);
  const yTicks = ticksFor(yMin, yMax);
  const x = (value) => margin.left + ((value - xMin) / (xMax - xMin || 1)) * innerWidth;
  const y = (value) => margin.top + innerHeight - ((value - yMin) / (yMax - yMin || 1)) * innerHeight;
  const activeRow = activeRowId ? rows.find((row) => rrgRowId(row) === activeRowId) : null;
  const chartActiveRowId = activeRow ? activeRowId : null;
  const displayOffsets = useMemo(() => {
    const groups = new Map();
    rows.forEach((row) => {
      const latest = row.trail?.[row.trail.length - 1];
      if (!latest) {
        return;
      }
      const key = `${latest.rsRatio.toFixed(2)}:${latest.rsMomentum.toFixed(2)}`;
      groups.set(key, [...(groups.get(key) || []), rrgRowId(row)]);
    });
    const offsets = new Map();
    groups.forEach((ids) => {
      if (ids.length < 2) {
        return;
      }
      const radius = Math.min(5 + ids.length, 13);
      ids.forEach((id, index) => {
        const angle = -Math.PI / 2 + (index / ids.length) * Math.PI * 2;
        offsets.set(id, {
          dx: Math.cos(angle) * radius,
          dy: Math.sin(angle) * radius,
        });
      });
    });
    return offsets;
  }, [rows]);
  const interactive = typeof onRowClick === 'function';

  return (
    <div className="chart-wrap" aria-label="Sector rotation graph">
      <svg viewBox={`0 0 ${width} ${height}`} role="img">
        <title>Sector Rotation Graph</title>
        <rect className="chart-bg" x="0" y="0" width={width} height={height} />
        {xTicks.map((tick) => (
          <g key={`x-${tick}`}>
            <line className="grid-line" x1={x(tick)} x2={x(tick)} y1={margin.top} y2={height - margin.bottom} />
            <text className="axis-tick" x={x(tick)} y={height - 24} textAnchor="middle">
              {tick}
            </text>
          </g>
        ))}
        {yTicks.map((tick) => (
          <g key={`y-${tick}`}>
            <line className="grid-line" x1={margin.left} x2={width - margin.right} y1={y(tick)} y2={y(tick)} />
            <text className="axis-tick" x={margin.left - 18} y={y(tick) + 4} textAnchor="end">
              {tick}
            </text>
          </g>
        ))}
        <line className="neutral-line" x1={x(100)} x2={x(100)} y1={margin.top} y2={height - margin.bottom} />
        <line className="neutral-line" x1={margin.left} x2={width - margin.right} y1={y(100)} y2={y(100)} />
        <text className="axis-label" x={width / 2} y={height - 6} textAnchor="middle">
          RS-Ratio
        </text>
        <text className="axis-label" transform={`translate(18 ${height / 2}) rotate(-90)`} textAnchor="middle">
          RS-Momentum
        </text>
        {Object.entries(quadrantLabels).map(([label, position]) => (
          <text
            key={label}
            className={`quadrant-label ${label.toLowerCase()}`}
            x={margin.left + innerWidth * position.x}
            y={margin.top + innerHeight * position.y}
            textAnchor={position.x > 0.5 ? 'end' : 'start'}
          >
            {label}
          </text>
        ))}
        {rows.map((row) => {
          const color = quadrantColors[row.quadrant] || '#64748b';
          const trail = row.trail || [];
          const rowId = rrgRowId(row);
          const offset = displayOffsets.get(rowId) || { dx: 0, dy: 0 };
          const displayX = (point) => x(point.rsRatio) + offset.dx;
          const displayY = (point) => y(point.rsMomentum) + offset.dy;
          const path = trail.map((point) => `${displayX(point)},${displayY(point)}`).join(' ');
          const latest = trail[trail.length - 1];
          const isActive = chartActiveRowId === rowId;
          const isDimmed = chartActiveRowId && !isActive;
          const label = rrgRowLabel(row);
          if (!latest) {
            return null;
          }
          return (
            <g
              key={rowId}
              aria-label={label}
              className={isDimmed ? 'rrg-series dimmed' : isActive ? 'rrg-series active' : 'rrg-series'}
              onClick={() => onRowClick?.(row)}
              onFocus={() => onHoverRow?.(rowId)}
              onBlur={() => onHoverRow?.(null)}
              onKeyDown={(event) => {
                if (!interactive || (event.key !== 'Enter' && event.key !== ' ')) {
                  return;
                }
                event.preventDefault();
                onRowClick(row);
              }}
              onMouseEnter={() => onHoverRow?.(rowId)}
              onMouseLeave={() => onHoverRow?.(null)}
              role={interactive ? 'button' : undefined}
              tabIndex={interactive ? 0 : undefined}
            >
              {trail.length > 1 && <polyline className="trail-hitbox" points={path} />}
              {trail.length > 1 && <polyline className="trail-line" points={path} stroke={color} />}
              {trail.slice(0, -1).map((point) => (
                <circle
                  key={`${row.ticker}-${point.date}`}
                  className="trail-dot"
                  cx={displayX(point)}
                  cy={displayY(point)}
                  fill={color}
                  r="2.2"
                />
              ))}
              <circle className="latest-hitbox" cx={displayX(latest)} cy={displayY(latest)} r="12" />
              <circle className="latest-dot" cx={displayX(latest)} cy={displayY(latest)} fill={color} r="5.2" />
            </g>
          );
        })}
        {activeRow &&
          (() => {
            const trail = activeRow.trail || [];
            const latest = trail[trail.length - 1];
            if (!latest) {
              return null;
            }
            const offset = displayOffsets.get(rrgRowId(activeRow)) || { dx: 0, dy: 0 };
            const pointX = x(latest.rsRatio) + offset.dx;
            const pointY = y(latest.rsMomentum) + offset.dy;
            const tooltipWidth = 250;
            const tooltipHeight = 88;
            const displayedTrail = trail.map((point) => ({
              x: x(point.rsRatio) + offset.dx,
              y: y(point.rsMomentum) + offset.dy,
            }));
            const tooltipRect = chooseTooltipPlacement(
              { x: pointX, y: pointY },
              displayedTrail,
              { width: tooltipWidth, height: tooltipHeight },
              {
                left: margin.left,
                right: width - margin.right,
                top: margin.top,
                bottom: height - margin.bottom,
              },
            );
            const tooltipX = tooltipRect.x;
            const tooltipY = tooltipRect.y;
            const anchorX = clampNumber(pointX, tooltipX, tooltipX + tooltipWidth);
            const anchorY = clampNumber(pointY, tooltipY, tooltipY + tooltipHeight);
            const activeLabel = rrgRowLabel(activeRow);
            return (
              <g className="rrg-tooltip" pointerEvents="none">
                <line className="tooltip-anchor" x1={pointX} x2={anchorX} y1={pointY} y2={anchorY} />
                <rect height={tooltipHeight} rx="7" width={tooltipWidth} x={tooltipX} y={tooltipY} />
                <text className="tooltip-title" x={tooltipX + 14} y={tooltipY + 24}>
                  {truncateLabel(activeLabel)}
                </text>
                <text className="tooltip-meta" x={tooltipX + 14} y={tooltipY + 46}>
                  {activeRow.ticker} · {activeRow.quadrant}
                </text>
                <text className="tooltip-stat" x={tooltipX + 14} y={tooltipY + 68}>
                  RS {formatNumber(activeRow.rsRatio)} · Mom {formatNumber(activeRow.rsMomentum)} · 20d {formatNumber(activeRow.ret20)}%
                </text>
              </g>
            );
          })()}
      </svg>
      <div className="legend">
        {Object.entries(quadrantColors).map(([label, color]) => (
          <span key={label}>
            <i style={{ backgroundColor: color }} />
            {label}
          </span>
        ))}
      </div>
    </div>
  );
}

function rrgSortValue(row, key) {
  if (key === 'name') {
    return rrgRowLabel(row);
  }
  return row[key];
}

function trendSortValue(row, key) {
  if (key === 'sector') {
    return row.mainSector || row.sector;
  }
  if (key === 'subSector') {
    return row.subSector || row.sector;
  }
  return row[key];
}

function compareValues(a, b) {
  const aNumber = Number(a);
  const bNumber = Number(b);
  if (Number.isFinite(aNumber) && Number.isFinite(bNumber)) {
    return aNumber - bNumber;
  }
  return String(a ?? '').localeCompare(String(b ?? ''), undefined, { numeric: true, sensitivity: 'base' });
}

function isMissingSortValue(value) {
  return value === null || value === undefined || Number.isNaN(value);
}

function compareRows(a, b, sort, valueForRow) {
  const aValue = valueForRow(a, sort.key);
  const bValue = valueForRow(b, sort.key);
  const aMissing = isMissingSortValue(aValue);
  const bMissing = isMissingSortValue(bValue);
  if (aMissing || bMissing) {
    if (aMissing && bMissing) {
      return 0;
    }
    return aMissing ? 1 : -1;
  }
  const result = compareValues(aValue, bValue);
  return sort.direction === 'asc' ? result : -result;
}

function SortableHeader({ column, label, sort, onSort, align = 'left' }) {
  const active = sort.key === column;
  const ariaSort = active ? (sort.direction === 'asc' ? 'ascending' : 'descending') : 'none';
  return (
    <th aria-sort={ariaSort} className={align === 'right' ? 'number sortable-heading' : 'sortable-heading'}>
      <button onClick={() => onSort(column)} type="button">
        <span>{label}</span>
        {active && <i aria-hidden="true">{sort.direction === 'asc' ? '↑' : '↓'}</i>}
      </button>
    </th>
  );
}

function RrgTable({ activeRowId, rows, onHoverRow, onRowClick }) {
  const [sort, setSort] = useState({ key: 'name', direction: 'asc' });
  const sortedRows = useMemo(
    () =>
      [...rows].sort((a, b) => compareRows(a, b, sort, rrgSortValue)),
    [rows, sort],
  );
  const handleSort = (key) => {
    setSort((current) => ({
      key,
      direction: current.key === key && current.direction === 'asc' ? 'desc' : 'asc',
    }));
  };

  return (
    <div className="table-shell rrg-table-shell">
      <table>
        <colgroup>
          <col className="rrg-name-column" />
          <col className="rrg-ratio-column" />
          <col className="rrg-momentum-column" />
          <col className="rrg-quadrant-column" />
          <col className="rrg-return-column" />
        </colgroup>
        <thead>
          <tr>
            <SortableHeader column="name" label="Name" onSort={handleSort} sort={sort} />
            <SortableHeader align="right" column="rsRatio" label="RS Ratio" onSort={handleSort} sort={sort} />
            <SortableHeader align="right" column="rsMomentum" label="RS Mom" onSort={handleSort} sort={sort} />
            <SortableHeader column="quadrant" label="Quadrant" onSort={handleSort} sort={sort} />
            <SortableHeader align="right" column="ret20" label="20d %" onSort={handleSort} sort={sort} />
          </tr>
        </thead>
        <tbody>
          {sortedRows.map((row) => {
            const rowId = rrgRowId(row);
            const rowClass = [onRowClick ? 'clickable-row' : '', activeRowId === rowId ? 'highlighted-row' : ''].filter(Boolean).join(' ');
            return (
              <tr
                className={rowClass || undefined}
                key={rowId}
                onClick={() => onRowClick?.(row)}
                onMouseEnter={() => onHoverRow?.(rowId)}
                onMouseLeave={() => onHoverRow?.(null)}
              >
                <td>{rrgRowLabel(row)}</td>
                <td className="number">{formatNumber(row.rsRatio)}</td>
                <td className="number">{formatNumber(row.rsMomentum)}</td>
                <td>{row.quadrant}</td>
                <td className="number">{formatNumber(row.ret20)}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

const trendColumns = [
  { key: 'ticker', label: 'Ticker' },
  { key: 'name', label: 'Name' },
  { key: 'sector', label: 'Sector' },
  { key: 'subSector', label: 'Subsector' },
  { key: 'ret20', label: '20d Ret %', align: 'right' },
  { key: 'ret60', label: '60d Ret %', align: 'right' },
  { key: 'rs20', label: 'RS_20d', align: 'right' },
  { key: 'rs60', label: 'RS_60d', align: 'right' },
  { key: 'vol20', label: '20d Vol', align: 'right' },
  { key: 'volVsAvg', label: 'Vol vs Avg', align: 'right' },
  { key: 'score', label: 'Score', align: 'right' },
];

function sortTrendRowsByScore(rows) {
  return [...rows].sort((a, b) => compareRows(a, b, { key: 'score', direction: 'desc' }, trendSortValue));
}

const candleIntervals = [
  { value: 'daily', label: 'D' },
  { value: 'weekly', label: 'W' },
  { value: 'monthly', label: 'M' },
];

const chartTimeframes = [
  { value: '5y', label: '5Y' },
  { value: '3y', label: '3Y' },
  { value: '1y', label: '1Y' },
  { value: 'ytd', label: 'YTD' },
  { value: '3m', label: '3M' },
  { value: '1m', label: '1M' },
  { value: '1w', label: '1W' },
  { value: '1d', label: '1D' },
];

const movingAverageSpecs = [
  { key: 'sma200', label: 'SMA 200', type: 'sma', period: 200, color: '#475569', lineWidth: 1 },
  { key: 'sma100', label: 'SMA 100', type: 'sma', period: 100, color: '#7c3aed', lineWidth: 1 },
  { key: 'sma50', label: 'SMA 50', type: 'sma', period: 50, color: '#0f766e', lineWidth: 1 },
  { key: 'sma20', label: 'SMA 20', type: 'sma', period: 20, color: '#f97316', lineWidth: 1 },
  { key: 'ema21', label: 'EMA 21', type: 'ema', period: 21, color: '#2563eb', lineWidth: 2 },
  { key: 'ema9', label: 'EMA 9', type: 'ema', period: 9, color: '#dc2626', lineWidth: 2 },
];

function parseDate(value) {
  const [year, month, day] = value.split('-').map(Number);
  return new Date(Date.UTC(year, month - 1, day));
}

function dateKey(date) {
  return date.toISOString().slice(0, 10);
}

function addMonths(date, months) {
  const next = new Date(date);
  next.setUTCMonth(next.getUTCMonth() + months);
  return next;
}

function timeframeStartDate(points, timeframe) {
  if (!points.length) {
    return null;
  }
  const latest = parseDate(points[points.length - 1].time);
  if (timeframe === '5y') {
    return addMonths(latest, -60);
  }
  if (timeframe === '3y') {
    return addMonths(latest, -36);
  }
  if (timeframe === '1y') {
    return addMonths(latest, -12);
  }
  if (timeframe === 'ytd') {
    return new Date(Date.UTC(latest.getUTCFullYear(), 0, 1));
  }
  if (timeframe === '3m') {
    return addMonths(latest, -3);
  }
  if (timeframe === '1m') {
    return addMonths(latest, -1);
  }
  if (timeframe === '1w') {
    const start = new Date(latest);
    start.setUTCDate(start.getUTCDate() - 7);
    return start;
  }
  if (timeframe === '1d') {
    return latest;
  }
  return null;
}

function normalizePricePoint(point) {
  if (Array.isArray(point)) {
    const [time, open, high, low, close] = point;
    return {
      time,
      open: Number(open),
      high: Number(high),
      low: Number(low),
      close: Number(close),
    };
  }
  const close = Number(point.close ?? point.value);
  const open = Number(point.open ?? close);
  const high = Number(point.high ?? Math.max(open, close));
  const low = Number(point.low ?? Math.min(open, close));
  return {
    time: point.time,
    open,
    high,
    low,
    close,
  };
}

function candleGroupKey(time, interval) {
  const date = parseDate(time);
  if (interval === 'weekly') {
    const start = new Date(date);
    const day = start.getUTCDay() || 7;
    start.setUTCDate(start.getUTCDate() - day + 1);
    return dateKey(start);
  }
  return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
}

function aggregateCandles(points, interval) {
  if (interval === 'daily') {
    return points;
  }
  const grouped = [];
  let current = null;
  points.forEach((point) => {
    const key = candleGroupKey(point.time, interval);
    if (!current || current.key !== key) {
      current = {
        key,
        time: point.time,
        open: point.open,
        high: point.high,
        low: point.low,
        close: point.close,
      };
      grouped.push(current);
      return;
    }
    current.high = Math.max(current.high, point.high);
    current.low = Math.min(current.low, point.low);
    current.close = point.close;
    current.time = point.time;
  });
  return grouped.map(({ key, ...point }) => point);
}

function filterCandlesByTimeframe(points, timeframe) {
  const start = timeframeStartDate(points, timeframe);
  if (!start) {
    return points;
  }
  const filtered = points.filter((point) => parseDate(point.time) >= start);
  return filtered.length ? filtered : points.slice(-1);
}

function simpleMovingAverage(points, period) {
  const values = [];
  let sum = 0;
  points.forEach((point, index) => {
    sum += point.close;
    if (index >= period) {
      sum -= points[index - period].close;
    }
    if (index >= period - 1) {
      values.push({ time: point.time, value: Number((sum / period).toFixed(2)) });
    }
  });
  return values;
}

function exponentialMovingAverage(points, period) {
  const values = [];
  const multiplier = 2 / (period + 1);
  let ema = null;
  let sum = 0;
  points.forEach((point, index) => {
    sum += point.close;
    if (index === period - 1) {
      ema = sum / period;
      values.push({ time: point.time, value: Number(ema.toFixed(2)) });
      return;
    }
    if (index >= period && ema !== null) {
      ema = (point.close - ema) * multiplier + ema;
      values.push({ time: point.time, value: Number(ema.toFixed(2)) });
    }
  });
  return values;
}

function movingAverageData(points, spec, visibleStartTime) {
  const fullSeries = spec.type === 'ema' ? exponentialMovingAverage(points, spec.period) : simpleMovingAverage(points, spec.period);
  if (!visibleStartTime) {
    return fullSeries;
  }
  const start = parseDate(visibleStartTime);
  return fullSeries.filter((point) => parseDate(point.time) >= start);
}

function TrendTable({ rows }) {
  const [sort, setSort] = useState({ key: 'score', direction: 'desc' });
  const sortedRows = useMemo(
    () => [...rows].sort((a, b) => compareRows(a, b, sort, trendSortValue)),
    [rows, sort],
  );
  const handleSort = (key) => {
    setSort((current) => ({
      key,
      direction: current.key === key && current.direction === 'asc' ? 'desc' : 'asc',
    }));
  };

  return (
    <div className="table-shell trend-table-shell">
      <table>
        <colgroup>
          <col className="trend-ticker-column" />
          <col className="trend-name-column" />
          <col className="trend-sector-column" />
          <col className="trend-subsector-column" />
          <col className="trend-number-column" />
          <col className="trend-number-column" />
          <col className="trend-number-column" />
          <col className="trend-number-column" />
          <col className="trend-number-column" />
          <col className="trend-number-column" />
          <col className="trend-number-column" />
        </colgroup>
        <thead>
          <tr>
            {trendColumns.map((column) => (
              <SortableHeader
                align={column.align}
                column={column.key}
                key={column.key}
                label={column.label}
                onSort={handleSort}
                sort={sort}
              />
            ))}
          </tr>
        </thead>
        <tbody>
          {sortedRows.length ? (
            sortedRows.map((row) => (
              <tr key={row.id || row.ticker}>
                <td>{row.ticker}</td>
                <td>{row.name}</td>
                <td>{row.mainSector || row.sector}</td>
                <td>{row.subSector || row.sector}</td>
                <td className="number">{formatNumber(row.ret20)}</td>
                <td className="number">{formatNumber(row.ret60)}</td>
                <td className="number">{formatNumber(row.rs20)}</td>
                <td className="number">{formatNumber(row.rs60)}</td>
                <td className="number">{formatNumber(row.vol20)}</td>
                <td className="number">{formatNumber(row.volVsAvg)}</td>
                <td className="number">{formatNumber(row.score)}</td>
              </tr>
            ))
          ) : (
            <tr>
              <td className="empty-row" colSpan={trendColumns.length}>
                No stocks in this sector slice.
              </td>
            </tr>
          )}
        </tbody>
      </table>
    </div>
  );
}

function StockPriceChart({ error, interval, isLoading, series, stock, timeframe }) {
  const containerRef = useRef(null);
  const chartRef = useRef(null);
  const candleSeriesRef = useRef(null);
  const averageSeriesRef = useRef(new Map());
  const chartData = useMemo(() => {
    const dailyCandles = (series || []).map(normalizePricePoint).filter((point) => Number.isFinite(point.close));
    const aggregatedCandles = aggregateCandles(dailyCandles, interval);
    const visibleCandles = filterCandlesByTimeframe(aggregatedCandles, timeframe);
    const visibleStartTime = visibleCandles[0]?.time;
    const averages = movingAverageSpecs.map((spec) => ({
      ...spec,
      data: movingAverageData(aggregatedCandles, spec, visibleStartTime),
    }));
    return { candles: visibleCandles, averages };
  }, [interval, series, timeframe]);

  useEffect(() => {
    const container = containerRef.current;
    if (!container) {
      return undefined;
    }

    const chart = createChart(container, {
      width: container.clientWidth,
      height: container.clientHeight,
      attributionLogo: true,
      layout: {
        background: { type: ColorType.Solid, color: '#ffffff' },
        fontFamily: 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
        textColor: '#526173',
      },
      grid: {
        horzLines: { color: '#edf1f6' },
        vertLines: { color: '#edf1f6' },
      },
      crosshair: {
        mode: CrosshairMode.Normal,
      },
      rightPriceScale: {
        borderColor: '#d9e1ec',
      },
      timeScale: {
        borderColor: '#d9e1ec',
        fixLeftEdge: true,
        fixRightEdge: true,
      },
      localization: {
        priceFormatter: (price) =>
          Number(price).toLocaleString(undefined, {
            minimumFractionDigits: 2,
            maximumFractionDigits: 2,
          }),
      },
    });
    const candleSeries = chart.addSeries(CandlestickSeries, {
      borderDownColor: '#ef4444',
      borderUpColor: '#16a34a',
      downColor: '#ef4444',
      upColor: '#16a34a',
      wickDownColor: '#ef4444',
      wickUpColor: '#16a34a',
    });

    const averageSeries = new Map();
    movingAverageSpecs.forEach((spec) => {
      averageSeries.set(
        spec.key,
        chart.addSeries(LineSeries, {
          color: spec.color,
          lastValueVisible: false,
          lineWidth: spec.lineWidth,
          priceLineVisible: false,
        }),
      );
    });

    chartRef.current = chart;
    candleSeriesRef.current = candleSeries;
    averageSeriesRef.current = averageSeries;

    const resizeChart = () => {
      const bounds = container.getBoundingClientRect();
      chart.applyOptions({
        width: Math.max(320, Math.floor(bounds.width)),
        height: Math.max(320, Math.floor(bounds.height)),
      });
      chart.timeScale().fitContent();
    };

    const observer = new ResizeObserver(resizeChart);
    observer.observe(container);
    resizeChart();

    return () => {
      observer.disconnect();
      chart.remove();
      chartRef.current = null;
      candleSeriesRef.current = null;
      averageSeriesRef.current = new Map();
    };
  }, []);

  useEffect(() => {
    const candleSeries = candleSeriesRef.current;
    const chart = chartRef.current;
    if (!candleSeries || !chart) {
      return;
    }
    candleSeries.setData(chartData.candles);
    chartData.averages.forEach((average) => {
      averageSeriesRef.current.get(average.key)?.setData(average.data);
    });
    chart.timeScale().fitContent();
  }, [chartData]);

  return (
    <div className="stock-chart-canvas" ref={containerRef}>
      {!stock && <div className="chart-empty-state">No stock selected.</div>}
      {stock && isLoading && <div className="chart-empty-state">Loading chart for {stock.ticker}...</div>}
      {stock && !isLoading && error && <div className="chart-empty-state">{error}</div>}
      {stock && !isLoading && !error && !chartData.candles.length && <div className="chart-empty-state">No chart data for {stock.ticker}.</div>}
    </div>
  );
}

function ChartSection({ rows, stockChartBasePath, stockChartFiles }) {
  const sortedRows = useMemo(() => sortTrendRowsByScore(rows), [rows]);
  const [selectedTicker, setSelectedTicker] = useState('');
  const [candleInterval, setCandleInterval] = useState('daily');
  const [timeframe, setTimeframe] = useState('1y');
  const [chartCache, setChartCache] = useState({});
  const [chartLoadState, setChartLoadState] = useState({ ticker: '', loading: false, error: '' });
  const listRef = useRef(null);

  useEffect(() => {
    setSelectedTicker((currentTicker) => {
      if (sortedRows.some((row) => row.ticker === currentTicker)) {
        return currentTicker;
      }
      return sortedRows[0]?.ticker || '';
    });
  }, [sortedRows]);

  useEffect(() => {
    listRef.current?.querySelector('.stock-list-row.active')?.scrollIntoView({ block: 'nearest' });
  }, [selectedTicker]);

  const selectedIndex = sortedRows.findIndex((row) => row.ticker === selectedTicker);
  const selectedStock = selectedIndex >= 0 ? sortedRows[selectedIndex] : sortedRows[0];
  const selectedSeries = selectedStock ? chartCache[selectedStock.ticker] || [] : [];
  const selectedChartFile = selectedStock ? stockChartFiles?.[selectedStock.ticker] : null;
  const canNavigate = sortedRows.length > 1;

  useEffect(() => {
    if (!selectedStock) {
      setChartLoadState({ ticker: '', loading: false, error: '' });
      return undefined;
    }
    if (chartCache[selectedStock.ticker]) {
      setChartLoadState({ ticker: selectedStock.ticker, loading: false, error: '' });
      return undefined;
    }
    if (!selectedChartFile) {
      setChartLoadState({ ticker: selectedStock.ticker, loading: false, error: `No chart file for ${selectedStock.ticker}.` });
      return undefined;
    }

    const controller = new AbortController();
    const basePath = stockChartBasePath || 'data/charts';
    const chartUrl = `${import.meta.env.BASE_URL}${basePath}/${selectedChartFile}`;
    setChartLoadState({ ticker: selectedStock.ticker, loading: true, error: '' });
    fetch(chartUrl, { signal: controller.signal })
      .then((response) => {
        if (!response.ok) {
          throw new Error(`Unable to load chart for ${selectedStock.ticker}: ${response.status}`);
        }
        return response.json();
      })
      .then((series) => {
        setChartCache((currentCache) => ({ ...currentCache, [selectedStock.ticker]: series }));
        setChartLoadState({ ticker: selectedStock.ticker, loading: false, error: '' });
      })
      .catch((error) => {
        if (error.name === 'AbortError') {
          return;
        }
        setChartLoadState({ ticker: selectedStock.ticker, loading: false, error: error.message });
      });

    return () => controller.abort();
  }, [chartCache, selectedChartFile, selectedStock, stockChartBasePath]);

  const handlePrevious = () => {
    if (!canNavigate) {
      return;
    }
    setSelectedTicker((currentTicker) => {
      const currentIndex = sortedRows.findIndex((row) => row.ticker === currentTicker);
      const nextIndex = currentIndex > 0 ? currentIndex - 1 : sortedRows.length - 1;
      return sortedRows[nextIndex].ticker;
    });
  };
  const handleNext = () => {
    if (!canNavigate) {
      return;
    }
    setSelectedTicker((currentTicker) => {
      const currentIndex = sortedRows.findIndex((row) => row.ticker === currentTicker);
      const nextIndex = currentIndex >= 0 ? (currentIndex + 1) % sortedRows.length : 0;
      return sortedRows[nextIndex].ticker;
    });
  };

  return (
    <section className="stock-chart-section" aria-labelledby="stock-chart-title">
      <div className="section-heading compact-heading">
        <h2 id="stock-chart-title">Chart</h2>
        <span>
          {sortedRows.length} stocks · sorted by Score
          {selectedStock ? ` · ${selectedStock.ticker}` : ''}
        </span>
      </div>

      <div className="stock-chart-grid">
        <div className="stock-chart-panel">
          <div className="stock-chart-toolbar">
            <div className="stock-chart-title-block">
              <strong data-testid="stock-chart-selected-ticker">{selectedStock?.ticker || '-'}</strong>
              <span>{selectedStock?.name || 'No stocks in this slice'}</span>
            </div>
            <div className="stock-chart-actions">
              <button aria-label="Previous stock" disabled={!canNavigate} onClick={handlePrevious} title="Previous stock" type="button">
                <span aria-hidden="true">‹</span>
              </button>
              <button
                aria-label="Next stock"
                data-testid="stock-chart-next"
                disabled={!canNavigate}
                onClick={handleNext}
                title="Next stock"
                type="button"
              >
                <span aria-hidden="true">›</span>
              </button>
            </div>
          </div>

          <div className="stock-chart-controls" aria-label="Chart controls">
            <div className="segmented-control" aria-label="Candle interval">
              {candleIntervals.map((option) => (
                <button
                  className={option.value === candleInterval ? 'active' : ''}
                  key={option.value}
                  onClick={() => setCandleInterval(option.value)}
                  type="button"
                >
                  {option.label}
                </button>
              ))}
            </div>
            <div className="segmented-control timeframe-control" aria-label="Timeframe">
              {chartTimeframes.map((option) => (
                <button
                  className={option.value === timeframe ? 'active' : ''}
                  key={option.value}
                  onClick={() => setTimeframe(option.value)}
                  type="button"
                >
                  {option.label}
                </button>
              ))}
            </div>
          </div>

          <div className="stock-chart-stats" aria-label="Selected stock metrics">
            <span>Score {formatNumber(selectedStock?.score)}</span>
            <span>20d {formatNumber(selectedStock?.ret20)}%</span>
            <span>60d {formatNumber(selectedStock?.ret60)}%</span>
            <span>RS 20d {formatNumber(selectedStock?.rs20)}</span>
          </div>

          <div className="moving-average-legend" aria-label="Moving averages">
            {movingAverageSpecs.map((spec) => (
              <span key={spec.key}>
                <i style={{ backgroundColor: spec.color }} />
                {spec.label}
              </span>
            ))}
          </div>

          <StockPriceChart
            error={chartLoadState.ticker === selectedStock?.ticker ? chartLoadState.error : ''}
            interval={candleInterval}
            isLoading={chartLoadState.ticker === selectedStock?.ticker && chartLoadState.loading}
            series={selectedSeries}
            stock={selectedStock}
            timeframe={timeframe}
          />
        </div>

        <div className="table-shell chart-stock-table-shell" ref={listRef}>
          <table>
            <colgroup>
              <col className="chart-stock-ticker-column" />
              <col className="chart-stock-name-column" />
              <col className="chart-stock-score-column" />
            </colgroup>
            <thead>
              <tr>
                <th>Ticker</th>
                <th>Name</th>
                <th className="number">Score</th>
              </tr>
            </thead>
            <tbody>
              {sortedRows.length ? (
                sortedRows.map((row) => (
                  <tr
                    className={row.ticker === selectedStock?.ticker ? 'stock-list-row active' : 'stock-list-row'}
                    key={row.id || row.ticker}
                    onClick={() => setSelectedTicker(row.ticker)}
                  >
                    <td>{row.ticker}</td>
                    <td title={row.name}>{row.name}</td>
                    <td className="number">{formatNumber(row.score)}</td>
                  </tr>
                ))
              ) : (
                <tr>
                  <td className="empty-row" colSpan="3">
                    No stocks in this sector slice.
                  </td>
                </tr>
              )}
            </tbody>
          </table>
        </div>
      </div>
    </section>
  );
}

function Dashboard({ marketData }) {
  const benchmarkOptions = marketData.benchmarks.map((benchmark) => ({
    value: benchmark.ticker,
    label: `${benchmark.ticker} - ${benchmark.name}`,
  }));
  const mainSectors = marketData.mainSectors?.length ? marketData.mainSectors : marketData.sectors;
  const rrgMainSectors = useMemo(() => mainSectors, [mainSectors]);
  const initialMainSector = mainSectors.includes('Technology') ? 'Technology' : mainSectors[0];
  const [benchmark, setBenchmark] = useState(marketData.defaultBenchmark);
  const [lookback, setLookback] = useState(String(marketData.defaultLookback));
  const [trendMainSector, setTrendMainSector] = useState(initialMainSector);
  const [trendSubSector, setTrendSubSector] = useState('ALL');
  const [trendGroup, setTrendGroup] = useState('ALL');
  const [rrgFocusSector, setRrgFocusSector] = useState(null);
  const [hoveredRrgRowId, setHoveredRrgRowId] = useState(null);
  const [selectedRrgRowId, setSelectedRrgRowId] = useState(null);

  const selectedData = marketData.dataByBenchmark[benchmark] || marketData.dataByBenchmark[marketData.defaultBenchmark];
  const rrgRows = selectedData.lookbacks[lookback]?.rrg || [];
  const trendRows = selectedData.trendLeaders || [];
  const stockChartBasePath = marketData.stockChartBasePath || 'data/charts';
  const stockChartFiles = marketData.stockChartFiles || {};
  const subSectors = marketData.subSectorsByMain?.[trendMainSector] || [];
  const chartRows = useMemo(() => {
    if (!rrgFocusSector) {
      return selectMainSectorRows(rrgRows, rrgMainSectors);
    }
    return selectSubSectorRows(rrgRows, rrgFocusSector);
  }, [rrgFocusSector, rrgMainSectors, rrgRows]);
  const activeRrgRowId = hoveredRrgRowId || selectedRrgRowId;
  const selectedChartRow = selectedRrgRowId ? chartRows.find((row) => rrgRowId(row) === selectedRrgRowId) : null;
  const chartScopeLabel = rrgFocusSector ? `${rrgFocusSector} subsectors` : 'Main sectors';
  const chartStatusLabel = [
    chartScopeLabel,
    `${chartRows.length} plotted`,
    selectedChartRow ? `${rrgRowLabel(selectedChartRow)} selected` : null,
    `as of ${marketData.asOf}`,
  ]
    .filter(Boolean)
    .join(' · ');
  const trendFilteredRows = useMemo(
    () =>
      trendRows.filter((row) => {
        const mainSectorMatch = row.mainSector === trendMainSector;
        const subSectorMatch = trendSubSector === 'ALL' || row.subSector === trendSubSector;
        const groupMatch = trendGroup === 'ALL' || row.group === trendGroup;
        return mainSectorMatch && subSectorMatch && groupMatch;
      }),
    [trendRows, trendMainSector, trendSubSector, trendGroup],
  );

  const stockGroups = marketData.stockGroups || [];
  const stockUniverseLabel = marketData.stockUniverseName || 'Stock universe';
  const groupOptions = [
    { value: 'ALL', label: stockUniverseLabel },
    ...stockGroups.filter((group) => group !== stockUniverseLabel).map((group) => ({ value: group, label: group })),
  ];
  const subSectorRowId = (mainSector, subSector) => {
    if (subSector === 'ALL') {
      return null;
    }
    const row = selectSubSectorRows(rrgRows, mainSector).find((candidate) => rrgRowLabel(candidate) === subSector);
    return row ? rrgRowId(row) : null;
  };
  const clearRrgSelection = () => {
    setHoveredRrgRowId(null);
    setSelectedRrgRowId(null);
  };
  const handleMainSectorChange = (sector) => {
    clearRrgSelection();
    setTrendMainSector(sector);
    setTrendSubSector('ALL');
    setRrgFocusSector(sector);
  };
  const handleSubSectorChange = (subSector) => {
    setHoveredRrgRowId(null);
    setSelectedRrgRowId(subSectorRowId(trendMainSector, subSector));
    setTrendSubSector(subSector);
    setRrgFocusSector(trendMainSector);
  };
  const handleRrgRowClick = (row) => {
    setHoveredRrgRowId(null);
    if (row.chartLevel === 'mainSector' && row.mainSector) {
      handleMainSectorChange(row.mainSector);
      return;
    }
    if (row.chartLevel === 'subSector' && row.subSector) {
      setSelectedRrgRowId(rrgRowId(row));
      setTrendMainSector(row.mainSector);
      setTrendSubSector(row.subSector);
      setRrgFocusSector(row.mainSector);
    }
  };

  return (
    <main className="page-shell">
      <section className="rotation-section" aria-labelledby="rotation-title">
        <div className="top-controls">
          <SelectControl label="Benchmark" value={benchmark} onChange={setBenchmark} options={benchmarkOptions} />
          <LookbackControl value={lookback} onChange={setLookback} options={marketData.lookbacks} />
        </div>

        <div className="section-heading">
          <h2 id="rotation-title">1 · Sector Rotation Graph</h2>
          <div className="section-actions">
            <span>{chartStatusLabel}</span>
            {rrgFocusSector && (
              <button
                className="text-action"
                onClick={() => {
                  clearRrgSelection();
                  setRrgFocusSector(null);
                }}
                type="button"
              >
                Show main sectors
              </button>
            )}
          </div>
        </div>

        <div className="rotation-grid">
          <RrgChart activeRowId={activeRrgRowId} onHoverRow={setHoveredRrgRowId} onRowClick={handleRrgRowClick} rows={chartRows} />
          <RrgTable activeRowId={activeRrgRowId} onHoverRow={setHoveredRrgRowId} onRowClick={handleRrgRowClick} rows={chartRows} />
        </div>
      </section>

      <section className="trend-section" aria-labelledby="trend-title">
        <h1 id="trend-title">2 · Trend Leaders</h1>
        <div className="trend-controls">
          <SectorDrilldown
            mainSectors={mainSectors}
            onMainSectorChange={handleMainSectorChange}
            onSubSectorChange={handleSubSectorChange}
            selectedMainSector={trendMainSector}
            selectedSubSector={trendSubSector}
            subSectors={subSectors}
          />
          <div className="universe-control">
            <SelectControl label="Universe" value={trendGroup} onChange={setTrendGroup} options={groupOptions} />
          </div>
        </div>
        <TrendTable rows={trendFilteredRows} />
        <ChartSection rows={trendFilteredRows} stockChartBasePath={stockChartBasePath} stockChartFiles={stockChartFiles} />
      </section>
    </main>
  );
}

function App() {
  const [marketData, setMarketData] = useState(null);
  const [error, setError] = useState('');

  useEffect(() => {
    const controller = new AbortController();
    const dataUrl = `${import.meta.env.BASE_URL}data/marketData.json`;
    fetch(dataUrl, { signal: controller.signal })
      .then((response) => {
        if (!response.ok) {
          throw new Error(`Unable to load ${dataUrl}: ${response.status}`);
        }
        return response.json();
      })
      .then(setMarketData)
      .catch((loadError) => {
        if (loadError.name !== 'AbortError') {
          setError(loadError.message);
        }
      });
    return () => controller.abort();
  }, []);

  if (error) {
    return (
      <main className="page-shell">
        <div className="state-message">{error}</div>
      </main>
    );
  }

  if (!marketData) {
    return (
      <main className="page-shell">
        <div className="state-message">Loading market data...</div>
      </main>
    );
  }

  return <Dashboard marketData={marketData} />;
}

const rootElement = document.getElementById('root');
globalThis.__sectorRotationRoot ??= createRoot(rootElement);
globalThis.__sectorRotationRoot.render(<App />);
