import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { RefreshCw, Square } from "lucide-react"

const PROBE_TIMEOUT_MS = 3000

function withTimeout<T>(p: Promise<T>, ms: number, signal: AbortSignal): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    const t = setTimeout(() => {
      reject(new Error(`timeout after ${ms}ms`))
    }, ms)
    signal.addEventListener("abort", () => {
      clearTimeout(t)
      reject(new DOMException("Aborted", "AbortError"))
    })
    p.then(
      (v) => {
        clearTimeout(t)
        resolve(v)
      },
      (e) => {
        clearTimeout(t)
        reject(e)
      },
    )
  })
}
import {
  LLMProvider,
  MockLLM,
  OpenAICompatibleAdapter,
  listModels,
  useChat,
  type LLMAdapter,
} from "@crema/llm-ui"
import { ChatBubble, TypingIndicator } from "@crema/chat-ui"
import { CommandBar } from "@crema/aifirst-ui"

import { AppShell } from "~/components/layout/app-shell"
import { MessageBody } from "~/components/assistant/message-body"
import { Button } from "~/components/ui/button"
import {
  buildSystemPrompt,
  estimateTokens,
  runActionBlocks,
  trimMessages,
} from "@crema/action-bus"
import { useLLMSettings } from "~/lib/llm-settings"
import { pageTitle } from "~/lib/page-meta"

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

const STORAGE_KEY = "crema.assistant.model"
const UI_CONTROL_KEY = "crema.assistant.uiControl"

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: [
    {
      matches: (req) =>
        /hello|hi\b|hey/i.test(req.messages.at(-1)?.content ?? ""),
      response:
        "Hi — I'm the mock assistant. Try asking me anything; I'll stream a generic reply.",
    },
    {
      matches: (req) =>
        /(take me to|open|navigate|go to).*(resources|library|settings|activity|assistant|overview|home)/i.test(
          req.messages.at(-1)?.content ?? "",
        ),
      response: [
        "On it.\n\n",
        "```action\n",
        "navigate /resources\n",
        "```\n",
      ],
    },
  ],
})

export default function AssistantRoute() {
  const settings = useLLMSettings()
  const [status, setStatus] = useState<Status>({ kind: "probing" })
  const [model, setModel] = useState<string>(() => {
    if (typeof window === "undefined") return "mock"
    return localStorage.getItem(STORAGE_KEY) ?? ""
  })

  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: "LM Studio returned no models" })
          return
        }
        setStatus({ kind: "live", models: ids })
        setModel((cur) => (cur && ids.includes(cur) ? cur : ids[0]))
      })
      .catch((err: unknown) => {
        if ((err as DOMException)?.name === "AbortError") return
        setStatus({
          kind: "mock",
          reason: err instanceof Error ? err.message : "LM Studio unreachable",
        })
      })
    return () => ac.abort()
  }, [settings.baseURL])

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

  useEffect(() => {
    if (model && model !== "mock") localStorage.setItem(STORAGE_KEY, model)
  }, [model])

  const adapter: LLMAdapter = useMemo(
    () =>
      status.kind === "live"
        ? new OpenAICompatibleAdapter({ baseURL: settings.baseURL })
        : mockAdapter,
    [status.kind, settings.baseURL],
  )

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

  return (
    <AppShell title="Assistant" theme="mightypix">
      <LLMProvider adapter={adapter} model={activeModel}>
        <AssistantSurface
          status={status}
          model={model}
          onModelChange={setModel}
          contextTokens={settings.contextTokens}
          responseBudget={settings.responseBudget}
          baseURL={settings.baseURL}
          onRetryProbe={probe}
        />
      </LLMProvider>
    </AppShell>
  )
}

