import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react"
import {
  Archive,
  ArrowRight,
  BookmarkPlus,
  ChevronDown,
  Command as CommandIcon,
  Copy,
  Download,
  FileText,
  Loader2,
  Mic,
  MicOff,
  Plus,
  RefreshCw,
  RotateCcw,
  Square,
  Trash2,
  Undo2,
  X,
} from "lucide-react"

import {
  LLMProvider,
  MockLLM,
  OpenAICompatibleAdapter,
  listModels,
  useChat,
  useCompletion,
  type LLMAdapter,
} from "@crema/llm-ui"
import { TypingIndicator } from "@crema/chat-ui"

import { AppShell } from "~/components/layout/app-shell"
import { MessageBody } from "~/components/assistant/message-body"
import { Button } from "~/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "~/components/ui/popover"
import { useLLMSettings } from "~/lib/llm-settings"
import {
  composeSystemPrompt,
  loadActiveAgentId,
  saveActiveAgentId,
  useAgents,
  type Agent,
} from "~/lib/agents"
import { addLibraryItem } from "~/lib/library"
import { Avatar, AvatarFallback } from "~/components/ui/avatar"
import { pageTitle } from "~/lib/page-meta"

const SNAPSHOT_KEY = "crema.ai.snapshot"
type StoredMessage = { role: "user" | "assistant"; content: string }
function loadAISnapshot(): StoredMessage[] | null {
  if (typeof window === "undefined") return null
  try {
    const raw = localStorage.getItem(SNAPSHOT_KEY)
    if (!raw) return null
    const parsed = JSON.parse(raw)
    if (Array.isArray(parsed)) return parsed as StoredMessage[]
  } catch {}
  return null
}
function saveAISnapshot(msgs: StoredMessage[]) {
  if (typeof window === "undefined") return
  localStorage.setItem(SNAPSHOT_KEY, JSON.stringify(msgs))
}
function clearAISnapshot() {
  if (typeof window === "undefined") return
  localStorage.removeItem(SNAPSHOT_KEY)
}

export const meta = () => pageTitle("AI")

const MODEL_KEY = "crema.ai.model"
const PROBE_TIMEOUT_MS = 3000

type Status =
  | { kind: "probing" }
  | { kind: "live"; models: string[] }
  | { kind: "mock"; reason: string }

const mockAdapter = new MockLLM({
  label: "Mock",
  delayMs: 18,
  fallback:
    "I'm a stand-in for the local model. Start LM Studio at localhost:1234 and reload to swap me out.",
  responses: [],
})

function withTimeout<T>(p: Promise<T>, ms: number, signal: AbortSignal) {
  return new Promise<T>((resolve, reject) => {
    const t = setTimeout(() => reject(new Error("timeout")), ms)
    signal.addEventListener("abort", () => {
      clearTimeout(t)
      reject(new DOMException("Aborted", "AbortError"))
    })
    p.then(
      (v) => {
        clearTimeout(t)
        resolve(v)
      },
      (e) => {
        clearTimeout(t)
        reject(e)
      },
    )
  })
}

