import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import { useParams, useNavigate, useLocation } from "react-router";
import { ArrowLeft, Sparkles } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { client } from "@/client";
import {
  PromptInput,
  PromptInputBody,
  PromptInputTextarea,
  PromptInputSubmit,
  PromptInputFooter,
  PromptInputTools,
  PromptInputProvider,
  usePromptInputController,
} from "@/components/ai-elements/prompt-input";
import type {
  Message,
  Conversation,
  Phase,
  StreamingState,
  ToolCallState,
} from "./chat/types";
import { EMPTY_STREAM } from "./chat/types";
import { ModelSelectorPopover } from "./chat/model-selector-popover";
import { RepoMentionPopover } from "./chat/repo-mention-popover";
import { MessageBubble } from "./chat/message-bubble";
import { StreamingView } from "./chat/streaming-view";
import { CodePreviewProvider } from "./chat/code-preview-context";
import { CodePreviewPanel } from "./chat/code-preview-panel";
import { MessageNavDots } from "./chat/message-nav-dots";
import { mentionAtCursor } from "@/components/ai-elements/mention-textarea-overlay";
import { extractMentions } from "@/lib/ir/parser";
import { MentionChip } from "@/lib/ir/mention-chip";

// ---- Helpers ----

const formatDateLabel = (date: string) =>
  new Date(date).toLocaleDateString([], {
    month: "long",
    day: "numeric",
    year: "numeric",
  });

function parseSSELine(line: string): Record<string, unknown> | null {
  const trimmed = line.trim();
  if (!trimmed.startsWith("data: ")) return null;
  try {
    return JSON.parse(trimmed.slice(6));
  } catch {
    return null;
  }
}

function drainSSEBuffer(buffer: string): {
  chunks: Record<string, unknown>[];
  remainder: string;
} {
  const lines = buffer.split("\n");
  const remainder = lines.pop() ?? "";
  const chunks: Record<string, unknown>[] = [];
  for (const line of lines) {
    const parsed = parseSSELine(line);
    if (parsed) chunks.push(parsed);
  }
  return { chunks, remainder };
}

function makeUserMessage(text: string, conversationId: string): Message {
  return {
    id: crypto.randomUUID(),
    conversation_id: conversationId,
    role: "user",
    content: text,
    content_type: "text",
    created_at: new Date().toISOString(),
  };
}

function makeAssistantMessage(
  id: string,
  content: string,
  conversationId: string,
): Message {
  return {
    id,
    conversation_id: conversationId,
    role: "assistant",
    content,
    content_type: "text",
    created_at: new Date().toISOString(),
  };
}

function safeStringify(v: unknown): string | undefined {
  if (typeof v === "string") return v;
  if (v == null) return undefined;
  try {
    return JSON.stringify(v);
  } catch {
    return String(v);
  }
}

// ---- Main component ----

