/**
 * DemoMode — scripted autoplay showcase for SQL Agent OpenEnv.
 *
 * Right panel: live reward chart + GitHub-style prompt diff.
 * Chat: single-difficulty rounds separated by GEPA evolution.
 * No looping — scrollable at the end.
 */

import { useState, useRef, useCallback, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import {
  LineChart, Line, XAxis, YAxis, CartesianGrid,
  Tooltip, ResponsiveContainer, ReferenceLine,
} from 'recharts'
import {
  Play, X, Zap, CheckCircle2, XCircle,
  ChevronDown, ChevronUp, Sparkles, Loader2, GitCommitHorizontal,
} from 'lucide-react'

// ─── Prompts (3 generations) ─────────────────────────────────────────────────

const PROMPTS = [
  `You are a SQL expert. Given a question and a SQLite schema, write correct SQL.

Rules:
- Output ONLY the SQL query
- Use SQLite syntax
- No markdown, no code fences`,

  `You are a SQL expert. Given a question and a SQLite schema, write correct SQL.

Rules:
- Output ONLY the SQL query
- Use SQLite syntax
- No markdown, no code fences
- Always qualify column names with table aliases in JOINs
- Use t.column_name to avoid ambiguous column errors
- Verify every column name against the schema before use`,

  `You are a SQL expert. Given a question and a SQLite schema, write correct SQL.

Rules:
- Output ONLY the SQL query
- Use SQLite syntax
- No markdown, no code fences
- Always qualify column names with table aliases in JOINs
- Use t.column_name to avoid ambiguous column errors
- Verify every column name against the schema before use
- For aggregations: include all non-aggregated columns in GROUP BY
- For revenue: use orders.total_price (not price or amount)
- For top-N: use ORDER BY … DESC LIMIT N`,
]

const SCORES = [0.42, 0.74, 0.91]

// ─── Scripted query data ─────────────────────────────────────────────────────

interface Attempt {
  sql: string
  error?: string
  errorClass?: string
  rlAction?: string
  reward: number
  rows?: Record<string, string | number>[]
}

interface QueryDef {
  id: string
  question: string
  attempts: Attempt[]
}

const QUERIES: Record<string, QueryDef> = {
  // Round 1 — simple queries, some fail
  r1q1: {
    id: 'r1q1',
    question: 'Show all products',
    attempts: [
      { sql: 'SELECT * FROM product', error: 'no such table: product', errorClass: 'NO_SUCH_TABLE', rlAction: 'FIX_TABLE', reward: -0.15 },
      { sql: 'SELECT * FROM products LIMIT 10', reward: 0.90,
        rows: [{ id: 1, name: 'Wireless Headphones', category: 'Electronics', price: 79.99 }, { id: 2, name: 'Running Shoes', category: 'Footwear', price: 59.99 }, { id: 3, name: 'Coffee Maker', category: 'Kitchen', price: 49.99 }] },
    ],
  },
  r1q2: {
    id: 'r1q2',
    question: 'List all users from the USA',
    attempts: [
      { sql: "SELECT * FROM users WHERE country = 'USA'", reward: 1.00,
        rows: [{ id: 1, name: 'Alice Johnson', email: 'alice@example.com', country: 'USA' }, { id: 2, name: 'Bob Smith', email: 'bob@example.com', country: 'USA' }] },
    ],
  },

  // Round 2 — join queries, ambiguous columns fixed by GEPA
  r2q1: {
    id: 'r2q1',
    question: 'Top 5 sellers by total revenue',
    attempts: [
      { sql: `SELECT seller_id, SUM(total_price) as revenue\nFROM orders\nGROUP BY seller_id\nORDER BY revenue DESC\nLIMIT 5`, error: 'ambiguous column name: seller_id', errorClass: 'AMBIGUOUS_COLUMN', rlAction: 'FIX_COLUMN', reward: -0.20 },
      { sql: `SELECT s.name, SUM(o.total_price) as revenue\nFROM orders o\nJOIN sellers s ON o.user_id = s.id\nGROUP BY s.id, s.name\nORDER BY revenue DESC\nLIMIT 5`, reward: 0.80,
        rows: [{ seller: 'TechStore Pro', revenue: 12840 }, { seller: 'StyleHub', revenue: 9320 }, { seller: 'GadgetWorld', revenue: 8750 }] },
    ],
  },
  r2q2: {
    id: 'r2q2',
    question: 'Average order value by country',
    attempts: [
      { sql: `SELECT country, AVG(total_price) as avg_order\nFROM orders\nJOIN users ON user_id = users.id\nGROUP BY country`, error: 'ambiguous column name: country', errorClass: 'AMBIGUOUS_COLUMN', rlAction: 'FIX_COLUMN', reward: -0.15 },
      { sql: `SELECT u.country, ROUND(AVG(o.total_price), 2) as avg_order\nFROM orders o\nJOIN users u ON o.user_id = u.id\nGROUP BY u.country\nORDER BY avg_order DESC`, reward: 0.85,
        rows: [{ country: 'USA', avg_order: 142.50 }, { country: 'UK', avg_order: 138.20 }, { country: 'Germany', avg_order: 121.80 }] },
    ],
  },

  // Round 3 — after GEPA 2, same joins succeed first try
  r3q1: {
    id: 'r3q1',
    question: 'Top 5 sellers by total revenue',
    attempts: [
      { sql: `SELECT s.name, SUM(o.total_price) as revenue\nFROM orders o\nJOIN sellers s ON o.user_id = s.id\nGROUP BY s.id, s.name\nORDER BY revenue DESC\nLIMIT 5`, reward: 1.00,
        rows: [{ seller: 'TechStore Pro', revenue: 12840 }, { seller: 'StyleHub', revenue: 9320 }, { seller: 'GadgetWorld', revenue: 8750 }] },
    ],
  },
  r3q2: {
    id: 'r3q2',
    question: 'Monthly revenue for the last 6 months',
    attempts: [
      { sql: `SELECT strftime('%Y-%m', created_at) as month,\n  ROUND(SUM(total_price), 2) as revenue\nFROM orders\nGROUP BY month\nORDER BY month DESC\nLIMIT 6`, reward: 1.00,
        rows: [{ month: '2024-11', revenue: 24180 }, { month: '2024-10', revenue: 21340 }, { month: '2024-09', revenue: 19800 }] },
    ],
  },
}

const ROUND_1 = ['r1q1', 'r1q2']
const ROUND_2 = ['r2q1', 'r2q2']
const ROUND_3 = ['r3q1', 'r3q2']

// ─── GitHub-style diff ───────────────────────────────────────────────────────

interface DiffLine { type: 'add' | 'remove' | 'same'; text: string }

function diffPrompts(fromIdx: number, toIdx: number): DiffLine[] {
  const oldLines = PROMPTS[fromIdx].split('\n')
  const newLines = PROMPTS[toIdx].split('\n')
  const result: DiffLine[] = []

  // Simple patience diff: lines in both = same, only in old = remove, only in new = add
  const oldSet = new Set(oldLines)
  const newSet = new Set(newLines)

  // Walk new lines, carrying old-only lines before matching
  let oi = 0
  for (let ni = 0; ni < newLines.length; ni++) {
    const line = newLines[ni]
    if (oldSet.has(line)) {
      // Flush any old-only lines before this match
      while (oi < oldLines.length && oldLines[oi] !== line) {
        if (!newSet.has(oldLines[oi])) result.push({ type: 'remove', text: oldLines[oi] })
        oi++
      }
      result.push({ type: 'same', text: line })
      oi++
    } else {
      result.push({ type: 'add', text: line })
    }
  }
  // Remaining old-only
  while (oi < oldLines.length) {
    if (!newSet.has(oldLines[oi])) result.push({ type: 'remove', text: oldLines[oi] })
    oi++
  }
  return result
}

function GithubDiff({ fromIdx, toIdx }: { fromIdx: number; toIdx: number }) {
  const lines = diffPrompts(fromIdx, toIdx)
  const added = lines.filter((l) => l.type === 'add').length
  const removed = lines.filter((l) => l.type === 'remove').length

  return (
    <div className="rounded-xl border overflow-hidden text-[11px] font-mono" style={{ borderColor: 'var(--border-color)' }}>
      {/* Header */}
      <div className="flex items-center gap-2 px-3 py-2 border-b" style={{ background: 'var(--bg-tertiary)', borderColor: 'var(--border-color)' }}>
        <GitCommitHorizontal size={11} className="text-gray-500" />
        <span className="text-gray-400 font-semibold">system_prompt.txt</span>
        <span className="ml-auto flex items-center gap-2 text-[10px]">
          <span className="text-green-400 font-bold">+{added}</span>
          <span className="text-red-400 font-bold">−{removed}</span>
        </span>
      </div>
      {/* Diff lines */}
      <div className="max-h-52 overflow-y-auto" style={{ background: 'var(--bg-secondary)' }}>
        {lines.map((line, i) => {
          const bg = line.type === 'add' ? 'bg-green-500/10' : line.type === 'remove' ? 'bg-red-500/10' : ''
          const prefix = line.type === 'add' ? '+' : line.type === 'remove' ? '-' : ' '
          const color = line.type === 'add' ? 'text-green-300' : line.type === 'remove' ? 'text-red-300' : 'text-gray-500'
          return (
            <div key={i} className={`flex items-start gap-2 px-3 py-0.5 leading-relaxed ${bg}`}>
              <span className={`shrink-0 w-3 ${color} font-bold select-none`}>{prefix}</span>
              <span className={color}>{line.text || <span className="opacity-0">—</span>}</span>
            </div>
          )
        })}
      </div>
    </div>
  )
}

// ─── Reward chart ────────────────────────────────────────────────────────────

interface RewardPoint { step: number; reward: number }

function RewardChart({ points }: { points: RewardPoint[] }) {
  if (points.length === 0) {
    return (
      <div className="flex flex-col items-center justify-center h-28 gap-1.5 text-gray-700">
        <span className="text-[11px]">Rewards appear as agent runs</span>
      </div>
    )
  }
  return (
    <ResponsiveContainer width="100%" height={110}>
      <LineChart data={points} margin={{ top: 4, right: 8, bottom: 0, left: -20 }}>
        <CartesianGrid strokeDasharray="3 3" stroke="#ffffff08" />
        <XAxis dataKey="step" tick={{ fontSize: 9, fill: '#6b7280' }} />
        <YAxis domain={[-0.5, 1.1]} tick={{ fontSize: 9, fill: '#6b7280' }} />
        <Tooltip
          contentStyle={{ background: '#1a1a2e', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, fontSize: 11 }}
          labelStyle={{ color: '#9ca3af' }}
          itemStyle={{ color: '#f97316' }}
          formatter={(v: number) => [v.toFixed(2), 'reward']}
          labelFormatter={(l) => `Step ${l}`}
        />
        <ReferenceLine y={0} stroke="#ffffff18" strokeDasharray="3 3" />
        <Line
          type="monotone" dataKey="reward" name="Reward"
          stroke="#f97316" strokeWidth={2}
          dot={(props) => {
            const { cx, cy, payload } = props
            const color = (payload as RewardPoint).reward >= 0 ? '#22c55e' : '#ef4444'
            return <circle key={`dot-${(payload as RewardPoint).step}`} cx={cx} cy={cy} r={3} fill={color} stroke="none" />
          }}
          activeDot={{ r: 4, fill: '#f97316' }}
          isAnimationActive={false}
        />
      </LineChart>
    </ResponsiveContainer>
  )
}

// ─── Right panel ─────────────────────────────────────────────────────────────

interface RightPanelProps {
  gen: number
  score: number
  rewardPoints: RewardPoint[]
  latestDiff: { from: number; to: number } | null
}

function RightPanel({ gen, score, rewardPoints, latestDiff }: RightPanelProps) {
  return (
    <div className="flex flex-col gap-4 px-4 py-4 overflow-y-auto h-full">
      {/* Gen badge + score */}
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-1.5">
          <Sparkles size={12} className="text-violet-400" />
          <span className="text-[11px] font-semibold text-gray-400 uppercase tracking-wider">GEPA</span>
        </div>
        <span className="text-[10px] px-2 py-0.5 rounded-full bg-violet-500/15 border border-violet-500/25 text-violet-400 font-semibold">
          Gen {gen}
        </span>
      </div>

      {/* Score bar */}
      <div className="border rounded-xl p-3" style={{ background: 'var(--bg-tertiary)', borderColor: 'var(--border-color)' }}>
        <div className="flex items-end gap-2 mb-2">
          <span className="text-2xl font-bold text-green-400 tabular-nums leading-none">
            {(score * 100).toFixed(0)}%
          </span>
          <span className="text-[10px] text-gray-600 mb-0.5">benchmark score</span>
        </div>
        <div className="h-1.5 rounded-full overflow-hidden" style={{ background: 'var(--border-color)' }}>
          <motion.div
            className="h-full rounded-full"
            style={{ background: 'linear-gradient(90deg,#8b5cf6,#22c55e)' }}
            animate={{ width: `${score * 100}%` }}
            transition={{ duration: 0.9, ease: 'easeOut' }}
          />
        </div>
        {/* Generation history */}
        <div className="mt-2.5 flex flex-col gap-1">
          {SCORES.slice(0, gen + 1).map((s, i) => (
            <div key={i} className="flex items-center gap-2 text-[10px]">
              <span className="text-gray-600 w-10 shrink-0">Gen {i}</span>
              <div className="flex-1 h-1 rounded-full overflow-hidden" style={{ background: 'var(--border-color)' }}>
                <div className="h-full rounded-full transition-all duration-700"
                  style={{ width: `${s * 100}%`, background: i === gen ? 'linear-gradient(90deg,#8b5cf6,#22c55e)' : 'var(--text-dim)' }} />
              </div>
              <span className={`font-bold tabular-nums ${i === gen ? 'text-green-400' : 'text-gray-600'}`}>
                {(s * 100).toFixed(0)}%
              </span>
            </div>
          ))}
        </div>
      </div>

      {/* Reward chart */}
      <div>
        <div className="text-[10px] text-gray-600 font-semibold uppercase tracking-wider mb-2">
          RL Reward per Step
        </div>
        <RewardChart points={rewardPoints} />
      </div>

      {/* GitHub diff — appears after each GEPA cycle */}
      {latestDiff && (
        <div>
          <div className="flex items-center gap-1.5 mb-2">
            <GitCommitHorizontal size={11} className="text-violet-400" />
            <span className="text-[10px] text-gray-500 font-semibold uppercase tracking-wider">
              Prompt diff — Gen {latestDiff.from} → {latestDiff.to}
            </span>
          </div>
          <GithubDiff fromIdx={latestDiff.from} toIdx={latestDiff.to} />
        </div>
      )}
    </div>
  )
}

// ─── SQL highlighter ─────────────────────────────────────────────────────────

function HighlightSQL({ sql }: { sql: string }) {
  const re = /\b(SELECT|FROM|WHERE|JOIN|LEFT|RIGHT|ON|GROUP BY|ORDER BY|HAVING|LIMIT|AS|AND|OR|NOT|IN|IS|NULL|SUM|AVG|COUNT|ROUND|DISTINCT|DESC|ASC|strftime|JULIANDAY)\b/gi
  const parts: React.ReactNode[] = []
  let last = 0; let match: RegExpExecArray | null
  const r = new RegExp(re.source, 'gi')
  while ((match = r.exec(sql)) !== null) {
    if (match.index > last) parts.push(<span key={`t${last}`}>{sql.slice(last, match.index)}</span>)
    parts.push(<span key={`k${match.index}`} className="text-violet-300 font-semibold">{match[0]}</span>)
    last = match.index + match[0].length
  }
  if (last < sql.length) parts.push(<span key="end">{sql.slice(last)}</span>)
  return <>{parts}</>
}

// ─── Bubble types ─────────────────────────────────────────────────────────────

interface BaseBubble { id: string }
interface UserBubble extends BaseBubble { type: 'user'; text: string }
interface ThinkingBubble extends BaseBubble { type: 'thinking'; label: string }
interface SqlStreamBubble extends BaseBubble { type: 'sql_stream'; sql: string; attempt: number }
interface SqlErrBubble extends BaseBubble { type: 'sql_err'; sql: string; error: string; errorClass: string; rlAction: string; reward: number; attempt: number }
interface SqlOkBubble extends BaseBubble { type: 'sql_ok'; sql: string; rows: Record<string, string | number>[]; reward: number; attempt: number; firstTry: boolean }
interface GepaBubble extends BaseBubble { type: 'gepa'; fromGen: number; toGen: number; scoreFrom: number; scoreTo: number }
interface GroupBubble extends BaseBubble { type: 'group'; question: string; success: boolean; attempts: number; children: BubbleData[] }
type BubbleData = UserBubble | ThinkingBubble | SqlStreamBubble | SqlErrBubble | SqlOkBubble | GepaBubble | GroupBubble

let _id = 0
const uid = () => `b${++_id}`
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms))