export default function AIRoute() {
  const settings = useLLMSettings()
  const agents = useAgents()
  const [status, setStatus] = useState<Status>({ kind: "probing" })
  const [model, setModel] = useState<string>(() => {
    if (typeof window === "undefined") return ""
    return localStorage.getItem(MODEL_KEY) ?? ""
  })
  const [activeAgentId, setActiveAgentIdState] = useState<string>(() =>
    loadActiveAgentId(),
  )
  const setActiveAgentId = useCallback((id: string) => {
    saveActiveAgentId(id)
    setActiveAgentIdState(id)
  }, [])
  const activeAgent =
    agents.find((a) => a.id === activeAgentId) ?? agents[0]

  const probe = useCallback(() => {
    const ac = new AbortController()
    setStatus({ kind: "probing" })
    withTimeout(
      listModels({ baseURL: settings.baseURL, signal: ac.signal }),
      PROBE_TIMEOUT_MS,
      ac.signal,
    )
      .then((rows) => {
        const ids = rows.map((m) => m.id)
        if (ids.length === 0) {
          setStatus({ kind: "mock", reason: "endpoint returned no models" })
          return
        }
        setStatus({ kind: "live", models: ids })
        setModel((cur) => (cur && ids.includes(cur) ? cur : ids[0]))
      })
      .catch(() => {
        setStatus({ kind: "mock", reason: "endpoint unreachable" })
      })
    return () => ac.abort()
  }, [settings.baseURL])

  useEffect(() => probe(), [probe])

  useEffect(() => {
    if (model) localStorage.setItem(MODEL_KEY, model)
  }, [model])

  const adapter: LLMAdapter = useMemo(() => {
    if (status.kind === "live") {
      return new OpenAICompatibleAdapter({
        baseURL: settings.baseURL,
        apiKey: settings.apiKey || "lm-studio",
      })
    }
    return mockAdapter
  }, [status.kind, settings.baseURL, settings.apiKey])

  const activeModel =
    status.kind === "live" ? model || status.models[0] : "mock"

  const availableModels = status.kind === "live" ? status.models : ["mock"]

  return (
    <AppShell title="AI">
      <LLMProvider adapter={adapter} model={activeModel}>
        <ChatSurface
          models={availableModels}
          model={activeModel}
          onModelChange={setModel}
          agents={agents}
          activeAgent={activeAgent}
          onAgentChange={setActiveAgentId}
          isMock={status.kind === "mock"}
          onRetryProbe={probe}
        />
      </LLMProvider>
    </AppShell>
  )
}