export default function WorkplanChatConversation() {
  const { projectName = "", conversationId = "" } = useParams();
  const navigate = useNavigate();
  const location = useLocation();
  const prefill = (location.state as { prefill?: string } | null)?.prefill;

  const [conversation, setConversation] = useState<Conversation | null>(null);
  const [messages, setMessages] = useState<Message[]>([]);
  const [stream, setStream] = useState<StreamingState>(EMPTY_STREAM);
  const scrollRef = useRef<HTMLDivElement>(null);
  const abortRef = useRef<AbortController | null>(null);
  const sendingRef = useRef(false);

  const [sending, setSending] = useState(false);
  const [modelProvider, setModelProvider] = useState<string>("");
  const setSendingBoth = useCallback((v: boolean) => {
    sendingRef.current = v;
    setSending(v);
  }, []);

  // ---- Data fetching ----

  const loadData = useCallback(async () => {
    if (!conversationId) return;
    try {
      const [convRes, msgRes] = await Promise.all([
        client.agentGetConversation(conversationId),
        client.agentListMessages(conversationId, { limit: 100 }),
      ]);
      setConversation(convRes.data);
      setMessages(msgRes.data as unknown as Message[]);
      requestAnimationFrame(() => {
        scrollRef.current?.scrollTo({
          top: scrollRef.current.scrollHeight,
          behavior: "instant",
        });
      });
    } catch {
      // Non-critical; retried on next navigation.
    }
  }, [conversationId]);

  useEffect(() => {
    loadData();
  }, [loadData]);

  // ---- Auto-scroll ----

  const scrollToBottom = useCallback(() => {
    requestAnimationFrame(() => {
      scrollRef.current?.scrollTo({
        top: scrollRef.current.scrollHeight,
        behavior: "smooth",
      });
    });
  }, []);

  useEffect(() => {
    scrollToBottom();
  }, [messages, stream, scrollToBottom]);

  // ---- SSE stream processing ----

  function reduceStreamChunk(
    prev: StreamingState,
    chunk: Record<string, unknown>,
  ): {
    next: StreamingState;
    addAssistantMessage?: { id: string; content: string };
    reloadData?: boolean;
  } {
    const next: StreamingState = {
      ...prev,
      toolCalls: prev.toolCalls.map((tc) => ({ ...tc })),
    };

    switch (chunk.type) {
      case "phase_change":
        next.phase = chunk.phase as Phase;
        return { next };

      case "thinking":
        if (typeof chunk.content === "string") {
          next.phase = "think";
          next.thinkText += chunk.content;
        }
        return { next };

      case "delta":
        if (typeof chunk.content === "string") {
          next.phase = "answer";
          next.answerText += chunk.content;
        }
        return { next };

      case "tool_call_started": {
        next.phase = "act";
        const tc: ToolCallState = {
          toolCallId: chunk.tool_call_id as string,
          toolName: chunk.tool_name as string,
          arguments: chunk.arguments as string,
        };
        if (!next.toolCalls.some((t) => t.toolCallId === tc.toolCallId)) {
          next.toolCalls = [...next.toolCalls, tc];
        }
        return { next };
      }

      case "tool_call_finished": {
        next.phase = "act";
        const idx = next.toolCalls.findIndex(
          (t) => t.toolCallId === (chunk.tool_call_id as string),
        );
        if (idx !== -1) {
          const updated = { ...next.toolCalls[idx] };
          updated.output = safeStringify(chunk.output);
          updated.error = chunk.error as string | null;
          next.toolCalls = [
            ...next.toolCalls.slice(0, idx),
            updated,
            ...next.toolCalls.slice(idx + 1),
          ];
        }
        return { next };
      }

      case "done": {
        const output = safeStringify(chunk.output) ?? next.answerText;
        const messageId =
          (chunk.message_id as string) || crypto.randomUUID();
        return {
          next: EMPTY_STREAM,
          addAssistantMessage: { id: messageId, content: output },
          reloadData: !!chunk.message_id,
        };
      }

      case "title_updated":
        return { next, reloadData: true };

      case "subagent_started":
        return {
          next: {
            ...next,
            phase: "act" as Phase,
            toolCalls: [
              ...next.toolCalls,
              {
                toolCallId: chunk.subagent_id as string,
                toolName: `Subagent: ${chunk.role || "specialist"}`,
                arguments: chunk.task as string,
              },
            ],
          },
        };

      case "subagent_completed": {
        next.phase = "act";
        const saIdx = next.toolCalls.findIndex(
          (t) => t.toolCallId === (chunk.subagent_id as string),
        );
        if (saIdx !== -1) {
          const updated = { ...next.toolCalls[saIdx] };
          updated.output = safeStringify(chunk.output);
          next.toolCalls = [
            ...next.toolCalls.slice(0, saIdx),
            updated,
            ...next.toolCalls.slice(saIdx + 1),
          ];
        }
        return { next };
      }

      case "subagent_failed":
        return { next, reloadData: false };

      case "error":
        return {
          next: EMPTY_STREAM,
          addAssistantMessage: {
            id: crypto.randomUUID(),
            content: `Error: ${chunk.error || "Stream error"}`,
          },
        };

      default:
        return { next };
    }
  }

  const processChunk = useCallback(
    (chunk: Record<string, unknown>) => {
      setStream((prev) => {
        const { next, addAssistantMessage, reloadData } = reduceStreamChunk(
          prev,
          chunk,
        );

        if (addAssistantMessage && !reloadData) {
          const msg = makeAssistantMessage(
            addAssistantMessage.id,
            addAssistantMessage.content,
            conversationId,
          );
          queueMicrotask(() => {
            setMessages((prevMessages) => [...prevMessages, msg]);
          });
        }

        if (reloadData) {
          queueMicrotask(() => loadData());
        }

        return next;
      });
    },
    [conversationId, loadData],
  );

  // ---- Send message ----

  const sendMessage = useCallback(
    async (text: string) => {
      if (!text.trim() || !conversation || sendingRef.current) return;

      setSendingBoth(true);
      setStream(EMPTY_STREAM);

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

      setMessages((prev) => [
        ...prev,
        makeUserMessage(text, conversationId),
      ]);

      try {
        const response = await fetch(
          `/api/v1/agent/conversations/${conversationId}/stream`,
          {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            credentials: "include",
            body: JSON.stringify({
              session_id: conversation.session_id,
              conversation_id: conversationId,
              input: text,
              stream: true,
            }),
            signal: controller.signal,
          },
        );

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const reader = response.body?.getReader();
        if (!reader) throw new Error("No response body");

        const decoder = new TextDecoder();
        let buffer = "";

        while (true) {
          const { done, value } = await reader.read();
          if (done) break;

          buffer += decoder.decode(value, { stream: true });
          const { chunks, remainder } = drainSSEBuffer(buffer);
          buffer = remainder;

          for (const chunk of chunks) {
            processChunk(chunk);
          }
        }

        if (buffer.trim()) {
          const { chunks } = drainSSEBuffer(buffer + "\n");
          for (const chunk of chunks) {
            processChunk(chunk);
          }
        }
      } catch (err: unknown) {
        if (err instanceof Error && err.name === "AbortError") return;
        setMessages((prev) => [
          ...prev,
          makeAssistantMessage(
            crypto.randomUUID(),
            err instanceof Error
              ? `Error: ${err.message}`
              : "Request failed",
            conversationId,
          ),
        ]);
      } finally {
        setSendingBoth(false);
        abortRef.current = null;
      }
    },
    [conversation, conversationId, processChunk, setSendingBoth],
  );

  // ---- Prefill (quick-start) ----

  const prefilledRef = useRef(false);
  useEffect(() => {
    if (prefill && !prefilledRef.current && conversation && !sending) {
      prefilledRef.current = true;
      const timer = setTimeout(() => {
        sendMessage(prefill);
      }, 100);
      return () => clearTimeout(timer);
    }
  }, [prefill, conversation, sending, sendMessage]);

  // ---- Stop generation ----

  const handleStop = useCallback(() => {
    abortRef.current?.abort();
  }, []);

  // ---- Computed values ----

  const messageGroups = useMemo(() => {
    const groups: { date: string; messages: Message[] }[] = [];
    for (const msg of messages) {
      const ds = formatDateLabel(msg.created_at);
      const last = groups[groups.length - 1];
      if (last?.date === ds) {
        last.messages.push(msg);
      } else {
        groups.push({ date: ds, messages: [msg] });
      }
    }
    return groups;
  }, [messages]);

  const hasActiveStream = sending || stream.phase !== "idle";

  return (
    <PromptInputProvider>
      <CodePreviewProvider>
        <div className="flex h-full">
          <div className="flex min-w-0 flex-1 flex-col">
            <ChatInner
              projectName={projectName}
              navigate={navigate}
              conversation={conversation}
              messageGroups={messageGroups}
              stream={stream}
              hasActiveStream={hasActiveStream}
              sending={sending}
              modelProvider={modelProvider}
              onModelChange={(_name, provider) => {
                setModelProvider(provider);
              }}
              scrollRef={scrollRef}
              sendMessage={sendMessage}
              handleStop={handleStop}
            />
          </div>
          <CodePreviewPanel />
        </div>
      </CodePreviewProvider>
    </PromptInputProvider>
  );
}

