import { Suspense, lazy, useEffect, useRef, useState, type ReactNode } from 'react'
import { Alert } from '@/components/ui/Alert'
import { errorMessage } from '@/lib/api'
import { Button } from '@/components/ui/Button'
import { TextArea } from '@/components/ui/TextArea'
import { cn } from '@/lib/cn'
import {
  describeStep,
  streamChat,
  useAgentHistory,
  useAgentRefresh,
  useClearAgentHistory,
  type AgentStep,
  type AgentTurn,
} from '@/lib/agent'
import { useUndo } from '@/lib/history'
import { UndoButton } from './UndoButton'

// Lazy so the markdown renderer + its ecosystem (~150 KB) loads only when an
// assistant message actually renders, not for everyone who opens the editor.
const MarkdownMessage = lazy(() =>
  import('./MarkdownMessage').then((m) => ({ default: m.MarkdownMessage })),
)

/**
 * Talk to the garden assistant, in the editor beside the canvas.
 *
 * Here rather than on its own page because watching the garden change as the
 * agent works IS the confirmation — which is what makes acting without asking
 * first tolerable. It also means the agent never has to guess which garden you
 * mean.
 */
export function ChatPanel({ gardenId, canEdit }: { gardenId: number; canEdit: boolean }) {
  const history = useAgentHistory(gardenId, true)
  const clear = useClearAgentHistory(gardenId)
  const refresh = useAgentRefresh(gardenId)
  const undo = useUndo(gardenId)

  const [input, setInput] = useState('')
  // The turn in flight: what we sent, the steps so far, and how it ended.
  const [pending, setPending] = useState<{ message: string; steps: AgentStep[] } | null>(null)
  const [error, setError] = useState<string | null>(null)
  const [warning, setWarning] = useState<string | null>(null)
  const abort = useRef<AbortController | null>(null)
  const bottom = useRef<HTMLDivElement>(null)

  // Deliberately NOT aborted on unmount. Selecting an object auto-switches the
  // rail to the inspector, so aborting here would mean clicking the canvas
  // mid-turn silently killed the turn — and the canvas is exactly what you're
  // meant to be watching. The request continues, the exchange is persisted
  // server-side, and coming back to this tab shows it. Only Stop aborts, and
  // even that only stops us READING: the turn keeps running server-side, which
  // is why its work still lands in History either way.

  // Follow the conversation as it grows, including mid-turn as steps arrive.
  useEffect(() => {
    bottom.current?.scrollIntoView({ behavior: 'smooth', block: 'end' })
  }, [history.data, pending])

  const send = () => {
    const message = input.trim()
    if (!message || pending) return
    setInput('')
    setError(null)
    setWarning(null)
    setPending({ message, steps: [] })

    const controller = new AbortController()
    abort.current = controller

    void streamChat(
      gardenId,
      message,
      {
        onStep: (step) => {
          setPending((p) => (p ? { ...p, steps: [...p.steps, step] } : p))
          // The canvas updating under the conversation is the whole point of
          // putting the chat here, so refresh it as each step lands — but only
          // it: nothing else can have changed until the turn commits.
          refresh.canvas()
        },
        onDone: (turn: AgentTurn) => {
          setPending(null)
          refresh.everything()
          if (turn.truncated) {
            setError('That turned into more steps than I should take at once — check what changed before continuing.')
          }
        },
        onWarning: setWarning,
        onError: (message) => {
          setPending(null)
          setError(message)
          // Something may still have landed before it failed.
          refresh.everything()
        },
      },
      controller.signal,
    )
  }

  const messages = history.data ?? []

  return (
    <div className="flex h-full flex-col gap-3">
      <div className="flex items-center justify-between gap-2">
        <h2 className="text-sm font-semibold text-fg">Assistant</h2>
        {messages.length > 0 && (
          <button
            type="button"
            onClick={() =>
              clear.mutate(undefined, {
                onError: (err) => setError(errorMessage(err, "Couldn't clear the conversation.")),
              })
            }
            disabled={clear.isPending}
            className="rounded px-1.5 py-0.5 text-xs text-muted outline-none hover:text-fg focus-visible:ring-2 focus-visible:ring-accent/40"
          >
            Start over
          </button>
        )}
      </div>

      {!canEdit && (
        <p className="rounded-md bg-border/40 px-2 py-1 text-xs text-muted">
          You can only view this garden, so the assistant can't change anything in it.
        </p>
      )}

      <div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto">
        {history.isPending && <p className="text-sm text-muted">Loading the conversation…</p>}
        {/* A failed load rendering as an empty thread would look like the
            conversation had been lost, which is a much worse thing to believe. */}
        {history.isError && (
          <Alert>{errorMessage(history.error, "Couldn't load the conversation.")}</Alert>
        )}

        {history.isSuccess && messages.length === 0 && !pending && (
          <p className="text-sm text-muted">
            Ask for what you want and it'll do it — “change the garlic bed to cucumbers this year”, “fill the
            north half of the west bed with beans”, “what's in here?”. Everything it does lands as one change you
            can undo.
          </p>
        )}

        {messages.map((m) => (
          <Bubble key={m.id} role={m.role} body={m.body}>
            {/* Undo on the turn itself, so the common case never involves
                opening the History panel. Same hook #49 uses, not a second
                implementation. */}
            {m.role === 'assistant' && m.changeSetId != null && canEdit && (
              <UndoButton changeSet={{ id: m.changeSetId }} undo={undo} className="mt-1 items-start" />
            )}
          </Bubble>
        ))}

        {pending && (
          <>
            <Bubble role="user" body={pending.message} />
            <div className="rounded-lg border border-border px-2.5 py-2 text-sm">
              <ol className="flex flex-col gap-0.5">
                {pending.steps.map((s) => (
                  <li key={s.index} className="text-xs text-muted">
                    {describeStep(s)}…
                  </li>
                ))}
              </ol>
              <p className="mt-1 flex items-center gap-1.5 text-xs text-muted">
                <span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-accent" />
                {pending.steps.length === 0 ? 'Thinking…' : 'Working…'}
              </p>
            </div>
          </>
        )}

        {warning && <Alert tone="info">{warning}</Alert>}
        {error && <Alert>{error}</Alert>}
        <div ref={bottom} />
      </div>

      <div className="flex flex-col gap-2 border-t border-border pt-2">
        <TextArea
          label="Message"
          name="agentMessage"
          rows={2}
          placeholder="Change the garlic bed to cucumbers this year"
          value={input}
          disabled={!!pending}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => {
            // Enter sends, Shift+Enter breaks the line — the convention every
            // chat box uses, and typing a newline by accident mid-thought is a
            // worse failure than the reverse.
            if (e.key === 'Enter' && !e.shiftKey) {
              e.preventDefault()
              send()
            }
          }}
        />
        <div className="flex justify-end gap-2">
          {pending && (
            <Button
              variant="ghost"
              className="px-2 py-1 text-xs"
              onClick={() => {
                abort.current?.abort()
                setPending(null)
                refresh.everything()
              }}
            >
              Stop
            </Button>
          )}
          <Button className="px-3 py-1.5 text-sm" disabled={!!pending || input.trim() === ''} onClick={send}>
            {pending ? 'Working…' : 'Send'}
          </Button>
        </div>
      </div>
    </div>
  )
}

function Bubble({
  role,
  body,
  children,
}: {
  role: 'user' | 'assistant'
  body: string
  children?: ReactNode
}) {
  const mine = role === 'user'
  return (
    <div className={cn('flex flex-col', mine ? 'items-end' : 'items-start')}>
      <div
        className={cn(
          'rounded-lg px-2.5 py-2 text-sm',
          // The user's own text is literal (their `*` shouldn't become a bullet)
          // and hugs the right; the assistant's Markdown is rendered and gets the
          // full width so a table has room.
          mine
            ? 'max-w-[90%] whitespace-pre-wrap bg-accent/15 text-fg'
            : 'w-full border border-border text-fg',
        )}
      >
        {mine ? (
          body
        ) : (
          // Show the raw text until the renderer chunk arrives, so a message never
          // flashes blank.
          <Suspense fallback={<span className="whitespace-pre-wrap">{body}</span>}>
            <MarkdownMessage>{body}</MarkdownMessage>
          </Suspense>
        )}
      </div>
      {children}
    </div>
  )
}