function ChatSurface({
  models,
  model,
  onModelChange,
  agents,
  activeAgent,
  onAgentChange,
  isMock,
  onRetryProbe,
}: {
  models: string[]
  model: string
  onModelChange: (m: string) => void
  agents: Agent[]
  activeAgent: Agent | undefined
  onAgentChange: (id: string) => void
  isMock: boolean
  onRetryProbe: () => void
}) {
  const baseSystem =
    "You are a helpful AI assistant. Be concise, warm, and direct."
  const systemPrompt = composeSystemPrompt(baseSystem, activeAgent)
  const { messages, setMessages, send, abort, isStreaming, reset } = useChat({
    system: systemPrompt,
  })
  const { complete: completeOneShot, isLoading: compacting } = useCompletion()
  const [input, setInput] = useState("")
  const [showPromptOpen, setShowPromptOpen] = useState(false)
  const [hasCompactSnapshot, setHasCompactSnapshot] = useState(
    () => !!loadAISnapshot(),
  )

  const hasAssistantReply = messages.some((m) => m.role === "assistant")

  const buildTranscript = useCallback(() => {
    const lines: string[] = [
      `# Conversation`,
      "",
      activeAgent
        ? `**Persona:** ${activeAgent.name} — ${activeAgent.role}`
        : "",
      `**Date:** ${new Date().toISOString()}`,
      "",
    ].filter(Boolean)
    for (const m of messages) {
      lines.push(`### ${m.role === "user" ? "User" : "Assistant"}`)
      lines.push("")
      lines.push(m.content.trim())
      lines.push("")
    }
    return lines.join("\n")
  }, [messages, activeAgent])

  const copyMarkdown = useCallback(async () => {
    if (messages.length === 0) return
    try {
      await navigator.clipboard.writeText(buildTranscript())
    } catch {}
  }, [buildTranscript, messages.length])

  const exportMarkdown = useCallback(() => {
    if (messages.length === 0) return
    const md = buildTranscript()
    const blob = new Blob([md], { type: "text/markdown;charset=utf-8" })
    const url = URL.createObjectURL(blob)
    const a = document.createElement("a")
    a.href = url
    const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)
    a.download = `ai-${stamp}.md`
    a.click()
    URL.revokeObjectURL(url)
  }, [buildTranscript, messages.length])

  const saveToLibrary = useCallback(() => {
    if (messages.length === 0) return
    const md = buildTranscript()
    addLibraryItem({
      kind: "conversation",
      title:
        messages[0]?.content.slice(0, 60).replace(/\s+/g, " ").trim() ||
        "AI conversation",
      content: md,
      tags: activeAgent ? [activeAgent.role.toLowerCase()] : [],
      agentName: activeAgent?.name,
      agentRole: activeAgent?.role,
      messageCount: messages.length,
    })
  }, [buildTranscript, messages, activeAgent])

  const regenerateLast = useCallback(() => {
    if (isStreaming) return
    let lastUserIdx = -1
    for (let i = messages.length - 1; i >= 0; i--) {
      if (messages[i].role === "user") {
        lastUserIdx = i
        break
      }
    }
    if (lastUserIdx === -1) return
    const text = messages[lastUserIdx].content
    setMessages(messages.slice(0, lastUserIdx))
    // Defer so the state flush completes before send() reads `messages`.
    setTimeout(() => void send(text), 0)
  }, [messages, setMessages, send, isStreaming])

  const continueLast = useCallback(() => {
    if (isStreaming || messages.length === 0) return
    void send("Please continue your previous reply.")
  }, [isStreaming, messages.length, send])

  const compactConversation = useCallback(async () => {
    if (compacting || isStreaming || messages.length < 2) return
    const transcript = messages
      .map(
        (m) =>
          `${m.role === "user" ? "User" : "Assistant"}: ${m.content.trim()}`,
      )
      .join("\n\n")
    const summarySystem =
      "You compress conversations. Output a tight 1–2 paragraph summary that preserves: user goals, key facts, names, decisions, file paths, code snippets, and unfinished tasks. Use third person ('the user wants X'). No commentary, no preamble, no markdown headings."
    try {
      const summary = await completeOneShot(
        [
          { role: "system", content: summarySystem },
          {
            role: "user",
            content: `Summarize this conversation:\n\n${transcript}`,
          },
        ],
        { maxTokens: 800 },
      )
      // Snapshot first so Restore can undo.
      saveAISnapshot(messages as StoredMessage[])
      setHasCompactSnapshot(true)
      setMessages([
        {
          role: "assistant",
          content: `📋 **Conversation summary** (older turns compacted)\n\n${summary.trim()}`,
        },
      ])
    } catch {}
  }, [compacting, isStreaming, messages, completeOneShot, setMessages])

  const restoreCompact = useCallback(() => {
    const snap = loadAISnapshot()
    if (!snap) return
    setMessages(snap)
    clearAISnapshot()
    setHasCompactSnapshot(false)
  }, [setMessages])
  const endRef = useRef<HTMLDivElement | null>(null)
  const composerRef = useRef<HTMLDivElement | null>(null)
  const lastContent = messages.at(-1)?.content ?? ""

  // Track the composer's actual height so the auto-scroll sentinel can
  // keep the latest text ~24px above its top edge regardless of how many
  // lines the textarea has grown to.
  const [composerHeight, setComposerHeight] = useState(160)
  useEffect(() => {
    const el = composerRef.current
    if (!el || typeof ResizeObserver === "undefined") return
    const ro = new ResizeObserver(([entry]) => {
      if (entry) setComposerHeight(entry.contentRect.height)
    })
    ro.observe(el)
    return () => ro.disconnect()
  }, [])

  // Auto-stick to the bottom only when the user is already near it. If they
  // scroll up to read earlier turns mid-stream, don't yank them back down.
  // The ref dodges React render cycles so scroll events feel instant.
  const stickRef = useRef(true)
  useEffect(() => {
    const onScroll = () => {
      const distFromBottom =
        document.documentElement.scrollHeight -
        (window.scrollY + window.innerHeight)
      stickRef.current = distFromBottom < 120
    }
    window.addEventListener("scroll", onScroll, { passive: true })
    onScroll()
    return () => window.removeEventListener("scroll", onScroll)
  }, [])

  useEffect(() => {
    if (!stickRef.current) return
    endRef.current?.scrollIntoView({ block: "end" })
  }, [messages.length, lastContent, isStreaming])

  const submit = useCallback(() => {
    const text = input.trim()
    if (!text || isStreaming) return
    setInput("")
    stickRef.current = true
    void send(text)
  }, [input, isStreaming, send])

  const isEmpty = messages.length === 0

  return (
    <div className="relative -mb-6 flex h-full min-h-0 flex-col">
      {/* Greeting — only when empty, fades out as composer slides down */}
      <div
        aria-hidden={!isEmpty}
        className="pointer-events-none absolute inset-x-0 top-[28%] -translate-y-1/2 px-6 text-center transition-opacity duration-300"
        style={{ opacity: isEmpty ? 1 : 0 }}
      >
        <h1
          className="text-4xl font-semibold tracking-tight text-foreground"
          style={{ fontFamily: "var(--font-heading)" }}
        >
          How can I help you today?
        </h1>
      </div>

      {/* Messages — rendered when there are any. In empty state a flex-grow
       * spacer takes its place so the sticky-bottom composer lands at the
       * actual bottom of the surface (otherwise it'd sit at the top with
       * nothing above it, and the lift transform would push it off-screen). */}
      {isEmpty ? (
        <div className="flex-1" aria-hidden="true" />
      ) : (
        <div className="flex-1 px-4 py-6 sm:px-6">
          <div className="mx-auto flex w-full max-w-3xl flex-col gap-6">
            {messages.map((m, i) => (
              <MessageRow key={i} role={m.role} content={m.content} />
            ))}
            {isStreaming && messages.at(-1)?.role !== "assistant" && (
              <div className="self-start">
                <TypingIndicator />
              </div>
            )}
            <div
              ref={endRef}
              aria-hidden="true"
              style={{ scrollMarginBottom: `${composerHeight + 24}px` }}
            />
          </div>
        </div>
      )}

      {/* Composer — single persistent mount; transform lifts it to viewport
       * center when empty, then springs to sticky-bottom on the first message. */}
      <div
        ref={composerRef}
        className="sticky bottom-0 z-20 px-4 pb-3 pt-3 sm:px-6"
        style={{
          transform: isEmpty
            ? "translateY(calc(-50dvh + 50% + 4rem))"
            : "translateY(0)",
        }}
      >
        <div className="mx-auto w-full max-w-3xl">
          <Composer
            value={input}
            onChange={setInput}
            onSubmit={submit}
            onAbort={abort}
            isStreaming={isStreaming}
            models={models}
            model={model}
            onModelChange={onModelChange}
            agents={agents}
            activeAgent={activeAgent}
            onAgentChange={onAgentChange}
            onRegenerate={regenerateLast}
            onContinue={continueLast}
            onCompact={() => void compactConversation()}
            onRestoreCompact={restoreCompact}
            onCopyMarkdown={() => void copyMarkdown()}
            onExportMarkdown={exportMarkdown}
            onSaveToLibrary={saveToLibrary}
            onShowPrompt={() => setShowPromptOpen(true)}
            onRetryProbe={onRetryProbe}
            onClear={reset}
            hasMessages={messages.length > 0}
            hasUserMessage={messages.some((m) => m.role === "user")}
            hasCompactSnapshot={hasCompactSnapshot}
            isMock={isMock}
            isCompacting={compacting}
            placeholder={isEmpty ? "Ask anything…" : "Reply…"}
          />
          {showPromptOpen && (
            <SystemPromptDialog
              prompt={systemPrompt}
              onClose={() => setShowPromptOpen(false)}
            />
          )}
        </div>
      </div>
    </div>
  )
}