// ---- Inner component ----

function ChatInner({
  projectName,
  navigate,
  conversation,
  messageGroups,
  stream,
  hasActiveStream,
  sending,
  modelProvider,
  onModelChange,
  scrollRef,
  sendMessage,
  handleStop,
}: {
  projectName: string;
  navigate: ReturnType<typeof useNavigate>;
  conversation: Conversation | null;
  messageGroups: { date: string; messages: Message[] }[];
  stream: StreamingState;
  hasActiveStream: boolean;
  sending: boolean;
  modelProvider: string;
  onModelChange: (name: string, provider: string) => void;
  scrollRef: React.RefObject<HTMLDivElement | null>;
  sendMessage: (text: string) => Promise<void>;
  handleStop: () => void;
}) {
  const { textInput } = usePromptInputController();
  const allMessages = useMemo(
    () => messageGroups.flatMap((g) => g.messages),
    [messageGroups],
  );

  const handleSubmit = useCallback(
    (text: string) => {
      if (text.trim()) {
        sendMessage(text);
        textInput.clear();
      }
    },
    [sendMessage, textInput],
  );

  const handleKeyDown = useCallback(
    (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
      // ---- Mention-aware backspace ----
      if (e.key === "Backspace") {
        const ta = e.currentTarget;
        const { selectionStart, selectionEnd } = ta;
        if (selectionStart === selectionEnd && selectionStart > 0) {
          const hit = mentionAtCursor(textInput.value, selectionStart);
          if (hit) {
            e.preventDefault();
            const newText =
              textInput.value.slice(0, hit.start) +
              textInput.value.slice(hit.end);
            textInput.setInput(newText);
            // Restore cursor to the position where the mention was.
            requestAnimationFrame(() => {
              ta.selectionStart = ta.selectionEnd = hit.start;
            });
            return;
          }
        }
      }

      // ---- Enter to submit ----
      if (e.key === "Enter" && !e.shiftKey && !sending) {
        e.preventDefault();
        const text = textInput.value;
        if (text.trim()) {
          sendMessage(text);
          textInput.clear();
        }
      }
    },
    [sendMessage, textInput, sending],
  );

  return (
    <div className="flex h-full flex-col overflow-hidden bg-background">
      {/* Header */}
      <header className="flex h-12 shrink-0 items-center gap-2.5 border-b border-border/60 bg-card/50 px-4 backdrop-blur-sm">
        <motion.button
          type="button"
          whileHover={{ scale: 1.05, x: -1 }}
          whileTap={{ scale: 0.92 }}
          className="inline-flex size-8 cursor-pointer items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
          onClick={() => navigate(`/${projectName}/workplan/chat`)}
          aria-label="Back to chat list"
        >
          <ArrowLeft className="size-4" aria-hidden="true" />
        </motion.button>

        <div className="flex min-w-0 flex-1 items-center gap-2">
          <div className="grid size-6 place-items-center rounded-md bg-primary/10">
            <Sparkles className="size-3 text-primary/70" />
          </div>
          <h1 className="truncate text-sm font-semibold text-foreground">
            {conversation?.title && conversation.title.trim().length > 2 ? conversation.title : "New conversation"}
          </h1>
        </div>
      </header>

      {/* Messages */}
      <div
        className="scrollbar-none relative min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-6"
        ref={scrollRef}
        role="log"
        aria-label="Conversation messages"
        aria-live="polite"
      >
        <MessageNavDots
          messages={allMessages}
          scrollContainerRef={scrollRef}
        />

        <AnimatePresence>
          {messageGroups.length === 0 && !hasActiveStream && (
            <motion.div
              initial={{ opacity: 0, scale: 0.96 }}
              animate={{ opacity: 1, scale: 1 }}
              exit={{ opacity: 0, scale: 0.96 }}
              transition={{ duration: 0.3, ease: "easeOut" }}
              className="flex h-full items-center justify-center"
            >
              <EmptyConversationState />
            </motion.div>
          )}
        </AnimatePresence>

        <div className="mx-auto max-w-3xl space-y-6">
          {messageGroups.map((group) => (
            <div key={group.date}>
              {/* Elegant date separator */}
              <div className="my-8 flex items-center justify-center gap-3">
                <div className="h-px w-12 bg-gradient-to-r from-transparent to-border/50" />
                <span className="rounded-full bg-muted/50 px-3 py-1 text-[11px] font-medium text-muted-foreground/60 shadow-sm">
                  {group.date}
                </span>
                <div className="h-px w-12 bg-gradient-to-l from-transparent to-border/50" />
              </div>

              <div className="space-y-4">
                {group.messages.map((msg) => (
                  <motion.div
                    key={msg.id}
                    data-msg-index={allMessages.indexOf(msg)}
                    data-msg-role={msg.role}
                    initial={{ opacity: 0, y: 10 }}
                    animate={{ opacity: 1, y: 0 }}
                    transition={{ duration: 0.25, ease: "easeOut" }}
                  >
                    <MessageBubble msg={msg} modelProvider={modelProvider} />
                  </motion.div>
                ))}
              </div>
            </div>
          ))}

          <AnimatePresence>
            {hasActiveStream && (
              <motion.div
                key="streaming"
                initial={{ opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
                exit={{ opacity: 0, y: 6 }}
                transition={{ duration: 0.2 }}
              >
                <StreamingView stream={stream} modelProvider={modelProvider} />
              </motion.div>
            )}
          </AnimatePresence>
        </div>
      </div>

      {/* Composer */}
      <footer className="shrink-0 border-t border-border/60 bg-card/70 backdrop-blur-md">
        <div className="mx-auto max-w-3xl px-4 py-3">
          {/* Repo mention popover — positioned above the input */}
          <div className="relative">
            <RepoMentionPopover
              workspaceName={projectName}
              textValue={textInput.value}
              onInsertMention={(mentionText, replaceFrom, replaceTo) => {
                const before = textInput.value.slice(0, replaceFrom);
                const after = textInput.value.slice(replaceTo);
                textInput.setInput(before + mentionText + after);
              }}
            />
          </div>
          {/* Active mention chips — shows which repos are referenced */}
          {(() => {
            const mentions = extractMentions(textInput.value);
            if (mentions.length === 0) return null;
            return (
              <div className="mb-1.5 flex flex-wrap gap-1.5">
                {mentions.map((m, i) => (
                  <MentionChip
                    key={`${m.entityId}-${i}`}
                    entityType={m.entityType}
                    entityId={m.entityId}
                    entityLabel={m.entityLabel}
                  />
                ))}
              </div>
            );
          })()}
          <PromptInput
            globalDrop={false}
            onSubmit={({ text }) => handleSubmit(text)}
          >
            <PromptInputBody>
              <PromptInputTextarea
                className="min-h-[48px] max-h-[200px] resize-none rounded-xl border-border/60 bg-muted/40 text-sm shadow-inner transition-colors focus-visible:bg-background focus-visible:ring-1 focus-visible:ring-primary/20"
                placeholder="Type a message… (use @ to mention a repo)"
                onKeyDown={handleKeyDown}
              />
            </PromptInputBody>
            <PromptInputFooter>
              <PromptInputTools>
                <ModelSelectorPopover
                  sessionId={conversation?.session_id}
                  onModelChange={onModelChange}
                />
              </PromptInputTools>
              <PromptInputSubmit
                status={sending ? "streaming" : undefined}
                onStop={handleStop}
              />
            </PromptInputFooter>
          </PromptInput>
          <p className="mt-1.5 text-center text-[11px] text-muted-foreground/30">
            AI responses may be inaccurate. Verify important information.
          </p>
        </div>
      </footer>
    </div>
  );
}

/** Empty state shown when conversation has no messages. */
function EmptyConversationState() {
  return (
    <div className="text-center">
      <div className="mx-auto grid size-16 place-items-center rounded-2xl bg-gradient-to-br from-primary/10 to-primary/[0.02] ring-1 ring-primary/10 shadow-sm">
        <Sparkles className="size-7 text-primary/30" />
      </div>
      <p className="mt-4 text-sm font-medium text-muted-foreground/60">
        Start a conversation
      </p>
      <p className="mt-1 text-xs text-muted-foreground/35">
        Type a message below to begin
      </p>
    </div>
  );
}
