import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { Binary, Bot, BookOpen, ChevronDown, Cpu, Database, FileText, Layers, Loader2, MessageSquare, RefreshCw, RotateCcw, ScanText, Scissors, Search, Send, Sparkles, Upload, Workflow, Wrench } from 'lucide-react'
import { cn } from '../../lib/utils'
import { subTabActive, subTabIdle } from '../../lib/tabActive'

type Collection = { name: string; documents: number; files?: number; filenames?: string[] }
type StoredDoc = {
  id: string
  filename: string
  collection: string
  chunks: number
  characters?: number
  ingested_at: string
  bytes?: number
}
type Source = { source?: string; chunk?: number; preview?: string }
type AgentStep = { tool: string; input?: Record<string, unknown> }
type ChatMsg = { role: 'user' | 'assistant'; content: string; sources?: Source[]; steps?: AgentStep[] }

type Health = { ok: boolean; chroma: boolean; docling: boolean; llm: boolean; embed_model?: string }

type Props = { onGpuActivity?: (active: boolean) => void }

export function KnowledgeChatView({ onGpuActivity }: Props = {}) {
  const [health, setHealth] = useState<Health | null>(null)
  const [collections, setCollections] = useState<Collection[]>([])
  const [collection, setCollection] = useState('default')
  const [newCol, setNewCol] = useState('')
  const [messages, setMessages] = useState<ChatMsg[]>([])
  const [input, setInput] = useState('')
  const [loading, setLoading] = useState(false)
  const [ingesting, setIngesting] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [storedDocs, setStoredDocs] = useState<StoredDoc[]>([])
  const [selectedDocId, setSelectedDocId] = useState<string | null>(null)
  const [summarizing, setSummarizing] = useState(false)
  const [reindexing, setReindexing] = useState<string | null>(null)
  const [agentMode, setAgentMode] = useState(false)
  const [syncing, setSyncing] = useState(false)
  const [catalogDocs, setCatalogDocs] = useState<number | null>(null)
  const [showArch, setShowArch] = useState(true)
  const [activeStage, setActiveStage] = useState<number | null>(null)
  const bottomRef = useRef<HTMLDivElement>(null)

  const loadMeta = useCallback(async () => {
    try {
      const [h, c, d] = await Promise.all([
        fetch('/rag/health'),
        fetch('/rag/collections'),
        fetch('/rag/documents'),
      ])
      if (h.ok) setHealth(await h.json())
      if (c.ok) {
        const j = await c.json()
        setCollections(j.collections || [])
      }
      if (d.ok) {
        const j = await d.json()
        setStoredDocs(j.documents || [])
      }
    } catch {
      setHealth(null)
    }
  }, [])

  const loadCatalog = useCallback(async () => {
    try {
      const r = await fetch('/rag/catalog/status')
      if (r.ok) setCatalogDocs((await r.json()).documents ?? 0)
    } catch {
      setCatalogDocs(null)
    }
  }, [])

  useEffect(() => {
    loadMeta()
    loadCatalog()
  }, [loadMeta, loadCatalog])

  useEffect(() => {
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages, loading])

  useEffect(() => {
    onGpuActivity?.(loading || ingesting || summarizing || reindexing !== null)
  }, [loading, ingesting, summarizing, reindexing, onGpuActivity])

  const onIngest = async (file: File) => {
    setIngesting(true)
    setError(null)
    const fd = new FormData()
    fd.append('file', file)
    fd.append('collection', collection)
    try {
      const r = await fetch('/rag/ingest', { method: 'POST', body: fd })
      const j = await r.json()
      if (!r.ok || !j.ok) {
        setError(j.error || 'Ingest failed')
        return
      }
      setMessages((m) => [...m, {
        role: 'assistant',
        content: j.duplicate
          ? `Already indexed: ${j.filename} (${j.chunks} chunks). You can chat immediately — no re-upload needed.`
          : `Indexed ${j.filename} → collection "${j.collection}" — ${j.chunks} chunks (${j.characters?.toLocaleString()} chars). Stored permanently.`,
      }])
      loadMeta()
    } catch {
      setError('RAG API unavailable')
    } finally {
      setIngesting(false)
    }
  }

  const onSummarize = async (doc: StoredDoc) => {
    setSummarizing(true)
    setError(null)
    setSelectedDocId(doc.id)
    setCollection(doc.collection)
    setMessages((m) => [...m, { role: 'user', content: `Summarize: ${doc.filename}` }])
    try {
      const r = await fetch('/rag/summarize', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ collection: doc.collection, doc_id: doc.id }),
      })
      const j = await r.json()
      if (!r.ok || !j.ok) {
        setError(j.error || 'Summarize failed')
        return
      }
      setMessages((m) => [...m, {
        role: 'assistant',
        content: `Summary of ${j.filename} (${j.characters?.toLocaleString()} chars):\n\n${j.summary}`,
      }])
    } catch {
      setError('Summarize request failed')
    } finally {
      setSummarizing(false)
    }
  }

  const onReindex = async (doc: StoredDoc) => {
    setReindexing(doc.id)
    setError(null)
    try {
      const r = await fetch(`/rag/documents/${doc.id}/reindex`, { method: 'POST' })
      const j = await r.json()
      if (!r.ok || !j.ok) {
        setError(j.error || 'Re-index failed')
        return
      }
      setMessages((m) => [...m, {
        role: 'assistant',
        content: `Re-indexed ${j.filename}: ${j.chunks} clean text chunks (${j.characters?.toLocaleString()} chars). You can now chat and summarize.`,
      }])
      loadMeta()
    } catch {
      setError('Re-index request failed')
    } finally {
      setReindexing(null)
    }
  }

  const onSyncCatalog = async () => {
    setSyncing(true)
    setError(null)
    try {
      const r = await fetch('/rag/catalog/sync', { method: 'POST' })
      const j = await r.json()
      if (!r.ok || !j.ok) {
        setError(j.error || 'Catalog sync failed')
        return
      }
      const c = j.counts || {}
      setMessages((m) => [...m, {
        role: 'assistant',
        content: `Catalog synced — ${j.documents} entries (${c.tables ?? 0} tables, ${c.pii_datasets ?? 0} PII datasets, ${c.flows ?? 0} lineage edges, ${c.movements ?? 0} movements). Agent mode can now answer catalog/PII/lineage questions.`,
      }])
      loadCatalog()
    } catch {
      setError('Catalog sync request failed')
    } finally {
      setSyncing(false)
    }
  }

  const onSendAgent = async (msg: string) => {
    setLoading(true)
    setActiveStage(0)
    const steps: AgentStep[] = []
    let answer = ''
    let idx = -1
    setMessages((m) => {
      idx = m.length
      return [...m, { role: 'assistant', content: '', steps: [] }]
    })
    const patch = () =>
      setMessages((m) => m.map((x, i) => (i === idx ? { ...x, content: answer, steps: [...steps] } : x)))
    try {
      const r = await fetch('/rag/agent', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: msg, max_steps: 5 }),
      })
      if (!r.ok || !r.body) {
        setError('Agent unavailable')
        return
      }
      const reader = r.body.getReader()
      const decoder = new TextDecoder()
      let buf = ''
      for (;;) {
        const { done, value } = await reader.read()
        if (done) break
        buf += decoder.decode(value, { stream: true })
        const events = buf.split('\n\n')
        buf = events.pop() || ''
        for (const ev of events) {
          const eMatch = ev.match(/^event: (.+)$/m)
          const dMatch = ev.match(/^data: (.+)$/m)
          if (!eMatch || !dMatch) continue
          const type = eMatch[1].trim()
          let data: Record<string, unknown> = {}
          try { data = JSON.parse(dMatch[1]) } catch { continue }
          if (type === 'step') {
            setActiveStage(2)
            steps.push({ tool: String(data.tool), input: data.input as Record<string, unknown> })
            patch()
          } else if (type === 'token') {
            setActiveStage(5)
            answer += String(data.t || '')
            patch()
          } else if (type === 'error') {
            setError(String(data.error || 'Agent error'))
          }
        }
      }
    } catch {
      setError('Failed to reach agent')
    } finally {
      patch()
      setLoading(false)
      setActiveStage(null)
    }
  }

  const STAGE_INDEX: Record<string, number> = { question: 0, embed: 1, retrieve: 2, context: 3, llm: 4, answer: 5 }

  const onSendChatStream = async (msg: string) => {
    setLoading(true)
    setActiveStage(0)
    let answer = ''
    let srcs: Source[] | undefined
    let idx = -1
    setMessages((m) => {
      idx = m.length
      return [...m, { role: 'assistant', content: '' }]
    })
    const patch = () => setMessages((m) => m.map((x, i) => (i === idx ? { ...x, content: answer, sources: srcs } : x)))
    try {
      const r = await fetch('/rag/chat/stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: msg, collection, top_k: 5 }),
      })
      if (!r.ok || !r.body) {
        setError('Chat service unavailable')
        return
      }
      const reader = r.body.getReader()
      const decoder = new TextDecoder()
      let buf = ''
      for (;;) {
        const { done, value } = await reader.read()
        if (done) break
        buf += decoder.decode(value, { stream: true })
        const events = buf.split('\n\n')
        buf = events.pop() || ''
        for (const ev of events) {
          const eMatch = ev.match(/^event: (.+)$/m)
          const dMatch = ev.match(/^data: (.+)$/m)
          if (!eMatch || !dMatch) continue
          const type = eMatch[1].trim()
          let data: Record<string, unknown> = {}
          try { data = JSON.parse(dMatch[1]) } catch { continue }
          if (type === 'stage') {
            const si = STAGE_INDEX[String(data.stage)]
            if (si != null) setActiveStage(si)
          } else if (type === 'sources') {
            srcs = data.sources as Source[]
            patch()
          } else if (type === 'token') {
            setActiveStage(5)
            answer += String(data.t || '')
            patch()
          } else if (type === 'error') {
            setError(String(data.error || 'Chat error'))
          }
        }
      }
    } catch {
      setError('Failed to reach RAG / LLM service')
    } finally {
      patch()
      setLoading(false)
      setActiveStage(null)
    }
  }

  const onSend = async () => {
    const msg = input.trim()
    if (!msg || loading) return
    setInput('')
    setError(null)
    setMessages((m) => [...m, { role: 'user', content: msg }])
    if (agentMode) {
      await onSendAgent(msg)
      return
    }
    await onSendChatStream(msg)
  }

  const createCollection = async () => {
    if (!newCol.trim()) return
    const fd = new FormData()
    fd.append('name', newCol.trim())
    await fetch('/rag/collections', { method: 'POST', body: fd })
    setCollection(newCol.trim())
    setNewCol('')
    loadMeta()
  }

  const doclingUiUrl = `${window.location.protocol}//${window.location.hostname}:5001/ui/`

  return (
    <div className="flex h-full min-h-[calc(100vh-140px)] flex-col rounded-lg border border-border bg-surface-raised">
      <header className="shrink-0 border-b border-border bg-surface-overlay/30 px-4 py-3">
        <div className="flex flex-wrap items-start justify-between gap-3">
          <div>
            <h2 className="text-base font-semibold text-foreground">Knowledge Chat (RAG)</h2>
            <p className="text-[11px] text-foreground-muted">
              LangChain + ChromaDB — chat with your ingested documents via Llama 70B
            </p>
          </div>
          <div className="flex flex-wrap items-center gap-2 text-[10px]">
            {health && (
              <>
                <StatusPill ok={health.chroma} label="ChromaDB" />
                <StatusPill ok={health.docling} label="Docling" />
                <StatusPill ok={health.llm} label="LLM" />
              </>
            )}
            <button
              type="button"
              onClick={() => setShowArch((v) => !v)}
              className={cn('inline-flex items-center gap-1 rounded border px-2 py-1 text-[10px] font-medium transition-colors',
                showArch ? 'border-docker/50 bg-docker/15 text-docker' : 'border-border text-foreground-muted hover:bg-surface-overlay')}
            >
              <Workflow className="h-3.5 w-3.5" /> How it works
              <ChevronDown className={cn('h-3 w-3 transition-transform', showArch && 'rotate-180')} />
            </button>
            <button type="button" onClick={loadMeta} className="rounded border border-border p-1.5 hover:bg-surface-overlay">
              <RefreshCw className="h-4 w-4" />
            </button>
          </div>
        </div>
      </header>

      {showArch && (
        <div className="shrink-0 border-b border-border bg-surface-overlay/20 px-4 py-3">
          <RagFlow loading={loading} ingesting={ingesting} activeStage={activeStage} />
        </div>
      )}

      <div className="flex min-h-0 flex-1 flex-col lg:flex-row">
        <aside className="shrink-0 border-b border-border p-4 lg:w-72 lg:border-b-0 lg:border-r">
          <h3 className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-foreground-faint">Collection</h3>
          <select
            value={collection}
            onChange={(e) => setCollection(e.target.value)}
            className="mb-2 w-full rounded border border-border bg-surface-overlay px-2 py-1.5 text-[11px]"
          >
            {collections.length === 0 && <option value="default">default (empty)</option>}
            {collections.map((c) => (
              <option key={c.name} value={c.name}>{c.name} ({c.documents} docs)</option>
            ))}
          </select>
          <div className="mb-4 flex gap-1">
            <input
              value={newCol}
              onChange={(e) => setNewCol(e.target.value)}
              placeholder="New collection name"
              className="min-w-0 flex-1 rounded border border-border bg-surface-overlay px-2 py-1 text-[10px]"
            />
            <button type="button" onClick={createCollection} className={cn('shrink-0 rounded px-2 py-1 text-[10px]', subTabIdle)}>Add</button>
          </div>

          <h3 className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-foreground-faint">Platform agent</h3>
          <button
            type="button"
            onClick={() => setAgentMode((v) => !v)}
            className={cn(
              'mb-1 flex w-full items-center justify-between rounded border px-2 py-1.5 text-[11px] transition-colors',
              agentMode ? 'border-docker/50 bg-docker/15 text-docker' : 'border-border text-foreground-muted',
            )}
          >
            <span className="flex items-center gap-1"><Bot className="h-3.5 w-3.5" />Agent mode {agentMode ? 'on' : 'off'}</span>
            <span className={cn('h-2 w-2 rounded-full', agentMode ? 'bg-docker' : 'bg-foreground-faint/40')} />
          </button>
          <p className="mb-2 text-[9px] text-foreground-faint">
            Live tools: catalog, PII, CDC, movements + approval-gated triggers.
          </p>
          <button
            type="button"
            onClick={onSyncCatalog}
            disabled={syncing}
            className={cn('mb-4 flex w-full items-center justify-center gap-1 rounded px-2 py-1 text-[10px]', subTabIdle)}
          >
            {syncing ? <Loader2 className="h-3 w-3 animate-spin" /> : <Database className="h-3 w-3" />}
            Sync catalog{catalogDocs != null ? ` (${catalogDocs})` : ''}
          </button>

          <h3 className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-foreground-faint">Ingest documents</h3>
          <label className={cn('flex cursor-pointer flex-col items-center rounded-lg border-2 border-dashed border-border px-3 py-4 text-center hover:border-docker/40', ingesting && 'opacity-50')}>
            <Upload className="mb-1 h-6 w-6 text-docker opacity-60" />
            <span className="text-[10px] font-medium">PDF, PPTX, DOCX, CSV, TXT, MD</span>
            <span className="text-[9px] text-foreground-faint">Stored in ChromaDB + disk — upload once</span>
            <input type="file" className="hidden" disabled={ingesting} accept=".pdf,.pptx,.ppt,.docx,.csv,.txt,.md,.json" onChange={(e) => e.target.files?.[0] && onIngest(e.target.files[0])} />
          </label>
          {ingesting && (
            <p className="mt-2 flex items-center gap-1 text-[10px] text-foreground-muted">
              <Loader2 className="h-3 w-3 animate-spin" /> Ingesting & embedding…
            </p>
          )}

          <h3 className="mb-2 mt-4 text-[10px] font-semibold uppercase tracking-wider text-foreground-faint">
            Document library ({storedDocs.length})
          </h3>
          <div className="scrollbar-thin max-h-40 space-y-1 overflow-y-auto">
            {storedDocs.length === 0 ? (
              <p className="text-[9px] text-foreground-faint">No documents yet — upload above.</p>
            ) : (
              storedDocs.map((doc) => (
                <div
                  key={doc.id}
                  className={cn(
                    'rounded border px-2 py-1.5 text-[9px] transition-colors',
                    selectedDocId === doc.id ? 'border-docker/40 bg-docker/10' : 'border-border',
                  )}
                >
                  <button type="button" onClick={() => { setSelectedDocId(doc.id); setCollection(doc.collection) }} className="w-full text-left">
                    <p className="truncate font-medium text-foreground">{doc.filename}</p>
                    <p className="text-foreground-faint">{doc.collection} · {doc.chunks} chunks · {new Date(doc.ingested_at).toLocaleDateString()}</p>
                  </button>
                  <div className="mt-1 flex gap-1">
                    <button
                      type="button"
                      disabled={summarizing}
                      onClick={() => onSummarize(doc)}
                      className={cn('flex flex-1 items-center justify-center gap-0.5 rounded px-1 py-0.5', subTabIdle)}
                    >
                      {summarizing && selectedDocId === doc.id ? <Loader2 className="h-2.5 w-2.5 animate-spin" /> : <FileText className="h-2.5 w-2.5" />}
                      Summarize
                    </button>
                    <button
                      type="button"
                      disabled={reindexing === doc.id}
                      onClick={() => onReindex(doc)}
                      className={cn('flex flex-1 items-center justify-center gap-0.5 rounded px-1 py-0.5', subTabIdle)}
                      title="Re-parse with clean text (fixes corrupted PDF index)"
                    >
                      {reindexing === doc.id ? <Loader2 className="h-2.5 w-2.5 animate-spin" /> : <RotateCcw className="h-2.5 w-2.5" />}
                      Re-index
                    </button>
                  </div>
                </div>
              ))
            )}
          </div>

          <div className="mt-4 space-y-1 text-[9px] text-foreground-faint">
            <p><BookOpen className="mr-1 inline h-3 w-3" />Embed: {health?.embed_model || 'all-MiniLM-L6-v2'}</p>
            <a href={doclingUiUrl} target="_blank" rel="noreferrer" className="text-docker hover:underline">Docling UI (port 5001) ↗</a>
          </div>
        </aside>

        <div className="flex min-h-0 flex-1 flex-col">
          <div className="scrollbar-thin flex-1 overflow-y-auto p-4">
            {messages.length === 0 && (
              <div className="flex h-full flex-col items-center justify-center gap-2 text-center text-sm text-foreground-muted">
                <MessageSquare className="h-10 w-10 opacity-30" />
                <p>Upload once — documents stay in ChromaDB. Ask anytime without re-uploading.</p>
                <p className="text-[11px]">Example: &quot;What maturity gaps exist in the customer dataset?&quot;</p>
              </div>
            )}
            {messages.map((m, i) => (
              <div key={i} className={cn('mb-3 max-w-[90%] rounded-lg px-3 py-2 text-[12px]', m.role === 'user' ? 'ml-auto bg-docker/20 text-foreground' : 'bg-surface-overlay text-foreground-muted')}>
                {m.steps && m.steps.length > 0 && (
                  <div className="mb-2 flex flex-wrap gap-1">
                    {m.steps.map((s, j) => (
                      <span key={j} className="inline-flex items-center gap-1 rounded-full bg-docker/15 px-2 py-0.5 text-[9px] text-docker">
                        <Wrench className="h-2.5 w-2.5" />{s.tool}
                      </span>
                    ))}
                  </div>
                )}
                <p className="whitespace-pre-wrap leading-relaxed">{m.content || (loading && i === messages.length - 1 ? '…' : '')}</p>
                {m.sources && m.sources.length > 0 && (
                  <div className="mt-2 border-t border-border pt-2">
                    <p className="mb-1 text-[9px] font-semibold uppercase text-foreground-faint">Sources</p>
                    {m.sources.map((s, j) => (
                      <p key={j} className="text-[9px] text-foreground-faint">
                        {s.source} · chunk {s.chunk}: {s.preview?.slice(0, 120)}…
                      </p>
                    ))}
                  </div>
                )}
              </div>
            ))}
            {loading && (
              <div className="flex items-center gap-2 text-[11px] text-foreground-muted">
                <Loader2 className="h-4 w-4 animate-spin text-docker" /> Retrieving context & generating answer…
              </div>
            )}
            {error && <p className="text-[11px] text-danger">{error}</p>}
            <div ref={bottomRef} />
          </div>

          <div className="flex shrink-0 gap-2 border-t border-border p-3">
            <input
              value={input}
              onChange={(e) => setInput(e.target.value)}
              onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && (e.preventDefault(), onSend())}
              placeholder={agentMode ? 'Ask the platform agent (catalog, PII, CDC, run a movement…)' : 'Ask a question about your ingested data…'}
              className="min-w-0 flex-1 rounded-lg border border-border bg-surface-overlay px-3 py-2 text-[12px]"
              disabled={loading}
            />
            <button type="button" onClick={onSend} disabled={loading || !input.trim()} className={cn('rounded-lg px-3 py-2', subTabActive, 'disabled:opacity-40')}>
              <Send className="h-4 w-4" />
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

