import React, { useState, useRef, useCallback } from 'react';
import { parseCSVHeaders, parseCSV, getUniqueNoteValues, runAnalysis, checkFrameConsecutive, computeNumericSeries } from '../lib/csvAnalysis';
import {
  LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
} from 'recharts';
import { analysesApi, dailyStatusesApi, animalsApi } from '../api/client';
import Button from './ui/Button';
import RunSequenceView from './RunSequenceView';

// ── Constants ──────────────────────────────────────────────────────────────────

const DEFAULT_BUCKETS = [
  { name: 'Success', type: 'note', notes: [] },
  { name: 'Failure', type: 'note', notes: [] },
  { name: 'Other',   type: 'note', notes: [] },
];
const DEFAULT_BUCKET_NAMES = new Set(DEFAULT_BUCKETS.map((b) => b.name));

const CATEGORY_COLORS = {
  Success: { bg: 'bg-green-100', border: 'border-green-300', text: 'text-green-800', dot: 'bg-green-500' },
  Failure: { bg: 'bg-red-100',   border: 'border-red-300',   text: 'text-red-800',   dot: 'bg-red-500'   },
  Other:   { bg: 'bg-gray-100',  border: 'border-gray-300',  text: 'text-gray-700',  dot: 'bg-gray-400'  },
};

const EXTRA_COLORS = [
  { bg: 'bg-indigo-100', border: 'border-indigo-300', text: 'text-indigo-800', dot: 'bg-indigo-500' },
  { bg: 'bg-yellow-100', border: 'border-yellow-300', text: 'text-yellow-800', dot: 'bg-yellow-500' },
  { bg: 'bg-purple-100', border: 'border-purple-300', text: 'text-purple-800', dot: 'bg-purple-500' },
  { bg: 'bg-pink-100',   border: 'border-pink-300',   text: 'text-pink-800',   dot: 'bg-pink-500'   },
  { bg: 'bg-teal-100',   border: 'border-teal-300',   text: 'text-teal-800',   dot: 'bg-teal-500'   },
];

function getCategoryColors(cat, customCategories) {
  if (CATEGORY_COLORS[cat]) return CATEGORY_COLORS[cat];
  const idx = customCategories.indexOf(cat) % EXTRA_COLORS.length;
  return EXTRA_COLORS[Math.max(0, idx)];
}

// ── Sub-components ─────────────────────────────────────────────────────────────

function NoteChip({ value, category, colors, selected, onSelect, draggable, onDragStart }) {
  return (
    <span
      draggable={draggable}
      onDragStart={onDragStart}
      onClick={onSelect}
      title={category ? `Assigned to: ${category}` : 'Unassigned — click or drag to assign'}
      className={`
        inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-mono font-semibold
        border cursor-pointer select-none transition-all
        ${colors ? `${colors.bg} ${colors.border} ${colors.text}` : 'bg-white border-gray-300 text-gray-700 hover:border-indigo-400'}
        ${selected ? 'ring-2 ring-indigo-500 ring-offset-1' : ''}
      `}
    >
      {value}
      {category && (
        <span className={`w-1.5 h-1.5 rounded-full ${colors?.dot}`} />
      )}
    </span>
  );
}