function MessageRow({
  role,
  content,
}: {
  role: "user" | "assistant"
  content: string
}) {
  if (role === "user") {
    return (
      <div className="self-end">
        <div
          className="max-w-[80ch] rounded-3xl px-5 py-3 shadow-sm"
          style={{
            background: "oklch(0.55 0.22 295)",
            color: "oklch(1 0 0)",
          }}
        >
          <div className="whitespace-pre-wrap leading-relaxed">{content}</div>
        </div>
      </div>
    )
  }
  return (
    <div className="self-start max-w-[80ch]">
      <MessageBody content={content} />
    </div>
  )
}

function Composer({
  value,
  onChange,
  onSubmit,
  onAbort,
  isStreaming,
  models,
  model,
  onModelChange,
  agents,
  activeAgent,
  onAgentChange,
  onRegenerate,
  onContinue,
  onCompact,
  onRestoreCompact,
  onCopyMarkdown,
  onExportMarkdown,
  onSaveToLibrary,
  onShowPrompt,
  onRetryProbe,
  onClear,
  hasMessages,
  hasUserMessage,
  hasCompactSnapshot,
  isMock,
  isCompacting,
  placeholder,
}: {
  value: string
  onChange: (v: string) => void
  onSubmit: () => void
  onAbort: () => void
  isStreaming: boolean
  models: string[]
  model: string
  onModelChange: (m: string) => void
  agents: Agent[]
  activeAgent: Agent | undefined
  onAgentChange: (id: string) => void
  onRegenerate: () => void
  onContinue: () => void
  onCompact: () => void
  onRestoreCompact: () => void
  onCopyMarkdown: () => void
  onExportMarkdown: () => void
  onSaveToLibrary: () => void
  onShowPrompt: () => void
  onRetryProbe: () => void
  onClear: () => void
  hasMessages: boolean
  hasUserMessage: boolean
  hasCompactSnapshot: boolean
  isMock: boolean
  isCompacting: boolean
  placeholder: string
}) {
  const taRef = useRef<HTMLTextAreaElement | null>(null)

  // Auto-grow the textarea.
  useEffect(() => {
    const el = taRef.current
    if (!el) return
    el.style.height = "auto"
    el.style.height = Math.min(el.scrollHeight, 240) + "px"
  }, [value])

  const onKey = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault()
      onSubmit()
    }
  }

  return (
    <div
      className="rounded-3xl border shadow-sm transition-shadow focus-within:shadow-md"
      style={{
        background: "var(--card)",
        borderColor: "var(--border)",
        backdropFilter: "blur(32px) saturate(180%)",
      }}
    >
      <div className="flex flex-col gap-3 px-5 pt-4 pb-3">
        <textarea
          ref={taRef}
          value={value}
          onChange={(e) => onChange(e.target.value)}
          onKeyDown={onKey}
          placeholder={placeholder}
          rows={2}
          data-action="ai-composer-input"
          className="min-h-[3.5rem] w-full resize-none bg-transparent text-base leading-relaxed outline-none placeholder:text-muted-foreground"
          style={{ fontFamily: "var(--font-sans)" }}
        />
        <div className="flex items-center justify-between gap-2">
          <div className="flex items-center gap-1">
            <button
              type="button"
              data-action="ai-attach"
              aria-label="Attach"
              className="inline-flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
            >
              <Plus className="size-5" />
            </button>
            <CommandsMenu
              onRegenerate={onRegenerate}
              onContinue={onContinue}
              onCompact={onCompact}
              onRestoreCompact={onRestoreCompact}
              onCopyMarkdown={onCopyMarkdown}
              onExportMarkdown={onExportMarkdown}
              onSaveToLibrary={onSaveToLibrary}
              onShowPrompt={onShowPrompt}
              onRetryProbe={onRetryProbe}
              onClear={onClear}
              isStreaming={isStreaming}
              isCompacting={isCompacting}
              hasMessages={hasMessages}
              hasUserMessage={hasUserMessage}
              hasCompactSnapshot={hasCompactSnapshot}
              isMock={isMock}
            />
          </div>
          <div className="flex items-center gap-1">
            <AgentChip
              agents={agents}
              activeAgent={activeAgent}
              onAgentChange={onAgentChange}
            />
            <ModelSelector
              models={models}
              model={model}
              onModelChange={onModelChange}
            />
            <VoiceInputButton
              onTranscript={(t) => onChange((value ? value + " " : "") + t)}
            />
            {isStreaming ? (
              <Button
                type="button"
                size="sm"
                variant="ghost"
                onClick={onAbort}
                aria-label="Stop"
                data-action="ai-stop"
                className="rounded-full"
              >
                <Square className="size-4" />
              </Button>
            ) : null}
          </div>
        </div>
      </div>
    </div>
  )
}

