// PURPOSE: Global assistant dock — a floating Sparkles FAB that opens a
//          right-side slide-over chat with an agents-platform agent. The
//          shell, threading, compose box, and rich-block rendering live
//          here; the app injects identity (which agent), context (what the
//          user is looking at), and transport (which closes over auth).
//          Source of truth for the dock across every Crema webapp.
// ===========================================================================
// EXPORTS
// Component: AgentDock
// Types:     AgentDockProps, AgentDockTransport, AgentRef, PageContext
// ===========================================================================
"use client"

import { useCallback, useEffect, useRef, useState } from "react"
import { Loader2, Maximize2, Plus, Send, Sparkles, X } from "lucide-react"

import { MessageBody } from "@crema/agent-ui"

/* ------------------------------------------------------------------ */
/*  Public types                                                       */
/* ------------------------------------------------------------------ */

export type AgentRef = { id: string; name: string }

/** Structured page-awareness signal. Travels to the platform as its own
 *  field — never concatenated into the user message — so page content
 *  cannot masquerade as a user instruction. Pass stable identifiers, not
 *  scraped DOM text. */
export type PageContext = {
  /** App route the user is currently viewing, e.g. `/invoices`. */
  route: string
  /** Optional stable resource identifier — type + id, app-authored. */
  resource?: { type: string; id: string }
  /** Optional short, app-authored label (a stable descriptor). */
  label?: string
}

/** The single platform call the dock needs. The app implements this —
 *  typically by delegating to its `@crema/arcadia-agents-client` instance
 *  — so auth stays closed over inside the transport; the dock never sees
 *  a token. */
export interface AgentDockTransport {
  chat(
    agentId: string,
    req: {
      user_message: string
      conversation_id?: string
      page_context?: PageContext
    },
  ): Promise<{
    message: string
    conversation_id: string
    citations?: { chunk_id: string }[]
  }>
}

export interface AgentDockProps {
  /** Platform transport — auth is baked in here by the app. */
  transport: AgentDockTransport
  /** Resolve the agent(s) the user can talk to. The dock renders a picker
   *  iff more than one is returned; the first (or `defaultAgentId`) is the
   *  default. A rejected promise disables the dock (renders nothing) —
   *  e.g. a tenant with no personal-agent template. */
  resolveAgents: () => Promise<AgentRef[]>
  /** Preferred default agent id when `resolveAgents` returns several. */
  defaultAgentId?: string
  /** Called on every send. Return the structured page context to attach,
   *  or null to send none. The app decides what counts as "context". */
  getPageContext?: () => PageContext | null
  /** ⤢ handler. Omit to hide the expand button. The app owns what expand
   *  means — navigate to a full chat route, mount its own modal, etc. */
  onExpand?: (a: { agentId: string; conversationId: string | null }) => void
  /** App-computed per-route suppression — the app knows its own routes. */
  hidden?: boolean
  /** localStorage namespace for the open/closed state. */
  storageKey?: string
}

/* ------------------------------------------------------------------ */
/*  Internal                                                           */
/* ------------------------------------------------------------------ */

type Bubble =
  | { role: "user"; text: string }
  | { role: "assistant"; text: string; finished: boolean; error: string | null }

const DEFAULT_STORAGE_KEY = "crema.agent-dock.open"

function pickDefault(agents: AgentRef[], preferredId?: string): AgentRef | null {
  if (preferredId) {
    const match = agents.find((a) => a.id === preferredId)
    if (match) return match
  }
  return agents[0] ?? null
}

/* ------------------------------------------------------------------ */
/*  Component                                                          */
/* ------------------------------------------------------------------ */

