'use client'

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import Link from 'next/link'
import { ChevronLeft, Star, Trash2, Pencil, Check, X, Plus, Search, Sparkles, Copy, Image as ImageIcon, Wand2, Mic, Square, ListTodo, Target } from 'lucide-react'
import MiniSearch from 'minisearch'
import { get as idbGet, set as idbSet } from 'idb-keyval'
import { AttachmentRow, uploadAttachments, deleteAttachment, type Att } from './attachments'

interface Thought { id: string; body: string; tags: string[]; topic: string | null; duplicateOf: string | null; pinned: boolean; createdAt: string; attachments: Att[] }
interface ThoughtsCache { v: string; thoughts: Thought[]; summaries: Record<string, string | null> }

const CACHE_KEY = 'thoughts-cache-v3' // v3: thoughts carry attachments (any file type)

export function ThoughtsList() {
  const [thoughts, setThoughts] = useState<Thought[]>([])
  const [summaries, setSummaries] = useState<Record<string, string | null>>({})
  const [loading, setLoading] = useState(true)
  const [q, setQ] = useState('')
  const [editing, setEditing] = useState<string | null>(null)
  const [draft, setDraft] = useState('')
  const [newBody, setNewBody] = useState('')
  const [organizing, setOrganizing] = useState(false)
  const [staged, setStaged] = useState<{ file: File; url: string }[]>([])
  const fileInputRef = useRef<HTMLInputElement>(null)
  // Convert a misrouted thought into a reminder or focus item.
  const [converting, setConverting] = useState<string | null>(null)
  // "AI consideration": voice/text addendum the agent synthesizes into a thought.
  const [considering, setConsidering] = useState<string | null>(null)
  const [considerText, setConsiderText] = useState('')
  const [recState, setRecState] = useState<'idle' | 'starting' | 'recording'>('idle')
  const [considerAudio, setConsiderAudio] = useState<Blob | null>(null)
  const [synthesizing, setSynthesizing] = useState<Set<string>>(new Set())
  const recRef = useRef<MediaRecorder | null>(null)
  const chunksRef = useRef<Blob[]>([])
  const mimeRef = useRef('audio/webm')

  // Render from the IndexedDB cache instantly, then a ~100-byte version check
  // decides whether the multi-MB payload needs re-downloading at all.
  const load = useCallback(async () => {
    const cached = await idbGet<ThoughtsCache>(CACHE_KEY).catch(() => undefined)
    if (cached?.thoughts) {
      setThoughts(cached.thoughts); setSummaries(cached.summaries || {}); setLoading(false)
      const vres = await fetch('/api/thoughts/version', { cache: 'no-store' }).catch(() => null)
      if (vres?.ok && (await vres.json()).v === cached.v) return // cache is current
    }
    const res = await fetch('/api/thoughts', { cache: 'no-store' })
    if (res.ok) {
      const d = await res.json()
      setThoughts(d.thoughts || []); setSummaries(d.topicSummaries || {})
      idbSet(CACHE_KEY, { v: d.v, thoughts: d.thoughts || [], summaries: d.topicSummaries || {} }).catch(() => {})
    }
    setLoading(false)
  }, [])
  useEffect(() => { load() }, [load])

  // Mutations keep the cache warm but mark it stale ('dirty' never matches a
  // server version), so the next visit shows it instantly and refetches behind it.
  const cacheLocal = (next: Thought[]) => {
    idbSet(CACHE_KEY, { v: 'dirty', thoughts: next, summaries }).catch(() => {})
  }

  // Omnisearch-style full-text index: first line acts as the title (boosted),
  // fuzzy handles typos, prefix makes it match as you type.
  const index = useMemo(() => {
    const ms = new MiniSearch<Thought>({
      fields: ['title', 'body', 'tags'],
      extractField: (t, field) => {
        if (field === 'title') return t.body.slice(0, 200).split('\n', 1)[0]
        if (field === 'tags') return t.tags.join(' ')
        return String(t[field as keyof Thought] ?? '')
      },
    })
    ms.addAll(thoughts)
    return ms
  }, [thoughts])

  // Ranked results while searching (null when the query is empty → grouped view).
  const results = useMemo(() => {
    const query = q.trim()
    if (!query) return null
    const byId = new Map(thoughts.map(t => [t.id, t]))
    return index.search(query, { prefix: true, fuzzy: 0.2, boost: { title: 3, tags: 2 } })
      .map(h => byId.get(h.id as string))
      .filter((t): t is Thought => !!t)
      .slice(0, 50)
  }, [q, index, thoughts])

  // ── images ──────────────────────────────────────────────────────────────
  const uploadImages = async (id: string, files: File[]) => {
    const atts = await uploadAttachments('thought', id, files)
    if (atts.length === 0) return []
    const next = thoughts.map(t => t.id === id ? { ...t, attachments: [...(t.attachments || []), ...atts] } : t)
    setThoughts(next); cacheLocal(next)
    return atts
  }
  const delImage = async (thoughtId: string, attId: string) => {
    const next = thoughts.map(t => t.id === thoughtId ? { ...t, attachments: (t.attachments || []).filter(a => a.id !== attId) } : t)
    setThoughts(next); cacheLocal(next)
    await deleteAttachment(attId)
  }
  const stageFiles = (files: File[]) => {
    if (files.length === 0) return
    setStaged(prev => [...prev, ...files.map(f => ({ file: f, url: URL.createObjectURL(f) }))])
  }
  const removeStaged = (url: string) => {
    URL.revokeObjectURL(url)
    setStaged(prev => prev.filter(s => s.url !== url))
  }
  // Pasted images (⌘V) attach to the thought being edited, or stage for the
  // composer otherwise. Document-level, same idiom as the capture page.
  useEffect(() => {
    const onPaste = (e: ClipboardEvent) => {
      const files = Array.from(e.clipboardData?.items || [])
        .filter(item => item.kind === 'file' && item.type.startsWith('image/'))
        .map(item => item.getAsFile())
        .filter((f): f is File => f !== null)
      if (files.length === 0) return
      e.preventDefault()
      if (editing) uploadImages(editing, files)
      else stageFiles(files)
    }
    document.addEventListener('paste', onPaste)
    return () => document.removeEventListener('paste', onPaste)
  })
  const onPickImages = (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = Array.from(e.target.files || [])
    e.target.value = ''
    if (editing) uploadImages(editing, files)
    else stageFiles(files)
  }

  // ── AI consideration (voice/text → agent synthesizes into the thought) ──
  const MIME_PREFS = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4', 'audio/ogg;codecs=opus']
  const startRec = async () => {
    // Paint immediate feedback before awaiting the (slow) permission/init.
    setRecState('starting')
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
      const mime = MIME_PREFS.find(m => MediaRecorder.isTypeSupported(m)) || ''
      mimeRef.current = mime || 'audio/webm'
      const rec = new MediaRecorder(stream, mime ? { mimeType: mime } : undefined)
      chunksRef.current = []
      rec.ondataavailable = e => { if (e.data.size > 0) chunksRef.current.push(e.data) }
      rec.onstop = () => {
        stream.getTracks().forEach(tr => tr.stop())
        setConsiderAudio(new Blob(chunksRef.current, { type: mimeRef.current }))
      }
      rec.start()
      recRef.current = rec
      setRecState('recording')
    } catch {
      setRecState('idle') // mic denied — typing still works
    }
  }
  const stopRec = () => { recRef.current?.stop(); setRecState('idle') }
  const closeConsider = () => {
    if (recState === 'recording') stopRec()
    setConsidering(null); setConsiderText(''); setConsiderAudio(null)
  }
  const submitConsideration = async (id: string) => {
    // Still recording? Stop and take the finished blob in the same tap.
    let audio = considerAudio
    if (recState === 'recording' && recRef.current) {
      const rec = recRef.current
      audio = await new Promise<Blob>(resolve => {
        rec.addEventListener('stop', () => resolve(new Blob(chunksRef.current, { type: mimeRef.current })), { once: true })
        rec.stop()
      })
      setRecState('idle')
    }
    const text = considerText.trim()
    if (!text && !audio) return
    const fd = new FormData()
    fd.append('refineThoughtId', id)
    if (text) fd.append('text', text)
    if (audio) fd.append('audio', audio, 'consideration.webm')
    closeConsider()
    const res = await fetch('/api/captures', { method: 'POST', body: fd })
    if (!res.ok) return
    setSynthesizing(s => new Set(s).add(id))
    // The Mac agent transcribes + rewrites, then the version check pulls the
    // new body in. Check twice in case transcription runs long.
    const recheck = (ms: number, last: boolean) => setTimeout(async () => {
      await load()
      if (last) setSynthesizing(s => { const n = new Set(s); n.delete(id); return n })
    }, ms)
    recheck(15000, false); recheck(35000, true)
  }

  // ── convert misrouted thought → reminder / focus item ────────────────────
  // Archives the thought (recoverable in the DB) instead of deleting it.
  const convertThought = async (t: Thought, dest: 'reminder' | 'focus') => {
    const title = t.body.replace(/\s+/g, ' ').trim()
    if (!confirm(`${dest === 'reminder' ? 'Make this a reminder due today' : 'Add this to Focus'}? The thought will be archived.\n\n“${title.slice(0, 120)}”`)) return
    setConverting(null)
    if (dest === 'reminder') {
      // Same due-today default as the Reminders quick-add (next full hour).
      const d = new Date()
      d.setMinutes(0, 0, 0); d.setHours(d.getHours() + 1)
      const res = await fetch('/api/reminders', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(title.length > 200
          ? { title: title.slice(0, 200), notes: t.body, dueDate: d.toISOString() }
          : { title, dueDate: d.toISOString() }),
      })
      if (!res.ok) return
    } else {
      const res = await fetch('/api/focus', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title: title.slice(0, 200) }),
      })
      if (!res.ok) return
    }
    const next = thoughts.filter(x => x.id !== t.id)
    setThoughts(next); cacheLocal(next)
    await fetch(`/api/thoughts/${t.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ archived: true }) })
  }

  // Mutations update local state directly — no multi-MB refetch per edit.
  const patch = async (id: string, data: Partial<Thought>) => {
    const next = thoughts.map(t => t.id === id ? { ...t, ...data } : t)
    setThoughts(next); cacheLocal(next)
    await fetch(`/api/thoughts/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) })
  }
  const del = async (id: string) => {
    if (!confirm('Delete this thought?')) return
    const next = thoughts.filter(t => t.id !== id)
    setThoughts(next); cacheLocal(next)
    await fetch(`/api/thoughts/${id}`, { method: 'DELETE' })
  }
  const add = async () => {
    const body = newBody.trim()
    if (!body) return
    setNewBody('')
    const res = await fetch('/api/thoughts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ body }) })
    if (res.ok) {
      const { id } = await res.json()
      let attachments: Att[] = []
      if (staged.length > 0) {
        attachments = await uploadAttachments('thought', id, staged.map(x => x.file))
        staged.forEach(s => URL.revokeObjectURL(s.url))
        setStaged([])
      }
      const next: Thought[] = [{ id, body, tags: [], topic: null, duplicateOf: null, pinned: false, createdAt: new Date().toISOString(), attachments }, ...thoughts]
      setThoughts(next); cacheLocal(next)
    }
  }
  const organize = async () => {
    setOrganizing(true)
    await fetch('/api/organize', { method: 'POST' })
    // Give the Mac time to run Claude, then reload to show topics + synthesis.
    setTimeout(() => { load(); setOrganizing(false) }, 12000)
  }

  // Group by topic (null topic → "Other"), topics alphabetical, Other last.
  // Local pin/edit keeps order honest via the sort.
  const sorted = [...thoughts].sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.createdAt.localeCompare(a.createdAt))
  const groups = new Map<string, Thought[]>()
  for (const t of sorted) {
    const key = t.topic || '￿Other'
    if (!groups.has(key)) groups.set(key, [])
    groups.get(key)!.push(t)
  }
  const orderedKeys = [...groups.keys()].sort()

  const card = (t: Thought) => (
    <li key={t.id} className="rounded-xl border border-border bg-card p-3">
      {editing === t.id ? (
        <div className="space-y-2">
          <textarea value={draft} onChange={e => setDraft(e.target.value)} rows={3}
            className="w-full rounded-lg border border-input bg-background px-3 py-2 text-base resize-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" />
          <AttachmentRow atts={t.attachments || []} onDelete={id => delImage(t.id, id)} />
          <div className="flex gap-2 items-center">
            <button onClick={() => fileInputRef.current?.click()} className="inline-flex items-center gap-1 text-sm text-muted-foreground px-2 h-8 rounded hover:bg-muted"><ImageIcon className="h-4 w-4" /> Add image</button>
            <div className="ml-auto flex gap-2">
              <button onClick={() => setEditing(null)} className="inline-flex items-center gap-1 text-sm text-muted-foreground px-2 h-8"><X className="h-4 w-4" /> Cancel</button>
              <button onClick={() => { patch(t.id, { body: draft }); setEditing(null) }} className="inline-flex items-center gap-1 text-sm rounded-lg bg-primary text-primary-foreground px-3 h-8"><Check className="h-4 w-4" /> Save</button>
            </div>
          </div>
        </div>
      ) : (
        // Clicking the tile opens the editor; tag chips and the action row
        // stop propagation so their own clicks don't. A drag that selected
        // text isn't an edit intent — leave the selection alone.
        <div onClick={() => { if (window.getSelection()?.toString()) return; setEditing(t.id); setDraft(t.body) }} className="cursor-pointer">
          {t.duplicateOf && (
            <div className="text-xs text-amber-600 inline-flex items-center gap-1 mb-1"><Copy className="h-3 w-3" /> possible duplicate</div>
          )}
          {synthesizing.has(t.id) && (
            <div className="text-xs text-primary inline-flex items-center gap-1 mb-1"><Sparkles className="h-3 w-3 animate-pulse" /> synthesizing…</div>
          )}
          <p className="text-sm whitespace-pre-wrap">{t.body}</p>
          <AttachmentRow atts={t.attachments || []} className="mt-1.5" />
          {t.tags.length > 0 && (
            <div className="flex flex-wrap gap-1 mt-1.5" onClick={e => e.stopPropagation()}>
              {t.tags.map(tag => (
                <button
                  key={tag}
                  onClick={() => setQ(tag)}
                  className={`text-xs rounded-full px-2 py-0.5 transition-colors ${q === tag ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground hover:bg-muted-foreground/20'}`}
                  title={`Filter by ${tag}`}
                >
                  {tag}
                </button>
              ))}
            </div>
          )}
          <div className="flex items-center gap-1 mt-2 text-muted-foreground" onClick={e => e.stopPropagation()}>
            <button onClick={() => patch(t.id, { pinned: !t.pinned })} className={`p-1.5 rounded hover:bg-muted ${t.pinned ? 'text-amber-500' : ''}`} aria-label="Pin"><Star className="h-4 w-4" fill={t.pinned ? 'currentColor' : 'none'} /></button>
            <button onClick={() => { setEditing(t.id); setDraft(t.body) }} className="p-1.5 rounded hover:bg-muted" aria-label="Edit"><Pencil className="h-4 w-4" /></button>
            <button onClick={() => { setConsidering(considering === t.id ? null : t.id); setConsiderText(''); setConsiderAudio(null) }} className={`p-1.5 rounded hover:bg-muted ${considering === t.id ? 'text-primary' : ''}`} aria-label="AI consideration" title="Add a consideration — AI folds it into this thought"><Wand2 className="h-4 w-4" /></button>
            <button onClick={() => setConverting(converting === t.id ? null : t.id)} className={`p-1.5 rounded hover:bg-muted ${converting === t.id ? 'text-primary' : ''}`} aria-label="Convert to todo" title="This should've been a todo — convert it"><ListTodo className="h-4 w-4" /></button>
            <button onClick={() => del(t.id)} className="p-1.5 rounded hover:bg-muted hover:text-destructive" aria-label="Delete"><Trash2 className="h-4 w-4" /></button>
            <span className="ml-auto text-xs">{new Date(t.createdAt).toLocaleDateString()}</span>
          </div>
          {converting === t.id && (
            <div className="mt-2 rounded-lg border border-border bg-background p-2 space-y-1.5" onClick={e => e.stopPropagation()}>
              <div className="text-xs text-muted-foreground px-0.5">Convert this thought into a todo (archives the thought):</div>
              <div className="flex gap-2">
                <button onClick={() => convertThought(t, 'reminder')} className="flex-1 inline-flex items-center justify-center gap-1.5 text-sm rounded-lg border border-border px-2.5 h-9 hover:bg-muted">
                  <ListTodo className="h-4 w-4" /> Reminder (due today)
                </button>
                <button onClick={() => convertThought(t, 'focus')} className="flex-1 inline-flex items-center justify-center gap-1.5 text-sm rounded-lg border border-border px-2.5 h-9 hover:bg-muted">
                  <Target className="h-4 w-4" /> Focus item
                </button>
              </div>
            </div>
          )}
          {considering === t.id && (
            <div className="mt-2 space-y-2 rounded-lg border border-border bg-background p-2" onClick={e => e.stopPropagation()}>
              <textarea
                value={considerText}
                onChange={e => setConsiderText(e.target.value)}
                rows={2}
                autoFocus
                placeholder="New consideration — the AI will fold it into this thought…"
                className="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm resize-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
              />
              <div className="flex items-center gap-2">
                <button
                  onClick={recState === 'recording' ? stopRec : startRec}
                  disabled={recState === 'starting'}
                  className={`inline-flex items-center gap-1.5 text-sm rounded-lg border px-2.5 h-8 ${
                    recState === 'recording' ? 'border-destructive text-destructive animate-pulse'
                    : recState === 'starting' ? 'border-primary text-primary animate-pulse'
                    : 'border-border text-muted-foreground hover:bg-muted'}`}
                >
                  {recState === 'recording' ? <><Square className="h-3.5 w-3.5" /> Stop</>
                    : recState === 'starting' ? <><Mic className="h-3.5 w-3.5" /> Starting…</>
                    : <><Mic className="h-3.5 w-3.5" /> Record</>}
                </button>
                {considerAudio && recState === 'idle' && (
                  <span className="text-xs text-muted-foreground inline-flex items-center gap-1">
                    <Mic className="h-3 w-3" /> voice note ready
                    <button onClick={() => setConsiderAudio(null)} aria-label="Discard recording"><X className="h-3 w-3" /></button>
                  </span>
                )}
                <div className="ml-auto flex gap-2">
                  <button onClick={closeConsider} className="inline-flex items-center gap-1 text-sm text-muted-foreground px-2 h-8"><X className="h-4 w-4" /> Cancel</button>
                  <button onClick={() => submitConsideration(t.id)} className="inline-flex items-center gap-1 text-sm rounded-lg bg-primary text-primary-foreground px-3 h-8"><Sparkles className="h-4 w-4" /> Synthesize</button>
                </div>
              </div>
            </div>
          )}
        </div>
      )}
    </li>
  )

  return (
    <div className="min-h-screen max-w-md mx-auto flex flex-col">
      <header className="safe-top px-4 pb-3 flex items-center gap-2">
        <Link href="/" className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
          <ChevronLeft className="h-4 w-4" /> Dashboard
        </Link>
        <span className="ml-auto font-semibold mr-2">Thoughts</span>
        <button onClick={organize} disabled={organizing} className="inline-flex items-center gap-1.5 text-sm rounded-lg border border-border px-2.5 h-8 hover:bg-muted disabled:opacity-50">
          <Sparkles className={`h-4 w-4 ${organizing ? 'animate-pulse' : ''}`} /> {organizing ? 'Organizing…' : 'Organize'}
        </button>
      </header>

      <main className="flex-1 px-4 pb-10 space-y-3">
        <div className="flex gap-2">
          <input value={newBody} onChange={e => setNewBody(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') add() }}
            placeholder="New thought…" className="flex-1 rounded-lg border border-input bg-background px-3 h-10 text-base focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" />
          <button onClick={() => fileInputRef.current?.click()} className="rounded-lg border border-border px-3 h-10 inline-flex items-center text-muted-foreground hover:bg-muted" aria-label="Add image"><ImageIcon className="h-4 w-4" /></button>
          <button onClick={add} className="rounded-lg bg-primary text-primary-foreground px-3 h-10 inline-flex items-center"><Plus className="h-4 w-4" /></button>
        </div>
        <input ref={fileInputRef} type="file" multiple onChange={onPickImages} className="hidden" />
        {staged.length > 0 && (
          <div className="flex flex-wrap gap-1.5">
            {staged.map(s => (
              <div key={s.url} className="relative">
                {s.file.type.startsWith('image/') ? (
                  // eslint-disable-next-line @next/next/no-img-element
                  <img src={s.url} alt="" className="w-14 h-14 rounded-md object-cover border border-border" />
                ) : (
                  <span className="inline-flex items-center text-xs rounded-lg border border-border bg-background px-2 py-1.5 text-muted-foreground max-w-44 truncate">{s.file.name}</span>
                )}
                <button onClick={() => removeStaged(s.url)} className="absolute -top-1.5 -right-1.5 bg-background border border-border rounded-full p-0.5" aria-label="Remove attachment"><X className="h-3 w-3" /></button>
              </div>
            ))}
          </div>
        )}
        <div className="relative">
          <Search className="h-4 w-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="Search thoughts & labels…"
            className="w-full rounded-lg border border-input bg-background pl-9 pr-3 h-10 text-base focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" />
        </div>

        {loading && <p className="text-sm text-muted-foreground text-center py-6">Loading…</p>}
        {!loading && thoughts.length === 0 && <p className="text-sm text-muted-foreground text-center py-6">No thoughts yet.</p>}

        {results ? (
          // Searching: flat list ranked by relevance (best match first).
          <section className="space-y-2">
            <div className="px-1 pt-1 text-sm font-semibold flex items-center gap-2">
              Results
              <span className="text-xs rounded-full bg-muted px-2 py-0.5 text-muted-foreground font-normal">{results.length}</span>
            </div>
            {results.length === 0 && <p className="text-sm text-muted-foreground text-center py-6">No thoughts match.</p>}
            <ul className="space-y-2">{results.map(card)}</ul>
          </section>
        ) : (
          orderedKeys.map(key => {
            const topic = key === '￿Other' ? null : key
            const list = groups.get(key)!
            const summary = topic ? summaries[topic] : null
            return (
              <section key={key} className="space-y-2">
                <div className="px-1 pt-1">
                  <div className="text-sm font-semibold flex items-center gap-2">
                    {topic || 'Other'}
                    <span className="text-xs rounded-full bg-muted px-2 py-0.5 text-muted-foreground font-normal">{list.length}</span>
                  </div>
                  {summary && (
                    <div className="mt-1 text-xs text-muted-foreground flex items-start gap-1.5">
                      <Sparkles className="h-3.5 w-3.5 mt-0.5 shrink-0 text-primary" />
                      <span className="italic">{summary}</span>
                    </div>
                  )}
                </div>
                <ul className="space-y-2">{list.map(card)}</ul>
              </section>
            )
          })
        )}
      </main>
    </div>
  )
}