const INDEX_STAGES = [
  { icon: Upload, label: 'Upload', sub: 'PDF·DOCX·MD', color: '#38bdf8', lc: false },
  { icon: ScanText, label: 'Docling', sub: 'OCR · tables', color: '#22d3ee', lc: false },
  { icon: Scissors, label: 'Split', sub: 'TextSplitter', color: '#a78bfa', lc: true },
  { icon: Binary, label: 'Embed', sub: 'MiniLM 384d', color: '#f472b6', lc: true },
  { icon: Database, label: 'ChromaDB', sub: 'vector store', color: '#34d399', lc: true },
]

const QUERY_STAGES = [
  { icon: MessageSquare, label: 'Question', sub: 'user', color: '#38bdf8', lc: false },
  { icon: Binary, label: 'Embed', sub: 'MiniLM', color: '#f472b6', lc: true },
  { icon: Search, label: 'Retrieve', sub: 'as_retriever', color: '#34d399', lc: true },
  { icon: Layers, label: 'Context', sub: 'top-5 chunks', color: '#fbbf24', lc: false },
  { icon: Cpu, label: 'LLM', sub: 'ChatOpenAI', color: '#818cf8', lc: true },
  { icon: Sparkles, label: 'Answer', sub: '+ sources', color: '#34d399', lc: false },
]

