"use client";

import { useCallback, useEffect, useRef, useState, useTransition } from "react";
import { ChevronDown, ImagePlus, Loader2, Mic, RefreshCw, Send, Square } from "lucide-react";
import { setAssistantModel } from "@/app/settings/assistant-actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { consumeAgentChatStream } from "../assistant-chat-stream";
import {
  clearAssistantChat,
  loadAssistantChat,
  saveAssistantChat,
  toClientChatMessage,
  type AssistantChatMessage,
} from "../assistant-chat-storage";
import { useVoiceInput } from "./use-voice-input";

type Props = {
  configured: boolean;
  userId: string;
  assistantName: string;
  assistantModel: string | null;
};

type PendingImage = {
  url: string;
};

type LlmModelOption = {
  id: string;
  label: string;
};

type ModelsResponse = {
  models: LlmModelOption[];
  selectedModel: string;
  fallbackModel: string;
  degraded: boolean;
};

async function uploadAssistantImage(file: File): Promise<string> {
  const formData = new FormData();
  formData.append("file", file);
  const response = await fetch("/api/uploads?scope=assistant", { method: "POST", body: formData });
  if (!response.ok) {
    const payload = (await response.json().catch(() => null)) as { error?: string } | null;
    throw new Error(payload?.error ?? "Image upload failed");
  }
  const payload = (await response.json()) as { url: string };
  return payload.url;
}