function ModelSelector({
  models,
  model,
  onModelChange,
}: {
  models: string[]
  model: string
  onModelChange: (m: string) => void
}) {
  const label = prettyModelName(model)
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <button
          type="button"
          data-action="ai-model"
          className="inline-flex items-center gap-1 rounded-full px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
        >
          <span>{label}</span>
          <ChevronDown className="size-4 opacity-70" />
        </button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        {models.map((m) => (
          <DropdownMenuItem
            key={m}
            onClick={() => onModelChange(m)}
            className={m === model ? "font-medium" : ""}
          >
            {prettyModelName(m)}
          </DropdownMenuItem>
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

function AgentChip({
  agents,
  activeAgent,
  onAgentChange,
}: {
  agents: Agent[]
  activeAgent: Agent | undefined
  onAgentChange: (id: string) => void
}) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        data-action="ai-agent"
        className="inline-flex items-center gap-1.5 rounded-full py-1 pl-1 pr-2.5 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
        title={
          activeAgent
            ? `${activeAgent.name} — ${activeAgent.role}`
            : "Pick a persona"
        }
      >
        <Avatar className="size-5">
          <AvatarFallback
            style={{
              background: agentTint(activeAgent?.id ?? ""),
              color: "var(--primary-foreground)",
            }}
            className="text-[10px] font-semibold"
          >
            {agentInitials(activeAgent?.name)}
          </AvatarFallback>
        </Avatar>
        <span className="font-medium">
          {activeAgent?.name ?? "Agent"}
        </span>
        <ChevronDown className="size-4 opacity-70" />
      </DropdownMenuTrigger>
      <DropdownMenuContent
        align="start"
        sideOffset={6}
        className="max-h-80 w-72 overflow-y-auto"
      >
        <div className="px-2 py-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
          Switch persona
        </div>
        {agents.map((a) => (
          <DropdownMenuItem
            key={a.id}
            onClick={() => onAgentChange(a.id)}
            data-state={activeAgent?.id === a.id ? "checked" : undefined}
            data-action={`ai-agent-${a.id}`}
            className="flex items-center gap-2.5"
          >
            <Avatar className="size-7">
              <AvatarFallback
                style={{
                  background: agentTint(a.id),
                  color: "var(--primary-foreground)",
                }}
                className="text-[11px] font-semibold"
              >
                {agentInitials(a.name)}
              </AvatarFallback>
            </Avatar>
            <span className="flex min-w-0 flex-col">
              <span className="truncate font-medium">{a.name}</span>
              <span className="truncate text-xs text-muted-foreground">
                {a.role}
              </span>
            </span>
          </DropdownMenuItem>
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

function agentInitials(name: string | undefined): string {
  if (!name) return "?"
  const words = name.trim().split(/\s+/).filter(Boolean)
  if (words.length === 0) return "?"
  if (words.length === 1) return words[0].slice(0, 2).toUpperCase()
  return (words[0][0] + words[words.length - 1][0]).toUpperCase()
}

function agentTint(id: string): string {
  let hash = 0
  for (let i = 0; i < id.length; i++) {
    hash = (hash * 31 + id.charCodeAt(i)) | 0
  }
  const hue = ((hash % 360) + 360) % 360
  return `oklch(0.55 0.18 ${hue})`
}

function CommandsMenu({
  onRegenerate,
  onContinue,
  onCompact,
  onRestoreCompact,
  onCopyMarkdown,
  onExportMarkdown,
  onSaveToLibrary,
  onShowPrompt,
  onRetryProbe,
  onClear,
  isStreaming,
  isCompacting,
  hasMessages,
  hasUserMessage,
  hasCompactSnapshot,
  isMock,
}: {
  onRegenerate: () => void
  onContinue: () => void
  onCompact: () => void
  onRestoreCompact: () => void
  onCopyMarkdown: () => void
  onExportMarkdown: () => void
  onSaveToLibrary: () => void
  onShowPrompt: () => void
  onRetryProbe: () => void
  onClear: () => void
  isStreaming: boolean
  isCompacting: boolean
  hasMessages: boolean
  hasUserMessage: boolean
  hasCompactSnapshot: boolean
  isMock: boolean
}) {
  return (
    <Popover>
      <PopoverTrigger
        render={
          <button
            type="button"
            data-action="ai-commands"
            aria-label="Commands"
            title="Commands"
            className="inline-flex size-9 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
          >
            <CommandIcon className="size-5" />
          </button>
        }
      />
      <PopoverContent align="start" side="top" className="w-80 p-2">
        <SectionLabel>Conversation</SectionLabel>
        <div className="grid grid-cols-2 gap-1">
          <ToolTile
            data-action="ai-cmd-regenerate"
            onClick={onRegenerate}
            disabled={isStreaming || !hasUserMessage}
            icon={<RotateCcw className="size-4" />}
            label="Regenerate"
            title="Re-run the most recent prompt"
          />
          <ToolTile
            data-action="ai-cmd-continue"
            onClick={onContinue}
            disabled={isStreaming || !hasMessages}
            icon={<ArrowRight className="size-4" />}
            label="Continue"
            title="Ask the model to keep going"
          />
          <ToolTile
            data-action="ai-cmd-compact"
            onClick={onCompact}
            disabled={isCompacting || isStreaming || !hasMessages}
            icon={
              isCompacting ? (
                <Loader2 className="size-4 animate-spin" />
              ) : (
                <Archive className="size-4" />
              )
            }
            label="Compact"
            title="Summarize older turns to free context"
          />
          <ToolTile
            data-action="ai-cmd-restore-compact"
            onClick={onRestoreCompact}
            disabled={!hasCompactSnapshot}
            icon={<Undo2 className="size-4" />}
            label="Restore"
            title={
              hasCompactSnapshot
                ? "Undo the most recent compact"
                : "No snapshot available"
            }
          />
        </div>

        <SectionLabel>Share</SectionLabel>
        <div className="grid grid-cols-2 gap-1">
          <ToolTile
            data-action="ai-cmd-copy-md"
            onClick={onCopyMarkdown}
            disabled={!hasMessages}
            icon={<Copy className="size-4" />}
            label="Copy MD"
            title="Copy conversation as Markdown"
          />
          <ToolTile
            data-action="ai-cmd-export-md"
            onClick={onExportMarkdown}
            disabled={!hasMessages}
            icon={<Download className="size-4" />}
            label="Export MD"
            title="Download a .md file"
          />
          <ToolTile
            data-action="ai-cmd-save-library"
            onClick={onSaveToLibrary}
            disabled={!hasMessages}
            icon={<BookmarkPlus className="size-4" />}
            label="Save to Library"
            title="Snapshot this conversation"
          />
          <ToolTile
            data-action="ai-cmd-show-prompt"
            onClick={onShowPrompt}
            icon={<FileText className="size-4" />}
            label="Show prompt"
            title="Preview the system prompt"
          />
        </div>

        {isMock && (
          <>
            <SectionLabel>Connection</SectionLabel>
            <div className="grid grid-cols-2 gap-1">
              <ToolTile
                data-action="ai-cmd-retry-probe"
                onClick={onRetryProbe}
                icon={<RefreshCw className="size-4" />}
                label="Reconnect"
                title="Probe the LLM endpoint again"
              />
            </div>
          </>
        )}

        <div className="my-2 h-px bg-border" />
        <ToolTile
          data-action="ai-cmd-clear"
          onClick={onClear}
          disabled={!hasMessages}
          icon={<Trash2 className="size-4" />}
          label="Clear conversation"
          title="Wipe history and start fresh"
          destructive
          fullWidth
        />
      </PopoverContent>
    </Popover>
  )
}

function SystemPromptDialog({
  prompt,
  onClose,
}: {
  prompt: string
  onClose: () => void
}) {
  const copy = async () => {
    try {
      await navigator.clipboard.writeText(prompt)
    } catch {}
  }
  return (
    <div
      role="dialog"
      aria-modal="true"
      onClick={onClose}
      className="fixed inset-0 z-50 flex items-center justify-center bg-background/70 p-4 backdrop-blur-sm"
    >
      <div
        onClick={(e) => e.stopPropagation()}
        className="flex max-h-[80vh] w-full max-w-2xl flex-col rounded-lg border bg-card shadow-lg"
      >
        <div className="flex items-center gap-2 border-b px-4 py-3">
          <FileText className="size-4 text-muted-foreground" />
          <div className="flex flex-1 flex-col">
            <span className="text-sm font-semibold">System prompt</span>
            <span className="text-xs text-muted-foreground">
              Base prompt + active persona
            </span>
          </div>
          <Button variant="ghost" size="sm" onClick={copy}>
            <Copy className="size-3.5" /> Copy
          </Button>
          <Button
            variant="ghost"
            size="icon-sm"
            onClick={onClose}
            aria-label="Close"
          >
            <X className="size-4" />
          </Button>
        </div>
        <pre className="flex-1 overflow-auto whitespace-pre-wrap p-4 font-mono text-xs leading-relaxed">
          {prompt}
        </pre>
      </div>
    </div>
  )
}

function SectionLabel({ children }: { children: React.ReactNode }) {
  return (
    <div className="mt-2 mb-1 px-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
      {children}
    </div>
  )
}

function ToolTile({
  icon,
  label,
  title,
  onClick,
  disabled,
  destructive,
  fullWidth,
  ...rest
}: {
  icon: React.ReactNode
  label: string
  title: string
  onClick: () => void
  disabled?: boolean
  destructive?: boolean
  fullWidth?: boolean
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick" | "title">) {
  return (
    <button
      type="button"
      onClick={onClick}
      disabled={disabled}
      title={title}
      className={
        "flex items-center gap-2 rounded-md border border-transparent px-2 py-1.5 text-left text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-50 " +
        (fullWidth ? "w-full " : "") +
        (destructive
          ? "text-destructive hover:bg-destructive/10"
          : "hover:bg-accent hover:text-accent-foreground")
      }
      {...rest}
    >
      <span className="shrink-0">{icon}</span>
      <span className="truncate font-medium">{label}</span>
    </button>
  )
}

function prettyModelName(id: string): string {
  if (!id) return "model"
  // Trim known prefixes / paths so the chip stays compact.
  const last = id.split(/[\\/]/).pop() ?? id
  return last.length > 28 ? last.slice(0, 26) + "…" : last
}

type SpeechRecognitionLike = {
  lang: string
  interimResults: boolean
  continuous: boolean
  onresult: (e: { results: { [k: number]: { [k: number]: { transcript: string } } } }) => void
  onerror: () => void
  onend: () => void
  start: () => void
  stop: () => void
}

function getSpeechRecognition(): (new () => SpeechRecognitionLike) | null {
  if (typeof window === "undefined") return null
  const w = window as unknown as {
    SpeechRecognition?: new () => SpeechRecognitionLike
    webkitSpeechRecognition?: new () => SpeechRecognitionLike
  }
  return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null
}

function VoiceInputButton({
  onTranscript,
}: {
  onTranscript: (text: string) => void
}) {
  const Ctor = getSpeechRecognition()
  const [listening, setListening] = useState(false)
  const recRef = useRef<SpeechRecognitionLike | null>(null)

  if (!Ctor) return null

  const start = () => {
    try {
      const rec = new Ctor()
      rec.lang = navigator.language || "en-US"
      rec.interimResults = false
      rec.continuous = false
      rec.onresult = (e) => {
        const text = e.results[0]?.[0]?.transcript ?? ""
        if (text.trim()) onTranscript(text.trim())
      }
      rec.onerror = () => setListening(false)
      rec.onend = () => setListening(false)
      recRef.current = rec
      rec.start()
      setListening(true)
    } catch {
      setListening(false)
    }
  }
  const stop = () => {
    recRef.current?.stop()
    setListening(false)
  }

  return (
    <button
      type="button"
      data-action="ai-voice"
      onClick={() => (listening ? stop() : start())}
      aria-label={listening ? "Stop listening" : "Voice input"}
      className={
        "inline-flex size-9 items-center justify-center rounded-full transition-colors " +
        (listening
          ? "bg-destructive/10 text-destructive animate-pulse"
          : "text-muted-foreground hover:bg-accent hover:text-foreground")
      }
    >
      {listening ? <MicOff className="size-5" /> : <Mic className="size-5" />}
    </button>
  )
}
