import type { KeyboardEvent } from 'react';
import { useState, useRef, useEffect, useCallback } from 'react';
import { useUserVisibleModels } from 'state/useModels';
import { useChat, type UseChatType } from '../../../hooks/useChat';
import { useAttachments } from '../useAttachments';
import { useSlashCommands } from '../useSlashCommands';

type UseEmbeddableChatParams = {
  sessionId?: string;
  initialModel?: string | null;
  initialMessage?: {
    text: string;
    attachmentIds?: string[];
    images?: { filename: string; dataUrl: string }[];
    cwd?: { root?: string; path: string };
  };
  defaultInput?: string;
  promptPrefix?: string;
  cwd?: { root?: string; path: string };
  replaceUrl?: boolean;
  autoSend?: boolean;
  chat?: UseChatType;
  context?: string;
  contextId?: string;
};

export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComplete?: () => void) {
  const { initialMessage, defaultInput = '', promptPrefix, cwd, autoSend = false, chat: externalChat } = params;

  const internalChat = useChat(params.sessionId, params.initialModel, {
    replaceUrl: params.replaceUrl ?? false,
    context: params.context,
    contextId: params.contextId,
  });
  const chat = externalChat ?? internalChat;

  const {
    messages,
    streamingText,
    isConnected,
    isGenerating,
    sessionId,
    model,
    selectedModel,
    hasStarted,
    thinkingLevel,
    setSelectedModel,
    setThinkingLevel,
    sendPrompt,
    stopGeneration: stopTurn,
    loadOlder,
    hasMoreOlder,
    isLoadingOlder,
  } = chat;

  const availableModels = useUserVisibleModels();
  const attachmentManager = useAttachments({ sessionId });
  const slashCommands = useSlashCommands({ sessionId });

  // Detect when agent finishes a message (isGenerating: true → false)
  const wasGeneratingRef = useRef(false);
  useEffect(() => {
    if (isGenerating) {
      wasGeneratingRef.current = true;
    } else if (wasGeneratingRef.current) {
      wasGeneratingRef.current = false;
      onMessageComplete?.();
    }
  }, [isGenerating]);

  const [input, setInput] = useState(defaultInput);
  const [showJumpToBottom, setShowJumpToBottom] = useState(false);
  const [commandFeedback, setCommandFeedback] = useState<string | null>(null);
  const textareaRef = useRef<HTMLTextAreaElement | null>(null);
  const scrollViewportRef = useRef<HTMLDivElement | null>(null);
  const bottomRef = useRef<HTMLDivElement | null>(null);
  const userScrolledRef = useRef(false);
  const didInitialScrollRef = useRef(false);
  // When the reader last touched the scroller. Scroll *position* cannot tell a deliberate scroll-up from
  // the list growing under an animation or a row measuring taller than its estimate, and reading it as
  // intent is what used to switch auto-scroll off mid-turn and leave it off.
  const gestureAtRef = useRef(0);
  // The prompt currently in flight, kept verbatim so a stop can put it back in the composer.
  const lastSentRef = useRef('');

  // Latest values for the (once-attached) scroll listener, without re-subscribing on every page load.
  const loadOlderRef = useRef(loadOlder);
  loadOlderRef.current = loadOlder;
  const hasMoreOlderRef = useRef(hasMoreOlder);
  hasMoreOlderRef.current = hasMoreOlder;
  const isLoadingOlderRef = useRef(isLoadingOlder);
  isLoadingOlderRef.current = isLoadingOlder;

  const handleSend = async () => {
    const text = input.trim();
    if (!text || isGenerating) return;

    // Handle slash commands
    if (text.startsWith('/')) {
      const result = await slashCommands.execute(text);
      if (result.handled) {
        setCommandFeedback(result.feedback);
        setInput('');
        if (textareaRef.current) textareaRef.current.style.height = 'auto';
        return;
      }
    }
    setCommandFeedback(null);

    const { prefix, ids, images } = attachmentManager.processAttachments();
    let prompt = promptPrefix ? `${promptPrefix}\n\n${text}` : text;
    if (prefix) prompt = `${prefix}${prompt}`;

    // Send the cwd on every message (not just the first): OpenCode rebuilds its working-directory
    // system prompt each turn, so it needs the cwd every time. It stays constant for a session.
    const displayText = promptPrefix ? text : undefined;
    sendPrompt(
      prompt,
      !sessionId && ids.length > 0 ? ids : undefined,
      images.length > 0 ? images : undefined,
      cwd,
      undefined,
      thinkingLevel,
      displayText,
    );

    attachmentManager.clearAttachments();
    lastSentRef.current = input; // verbatim, not the trimmed prompt — this is what comes back on a stop
    setInput('');
    userScrolledRef.current = false;
    if (textareaRef.current) textareaRef.current.style.height = 'auto';
  };

  /**
   * Stop, and hand the prompt back. Interrupting almost always means "not like that" — you want to say
   * it differently — and retyping it from the transcript is busywork. The text returns exactly as typed,
   * newlines and all.
   *
   * It never overwrites: if you've started composing something else while it ran, that wins and the old
   * prompt is dropped. Losing what you just typed to a stop you pressed would be the worse failure.
   */
  const stopGeneration = () => {
    stopTurn();
    const sent = lastSentRef.current;
    lastSentRef.current = '';
    if (!sent || input.trim()) return;
    setInput(sent);
    requestAnimationFrame(() => textareaRef.current?.focus());
  };

  // Escape is the stop button, as it is in Claude Code itself. Bound to the chat's own subtree rather
  // than the document: two chat panels can be generating at once, and a document listener in each would
  // make one Escape stop both.
  const handleEscape = (ev: KeyboardEvent<HTMLElement>) => {
    if (ev.key !== 'Escape' || !isGenerating) return;
    ev.preventDefault();
    stopGeneration();
  };

  const handleKeyDown = (ev: KeyboardEvent<HTMLTextAreaElement>) => {
    if (ev.key === 'Escape') {
      // Handled here so a standalone InputArea still stops on Escape; stopped from bubbling so the
      // container's copy of the same handler doesn't fire a second stop for one keypress.
      ev.stopPropagation();
      handleEscape(ev);
      return;
    }
    if (ev.key === 'Enter' && !ev.shiftKey) {
      ev.preventDefault();
      handleSend();
    }
  };

  const jumpToBottom = () => {
    userScrolledRef.current = false;
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  const appendToInput = (text: string) => {
    setInput((prev) => prev + (prev.length > 0 ? ' ' : '') + text);
  };

  // Auto-resize textarea
  const resizeTextarea = () => {
    const textarea = textareaRef.current;
    if (!textarea) return;
    textarea.style.height = 'auto';
    textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
  };

  useEffect(resizeTextarea, [input]);

  // Re-measure when textarea width changes (e.g. panel animation)
  useEffect(() => {
    const textarea = textareaRef.current;
    if (!textarea) return;
    let rafId = 0;
    let prevWidth = textarea.clientWidth;
    const observer = new ResizeObserver(() => {
      const width = textarea.clientWidth;
      if (width === prevWidth) return;
      prevWidth = width;
      cancelAnimationFrame(rafId);
      rafId = requestAnimationFrame(resizeTextarea);
    });
    observer.observe(textarea);
    return () => {
      observer.disconnect();
      cancelAnimationFrame(rafId);
    };
  }, []);

  /**
   * Pin to the bottom, unless the reader has deliberately scrolled away.
   *
   * Instant, never smoothed. A smooth scroll takes hundreds of milliseconds during which the view is *not*
   * at the bottom, and a turn producing output interrupts it long before it arrives — so the chase never
   * finished, and every frame of it read as the reader having scrolled up. Instant lands in one event and
   * cannot be outrun by the content it is chasing.
   */
  const stickToBottom = useCallback(() => {
    if (userScrolledRef.current) return;
    bottomRef.current?.scrollIntoView({ behavior: 'auto' });
  }, []);

  // Two frames on the first pass, so the jump repeats once the tail window has measured itself. Prepending
  // older messages doesn't fire this — the reader has scrolled up by then and `stickToBottom` declines.
  useEffect(() => {
    stickToBottom();
    if (didInitialScrollRef.current) return;
    didInitialScrollRef.current = true;
    requestAnimationFrame(stickToBottom);
  }, [messages, streamingText, stickToBottom]);

  // Detect user scrolling up; near the top, lazy-load the next older window (reverse infinite scroll).
  useEffect(() => {
    const viewport = scrollViewportRef.current;
    if (!viewport) return;

    // Anything that can move a scroller by hand: the wheel, a drag on the bar, a finger, the keyboard.
    // Momentum after a flick needs no window of its own — the gesture itself has already carried the view
    // off the bottom and set the flag, and only arriving back at the bottom clears it.
    const GESTURE_MS = 700;
    const markGesture = () => {
      gestureAtRef.current = performance.now();
    };

    const handleScroll = () => {
      const { scrollTop, scrollHeight, clientHeight } = viewport;
      const atBottom = scrollHeight - scrollTop - clientHeight < 60;
      // Being at the bottom is unambiguous, so it always re-arms. Leaving the bottom only counts when the
      // reader did it: otherwise a row measuring taller than its 150px estimate, or a fold shrinking, reads
      // as "they want to stay up here" and auto-scroll never comes back on.
      if (atBottom) userScrolledRef.current = false;
      else if (performance.now() - gestureAtRef.current < GESTURE_MS) userScrolledRef.current = true;
      setShowJumpToBottom(!atBottom);
      // Only after the initial bottom-anchor has run, so the mount's scrollTop=0 doesn't trigger a fetch.
      if (didInitialScrollRef.current && hasMoreOlderRef.current && !isLoadingOlderRef.current && scrollTop < 200) {
        loadOlderRef.current();
      }
    };

    viewport.addEventListener('scroll', handleScroll);
    viewport.addEventListener('wheel', markGesture, { passive: true });
    viewport.addEventListener('touchmove', markGesture, { passive: true });
    viewport.addEventListener('mousedown', markGesture);
    viewport.addEventListener('keydown', markGesture);
    return () => {
      viewport.removeEventListener('scroll', handleScroll);
      viewport.removeEventListener('wheel', markGesture);
      viewport.removeEventListener('touchmove', markGesture);
      viewport.removeEventListener('mousedown', markGesture);
      viewport.removeEventListener('keydown', markGesture);
    };
  }, []);

  // Focus textarea on mount
  useEffect(() => {
    textareaRef.current?.focus();
  }, []);

  // Auto-send initial message
  const initialSentRef = useRef(false);
  useEffect(() => {
    if (initialMessage && isConnected && !initialSentRef.current) {
      initialSentRef.current = true;
      if (params.initialModel) setSelectedModel(params.initialModel);
      sendPrompt(
        initialMessage.text,
        initialMessage.attachmentIds,
        initialMessage.images,
        initialMessage.cwd,
        undefined,
      );
    }
  }, [initialMessage, isConnected]);

  // Auto-send when autoSend is enabled
  const autoSentRef = useRef(false);
  useEffect(() => {
    if (autoSend && isConnected && !messages.length && input.trim() && !autoSentRef.current) {
      autoSentRef.current = true;
      handleSend();
    }
  }, [autoSend, isConnected, messages.length, input]);

  return {
    messages,
    streamingText,
    isConnected,
    isGenerating,
    sessionId,
    model,
    selectedModel,
    hasStarted,
    thinkingLevel,
    setSelectedModel,
    setThinkingLevel,
    sendPrompt,
    stopGeneration,
    input,
    setInput,
    handleSend,
    handleKeyDown,
    handleEscape,
    appendToInput,
    attachments: attachmentManager.attachments,
    attachWebpage: attachmentManager.attachWebpage,
    attachImage: attachmentManager.attachImage,
    removeAttachment: attachmentManager.removeAttachment,
    availableModels,
    showJumpToBottom,
    jumpToBottom,
    stickToBottom,
    commandFeedback,
    textareaRef,
    scrollViewportRef,
    bottomRef,
    hasMoreOlder,
    isLoadingOlder,
  };
}

export type UseEmbeddableChatType = ReturnType<typeof useEmbeddableChat>;