export function AssistantPanel({ configured, userId, assistantName, assistantModel }: Props) {
  const [messages, setMessages] = useState<AssistantChatMessage[]>(() => loadAssistantChat(userId));
  const [input, setInput] = useState("");
  const [pendingImage, setPendingImage] = useState<PendingImage | null>(null);
  const [uploadingImage, setUploadingImage] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [isPending, setIsPending] = useState(false);
  const [activityLabel, setActivityLabel] = useState<string | null>(null);
  const [modelOptions, setModelOptions] = useState<LlmModelOption[]>([]);
  const [selectedModel, setSelectedModel] = useState(assistantModel ?? "");
  const [fallbackModel, setFallbackModel] = useState("");
  const [modelsDegraded, setModelsDegraded] = useState(false);
  const [modelsLoading, setModelsLoading] = useState(true);
  const [savingModel, startTransition] = useTransition();
  const listRef = useRef<HTMLDivElement>(null);
  const abortRef = useRef<AbortController | null>(null);
  const imageInputRef = useRef<HTMLInputElement>(null);

  const { state: voiceState, toggleRecording } = useVoiceInput({
    disabled: isPending || uploadingImage,
    onTranscript: (text) => {
      setInput((current) => (current.trim() ? `${current.trim()} ${text}` : text));
      setError(null);
    },
    onError: (message) => setError(message),
  });

  useEffect(() => {
    saveAssistantChat(userId, messages);
  }, [messages, userId]);

  useEffect(() => {
    return () => {
      abortRef.current?.abort();
    };
  }, []);

  const loadModels = useCallback(async (options?: { refresh?: boolean; signal?: AbortSignal }) => {
    if (options?.signal?.aborted) return;

    setModelsLoading(true);
    try {
      const params = new URLSearchParams();
      if (options?.refresh) params.set("refresh", "1");
      const query = params.size > 0 ? `?${params.toString()}` : "";

      const response = await fetch(`/api/agent/models${query}`, {
        cache: "no-store",
        signal: options?.signal,
      });
      if (!response.ok) throw new Error("Model discovery unavailable");
      const payload = (await response.json()) as ModelsResponse;
      if (options?.signal?.aborted) return;
      setModelOptions(payload.models);
      setSelectedModel(payload.selectedModel);
      setFallbackModel(payload.fallbackModel);
      setModelsDegraded(payload.degraded);
      setError(null);
    } catch (err) {
      if (err instanceof Error && err.name === "AbortError") return;
      setModelsDegraded(true);
      setError("Model discovery unavailable");
    } finally {
      if (!options?.signal?.aborted) setModelsLoading(false);
    }
  }, []);

  useEffect(() => {
    const controller = new AbortController();

    queueMicrotask(() => {
      void loadModels({ signal: controller.signal });
    });

    return () => {
      controller.abort();
    };
  }, [loadModels]);

  function scrollToBottom() {
    requestAnimationFrame(() => {
      const node = listRef.current;
      if (node) node.scrollTop = node.scrollHeight;
    });
  }

  function clearChat() {
    abortRef.current?.abort();
    setMessages([]);
    setInput("");
    setPendingImage(null);
    setError(null);
    setActivityLabel(null);
    setIsPending(false);
    clearAssistantChat(userId);
  }

  async function handleImageSelect(event: React.ChangeEvent<HTMLInputElement>) {
    const file = event.target.files?.[0];
    event.target.value = "";
    if (!file || isPending || uploadingImage) return;

    setUploadingImage(true);
    setError(null);
    try {
      const url = await uploadAssistantImage(file);
      setPendingImage({ url });
    } catch (err) {
      setError(err instanceof Error ? err.message : "Image upload failed");
    } finally {
      setUploadingImage(false);
    }
  }

  function changeModel(nextModel: string | null) {
    if (!nextModel) return;

    setSelectedModel(nextModel);
    setError(null);

    startTransition(async () => {
      try {
        await setAssistantModel(nextModel === fallbackModel ? null : nextModel);
      } catch (err) {
        setError(err instanceof Error ? err.message : "Could not save assistant model");
      }
    });
  }

  async function sendMessage() {
    const text = input.trim();
    const hasImage = pendingImage !== null;
    if ((!text && !hasImage) || isPending || voiceState !== "idle") return;

    const content = text || "Help me with this image.";
    const userMessage: AssistantChatMessage = {
      role: "user",
      content,
      ...(pendingImage ? { imageUrl: pendingImage.url } : {}),
    };

    const nextMessages: AssistantChatMessage[] = [...messages, userMessage];
    setInput("");
    setPendingImage(null);
    setError(null);
    setMessages(nextMessages);
    setIsPending(true);
    setActivityLabel(hasImage ? "Reading your photo…" : "Understanding your request…");
    scrollToBottom();

    abortRef.current?.abort();
    const controller = new AbortController();
    abortRef.current = controller;

    try {
      const response = await fetch("/api/agent/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          messages: nextMessages.map(toClientChatMessage),
          model: selectedModel || undefined,
          stream: true,
        }),
        signal: controller.signal,
      });

      const result = await consumeAgentChatStream(response, (event) => {
        if (event.type === "thinking" || event.type === "tool" || event.type === "responding") {
          setActivityLabel(event.label);
          scrollToBottom();
        }
      });

      if (!result.message.content) {
        throw new Error("Assistant returned an empty response");
      }

      setMessages((current) => [
        ...current,
        {
          role: "assistant",
          content: result.message.content,
          ...(result.toolCalls.length > 0 ? { toolCalls: result.toolCalls } : {}),
        },
      ]);
      scrollToBottom();
    } catch (err) {
      if (err instanceof Error && err.name === "AbortError") return;
      setError(err instanceof Error ? err.message : "Something went wrong");
    } finally {
      setIsPending(false);
      setActivityLabel(null);
      abortRef.current = null;
    }
  }

  const showEmptyState = messages.length === 0 && !isPending;
  const inputDisabled = isPending || voiceState === "transcribing" || uploadingImage;
  const canSend =
    !isPending &&
    !savingModel &&
    voiceState === "idle" &&
    !uploadingImage &&
    (input.trim().length > 0 || pendingImage !== null);

  return (
    <div className="flex min-h-0 flex-1 flex-col gap-3">
      <div className="flex items-start justify-between gap-3">
        <div className="min-w-0 flex-1">
          <p className="muted text-[12px] leading-relaxed">
            {configured
              ? "Type, talk, or send a photo — I can update lists, calendar, notes, and more."
              : "Mock provider active — set LLM_BASE_URL for your homelab model."}
          </p>
          {modelsDegraded ? (
            <p className="muted mt-1 text-[11px]">Model discovery unavailable; using fallback.</p>
          ) : null}
        </div>
        <div className="flex shrink-0 items-center gap-2">
          {modelOptions.length > 0 ? (
            <div className="relative max-w-36">
              <select
                aria-label="Assistant model"
                value={selectedModel}
                onChange={(event) => changeModel(event.target.value)}
                disabled={modelsLoading || savingModel || isPending}
                className="h-9 w-full max-w-36 appearance-none truncate rounded-[min(var(--radius-md),10px)] border border-input bg-[var(--card)] py-0 pr-8 pl-3 text-[13px] leading-9 text-[var(--ink)] outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50"
              >
                {modelOptions.map((model) => (
                  <option key={model.id} value={model.id}>
                    {model.label}
                  </option>
                ))}
              </select>
              <ChevronDown
                className="pointer-events-none absolute top-1/2 right-2 size-4 -translate-y-1/2 text-muted-foreground"
                aria-hidden="true"
              />
            </div>
          ) : null}
          <Button
            type="button"
            size="sm"
            variant="outline"
            aria-label="Refresh assistant models"
            disabled={modelsLoading || savingModel || isPending}
            onClick={() => void loadModels({ refresh: true })}
          >
            {modelsLoading ? (
              <Loader2 className="size-4 animate-spin" />
            ) : (
              <RefreshCw className="size-4" />
            )}
          </Button>
          {messages.length > 0 ? (
            <button
              type="button"
              onClick={clearChat}
              disabled={isPending}
              className="shrink-0 text-[11px] text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50"
            >
              Clear
            </button>
          ) : null}
        </div>
      </div>

      <div
        ref={listRef}
        className="min-h-0 flex-1 overflow-y-auto rounded-[var(--r-md)] border-[0.5px] bg-[var(--shade)] p-3"
        style={{ borderColor: "var(--hair)" }}
      >
        {showEmptyState ? (
          <p className="muted text-[12px]">
            Try &quot;add milk to the shopping list&quot;, tap the mic, or attach a photo of an
            appointment card.
          </p>
        ) : (
          <div className="grid gap-2">
            {messages.map((message, index) => (
              <div
                key={`${message.role}-${index}`}
                className={`rounded-lg px-2.5 py-2 text-[12.5px] leading-relaxed whitespace-pre-wrap ${
                  message.role === "user"
                    ? "ml-6 bg-[var(--card)]"
                    : "mr-6 border-[0.5px] bg-[var(--card)]"
                }`}
                style={message.role === "assistant" ? { borderColor: "var(--hair)" } : undefined}
              >
                <div className="eyebrow mb-0.5 text-[10px]">
                  {message.role === "user" ? "You" : assistantName}
                </div>
                {message.imageUrl ? (
                  // User-uploaded assistant attachment preview
                  <img
                    src={message.imageUrl}
                    alt=""
                    className="mb-2 max-h-40 w-full rounded-md object-contain"
                  />
                ) : null}
                {message.content}
                {message.role === "assistant" &&
                message.toolCalls &&
                message.toolCalls.length > 0 ? (
                  <p className="mt-1 text-[10px] text-muted-foreground">
                    {message.toolCalls.map((call) => `${call.name}→${call.status}`).join(" · ")}
                  </p>
                ) : null}
              </div>
            ))}

            {isPending ? (
              <div
                className="mr-6 rounded-lg border-[0.5px] bg-[var(--card)] px-2.5 py-2"
                style={{ borderColor: "var(--hair)" }}
                aria-live="polite"
                aria-busy="true"
              >
                <div className="eyebrow mb-1 text-[10px]">{assistantName}</div>
                <div className="flex items-center gap-2 text-[12.5px] text-muted-foreground">
                  <Loader2 className="size-3.5 shrink-0 animate-spin" />
                  <span>{activityLabel ?? "Working…"}</span>
                </div>
              </div>
            ) : null}
          </div>
        )}
      </div>

      {pendingImage ? (
        <div className="flex items-center gap-2 rounded-[var(--r-md)] border-[0.5px] bg-[var(--shade)] p-2">
          <img
            src={pendingImage.url}
            alt=""
            className="max-h-16 max-w-[40%] rounded-md object-contain"
          />
          <div className="min-w-0 flex-1 text-[11px] text-muted-foreground">Photo attached</div>
          <button
            type="button"
            className="text-[11px] text-muted-foreground hover:text-foreground"
            onClick={() => setPendingImage(null)}
            disabled={inputDisabled}
          >
            Remove
          </button>
        </div>
      ) : null}

      {error ? <p className="text-[12px] text-destructive">{error}</p> : null}

      <form
        className="flex gap-2"
        onSubmit={(event) => {
          event.preventDefault();
          void sendMessage();
        }}
      >
        <input
          ref={imageInputRef}
          type="file"
          accept="image/*"
          className="hidden"
          onChange={(event) => void handleImageSelect(event)}
        />
        <Button
          type="button"
          size="sm"
          variant="outline"
          disabled={inputDisabled}
          aria-label="Attach photo"
          onClick={() => imageInputRef.current?.click()}
        >
          {uploadingImage ? (
            <Loader2 className="size-4 animate-spin" />
          ) : (
            <ImagePlus className="size-4" />
          )}
        </Button>
        <Button
          type="button"
          size="sm"
          variant={voiceState === "recording" ? "destructive" : "outline"}
          disabled={inputDisabled}
          aria-label={voiceState === "recording" ? "Stop recording" : "Record voice message"}
          aria-pressed={voiceState === "recording"}
          onClick={() => void toggleRecording()}
        >
          {voiceState === "transcribing" ? (
            <Loader2 className="size-4 animate-spin" />
          ) : voiceState === "recording" ? (
            <Square className="size-4" />
          ) : (
            <Mic className="size-4" />
          )}
        </Button>
        <Input
          value={input}
          onChange={(event) => setInput(event.target.value)}
          placeholder={
            voiceState === "recording"
              ? "Listening… tap mic to stop"
              : voiceState === "transcribing"
                ? "Transcribing…"
                : `Ask ${assistantName}…`
          }
          disabled={inputDisabled}
          aria-label={`Message for ${assistantName}`}
          className="h-9 min-w-0 flex-1"
        />
        <Button type="submit" size="sm" disabled={!canSend} aria-label="Send">
          {isPending ? <Loader2 className="size-4 animate-spin" /> : <Send className="size-4" />}
        </Button>
      </form>
    </div>
  );
}