function CategoryBucket({
  name, type, notes, colors, isOver, isCustom, unassignedCount,
  onDrop, onDragOver, onDragLeave, onRemoveNote, onClickAssign,
  onBulkAddUnassigned, onRemoveBucket,
}) {
  return (
    <div
      onDragOver={(e) => { e.preventDefault(); onDragOver(); }}
      onDragLeave={onDragLeave}
      onDrop={(e) => { e.preventDefault(); onDrop(); }}
      onClick={onClickAssign}
      className={`
        relative group rounded-lg border-2 border-dashed p-3 min-h-[80px] transition-all cursor-pointer
        ${isOver ? `${colors.border} ${colors.bg} scale-[1.02]` : 'border-gray-200 hover:border-gray-300'}
      `}
    >
      <div className="flex items-center justify-between mb-2">
        <p className={`text-xs font-semibold uppercase tracking-wide ${colors.text}`}>
          {name}
          {type === 'numerical' && (
            <span className="ml-2 text-[9px] font-bold uppercase bg-white/60 px-1 py-0.5 rounded border border-current/20">
              numerical
            </span>
          )}
        </p>
        {isCustom && (
          <button type="button"
            onClick={(e) => { e.stopPropagation(); onRemoveBucket(); }}
            className="text-[10px] text-gray-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
            title="Remove this field">✕</button>
        )}
      </div>

      {type === 'numerical' && unassignedCount > 0 && (
        <button type="button"
          onClick={(e) => { e.stopPropagation(); onBulkAddUnassigned(); }}
          className={`text-[10px] mb-2 px-2 py-0.5 rounded border ${colors.border} ${colors.text} hover:bg-white transition-colors`}>
          + Add all {unassignedCount} unassigned
        </button>
      )}

      <div className="flex flex-wrap gap-1.5 min-h-[28px]">
        {notes.length === 0 && (
          <span className="text-xs text-gray-400 italic">Drop here</span>
        )}
        {notes.map((note) => (
          <span key={note}
            className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-mono font-semibold ${colors.bg} ${colors.border} ${colors.text} border`}
          >
            {note}
            <button type="button" onClick={(e) => { e.stopPropagation(); onRemoveNote(note); }}
              className="opacity-50 hover:opacity-100 leading-none">✕</button>
          </span>
        ))}
      </div>
    </div>
  );
}

// ── Main component ─────────────────────────────────────────────────────────────

export default function CsvAnalysis({ dailyStatusId, animal, onSaved, onSummaryPushed }) {
  // Phase: 'upload' | 'columns' | 'classify' | 'results'
  const [phase, setPhase] = useState('upload');

  // Upload phase
  const [isDragOver, setIsDragOver] = useState(false);
  const [fileName, setFileName] = useState('');
  const [rawText, setRawText] = useState('');
  const [headers, setHeaders] = useState([]);
  const fileInputRef = useRef(null);

  // Columns phase
  const [timestampCol, setTimestampCol] = useState('');
  const [noteCol, setNoteCol] = useState('');
  const [frameCol, setFrameCol] = useState('');
  const [frameCheck, setFrameCheck] = useState(null);
  const [parsePending, setParsePending] = useState(false);

  // Classify phase
  const [rows, setRows] = useState([]);
  const [uniqueNotes, setUniqueNotes] = useState([]);
  const [buckets, setBuckets] = useState(() => animal?.csv_analysis_config?.buckets ?? DEFAULT_BUCKETS);
  const [selectedNote, setSelectedNote] = useState(null);
  const [draggedNote, setDraggedNote] = useState(null);
  const [overBucket, setOverBucket] = useState(null);
  const [newCatName, setNewCatName] = useState('');
  const [newCatType, setNewCatType] = useState('note');
  const [showNewCat, setShowNewCat] = useState(false);

  // Derived from buckets (recomputed every render — cheap)
  const categories       = buckets.map((b) => b.name);
  const customCategories = buckets.filter((b) => !DEFAULT_BUCKET_NAMES.has(b.name)).map((b) => b.name);
  const noteBuckets      = buckets.filter((b) => b.type === 'note');
  const numericalBuckets = buckets.filter((b) => b.type === 'numerical');
  const classifications  = Object.fromEntries(
    noteBuckets.flatMap((b) => b.notes.map((n) => [n, b.name]))
  );

  // Results phase
  const [results, setResults] = useState(null);
  const [savedAnalysisId, setSavedAnalysisId] = useState(null);
  const [saving, setSaving] = useState(false);
  const [saveError, setSaveError] = useState(null);
  const [summaryPushing, setSummaryPushing] = useState(false);
  const [summaryPushed, setSummaryPushed] = useState(false);
  const [summaryError, setSummaryError] = useState(null);
  const [configSaveError, setConfigSaveError] = useState(null);

  // ── File reading ─────────────────────────────────────────────────────────────

  function readFile(file) {
    if (!file || !file.name.endsWith('.csv')) {
      alert('Please upload a .csv file.');
      return;
    }
    const reader = new FileReader();
    reader.onload = (e) => {
      const text = e.target.result;
      const hdrs = parseCSVHeaders(text);
      setFileName(file.name);
      setRawText(text);
      setHeaders(hdrs);
      // Default columns
      const tsDefault = hdrs.find(h => /timestamp|time/i.test(h)) ?? hdrs[0] ?? '';
      const noteDefault = hdrs.find(h => /note|label|event/i.test(h)) ?? hdrs[hdrs.length - 1] ?? '';
      const frameDefault = hdrs.find(h => /frame/i.test(h)) ?? '';
      setTimestampCol(tsDefault);
      setNoteCol(noteDefault);
      setFrameCol(frameDefault);
      setFrameCheck(null);
      setPhase('columns');
    };
    reader.readAsText(file);
  }

  const handleDrop = useCallback((e) => {
    e.preventDefault();
    setIsDragOver(false);
    readFile(e.dataTransfer.files[0]);
  }, []);

  // ── Column selection → parse data ────────────────────────────────────────────

  function loadData() {
    setParsePending(true);
    setTimeout(() => {
      const { rows: parsed } = parseCSV(rawText);
      const uniq = getUniqueNoteValues(parsed, noteCol);
      setRows(parsed);
      setUniqueNotes(uniq);
      setBuckets((prev) => prev.map((b) => ({ ...b, notes: [] })));
      setSelectedNote(null);
      if (frameCol) setFrameCheck(checkFrameConsecutive(parsed, frameCol));
      setParsePending(false);
      setPhase('classify');
    }, 20);
  }

  // ── Classification helpers ───────────────────────────────────────────────────

  function assign(note, category) {
    setBuckets((prev) => prev.map((b) => {
      if (b.name === category) {
        return b.notes.includes(note) ? b : { ...b, notes: [...b.notes, note] };
      }
      // Remove from any other bucket so a note belongs to at most one
      return b.notes.includes(note) ? { ...b, notes: b.notes.filter((n) => n !== note) } : b;
    }));
    setSelectedNote(null);
  }

  function unassign(note) {
    setBuckets((prev) => prev.map((b) =>
      b.notes.includes(note) ? { ...b, notes: b.notes.filter((n) => n !== note) } : b
    ));
  }

  function addBucket(name, type) {
    const trimmed = name.trim();
    if (!trimmed || buckets.some((b) => b.name === trimmed)) return;
    setBuckets((prev) => [...prev, { name: trimmed, type, notes: [] }]);
  }

  const unassignedNotes = uniqueNotes.filter(n => !classifications[n]);
  const allAssigned = uniqueNotes.length > 0 && unassignedNotes.length === 0;

  // ── Run analysis ─────────────────────────────────────────────────────────────

  async function compute() {
    // Note-only classifications so numerical buckets don't pollute counts/runs
    const noteClassifications = Object.fromEntries(
      noteBuckets.flatMap((b) => b.notes.map((n) => [n, b.name]))
    );
    const res = runAnalysis(rows, timestampCol, noteCol, noteClassifications);

    // Numerical series — one per numerical bucket. xCol = frame if set, else timestamp.
    const xCol = frameCol || timestampCol;
    const numericSeries = numericalBuckets.map((b) => ({
      bucket: b,
      ...computeNumericSeries(rows, xCol, noteCol, b),
    }));

    setResults({ ...res, numericSeries });
    setPhase('results');

    // Persist bucket config to the animal so future uploads pre-classify.
    // Independent of dailyStatusId — runs even for ad-hoc analyses.
    if (animal?.id) {
      setConfigSaveError(null);
      animalsApi.updateCsvConfig(animal.id, { buckets })
        .catch((err) => setConfigSaveError(err.message ?? 'Failed to save bucket config'));
    }

    if (!dailyStatusId) return;
    setSaving(true);
    setSaveError(null);
    try {
      const saved = await analysesApi.create(dailyStatusId, {
        file_name:         fileName || null,
        timestamp_col:     timestampCol,
        note_col:          noteCol,
        classifications:   noteClassifications,
        total_note_rows:   res.totalNoteRows,
        session_end_ts:    res.sessionEndTs,
        sequence:          res.sequence,
        category_counts:   res.categoryCounts,
        consecutive_stats: res.consecutiveStats,
        runs:              res.runs,
      });
      setSavedAnalysisId(saved.id);
      onSaved?.(saved);
    } catch (err) {
      setSaveError(err.message ?? 'Failed to save analysis');
    } finally {
      setSaving(false);
    }
  }

  // ── Push summary to daily status ─────────────────────────────────────────────

  async function pushSummary() {
    if (!dailyStatusId || !results) return;
    setSummaryPushing(true);
    setSummaryPushed(false);
    setSummaryError(null);
    try {
      const counts = {};
      for (const [cat, data] of Object.entries(results.categoryCounts)) {
        counts[cat] = data.total;
      }
      const total = Object.values(counts).reduce((a, b) => a + b, 0);
      const successRate = counts['Success'] != null ? counts['Success'] / total : null;
      await dailyStatusesApi.updateAnalysisSummary(dailyStatusId, {
        counts,
        total,
        success_rate: successRate,
        source_analysis_id: savedAnalysisId ?? null,
      });
      setSummaryPushed(true);
      onSummaryPushed?.({ counts, total, success_rate: successRate });
    } catch (err) {
      setSummaryError(err.message ?? 'Failed to update');
    } finally {
      setSummaryPushing(false);
    }
  }

  // ── Reset ────────────────────────────────────────────────────────────────────

  function reset() {
    setPhase('upload');
    setRawText('');
    setHeaders([]);
    setRows([]);
    setUniqueNotes([]);
    setBuckets(animal?.csv_analysis_config?.buckets ?? DEFAULT_BUCKETS);
    setResults(null);
    setSavedAnalysisId(null);
    setSummaryPushed(false);
    setSummaryError(null);
    setFrameCheck(null);
  }

  // ── Render ───────────────────────────────────────────────────────────────────

  return (
    <div className="mt-8 border-t border-gray-200 pt-6">
      <div className="flex items-center justify-between mb-4">
        <h2 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">CSV Analysis</h2>
        {phase !== 'upload' && (
          <button type="button" onClick={reset} className="text-xs text-gray-400 hover:text-gray-600">
            ✕ Start over
          </button>
        )}
      </div>

      {/* ── PHASE: upload ── */}
      {phase === 'upload' && (
        <div
          onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }}
          onDragLeave={() => setIsDragOver(false)}
          onDrop={handleDrop}
          onClick={() => fileInputRef.current?.click()}
          className={`
            border-2 border-dashed rounded-xl p-10 text-center cursor-pointer transition-all
            ${isDragOver ? 'border-indigo-400 bg-indigo-50' : 'border-gray-200 hover:border-gray-300 bg-gray-50'}
          `}
        >
          <input ref={fileInputRef} type="file" accept=".csv" className="hidden"
            onChange={(e) => readFile(e.target.files[0])} />
          <p className="text-sm text-gray-500">
            {isDragOver ? 'Drop CSV file to upload' : 'Drop a CSV file here, or click to browse'}
          </p>
          <p className="text-xs text-gray-400 mt-1">Columns will be read; data stays in your browser</p>
        </div>
      )}

      {/* ── PHASE: columns ── */}
      {phase === 'columns' && (
        <div className="space-y-4">
          <p className="text-sm text-gray-600">
            <span className="font-medium">{fileName}</span> loaded — {headers.length} columns detected. Select the columns to analyse.
          </p>
          <div className="grid grid-cols-2 gap-4 max-w-lg">
            <div>
              <label className="block text-xs font-medium text-gray-500 mb-1">Timestamp column</label>
              <select value={timestampCol} onChange={(e) => setTimestampCol(e.target.value)}
                className="w-full text-sm border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-indigo-400">
                {headers.map(h => <option key={h} value={h}>{h}</option>)}
              </select>
            </div>
            <div>
              <label className="block text-xs font-medium text-gray-500 mb-1">Note column</label>
              <select value={noteCol} onChange={(e) => setNoteCol(e.target.value)}
                className="w-full text-sm border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-indigo-400">
                {headers.map(h => <option key={h} value={h}>{h}</option>)}
              </select>
            </div>
            <div>
              <label className="block text-xs font-medium text-gray-500 mb-1">
                Frame number column <span className="font-normal text-gray-400">(gap check)</span>
              </label>
              <select value={frameCol} onChange={(e) => setFrameCol(e.target.value)}
                className="w-full text-sm border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-indigo-400">
                <option value="">— skip —</option>
                {headers.map(h => <option key={h} value={h}>{h}</option>)}
              </select>
            </div>
          </div>
          <Button onClick={loadData} disabled={parsePending}>
            {parsePending ? 'Parsing…' : 'Load note values →'}
          </Button>
        </div>
      )}

      {/* ── PHASE: classify ── */}
      {phase === 'classify' && (
        <div className="space-y-5">

          {/* Frame gap check results */}
          {frameCheck && (
            <div className={`rounded-lg border px-3 py-2.5 text-xs ${frameCheck.consecutive ? 'bg-green-50 border-green-200 text-green-800' : 'bg-amber-50 border-amber-200 text-amber-800'}`}>
              <p className="font-semibold mb-1">
                Frame check ({frameCol}): {frameCheck.consecutive ? `✓ all ${frameCheck.total} frames consecutive` : `⚠ issues found in ${frameCheck.total} frames`}
              </p>
              {frameCheck.gaps.length > 0 && (
                <div className="mb-1">
                  <span className="font-medium">{frameCheck.gaps.length} gap{frameCheck.gaps.length !== 1 ? 's' : ''}:</span>
                  <ul className="mt-0.5 space-y-0.5 max-h-32 overflow-y-auto font-mono">
                    {frameCheck.gaps.map((g, i) => (
                      <li key={i}>row {g.rowIndex + 1}: expected {g.expected}, got {g.actual} (jump +{g.jump})</li>
                    ))}
                  </ul>
                </div>
              )}
              {frameCheck.duplicates.length > 0 && (
                <div>
                  <span className="font-medium">{frameCheck.duplicates.length} duplicate{frameCheck.duplicates.length !== 1 ? 's' : ''}:</span>
                  <ul className="mt-0.5 space-y-0.5 font-mono max-h-20 overflow-y-auto">
                    {frameCheck.duplicates.map((d, i) => (
                      <li key={i}>row {d.rowIndex + 1}: frame {d.value}</li>
                    ))}
                  </ul>
                </div>
              )}
            </div>
          )}

          <p className="text-sm text-gray-600">
            Found <span className="font-semibold">{uniqueNotes.length}</span> unique note value{uniqueNotes.length !== 1 ? 's' : ''}.
            {selectedNote && (
              <span className="ml-2 text-indigo-600 font-medium">
                Selected: <span className="font-mono">{selectedNote}</span> — click a category to assign
              </span>
            )}
          </p>

          {/* Unassigned chips */}
          {unassignedNotes.length > 0 && (
            <div>
              <p className="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-1.5">Unassigned</p>
              <div className="flex flex-wrap gap-2">
                {unassignedNotes.map(note => (
                  <NoteChip key={note} value={note} category={null} colors={null}
                    selected={selectedNote === note}
                    onSelect={() => setSelectedNote(prev => prev === note ? null : note)}
                    draggable onDragStart={() => setDraggedNote(note)}
                  />
                ))}
              </div>
            </div>
          )}

          {/* Category buckets */}
          <div className="grid grid-cols-3 gap-3">
            {buckets.map((b) => {
              const colors = getCategoryColors(b.name, customCategories);
              const isCustom = !DEFAULT_BUCKET_NAMES.has(b.name);
              return (
                <CategoryBucket key={b.name} name={b.name} type={b.type} notes={b.notes} colors={colors}
                  isOver={overBucket === b.name}
                  isCustom={isCustom}
                  unassignedCount={unassignedNotes.length}
                  onDragOver={() => setOverBucket(b.name)}
                  onDragLeave={() => setOverBucket(null)}
                  onDrop={() => {
                    if (draggedNote) { assign(draggedNote, b.name); setDraggedNote(null); setOverBucket(null); }
                  }}
                  onClickAssign={() => { if (selectedNote) assign(selectedNote, b.name); }}
                  onRemoveNote={(note) => unassign(note)}
                  onBulkAddUnassigned={() => {
                    for (const n of unassignedNotes) assign(n, b.name);
                  }}
                  onRemoveBucket={() => setBuckets((prev) => prev.filter((x) => x.name !== b.name))}
                />
              );
            })}
          </div>

          {/* Add new bucket — note or numerical */}
          <div className="flex items-center gap-2 flex-wrap">
            {showNewCat ? (
              <>
                <input
                  autoFocus
                  value={newCatName}
                  onChange={(e) => setNewCatName(e.target.value)}
                  onKeyDown={(e) => { if (e.key === 'Escape') setShowNewCat(false); }}
                  placeholder="Field name…"
                  className="text-sm border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-indigo-400 w-40"
                />
                <label className="text-xs text-gray-600 inline-flex items-center gap-1">
                  <input type="radio" name="newBucketType" value="note"
                    checked={newCatType === 'note'}
                    onChange={() => setNewCatType('note')} /> Note
                </label>
                <label className="text-xs text-gray-600 inline-flex items-center gap-1">
                  <input type="radio" name="newBucketType" value="numerical"
                    checked={newCatType === 'numerical'}
                    onChange={() => setNewCatType('numerical')} /> Numerical
                </label>
                <Button size="sm" type="button" onClick={() => {
                  addBucket(newCatName, newCatType);
                  setNewCatName('');
                  setNewCatType('note');
                  setShowNewCat(false);
                }}>Add</Button>
                <button type="button" onClick={() => setShowNewCat(false)}
                  className="text-xs text-gray-400 hover:text-gray-600">Cancel</button>
              </>
            ) : (
              <button type="button" onClick={() => setShowNewCat(true)}
                className="text-xs text-indigo-500 hover:text-indigo-700 border border-dashed border-indigo-300 rounded-full px-3 py-0.5 hover:border-indigo-500 transition-colors">
                + Add field
              </button>
            )}
          </div>

          {/* Unclassified warning */}
          {!allAssigned && uniqueNotes.length > 0 && (
            <p className="text-xs text-amber-600 bg-amber-50 border border-amber-200 rounded px-3 py-1.5">
              {unassignedNotes.length} unassigned note value{unassignedNotes.length !== 1 ? 's' : ''} will be excluded from the analysis.
            </p>
          )}

          <Button onClick={compute} disabled={uniqueNotes.length === 0}>
            Run analysis →
          </Button>
        </div>
      )}

      {/* ── PHASE: results ── */}
      {phase === 'results' && results && (
        <div className="space-y-6">
          <div className="flex items-center justify-between">
            <p className="text-sm text-gray-500">
              Analysed <span className="font-semibold">{results.totalNoteRows}</span> note rows
              · columns: <span className="font-mono text-xs">{timestampCol}</span> / <span className="font-mono text-xs">{noteCol}</span>
            </p>
            {dailyStatusId && (
              <span className="text-xs">
                {saving && <span className="text-gray-400">Saving…</span>}
                {!saving && !saveError && results && <span className="text-green-600">✓ Saved</span>}
                {saveError && <span className="text-red-500" title={saveError}>⚠ Save failed</span>}
              </span>
            )}
          </div>

          {/* Category counts */}
          <div>
            <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Category Counts</h3>
            <table className="w-full text-sm border-collapse">
              <thead>
                <tr className="bg-gray-50 border-b border-gray-200">
                  <th className="text-left px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wide">Category</th>
                  <th className="text-left px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wide">Note breakdown</th>
                  <th className="text-right px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wide">Total</th>
                </tr>
              </thead>
              <tbody>
                {Object.entries(results.categoryCounts)
                  .sort(([, a], [, b]) => b.total - a.total)
                  .map(([cat, data]) => {
                    const colors = getCategoryColors(cat, customCategories);
                    const breakdown = Object.entries(data.byNote)
                      .sort(([, a], [, b]) => b - a)
                      .map(([n, c]) => `${n}: ${c}`)
                      .join(' · ');
                    return (
                      <tr key={cat} className="border-b border-gray-100">
                        <td className="px-3 py-2">
                          <span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-semibold ${colors.bg} ${colors.text}`}>
                            <span className={`w-1.5 h-1.5 rounded-full ${colors.dot}`} />
                            {cat}
                          </span>
                        </td>
                        <td className="px-3 py-2 text-gray-500 font-mono text-xs">{breakdown}</td>
                        <td className="px-3 py-2 text-right font-semibold text-gray-800">{data.total}</td>
                      </tr>
                    );
                  })}
                {Object.keys(results.categoryCounts).length === 0 && (
                  <tr><td colSpan={3} className="px-3 py-4 text-center text-gray-400 italic text-sm">No classified notes</td></tr>
                )}
              </tbody>
            </table>
          </div>

          {/* Consecutive stats */}
          {Object.keys(results.consecutiveStats).length > 0 && (
            <div>
              <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Consecutive Runs</h3>
              <p className="text-xs text-gray-400 mb-2">
                How many times each category appeared in a row (run-length encoding of the category sequence, ordered by timestamp).
              </p>
              <table className="w-full text-sm border-collapse">
                <thead>
                  <tr className="bg-gray-50 border-b border-gray-200">
                    <th className="text-left px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wide">Category</th>
                    <th className="text-right px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wide">Total runs</th>
                    <th className="text-right px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wide">Max run</th>
                    <th className="text-right px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wide">Avg run</th>
                    <th className="text-left px-3 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wide">Run lengths</th>
                  </tr>
                </thead>
                <tbody>
                  {Object.entries(results.consecutiveStats)
                    .sort(([, a], [, b]) => b.totalRuns - a.totalRuns)
                    .map(([cat, s]) => {
                      const colors = getCategoryColors(cat, customCategories);
                      const runSummary = s.runLengths
                        .reduce((acc, len) => {
                          acc[len] = (acc[len] || 0) + 1;
                          return acc;
                        }, {});
                      const runDisplay = Object.entries(runSummary)
                        .sort(([a], [b]) => Number(a) - Number(b))
                        .map(([len, cnt]) => `${len}×${cnt}`)
                        .join(', ');
                      return (
                        <tr key={cat} className="border-b border-gray-100">
                          <td className="px-3 py-2">
                            <span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-semibold ${colors.bg} ${colors.text}`}>
                              <span className={`w-1.5 h-1.5 rounded-full ${colors.dot}`} />
                              {cat}
                            </span>
                          </td>
                          <td className="px-3 py-2 text-right text-gray-700">{s.totalRuns}</td>
                          <td className="px-3 py-2 text-right text-gray-700">{s.maxRun}</td>
                          <td className="px-3 py-2 text-right text-gray-700">{s.avgRun.toFixed(2)}</td>
                          <td className="px-3 py-2 text-gray-500 font-mono text-xs">{runDisplay}</td>
                        </tr>
                      );
                    })}
                </tbody>
              </table>
            </div>
          )}

          <RunSequenceView
            runs={results.runs}
            sequence={results.sequence}
            customCategories={customCategories}
            sessionEndTs={results.sessionEndTs}
          />

          {/* Numerical plots — one panel per numerical bucket */}
          {results.numericSeries && results.numericSeries.length > 0 && (
            <div>
              <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Numerical Plots</h3>
              <div className="space-y-4">
                {results.numericSeries.map(({ bucket, points, avg, skipped, total, xLabel }) => {
                  const colors = getCategoryColors(bucket.name, customCategories);
                  return (
                    <div key={bucket.name} className="border border-gray-200 rounded-lg p-3">
                      <div className="flex items-baseline justify-between mb-2">
                        <p className={`text-sm font-semibold ${colors.text}`}>
                          {bucket.name} <span className="text-xs text-gray-400 font-normal">vs {xLabel}</span>
                        </p>
                        <p className="text-xs text-gray-500">
                          {avg != null ? <>Avg: <strong className="text-gray-800">{avg.toFixed(2)}</strong> · </> : null}
                          {points.length}/{total} rows plottable
                          {skipped > 0 && <> · {skipped} skipped</>}
                        </p>
                      </div>
                      {points.length > 0 ? (
                        <ResponsiveContainer width="100%" height={220}>
                          <LineChart data={points} margin={{ top: 4, right: 16, left: 0, bottom: 4 }}>
                            <CartesianGrid strokeDasharray="3 3" stroke="#eee" />
                            <XAxis dataKey="x" type="number" domain={['dataMin', 'dataMax']}
                              tick={{ fontSize: 10 }} label={{ value: xLabel, position: 'insideBottom', offset: -2, fontSize: 10 }} />
                            <YAxis tick={{ fontSize: 10 }} />
                            <Tooltip />
                            <Line type="monotone" dataKey="value" stroke="currentColor" className={colors.text}
                              dot={{ r: 2 }} strokeWidth={2} />
                          </LineChart>
                        </ResponsiveContainer>
                      ) : (
                        <p className="text-xs text-gray-400 italic">
                          No plottable values — {total} of {total} note rows had no extractable number.
                        </p>
                      )}
                    </div>
                  );
                })}
              </div>
            </div>
          )}

          {/* Save metrics to daily status */}
          {dailyStatusId && (
            <div className="pt-3 border-t border-gray-100">
              <div className="flex items-center gap-3 flex-wrap">
                <button type="button" onClick={pushSummary} disabled={summaryPushing}
                  className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50 transition-colors">
                  {summaryPushing ? 'Saving…' : summaryPushed ? '✓ Metrics saved to daily status' : 'Save metrics to daily status'}
                </button>
                {summaryError && <span className="text-xs text-red-500">{summaryError}</span>}
              </div>
              {!summaryPushed && (
                <p className="text-[10px] text-gray-400 mt-1">
                  Saves success / failure counts, total attempts, and success rate to this day's record for cross-day and cross-subject graphing.
                </p>
              )}
            </div>
          )}

          {configSaveError && (
            <p className="text-xs text-amber-600 bg-amber-50 border border-amber-200 rounded px-3 py-1.5">
              Bucket config didn't save: {configSaveError}. Analysis still ran. Try running again to retry.
            </p>
          )}
          <div className="flex gap-3 pt-2 border-t border-gray-100">
            <Button variant="secondary" onClick={() => setPhase('classify')}>← Reclassify</Button>
            <Button variant="secondary" onClick={reset}>Start over</Button>
          </div>
        </div>
      )}
    </div>
  );
}