export function AgentDock({
  transport,
  resolveAgents,
  defaultAgentId,
  getPageContext,
  onExpand,
  hidden = false,
  storageKey = DEFAULT_STORAGE_KEY,
}: AgentDockProps) {
  const [open, setOpen] = useState<boolean>(() => {
    if (typeof window === "undefined") return false
    return localStorage.getItem(storageKey) === "1"
  })
  useEffect(() => {
    localStorage.setItem(storageKey, open ? "1" : "0")
  }, [open, storageKey])

  const [agents, setAgents] = useState<AgentRef[] | null>(null)
  const [agentId, setAgentId] = useState<string | null>(null)
  const [resolveFailed, setResolveFailed] = useState(false)
  const [bubbles, setBubbles] = useState<Bubble[]>([])
  const [conversationId, setConversationId] = useState<string | null>(null)
  const [draft, setDraft] = useState("")
  const [sending, setSending] = useState(false)
  const scrollRef = useRef<HTMLDivElement | null>(null)

  useEffect(() => {
    let cancelled = false
    // Clear any prior failure so a retry (e.g. tenant id arrives after
    // the first render) gets a clean slate — otherwise the flag latches
    // and the FAB stays hidden forever even once resolution succeeds.
    setResolveFailed(false)
    resolveAgents()
      .then((list) => {
        if (cancelled) return
        setAgents(list)
        setAgentId((cur) => cur ?? pickDefault(list, defaultAgentId)?.id ?? null)
      })
      .catch(() => {
        if (!cancelled) setResolveFailed(true)
      })
    return () => {
      cancelled = true
    }
  }, [resolveAgents, defaultAgentId])

  // Switching agent starts a fresh thread — don't cross-contaminate the
  // assistant's context across agents.
  useEffect(() => {
    setBubbles([])
    setConversationId(null)
  }, [agentId])

  useEffect(() => {
    scrollRef.current?.scrollTo({
      top: scrollRef.current.scrollHeight,
      behavior: "auto",
    })
  }, [bubbles])

  // `display` is what shows in the user bubble; `wire` is what the agent
  // receives. They differ only for rich-block action clicks, where the
  // button label is shown but the action's `value` is sent.
  const sendMessage = useCallback(
    async (display: string, wire: string) => {
      if (!agentId || sending) return
      const trimmed = wire.trim()
      if (!trimmed) return
      setBubbles((bs) => [
        ...bs,
        { role: "user", text: display },
        { role: "assistant", text: "", finished: false, error: null },
      ])
      setSending(true)
      try {
        // Non-streaming on purpose: the streaming path injects
        // stream_options.include_usage, which DeepSeek/Qwen/LM Studio
        // reject. Dock answers are short; ⤢ opens the full chat surface.
        const pc = getPageContext?.() ?? undefined
        const r = await transport.chat(agentId, {
          user_message: trimmed,
          conversation_id: conversationId ?? undefined,
          page_context: pc ?? undefined,
        })
        if (r.conversation_id && !conversationId)
          setConversationId(r.conversation_id)
        setBubbles((bs) => {
          const last = bs[bs.length - 1]
          if (!last || last.role !== "assistant") return bs
          const next = [...bs]
          next[next.length - 1] = { ...last, text: r.message, finished: true }
          return next
        })
      } catch (e) {
        const msg = e instanceof Error ? e.message : String(e)
        setBubbles((bs) => {
          const last = bs[bs.length - 1]
          if (!last || last.role !== "assistant") return bs
          const next = [...bs]
          next[next.length - 1] = { ...last, error: msg, finished: true }
          return next
        })
      } finally {
        setSending(false)
      }
    },
    [agentId, sending, conversationId, transport, getPageContext],
  )

  const submitDraft = useCallback(() => {
    const text = draft.trim()
    if (!text) return
    setDraft("")
    void sendMessage(text, text)
  }, [draft, sendMessage])

  function newThread() {
    if (sending) return
    setBubbles([])
    setConversationId(null)
  }

  if (hidden || resolveFailed) return null

  const currentAgent = agents?.find((a) => a.id === agentId) ?? null
  const title = currentAgent?.name ?? "Assistant"

  return (
    <>
      {!open && (
        <button
          type="button"
          aria-label="Open assistant"
          data-action="assistant-dock-toggle"
          onClick={() => setOpen(true)}
          className="fixed bottom-4 right-4 z-40 flex size-14 items-center justify-center rounded-full bg-[var(--primary)] text-[var(--primary-foreground)] shadow-lg transition hover:opacity-90"
        >
          <Sparkles className="size-6" />
        </button>
      )}

      {open && (
        <div
          className="fixed inset-0 z-50"
          role="dialog"
          aria-modal="true"
          aria-label="Assistant"
        >
          <div
            className="absolute inset-0 bg-black/40"
            onClick={() => setOpen(false)}
          />
          <div
            data-slot="sheet-content"
            className="absolute right-0 top-0 flex h-full w-full flex-col bg-[var(--background)] shadow-2xl sm:w-[480px] md:w-[560px]"
          >
            {/* Header */}
            <div className="flex shrink-0 items-center justify-between gap-2 border-b border-[var(--border)] p-3">
              <div className="flex min-w-0 items-center gap-2">
                <Sparkles className="size-4 shrink-0 text-[var(--primary)]" />
                <span className="truncate font-semibold" title={title}>
                  {title}
                </span>
              </div>
              <div className="flex shrink-0 items-center gap-1">
                {agents && agents.length > 1 && (
                  <select
                    aria-label="Switch agent"
                    value={agentId ?? ""}
                    onChange={(e) => setAgentId(e.target.value || null)}
                    disabled={sending}
                    data-action="assistant-dock-agent-select"
                    className="max-w-[160px] rounded border border-[var(--border)] bg-transparent px-2 py-1 text-sm"
                  >
                    {agents.map((a) => (
                      <option key={a.id} value={a.id}>
                        {a.name}
                      </option>
                    ))}
                  </select>
                )}
                <IconButton
                  label="New chat"
                  dataAction="assistant-dock-new"
                  disabled={sending || bubbles.length === 0}
                  onClick={newThread}
                >
                  <Plus className="size-4" />
                </IconButton>
                {onExpand && (
                  <IconButton
                    label="Open in full page"
                    dataAction="assistant-dock-expand"
                    disabled={!agentId}
                    onClick={() => {
                      if (!agentId) return
                      onExpand({ agentId, conversationId })
                      setOpen(false)
                    }}
                  >
                    <Maximize2 className="size-4" />
                  </IconButton>
                )}
                <IconButton
                  label="Close assistant"
                  dataAction="assistant-dock-close"
                  onClick={() => setOpen(false)}
                >
                  <X className="size-4" />
                </IconButton>
              </div>
            </div>

            {/* Messages */}
            <div ref={scrollRef} className="flex-1 space-y-4 overflow-y-auto p-4">
              {bubbles.length === 0 ? (
                <div className="pt-8 text-center text-sm text-[var(--foreground)]/80">
                  {agentId
                    ? "Ask anything. I can see what page you're on."
                    : agents && agents.length === 0
                      ? "No agents available yet."
                      : "Loading…"}
                </div>
              ) : (
                bubbles.map((b, i) =>
                  b.role === "user" ? (
                    <div
                      key={i}
                      className="ml-auto max-w-[85%] whitespace-pre-wrap rounded-lg bg-[var(--chat-user-bg)] px-3 py-2 text-sm text-[var(--chat-user-fg)]"
                    >
                      {b.text}
                    </div>
                  ) : (
                    <div key={i} className="mr-auto flex max-w-[95%] gap-2">
                      <Sparkles className="mt-2 size-3.5 shrink-0 text-[var(--primary)]" />
                      <div className="min-w-0 flex-1 rounded-lg border border-[var(--chat-assistant-border)] bg-[var(--chat-assistant-bg)] px-3 py-2 text-[15px] leading-[1.65] text-[var(--chat-assistant-fg)]">
                        {b.error ? (
                          <span className="text-[var(--destructive)]">
                            {b.error}
                          </span>
                        ) : !b.finished && !b.text ? (
                          <span className="inline-flex items-center gap-2 text-[var(--foreground)]/70">
                            <Loader2 className="size-4 animate-spin" />
                            Thinking…
                          </span>
                        ) : (
                          <MessageBody
                            content={b.text}
                            onAction={(label, value) =>
                              void sendMessage(label, value)
                            }
                          />
                        )}
                      </div>
                    </div>
                  ),
                )
              )}
            </div>

            {/* Composer */}
            <div className="shrink-0 border-t border-[var(--border)] p-3">
              <div className="flex items-end gap-2">
                <textarea
                  value={draft}
                  onChange={(e) => setDraft(e.target.value)}
                  onKeyDown={(e) => {
                    if (e.key === "Enter" && !e.shiftKey) {
                      e.preventDefault()
                      submitDraft()
                    }
                  }}
                  placeholder={agentId ? "Message…" : "No agent selected"}
                  disabled={!agentId}
                  rows={1}
                  data-action="assistant-dock-input"
                  className="max-h-32 min-h-9 flex-1 resize-none rounded-md border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-sm outline-none focus:border-[var(--primary)]/50"
                />
                <button
                  type="button"
                  aria-label={sending ? "Sending" : "Send"}
                  onClick={submitDraft}
                  disabled={sending || !draft.trim() || !agentId}
                  data-action="assistant-dock-send"
                  className="flex size-9 shrink-0 items-center justify-center rounded-md bg-[var(--primary)] text-[var(--primary-foreground)] transition hover:opacity-90 disabled:opacity-40"
                >
                  {sending ? (
                    <Loader2 className="size-4 animate-spin" />
                  ) : (
                    <Send className="size-4" />
                  )}
                </button>
              </div>
            </div>
          </div>
        </div>
      )}
    </>
  )
}

function IconButton({
  label,
  dataAction,
  disabled,
  onClick,
  children,
}: {
  label: string
  dataAction: string
  disabled?: boolean
  onClick: () => void
  children: React.ReactNode
}) {
  return (
    <button
      type="button"
      aria-label={label}
      title={label}
      data-action={dataAction}
      disabled={disabled}
      onClick={onClick}
      className="flex size-8 items-center justify-center rounded-md text-[var(--muted-foreground)] transition hover:bg-[var(--muted)] hover:text-[var(--foreground)] disabled:opacity-40"
    >
      {children}
    </button>
  )
}