// ─── Bubble renderer ─────────────────────────────────────────────────────────

function Bubble({ b }: { b: BubbleData }) {
  const [open, setOpen] = useState(false)

  if (b.type === 'user') return (
    <div className="flex justify-end">
      <div className="max-w-[78%] bg-violet-600/20 border border-violet-500/25 rounded-2xl rounded-tr-sm px-4 py-2.5">
        <p className="text-sm text-white">{b.text}</p>
      </div>
    </div>
  )

  if (b.type === 'thinking') return (
    <div className="flex items-center gap-2 px-1 text-xs text-gray-500">
      <Loader2 size={11} className="animate-spin text-violet-400 shrink-0" />
      {b.label}
    </div>
  )

  if (b.type === 'sql_stream') return (
    <div className="border rounded-xl overflow-hidden" style={{ borderColor: 'var(--border-color)' }}>
      <div className="px-3 py-1.5 flex items-center gap-2" style={{ background: 'var(--bg-tertiary)' }}>
        <span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
        <span className="text-[10px] text-gray-600">attempt {b.attempt}</span>
        <Loader2 size={9} className="animate-spin text-violet-400 ml-auto" />
      </div>
      <pre className="px-3 py-2 text-xs leading-relaxed whitespace-pre-wrap" style={{ background: 'rgba(139,92,246,0.05)' }}>
        <HighlightSQL sql={b.sql} />
        <span className="inline-block w-0.5 h-[1em] bg-violet-400 animate-pulse align-bottom ml-0.5" />
      </pre>
    </div>
  )

  if (b.type === 'sql_err') return (
    <div className="flex flex-col gap-1.5">
      <div className="border rounded-xl overflow-hidden" style={{ borderColor: 'var(--border-color)' }}>
        <div className="px-3 py-1.5 flex items-center gap-2" style={{ background: 'var(--bg-tertiary)' }}>
          <span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
          <span className="text-[10px] text-gray-600">attempt {b.attempt}</span>
        </div>
        <pre className="px-3 py-2 text-xs leading-relaxed whitespace-pre-wrap" style={{ background: 'rgba(139,92,246,0.05)' }}>
          <HighlightSQL sql={b.sql} />
        </pre>
      </div>
      <div className="flex items-center gap-2 flex-wrap bg-red-500/8 border border-red-500/20 rounded-xl px-3 py-2">
        <XCircle size={11} className="text-red-400 shrink-0" />
        <span className="text-xs text-red-300 flex-1">{b.error}</span>
        <span className="text-[10px] px-1.5 py-0.5 rounded-full bg-red-500/15 text-red-400 border border-red-500/20">{b.errorClass}</span>
        <span className="flex items-center gap-1 text-[10px] font-semibold px-2 py-0.5 rounded-full border border-orange-500/30 bg-orange-500/10 text-orange-400">
          <Zap size={8} />{b.rlAction}
        </span>
        <span className="text-[11px] font-bold text-red-400">{b.reward.toFixed(2)}</span>
      </div>
    </div>
  )

  if (b.type === 'sql_ok') return (
    <div className="flex flex-col gap-1.5">
      <div className="border rounded-xl overflow-hidden" style={{ borderColor: 'var(--border-color)' }}>
        <div className="px-3 py-1.5 flex items-center gap-2" style={{ background: 'var(--bg-tertiary)' }}>
          <span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider">SQL</span>
          {b.firstTry && (
            <span className="text-[10px] px-1.5 py-0.5 rounded-full bg-green-500/15 border border-green-500/25 text-green-400 font-semibold">first try ✓</span>
          )}
          <span className="ml-auto text-[11px] font-bold text-green-400">+{b.reward.toFixed(2)}</span>
        </div>
        <pre className="px-3 py-2 text-xs leading-relaxed whitespace-pre-wrap" style={{ background: 'rgba(139,92,246,0.05)' }}>
          <HighlightSQL sql={b.sql} />
        </pre>
      </div>
      <div className="flex items-center gap-1.5 text-[10px] px-0.5">
        <CheckCircle2 size={11} className="text-green-400" />
        <span className="text-green-400 font-semibold">Success</span>
        <span className="text-gray-600">· {b.rows.length}+ rows returned</span>
      </div>
      <div className="rounded-xl border overflow-hidden text-[10px]" style={{ borderColor: 'var(--border-color)' }}>
        <table className="w-full">
          <thead>
            <tr style={{ background: 'var(--bg-tertiary)' }}>
              {Object.keys(b.rows[0] ?? {}).map((k) => (
                <th key={k} className="px-2 py-1.5 text-left font-semibold text-gray-500 whitespace-nowrap">{k}</th>
              ))}
            </tr>
          </thead>
          <tbody>
            {b.rows.map((row, i) => (
              <tr key={i} style={i % 2 === 0 ? { background: 'var(--bg-hover)' } : {}}>
                {Object.values(row).map((v, j) => (
                  <td key={j} className="px-2 py-1 theme-text-secondary whitespace-nowrap">{String(v)}</td>
                ))}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  )

  if (b.type === 'gepa') return (
    <div className="border border-violet-500/25 rounded-2xl overflow-hidden bg-violet-500/5">
      <div className="px-4 py-3 flex items-center gap-2 border-b border-violet-500/15">
        <Sparkles size={12} className="text-violet-400" />
        <span className="text-xs font-semibold text-violet-300">GEPA — System Prompt Evolved</span>
        <span className="ml-auto text-[10px] text-violet-400/60">Gen {b.fromGen} → {b.toGen}</span>
      </div>
      <div className="px-4 py-3 flex items-center gap-6">
        <div className="text-center">
          <div className="text-[10px] text-gray-600 mb-0.5">Before</div>
          <div className="text-xl font-bold text-orange-400">{(b.scoreFrom * 100).toFixed(0)}%</div>
        </div>
        <div className="flex-1 flex flex-col items-center">
          <div className="text-xs text-violet-400 mb-1">↑ improved</div>
          <div className="w-full h-0.5 bg-gradient-to-r from-orange-400 to-green-400 rounded-full" />
        </div>
        <div className="text-center">
          <div className="text-[10px] text-gray-600 mb-0.5">After</div>
          <div className="text-xl font-bold text-green-400">{(b.scoreTo * 100).toFixed(0)}%</div>
        </div>
      </div>
      <div className="px-4 pb-3">
        <div className="text-[10px] text-gray-600 mb-1.5 flex items-center gap-1.5">
          <GitCommitHorizontal size={10} />
          Prompt diff (see sidebar for full view)
        </div>
        <div className="rounded-lg border overflow-hidden text-[10px] font-mono max-h-24 overflow-y-auto" style={{ borderColor: 'var(--border-color)', background: 'var(--bg-secondary)' }}>
          {diffPrompts(b.fromGen, b.toGen).filter((l) => l.type !== 'same').map((line, i) => (
            <div key={i} className={`flex gap-2 px-2 py-0.5 ${line.type === 'add' ? 'bg-green-500/10' : 'bg-red-500/10'}`}>
              <span className={`shrink-0 ${line.type === 'add' ? 'text-green-400' : 'text-red-400'}`}>{line.type === 'add' ? '+' : '-'}</span>
              <span className={line.type === 'add' ? 'text-green-300' : 'text-red-300'}>{line.text}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  )

  if (b.type === 'group') return (
    <div className="border rounded-2xl overflow-hidden" style={{ borderColor: 'var(--border-color)' }}>
      <button
        onClick={() => setOpen((v) => !v)}
        className="w-full flex items-center gap-2 px-3 py-2.5 transition-colors text-left"
        style={{ background: 'var(--bg-tertiary)' }}
        onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--bg-hover-strong)')}
        onMouseLeave={(e) => (e.currentTarget.style.background = 'var(--bg-tertiary)')}
      >
        {b.success
          ? <CheckCircle2 size={12} className="text-green-400 shrink-0" />
          : <XCircle size={12} className="text-red-400 shrink-0" />}
        <span className="text-xs theme-text-secondary flex-1 truncate">{b.question}</span>
        <span className="text-[10px] text-gray-600">{b.attempts} attempt{b.attempts !== 1 ? 's' : ''}</span>
        {open ? <ChevronUp size={11} className="text-gray-600 shrink-0" /> : <ChevronDown size={11} className="text-gray-600 shrink-0" />}
      </button>
      <AnimatePresence>
        {open && (
          <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.15 }} className="overflow-hidden">
            <div className="p-3 flex flex-col gap-2.5 border-t" style={{ borderColor: 'var(--border-color)' }}>
              {b.children.map((c) => <Bubble key={c.id} b={c} />)}
            </div>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  )

  return null
}

// ─── DemoMode ────────────────────────────────────────────────────────────────

export function DemoMode({ onClose }: { onClose: () => void }) {
  const [bubbles, setBubbles] = useState<BubbleData[]>([])
  const [appState, setAppState] = useState<'idle' | 'running' | 'done'>('idle')
  const [gen, setGen] = useState(0)
  const [score, setScore] = useState(SCORES[0])
  const [rewardPoints, setRewardPoints] = useState<RewardPoint[]>([])
  const [latestDiff, setLatestDiff] = useState<{ from: number; to: number } | null>(null)
  const stepRef = useRef(0)
  const cancel = useRef(false)
  const bottomRef = useRef<HTMLDivElement>(null)
  // Track all played queries and GEPA bubbles for end-of-demo collapse
  const allQueriesRef = useRef<Array<{ def: QueryDef; children: BubbleData[] }>>([])
  const gepaBubblesRef = useRef<BubbleData[]>([])

  const scroll = useCallback(() => {
    setTimeout(() => bottomRef.current?.scrollIntoView({ behavior: 'smooth' }), 60)
  }, [])

  const push = useCallback((b: BubbleData) => setBubbles((p) => [...p, b]), [])

  const addReward = useCallback((reward: number) => {
    stepRef.current += 1
    setRewardPoints((p) => [...p, { step: stepRef.current, reward }])
  }, [])

  const typeUser = useCallback(async (text: string) => {
    const id = uid()
    push({ id, type: 'user', text: '' })
    for (let i = 1; i <= text.length; i++) {
      if (cancel.current) return
      setBubbles((p) => p.map((b) => b.id === id ? { ...b, text: text.slice(0, i) } : b))
      await sleep(32 + Math.random() * 22)
    }
    scroll(); await sleep(300)
  }, [push, scroll])

  const streamSQL = useCallback(async (sql: string, attempt: number) => {
    const id = uid()
    push({ id, type: 'sql_stream', sql: '', attempt })
    let built = ''
    for (const line of sql.split('\n')) {
      if (cancel.current) return
      built += (built ? '\n' : '') + line
      setBubbles((p) => p.map((b) => b.id === id ? { ...b, sql: built } : b))
      await sleep(85 + Math.random() * 70)
    }
    scroll(); await sleep(200)
    setBubbles((p) => p.filter((b) => b.id !== id))
  }, [push, scroll])

  const playQuery = useCallback(async (def: QueryDef): Promise<BubbleData[]> => {
    const children: BubbleData[] = []
    await typeUser(def.question)

    for (let i = 0; i < def.attempts.length; i++) {
      if (cancel.current) return children
      const att = def.attempts[i]

      // Thinking
      const thId = uid()
      push({ id: thId, type: 'thinking', label: i === 0 ? 'Generating SQL…' : 'Applying repair strategy…' })
      scroll(); await sleep(750)
      if (cancel.current) return children
      setBubbles((p) => p.filter((b) => b.id !== thId))

      await streamSQL(att.sql, i + 1)
      if (cancel.current) return children

      addReward(att.reward)

      if (att.error) {
        const eb: SqlErrBubble = { id: uid(), type: 'sql_err', sql: att.sql, error: att.error, errorClass: att.errorClass ?? 'OTHER', rlAction: att.rlAction ?? 'REWRITE_FULL', reward: att.reward, attempt: i + 1 }
        push(eb); children.push(eb); scroll(); await sleep(900)
      } else {
        const ob: SqlOkBubble = { id: uid(), type: 'sql_ok', sql: att.sql, rows: att.rows ?? [], reward: att.reward, attempt: i + 1, firstTry: i === 0 }
        push(ob); children.push(ob); scroll(); await sleep(1100)
      }
    }
    return children
  }, [push, scroll, typeUser, streamSQL, addReward])

  const playGepa = useCallback(async (fromGen: number, toGen: number) => {
    const steps = ['Analyzing failure patterns…', 'Identifying missing rules from errors…', 'Rewriting system prompt…', 'Benchmarking candidate prompt…']
    for (const label of steps) {
      if (cancel.current) return
      setBubbles((p) => {
        const last = p[p.length - 1]
        if (last?.type === 'thinking') return [...p.slice(0, -1), { id: last.id, type: 'thinking' as const, label }]
        return [...p, { id: uid(), type: 'thinking' as const, label }]
      })
      scroll(); await sleep(1050)
    }
    setBubbles((p) => p.filter((b) => b.type !== 'thinking'))

    // Animate score
    const from = SCORES[fromGen], to = SCORES[toGen]
    for (let i = 0; i <= 45; i++) {
      if (cancel.current) return
      setScore(from + (to - from) * (i / 45))
      await sleep(18)
    }
    setGen(toGen)
    setLatestDiff({ from: fromGen, to: toGen })

    const gepaBubble: GepaBubble = { id: uid(), type: 'gepa', fromGen, toGen, scoreFrom: from, scoreTo: to }
    gepaBubblesRef.current.push(gepaBubble)
    push(gepaBubble)
    scroll(); await sleep(1000)
  }, [push, scroll])

  const autoPlay = useCallback(async () => {
    cancel.current = false
    allQueriesRef.current = []
    gepaBubblesRef.current = []
    setBubbles([]); setGen(0); setScore(SCORES[0]); setRewardPoints([]); setLatestDiff(null)
    stepRef.current = 0; setAppState('running')
    await sleep(300)

    for (const id of ROUND_1) {
      if (cancel.current) break
      const children = await playQuery(QUERIES[id])
      if (!cancel.current) {
        allQueriesRef.current.push({ def: QUERIES[id], children })
        await sleep(600)
      }
    }
    if (!cancel.current) { await playGepa(0, 1); await sleep(500) }

    for (const id of ROUND_2) {
      if (cancel.current) break
      const children = await playQuery(QUERIES[id])
      if (!cancel.current) {
        allQueriesRef.current.push({ def: QUERIES[id], children })
        await sleep(600)
      }
    }
    if (!cancel.current) { await playGepa(1, 2); await sleep(500) }

    for (const id of ROUND_3) {
      if (cancel.current) break
      const children = await playQuery(QUERIES[id])
      if (!cancel.current) {
        allQueriesRef.current.push({ def: QUERIES[id], children })
        await sleep(600)
      }
    }

    if (!cancel.current) {
      // Collapse all queries at once, rebuilding the bubble list
      const queries = allQueriesRef.current
      const gepas = gepaBubblesRef.current
      const result: BubbleData[] = []

      // Round 1 groups
      for (let i = 0; i < ROUND_1.length && i < queries.length; i++) {
        const { def, children } = queries[i]
        const lastAtt = def.attempts[def.attempts.length - 1]
        result.push({ id: uid(), type: 'group', question: def.question, success: !lastAtt.error, attempts: def.attempts.length, children })
      }
      // GEPA 0→1
      if (gepas[0]) result.push(gepas[0])
      // Round 2 groups
      for (let i = 0; i < ROUND_2.length; i++) {
        const qi = ROUND_1.length + i
        if (qi >= queries.length) break
        const { def, children } = queries[qi]
        const lastAtt = def.attempts[def.attempts.length - 1]
        result.push({ id: uid(), type: 'group', question: def.question, success: !lastAtt.error, attempts: def.attempts.length, children })
      }
      // GEPA 1→2
      if (gepas[1]) result.push(gepas[1])
      // Round 3 groups
      for (let i = 0; i < ROUND_3.length; i++) {
        const qi = ROUND_1.length + ROUND_2.length + i
        if (qi >= queries.length) break
        const { def, children } = queries[qi]
        const lastAtt = def.attempts[def.attempts.length - 1]
        result.push({ id: uid(), type: 'group', question: def.question, success: !lastAtt.error, attempts: def.attempts.length, children })
      }

      setBubbles(result)
      await sleep(300)
      scroll()
      setAppState('done')
    }
  }, [playQuery, playGepa, scroll])

  useEffect(() => () => { cancel.current = true }, [])

  return (
    <motion.div
      initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
      className="fixed inset-0 z-[100] flex flex-col"
      style={{ background: 'var(--bg-primary)' }}
    >
      {/* Header */}
      <div className="shrink-0 flex items-center justify-between px-4 py-3 border-b theme-border" style={{ background: 'var(--bg-secondary)' }}>
        <div className="flex items-center gap-3">
          <div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-violet-500/15 border border-violet-500/25">
            <Play size={9} className="text-violet-400" fill="currentColor" />
            <span className="text-[11px] font-semibold text-violet-300">Demo</span>
          </div>
          <span className="text-xs text-gray-600 hidden sm:block">
            RL repair loop · GEPA prompt evolution · 42% → 91%
          </span>
        </div>
        <button onClick={onClose} className="p-1.5 rounded-lg hover:bg-white/5 text-gray-500 hover:text-white transition-colors">
          <X size={16} />
        </button>
      </div>

      {/* Body */}
      <div className="flex flex-1 overflow-hidden">
        {/* Chat */}
        <div className="flex-1 flex flex-col overflow-hidden">
          <div className="flex-1 overflow-y-auto px-4 py-4">
            {appState === 'idle' ? (
              <div className="flex flex-col items-center justify-center h-full gap-5 text-center px-6">
                <div className="w-14 h-14 rounded-2xl flex items-center justify-center" style={{ background: 'linear-gradient(135deg,#3b0764,#1e3a5f)', boxShadow: '0 12px 32px rgba(139,92,246,0.3)' }}>
                  <Play size={24} className="text-white ml-0.5" fill="currentColor" />
                </div>
                <div>
                  <h2 className="text-base font-bold text-white mb-1.5">SQL Agent OpenEnv — Live Demo</h2>
                  <p className="text-sm text-gray-500 max-w-sm leading-relaxed">
                    Watch the agent fail, self-repair using LinUCB, then improve through two GEPA prompt evolution cycles.
                  </p>
                </div>
                <div className="flex flex-col gap-1.5 text-xs text-gray-600 max-w-xs text-left">
                  {['Round 1 — simple queries, RL repairs table/column errors', 'GEPA cycle 1 — prompt learns alias rules (42%→74%)', 'Round 2 — join queries, ambiguous column errors repaired', 'GEPA cycle 2 — prompt learns aggregation rules (74%→91%)', 'Round 3 — same queries, first-try success'].map((s, i) => (
                    <div key={i} className="flex items-start gap-2">
                      <div className="w-4 h-4 rounded-full bg-violet-500/20 border border-violet-500/30 flex items-center justify-center text-[9px] text-violet-400 font-bold shrink-0 mt-0.5">{i + 1}</div>
                      {s}
                    </div>
                  ))}
                </div>
                <button
                  onClick={() => void autoPlay()}
                  className="flex items-center gap-2 px-6 py-3 rounded-2xl font-semibold text-sm text-white active:scale-95 transition-transform"
                  style={{ background: 'linear-gradient(135deg,#7c3aed,#2563eb)', boxShadow: '0 8px 24px rgba(124,58,237,0.4)' }}
                >
                  <Play size={13} fill="currentColor" />
                  Start Demo
                </button>
              </div>
            ) : (
              <div className="flex flex-col gap-4 max-w-2xl mx-auto">
                <AnimatePresence initial={false}>
                  {bubbles.map((b) => (
                    <motion.div key={b.id} initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.18 }}>
                      <Bubble b={b} />
                    </motion.div>
                  ))}
                </AnimatePresence>

                {appState === 'done' && (
                  <motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="border border-violet-500/20 rounded-2xl p-5 bg-violet-500/5 text-center mt-2">
                    <div className="text-3xl font-bold text-violet-300 mb-1 tabular-nums">91%</div>
                    <div className="text-xs text-gray-500 max-w-xs mx-auto leading-relaxed">
                      Agent improved from 42% → 91% accuracy through RL repair strategies and two GEPA prompt evolution cycles.
                    </div>
                  </motion.div>
                )}
                <div ref={bottomRef} />
              </div>
            )}
          </div>
        </div>

        {/* Right panel */}
        <aside className="hidden lg:flex flex-col w-72 border-l theme-border overflow-hidden shrink-0" style={{ background: 'var(--bg-secondary)' }}>
          <RightPanel gen={gen} score={score} rewardPoints={rewardPoints} latestDiff={latestDiff} />
        </aside>
      </div>
    </motion.div>
  )
}