const STAGE_LABEL = ['Question', 'Embed', 'Retrieve', 'Context', 'LLM', 'Answer']

function RagFlow({ loading, ingesting, activeStage }: { loading: boolean; ingesting: boolean; activeStage: number | null }) {
  return (
    <div className="space-y-2">
      <div className="flex flex-wrap items-center gap-2 text-[9px] font-semibold uppercase tracking-wider text-foreground-faint">
        <Workflow className="h-3 w-3 text-docker" /> How it works — RAG pipeline
        <span className="inline-flex items-center gap-1 rounded-md border border-emerald-400/40 bg-emerald-500/15 px-1.5 py-0.5 font-mono text-[8px] normal-case text-emerald-300">
          <span className="h-1.5 w-1.5 rounded-full bg-emerald-400" /> orchestrated by LangChain
        </span>
        <span className="rounded bg-docker/15 px-1.5 py-0.5 font-mono text-[8px] normal-case text-docker">ChromaDB · Docling · sentence-transformers</span>
        {activeStage != null && (
          <span className="ml-auto flex items-center gap-1 normal-case text-docker">
            <span className="h-1.5 w-1.5 animate-ping rounded-full bg-docker" /> {STAGE_LABEL[activeStage] || 'working'}…
          </span>
        )}
        {activeStage == null && ingesting && (
          <span className="ml-auto flex items-center gap-1 normal-case text-docker">
            <span className="h-1.5 w-1.5 animate-ping rounded-full bg-docker" /> ingesting…
          </span>
        )}
      </div>
      <FlowRow tag="Index" stages={INDEX_STAGES} flowing={ingesting} />
      <FlowRow tag="Query" stages={QUERY_STAGES} flowing={loading} activeIndex={activeStage} />
    </div>
  )
}