function AssistantSurface({
  status,
  model,
  onModelChange,
  contextTokens,
  responseBudget,
  baseURL,
  onRetryProbe,
}: {
  status: Status
  model: string
  onModelChange: (m: string) => void
  contextTokens: number
  responseBudget: number
  baseURL: string
  onRetryProbe: () => void
}) {
  const [uiControl, setUiControl] = useState<boolean>(() => {
    if (typeof window === "undefined") return false
    return localStorage.getItem(UI_CONTROL_KEY) === "1"
  })
  const [actionLog, setActionLog] = useState<string | null>(null)

  useEffect(() => {
    localStorage.setItem(UI_CONTROL_KEY, uiControl ? "1" : "0")
  }, [uiControl])

  const { messages, send, abort, isStreaming, error, reset } = useChat({
    system:
      "You are a concise assistant inside this app. Prefer short, clear answers.",
  })

  const scrollerRef = useRef<HTMLDivElement>(null)
  const lastContent = messages.at(-1)?.content
  useEffect(() => {
    const el = scrollerRef.current
    if (!el) return
    el.scrollTop = el.scrollHeight
  }, [messages.length, lastContent, isStreaming])

  // Run action blocks when an assistant turn completes (and UI control is on).
  const wasStreaming = useRef(false)
  useEffect(() => {
    if (wasStreaming.current && !isStreaming) {
      const last = messages.at(-1)
      if (uiControl && last?.role === "assistant" && last.content) {
        void runActionBlocks(last.content).then((res) => {
          if (res.ran > 0) {
            setActionLog(`Ran ${res.ran} action block${res.ran > 1 ? "s" : ""}.`)
          } else if (res.errors.length > 0) {
            setActionLog(`Action error: ${res.errors[0]}`)
          }
        })
      }
    }
    wasStreaming.current = isStreaming
  }, [isStreaming, messages, uiControl])

  const handleSend = (text: string) => {
    const system = uiControl
      ? buildSystemPrompt({ path: window.location.pathname })
      : "You are a concise assistant inside this app. Prefer short, clear answers."
    const sysTokens = estimateTokens(system)
    const historyBudget = Math.max(
      256,
      contextTokens - sysTokens - responseBudget,
    )
    const userMsg = { role: "user" as const, content: text }
    const trimmed = trimMessages([...messages, userMsg], historyBudget)
    void send(text, {
      system,
      messages: trimmed,
      maxTokens: responseBudget,
    })
  }

  const usedTokens = useMemo(() => {
    const system = uiControl
      ? buildSystemPrompt({ path: typeof window !== "undefined" ? window.location.pathname : "" })
      : ""
    const sysT = estimateTokens(system)
    const histT = messages.reduce((n, m) => n + estimateTokens(m.content), 0)
    return sysT + histT
  }, [messages, uiControl])

  const suggestions = uiControl
    ? [
        "Take me to the resources page",
        "Show me what's on the screen",
        "Open the settings",
        "Go back to overview",
      ]
    : [
        "What can this app do?",
        "Draft a release note",
        "Summarize this week's activity",
        "Show me a SQL example",
      ]

  return (
    <div className="flex h-[calc(100svh-3.5rem-3rem)] flex-col gap-4">
      <div className="flex flex-wrap items-center gap-3 rounded-lg border bg-card/50 px-3 py-2">
        <StatusDot status={status} />
        <span className="text-sm text-muted-foreground">
          {status.kind === "probing" && "Probing…"}
          {status.kind === "live" && `Live · ${baseURL.replace(/^https?:\/\//, "")}`}
          {status.kind === "mock" && `Mock LLM · ${status.reason}`}
        </span>
        {status.kind === "mock" && (
          <Button
            data-action="assistant-retry-probe"
            variant="ghost"
            size="icon-sm"
            onClick={onRetryProbe}
            aria-label="Retry connection"
            title="Retry connection to LM Studio"
          >
            <RefreshCw className="size-4" />
          </Button>
        )}
        <span
          className={`rounded-md border px-2 py-1 text-xs tabular-nums ${
            usedTokens > contextTokens - responseBudget
              ? "border-amber-500/50 bg-amber-500/10 text-amber-700 dark:text-amber-300"
              : "bg-muted text-muted-foreground"
          }`}
          title={`System + history estimate. Response capped at ${responseBudget}.`}
        >
          {usedTokens} / {contextTokens}
        </span>
        <div className="ml-auto flex items-center gap-2">
          {status.kind === "live" ? (
            <select
              data-action="assistant-model"
              value={model || status.models[0]}
              onChange={(e) => onModelChange(e.target.value)}
              className="h-8 rounded-md border bg-background px-2 text-sm"
            >
              {status.models.map((m) => (
                <option key={m} value={m}>
                  {m}
                </option>
              ))}
            </select>
          ) : (
            <span className="rounded-md border bg-muted px-2 py-1 text-xs">
              mock
            </span>
          )}
          <Button
            data-action="assistant-ui-control"
            variant={uiControl ? "default" : "outline"}
            size="sm"
            onClick={() => setUiControl((v) => !v)}
            title="Let the assistant drive the UI on your behalf"
          >
            {uiControl ? "UI Control: ON" : "Enable UI Control"}
          </Button>
          {isStreaming ? (
            <Button
              data-action="assistant-stop"
              variant="outline"
              size="sm"
              onClick={abort}
            >
              <Square className="size-3 fill-current" /> Stop
            </Button>
          ) : (
            <Button
              data-action="assistant-clear"
              variant="ghost"
              size="sm"
              onClick={reset}
              disabled={messages.length === 0}
            >
              Clear
            </Button>
          )}
        </div>
      </div>

      {uiControl && (
        <div className="rounded-lg border border-primary/30 bg-primary/5 px-3 py-2 text-xs text-foreground/80">
          UI control is on. The assistant can navigate, click, and fill on your
          behalf. Watch the cursor.
        </div>
      )}

      <div
        ref={scrollerRef}
        className="flex-1 overflow-y-auto rounded-lg border bg-card/30 p-4"
      >
        {messages.length === 0 ? (
          <EmptyState uiControl={uiControl} />
        ) : (
          <div className="flex flex-col gap-3">
            {messages.map((m, i) =>
              m.role === "user" ? (
                <ChatBubble
                  key={i}
                  content={m.content}
                  sender="user"
                  name="You"
                />
              ) : (
                <div key={i} className="flex flex-row gap-2">
                  <div className="flex max-w-[80%] flex-col items-start">
                    <span className="mb-0.5 px-1 text-xs font-medium text-muted-foreground">
                      Assistant
                    </span>
                    <div className="rounded-2xl rounded-bl-md bg-muted px-3.5 py-2 text-sm leading-relaxed text-foreground">
                      {m.content ? (
                        <MessageBody content={m.content} />
                      ) : isStreaming && i === messages.length - 1 ? (
                        "…"
                      ) : null}
                    </div>
                  </div>
                </div>
              ),
            )}
            {isStreaming && messages.at(-1)?.role !== "assistant" && (
              <TypingIndicator />
            )}
          </div>
        )}
      </div>

      {actionLog && (
        <div className="rounded-lg border bg-card/40 px-3 py-2 text-xs text-muted-foreground">
          {actionLog}
        </div>
      )}

      {error && (
        <div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
          {error.message}
        </div>
      )}

      <CommandBar
        chips={suggestions}
        position="bottom"
        placeholder={
          uiControl ? "Ask me to do something…" : "Ask the assistant…"
        }
        onSubmit={handleSend}
        onChipSelect={handleSend}
      />
    </div>
  )
}

function StatusDot({ status }: { status: Status }) {
  const color =
    status.kind === "probing"
      ? "bg-muted-foreground/40"
      : status.kind === "live"
        ? "bg-green-500"
        : "bg-amber-500"
  return (
    <span
      aria-hidden
      className={`size-2 rounded-full ${color}`}
      data-status={status.kind}
    />
  )
}

function EmptyState({ uiControl }: { uiControl: boolean }) {
  return (
    <div className="flex h-full flex-col items-center justify-center gap-2 text-center">
      <p
        style={{ fontFamily: "var(--font-ai-prose)" }}
        className="text-lg text-foreground"
      >
        {uiControl ? "Tell me what you'd like to do." : "How can I help?"}
      </p>
      <p className="text-sm text-muted-foreground">
        {uiControl
          ? "I can navigate, click buttons, and fill forms. Watch the cursor."
          : "Pick a suggestion below or type a question."}
      </p>
    </div>
  )
}