function FlowRow({ tag, stages, flowing, activeIndex }: { tag: string; stages: typeof INDEX_STAGES; flowing: boolean; activeIndex?: number | null }) {
  return (
    <div className="flex items-center gap-2">
      <span className="w-10 shrink-0 text-[8px] font-semibold uppercase text-foreground-faint">{tag}</span>
      <div className={cn('scrollbar-thin flex flex-1 items-center overflow-x-auto py-0.5', flowing && 'rag-flowing')}>
        {stages.map((s, i) => {
          const state: 'idle' | 'active' | 'done' =
            activeIndex == null ? 'idle' : i < activeIndex ? 'done' : i === activeIndex ? 'active' : 'idle'
          return (
            <Fragment key={`${tag}-${s.label}-${i}`}>
              <FlowNode icon={s.icon} label={s.label} sub={s.sub} color={s.color} lc={s.lc} state={state} />
              {i < stages.length - 1 && <FlowConnector color={s.color} delay={i * 0.18} />}
            </Fragment>
          )
        })}
      </div>
    </div>
  )
}

function FlowNode({ icon: Icon, label, sub, color, lc, state }: { icon: typeof Upload; label: string; sub: string; color: string; lc: boolean; state: 'idle' | 'active' | 'done' }) {
  const cls = state === 'active' ? 'rag-node-active' : state === 'done' ? 'rag-node-done' : 'rag-node'
  const glow = state === 'active' ? `${color}cc` : `${color}55`
  return (
    <div
      className={cn('relative flex min-w-[64px] shrink-0 flex-col items-center gap-0.5 rounded-lg border border-border bg-surface-raised px-2 py-1.5 text-center transition-colors', cls)}
      style={{ '--rag-glow': glow, '--rag-line': color } as React.CSSProperties}
    >
      {lc && (
        <span className="absolute -right-1.5 -top-1.5 rounded bg-emerald-500/90 px-1 text-[7px] font-bold leading-none text-white shadow" title="LangChain component">LC</span>
      )}
      <Icon className="h-4 w-4" style={{ color }} />
      <span className="text-[9px] font-medium leading-tight text-foreground">{label}</span>
      <span className="text-[8px] leading-tight text-foreground-faint">{sub}</span>
    </div>
  )
}

function FlowConnector({ color, delay }: { color: string; delay: number }) {
  return (
    <div className="relative mx-0.5 h-5 w-7 shrink-0">
      <div
        className="rag-track absolute top-1/2 h-[2px] w-full -translate-y-1/2 rounded"
        style={{ backgroundImage: `linear-gradient(90deg, ${color} 0 7px, transparent 7px 20px)`, backgroundSize: '20px 2px', opacity: 0.6 }}
      />
      <span
        className="rag-dot absolute top-1/2 h-1.5 w-1.5 -translate-y-1/2 rounded-full"
        style={{ background: color, boxShadow: `0 0 6px ${color}`, animationDelay: `${delay}s` }}
      />
    </div>
  )
}

function StatusPill({ ok, label }: { ok: boolean; label: string }) {
  return (
    <span className={cn('rounded-full px-2 py-0.5 font-medium', ok ? 'bg-success/20 text-success' : 'bg-danger/20 text-danger')}>
      {label} {ok ? '●' : '○'}
    </span>
  )
}
