import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { themedColor } from '../../theme';
import { ActivityIndicator, Keyboard, Pressable, ScrollView, StyleSheet, Text, useWindowDimensions, View, type NativeScrollEvent, type NativeSyntheticEvent } from 'react-native';
import { ArrowDown } from 'lucide-react-native';

import { resolveThinkingPhase } from './agentPhase';
import {
  buildOutOfCreditsShownProperties,
  buildOutOfCreditsUpgradeProperties,
  isCreditSendAllowed,
} from '../credits/creditsUtils';
import { ClarifyingQuestionsCard } from '../../toolWidgets/clarifyingQuestions/ClarifyingQuestionsCard';
import { composerStyles } from './composerStyles';
import { ConversationComposer } from './ConversationComposer';
import { ConversationMessageList } from './ConversationMessageList';
import { QueuedMessages } from './QueuedMessages';
import { MarkdownScrollContext, type MarkdownScroller } from './markdownScrollContext';
import { useAgentBi } from '../../analytics/mixpanelContext';
import { TypingIndicator } from './conversationParts';
import { conversationStyles } from './conversationStyles';
import { styles } from '../../styles';
import { findPendingClarification } from './conversationRuntime';
import { buildAttachmentPrompt, MAX_ATTACHMENTS } from '../attachments/mediaUtils';
import { getModelLabel } from '../settings/modelOptions';
import {
  useSuperagentConnectors,
  useSuperagentConversationRuntime,
  useSuperagentMedia,
  useSuperagentShellOptions,
} from '../../runtime/runtimeContext';
import { hasRealAssistantMessage } from './promptSuggestionUtils';
import { useKeyboardHeight } from './useKeyboardHeight';
import { usePromptSuggestions } from './usePromptSuggestions';
import type {
  SuperagentAgent,
  SuperagentMediaAttachment,
  SuperagentMessage,
  SuperagentNativeClient,
  SuperagentReplyTo,
} from '../../types';
import type { useSuperagentConversation } from './useSuperagentConversation';

const MAX_CHAT_WIDTH = 900;
const SCROLL_TO_BOTTOM_THRESHOLD = 120; // px from bottom
const CLARIFICATION_MAX_HEIGHT_RATIO = 0.55; // leaves the transcript visible above
const CLARIFICATION_MIN_HEIGHT = 220; // floor once the keyboard eats into the ratio's budget

const mergeTranscript = (existing: string, transcript: string) =>
  existing ? `${existing} ${transcript}` : transcript;

type ConversationState = ReturnType<typeof useSuperagentConversation>;

export function ConversationChat({
  agent,
  apiClient,
  initialMessage,
  connectorIds,
  conversation,
  onOpenModelSettings,
}: {
  agent: SuperagentAgent;
  /** REST client, used here to fetch prompt suggestions. Absent in fallback-only mode. */
  apiClient?: SuperagentNativeClient;
  /** Auto-sent once as the first message when the conversation is ready (fresh agent). */
  initialMessage?: string;
  connectorIds?: string[];
  conversation: ConversationState;
  onOpenModelSettings?: () => void;
}) {
  const bi = useAgentBi();
  const { height: windowHeight } = useWindowDimensions();
  const keyboardHeight = useKeyboardHeight();
  const { availableConnectors, onConnectConnector } = useSuperagentConnectors();
  const { currentUserAvatarUrl } = useSuperagentConversationRuntime();
  const { onPickFiles, onPickPhotos, onTakePhoto, onStartLiveVoice } = useSuperagentMedia();
  const { contentTopInset, onViewPlans, showDebugPayloads } = useSuperagentShellOptions();
  // Import-from-drive/voice-input/markdown/tool-renderer overrides aren't part
  // of the mobile runtime surface; the message list + composer fall back to their
  // built-in defaults, so those child props are simply left unset.
  const scrollRef = useRef<ScrollView>(null);
  // Host-component ref for anchor-link measureLayout (Fabric requires a native
  // component ref as the relative node, not a findNodeHandle number).
  const messagesContainerRef = useRef<View>(null);
  const shouldAutoScrollRef = useRef(true);
  // Re-arm tail-follow only on an away→bottom transition, never while parked at the bottom.
  const wasAwayFromBottomRef = useRef(false);
  // Lets a content-size change (streaming growth) recompute the button without a scroll event.
  const lastScrollMetricsRef = useRef({ offsetY: 0, layoutHeight: 0 });
  const lastTailMessageIdRef = useRef<string | null>(null);
  const [draft, setDraft] = useState('');
  const [attachments, setAttachments] = useState<SuperagentMediaAttachment[]>([]);
  const [replyTo, setReplyTo] = useState<SuperagentReplyTo | null>(null);
  const [showScrollToBottom, setShowScrollToBottom] = useState(false);
  // Conversation failed to initialize (apiClient path): no conversation id to send
  // against, so block sends rather than clear the composer and drop the text.
  const conversationUnavailable = Boolean(conversation.initError && !conversation.conversationId);
  // Prompt-suggestion chips above the composer, gated on a real agent reply existing.
  const hasAssistantMessage = useMemo(
    () => hasRealAssistantMessage(conversation.messages),
    [conversation.messages],
  );
  const suggestions = usePromptSuggestions({
    apiClient,
    conversationId: conversation.conversationId,
    isSending: conversation.isSending,
    hasAssistantMessage,
    agentMessageDoneSignal: conversation.agentMessageDoneSignal,
  });
  // Gated on the answer being in flight, not the busy state: busy lags the tool
  // status both ways — still true as a question lands, already false mid-resume.
  const pendingClarification = useMemo(
    () => (conversation.isResolvingClarification ? undefined : findPendingClarification(conversation.messages)),
    [conversation.isResolvingClarification, conversation.messages],
  );
  const [isClarificationCollapsed, setIsClarificationCollapsed] = useState(false);
  const pendingClarificationId = pendingClarification?.id;
  useEffect(() => setIsClarificationCollapsed(false), [pendingClarificationId]); // a new question opens expanded
  const creditSendAllowed = isCreditSendAllowed(suggestions);
  const hasComposerContent = draft.trim().length > 0 || attachments.length > 0;
  const canSend = hasComposerContent
    && !conversation.isSending
    && !conversation.isLoading
    && !conversationUnavailable
    && creditSendAllowed;
  // Enqueue behind the running turn: same gates as canSend but while busy, and only
  // when the queue isn't already full.
  const canQueue = hasComposerContent
    && conversation.isSending
    && !conversation.isLoading
    && !conversationUnavailable
    && creditSendAllowed
    && !conversation.isQueueFull;
  const reportedOutOfCreditsRef = useRef(false);
  useEffect(() => {
    if (reportedOutOfCreditsRef.current || !suggestions.isOutOfCredits || !suggestions.currentUsage) return;
    reportedOutOfCreditsRef.current = true;
    void bi.track('User out of credits', buildOutOfCreditsShownProperties(suggestions.currentUsage));
  }, [bi, suggestions.currentUsage, suggestions.isOutOfCredits]);
  const handleOutOfCreditsViewPlans = useCallback(() => {
    if (suggestions.currentUsage) {
      void bi.track('User clicked upgrade', buildOutOfCreditsUpgradeProperties(suggestions.currentUsage));
    }
    onViewPlans?.();
    void suggestions.refreshUsage();
  }, [bi, onViewPlans, suggestions.currentUsage, suggestions.refreshUsage]);
  // Plain object (not useMemo): the hook returns a fresh state object each render, so a
  // memo keyed on it would never cache, and ConversationComposer isn't memoized so its
  // identity doesn't matter. This is the composer's view-model — only the fields the
  // chips need, decoupled from the hook's send-flow methods (dismissOnSend/undo...).
  const composerSuggestions = {
    items: suggestions.suggestions,
    visible: suggestions.visible,
    showRestore: suggestions.showRestore,
    refreshing: suggestions.refreshing,
    onSelect: suggestions.reportSelect,
    onRefresh: suggestions.refresh,
    onDismiss: suggestions.dismiss,
    onRestore: suggestions.restore,
  };
  const displayedMessages = useMemo(
    () => {
      if (conversation.messages.length > 0) return conversation.messages;
      // While the real conversation is still loading, don't fabricate a welcome
      // bubble — it would render next to the loading panel as if the agent already
      // sent an intro.
      if (conversation.isLoading) return [];
      // A seeded prompt will be the first turn (auto-sent, intro skipped), so don't
      // show the fabricated welcome placeholder before/alongside it.
      if (initialMessage?.trim()) return [];
      return [createWelcomeMessage(agent)];
    },
    [agent, conversation.messages, conversation.isLoading, initialMessage],
  );
  // Derived once per message-list change rather than on every streamed-token
  // re-render (the chat re-renders frequently while a turn is in flight).
  const thinkingPhase = useMemo(() => resolveThinkingPhase(displayedMessages), [displayedMessages]);
  const tailMessage = displayedMessages[displayedMessages.length - 1];
  const tailMessageId = tailMessage ? getMessageAutoScrollId(tailMessage, displayedMessages.length - 1) : null;
  const tailMessageContent = tailMessage?.content ?? '';

  const scrollToBottom = useCallback((animated = true) => {
    setTimeout(() => scrollRef.current?.scrollToEnd({ animated }), 0);
  }, []);

  // Bring a markdown heading into view when an in-document anchor link is tapped
  // (handed down to MarkdownText via context). Pausing auto-scroll keeps the
  // tail-follow effect from yanking us back to the bottom.
  const markdownScroller = useMemo<MarkdownScroller>(() => ({
    scrollViewIntoView: (target) => {
      const container = messagesContainerRef.current;
      if (!container) return;
      // measureLayout returns y relative to the messages container, which sits
      // below the scroll content's top padding (contentTopInset when the host
      // hides the header, otherwise the scrollContent paddingTop). Add it so the
      // heading lands at the top instead of below it.
      const scrollContentPaddingTop = Number(StyleSheet.flatten(conversationStyles.scrollContent).paddingTop) || 0;
      const topInset = contentTopInset ?? scrollContentPaddingTop;
      target.measureLayout(
        container,
        (_x, y) => {
          shouldAutoScrollRef.current = false;
          scrollRef.current?.scrollTo({ animated: true, y: Math.max(y + topInset - 12, 0) });
        },
        () => {},
      );
    },
  }), [contentTopInset]);

  useEffect(() => {
    if (!tailMessageId) return;

    const tailMessageChanged = lastTailMessageIdRef.current !== tailMessageId;
    if (tailMessageChanged) {
      lastTailMessageIdRef.current = tailMessageId;
      shouldAutoScrollRef.current = true;
    }

    if (shouldAutoScrollRef.current) {
      scrollToBottom(tailMessageChanged);
    }
  }, [conversation.isSending, scrollToBottom, tailMessageContent, tailMessageId]);

  const pauseAutoScroll = useCallback(() => {
    shouldAutoScrollRef.current = false;
  }, []);

  const handleChatTouchStart = useCallback(() => {
    pauseAutoScroll();
    Keyboard.dismiss();
  }, [pauseAutoScroll]);

  const scrollAfterContentResize = useCallback((_contentWidth: number, contentHeight: number) => {
    const { layoutHeight } = lastScrollMetricsRef.current;
    if (shouldAutoScrollRef.current) {
      scrollToBottom(false);
      // Non-animated scrollToEnd may emit no onScroll, so sync the derived state by hand.
      if (layoutHeight > 0) lastScrollMetricsRef.current.offsetY = Math.max(contentHeight - layoutHeight, 0);
      setShowScrollToBottom(false);
      wasAwayFromBottomRef.current = false;
      return;
    }
    // Streaming growth fires no scroll event while paused — recompute from the last metrics.
    const { offsetY } = lastScrollMetricsRef.current;
    if (layoutHeight > 0) {
      const distanceFromBottom = contentHeight - offsetY - layoutHeight;
      const isAway = distanceFromBottom > SCROLL_TO_BOTTOM_THRESHOLD;
      setShowScrollToBottom(isAway);
      wasAwayFromBottomRef.current = isAway;
    }
  }, [scrollToBottom]);

  const handleScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
    const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
    lastScrollMetricsRef.current = { offsetY: contentOffset.y, layoutHeight: layoutMeasurement.height };
    const distanceFromBottom = contentSize.height - contentOffset.y - layoutMeasurement.height;
    const atBottom = distanceFromBottom <= SCROLL_TO_BOTTOM_THRESHOLD;
    setShowScrollToBottom(!atBottom);
    if (atBottom && wasAwayFromBottomRef.current) shouldAutoScrollRef.current = true;
    wasAwayFromBottomRef.current = !atBottom;
  }, []);

  const handleScrollToBottomPress = useCallback(() => {
    shouldAutoScrollRef.current = true;
    scrollToBottom(true);
  }, [scrollToBottom]);

  // Bound to the seed text so a manual resend of the seeded prompt still carries its
  // connectors, but editing the draft away drops them (mirrors the web composer).
  const pendingSeedRef = useRef<{ text: string; connectorIds: string[] } | undefined>(undefined);

  // Core send path, parameterized on the draft text so callers that just merged
  // new text into the draft (e.g. dictation send-now) don't race the state update.
  const sendDraftContent = useCallback(async (selectedDraft: string) => {
    const selectedAttachments = attachments;
    const selectedReplyTo = replyTo;
    const content = selectedDraft.trim() || buildAttachmentPrompt(selectedAttachments);
    const seed = pendingSeedRef.current;
    // Only the unedited seed prompt inherits its connectors.
    const seedConnectors = seed && content === seed.text ? seed.connectorIds : undefined;

    if (!creditSendAllowed) return;

    Keyboard.dismiss();
    setDraft('');
    setAttachments([]);
    setReplyTo(null);
    // Hide suggestions for this turn; a fresh set arrives when the agent replies.
    suggestions.dismissOnSend();

    const delivered = await conversation.sendMessage(content, {
      fileUrls: selectedAttachments.map((attachment) => attachment.url),
      replyTo: selectedReplyTo ?? undefined,
      connectorIds: seedConnectors,
    });
    // Keep the seed only to retry the same failed prompt; drop it otherwise.
    if (delivered || !seed || content !== seed.text) {
      pendingSeedRef.current = undefined;
    }
    if (!delivered) {
      // The send failed before reaching the conversation — restore what the user
      // had typed (unless they've already started a new draft in the meantime), and
      // revert the optimistic dismiss so the chips they had don't stay hidden.
      setDraft((current) => current || selectedDraft);
      setAttachments((current) => (current.length > 0 ? current : selectedAttachments));
      setReplyTo((current) => current ?? selectedReplyTo);
      suggestions.undoDismissOnSend();
    }
  }, [attachments, conversation, conversationUnavailable, creditSendAllowed, replyTo, suggestions.dismissOnSend, suggestions.undoDismissOnSend]);

  const sendMessage = useCallback(() => sendDraftContent(draft), [draft, sendDraftContent]);

  // Dictation "send now": merge the transcript into the draft (so a failed send
  // restores it into the composer) and send that merged content directly. Reads
  // draft + send path through refs: it's invoked after the async transcription
  // gap, and the in-flight composer closure would otherwise send the draft (and
  // attachments) as they were when the recording was stopped — ignoring e.g. a
  // suggestion tapped or an attachment removed while "Transcribing…" was up.
  const draftRef = useRef(draft);
  draftRef.current = draft;
  const sendDraftContentRef = useRef(sendDraftContent);
  sendDraftContentRef.current = sendDraftContent;
  const sendTranscript = useCallback((transcript: string) => {
    const merged = mergeTranscript(draftRef.current, transcript);
    setDraft(merged);
    void sendDraftContentRef.current(merged);
  }, []);

  // Auto-send the seeded prompt (from the home composer / a tapped idea) as the
  // first message, once the conversation is ready. The seed is claimed once per mount
  // (see pendingFirstMessage), so this ref only guards against the async send being
  // re-entered by a deps-change re-render.
  const autoSendStartedRef = useRef(false);
  useEffect(() => {
    if (autoSendStartedRef.current) return;
    const text = initialMessage?.trim();
    if (!text) return;
    if (!suggestions.usageReady) return;
    // Auto-send can never run — credits are exhausted, or init failed terminally
    // (no conversation id and no fallback) — so surface the seed in the composer
    // once instead of losing it.
    if (conversationUnavailable || suggestions.isOutOfCredits) {
      autoSendStartedRef.current = true;
      if (connectorIds?.length) pendingSeedRef.current = { text, connectorIds };
      setDraft((current) => current || text);
      return;
    }
    // Wait until the send path can actually deliver — a conversation id (apiClient)
    // or an onSendMessage fallback; sending before then drops the message.
    if (!conversation.isSendReady || conversation.isLoading || conversation.isSending) return;
    // Latch before the async send so a deps-change re-render can't double-fire it.
    autoSendStartedRef.current = true;
    // Hide suggestions for this turn, same as a composer send (a fresh set arrives on
    // reply) — otherwise stale chips can flash once the seeded turn completes.
    suggestions.dismissOnSend();
    void (async () => {
      const delivered = await conversation.sendMessage(text, { connectorIds });
      if (!delivered) {
        // Surface the prompt + its connectors in the composer so the resend keeps them.
        if (connectorIds?.length) pendingSeedRef.current = { text, connectorIds };
        setDraft((current) => current || text);
        suggestions.undoDismissOnSend();
      }
    })();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    initialMessage,
    conversation.isSendReady,
    conversation.isLoading,
    conversation.isSending,
    conversationUnavailable,
    suggestions.isOutOfCredits,
    suggestions.usageReady,
  ]);

  const addAttachments = useCallback((incoming: SuperagentMediaAttachment[]) => {
    setAttachments((current) => [...current, ...incoming].slice(0, MAX_ATTACHMENTS));
  }, []);

  const appendTranscript = useCallback((transcript: string) => {
    setDraft((current) => mergeTranscript(current, transcript));
  }, []);

  const replyToMessage = useCallback((message: SuperagentMessage) => {
    const content = message.content?.trim();
    if (!content) return;
    setReplyTo({
      content,
      messageId: message.id,
    });
  }, []);

  const composerContext = useMemo(
    () => ({ agentId: agent.id, conversationId: conversation.conversationId }),
    [agent.id, conversation.conversationId],
  );

  const modelLabel = getModelLabel(agent.model);

  const clearReply = useCallback(() => setReplyTo(null), []);

  const removeAttachment = useCallback(
    (index: number) => setAttachments((current) => current.filter((_, itemIndex) => itemIndex !== index)),
    [],
  );

  return (
    <MarkdownScrollContext.Provider value={markdownScroller}>
      <View style={conversationStyles.chatArea}>
      <ScrollView
        ref={scrollRef}
        contentContainerStyle={[
          conversationStyles.scrollContent,
          contentTopInset != null ? { paddingTop: contentTopInset } : null,
        ]}
        keyboardDismissMode="on-drag"
        keyboardShouldPersistTaps="handled"
        onLayout={(event) => { lastScrollMetricsRef.current.layoutHeight = event.nativeEvent.layout.height; }}
        onContentSizeChange={scrollAfterContentResize}
        onScroll={handleScroll}
        onScrollBeginDrag={pauseAutoScroll}
        onTouchStart={handleChatTouchStart}
        scrollEventThrottle={16}
      >
        <View ref={messagesContainerRef} style={[conversationStyles.messagesContainer, { maxWidth: MAX_CHAT_WIDTH }]}>
          <ConversationStates conversation={conversation} />
          <ConversationMessageList
            agent={agent}
            availableConnectors={availableConnectors}
            conversationId={conversation.conversationId}
            currentUserAvatarUrl={currentUserAvatarUrl}
            messages={displayedMessages}
            onConnectConnector={onConnectConnector}
            onReplyMessage={replyToMessage}
            showDebugPayloads={showDebugPayloads}
            submitToolCallInput={conversation.submitToolCallInput}
          />
          {/* Show the live status whenever the agent is working — either a send is
              in flight, or the current turn has an active tool (e.g. resumed after
              an approval, or in_progress) where isSending isn't set. The in-bubble
              timeline is hidden in those cases, so this stays the single status row. */}
          {(conversation.isSending || thinkingPhase) ? <TypingIndicator agent={agent} phase={thinkingPhase} /> : null}
            <QueuedMessages
              messages={conversation.queuedMessages}
              onDelete={conversation.deleteQueuedMessage}
              onEdit={conversation.editQueuedMessage}
            />
        </View>
      </ScrollView>
        {showScrollToBottom ? (
          <Pressable
            accessibilityLabel="Scroll to latest messages"
            accessibilityRole="button"
            onPress={handleScrollToBottomPress}
            style={({ pressed }) => [conversationStyles.scrollToBottomButton, pressed && styles.pressed]}
          >
            <ArrowDown color={themedColor('#F4F4F5')} size={20} strokeWidth={2.4} />
          </Pressable>
        ) : null}
      </View>
      {/* Sits outside the transcript's ScrollView. maxHeight subtracts keyboardHeight
          because useWindowDimensions doesn't shrink for the keyboard on its own. */}
      {pendingClarification ? (
        <View style={composerStyles.wrap}>
          <View style={{ maxWidth: MAX_CHAT_WIDTH, width: '100%' }}>
            <ClarifyingQuestionsCard
              isCollapsed={isClarificationCollapsed}
              isKeyboardVisible={keyboardHeight > 0}
              key={pendingClarification.id}
              maxHeight={Math.max(
                CLARIFICATION_MIN_HEIGHT,
                windowHeight * CLARIFICATION_MAX_HEIGHT_RATIO - keyboardHeight,
              )}
              onToggleCollapsed={() => setIsClarificationCollapsed((collapsed) => !collapsed)}
              submitToolCallInput={conversation.submitToolCallInput}
              toolCall={pendingClarification}
            />
          </View>
        </View>
      ) : null}
      {/* Minimizing the card hands the turn back to the composer; suggestions stay
          hidden so the pending question keeps the user's attention. */}
      {!pendingClarification || isClarificationCollapsed ? (
      <ConversationComposer
        attachments={attachments}
        canQueue={canQueue}
        canSend={canSend}
        context={composerContext}
        creditSendAllowed={creditSendAllowed}
        draft={draft}
        isOutOfCredits={suggestions.isOutOfCredits}
        isQueueFull={conversation.isQueueFull}
        isSending={conversation.isSending}
        modelLabel={modelLabel}
        onAddAttachments={addAttachments}
        onAppendTranscript={appendTranscript}
        onChangeDraft={setDraft}
        onClearReply={clearReply}
        onOpenModelSettings={onOpenModelSettings}
        onPickFiles={onPickFiles}
        onPickPhotos={onPickPhotos}
        onRemoveAttachment={removeAttachment}
        onSend={sendMessage}
        onSendTranscript={sendTranscript}
        onStartLiveVoice={onStartLiveVoice}
        onStop={conversation.stop}
        onTakePhoto={onTakePhoto}
        onViewPlans={onViewPlans ? handleOutOfCreditsViewPlans : undefined}
        replyTo={replyTo}
        suggestions={isClarificationCollapsed ? { ...composerSuggestions, visible: false } : composerSuggestions}
      />
      ) : null}
    </MarkdownScrollContext.Provider>
  );
}

function ConversationStates({ conversation }: { conversation: ConversationState }) {
  return (
    <>
      {conversation.hasMoreMessages ? (
        <Pressable accessibilityRole="button" onPress={conversation.loadPrevious} style={conversationStyles.loadPreviousButton}>
          <Text style={conversationStyles.loadPreviousText}>Load previous messages</Text>
        </Pressable>
      ) : null}
      {conversation.initError ? <ErrorPanel message={conversation.initError} /> : null}
      {conversation.isLoading ? <LoadingPanel /> : null}
    </>
  );
}

function LoadingPanel() {
  return (
    <View style={conversationStyles.statePanel}>
      <ActivityIndicator color={themedColor('#FF5A1F')} size="small" />
      <Text style={conversationStyles.statePanelText}>Loading conversation...</Text>
    </View>
  );
}

function ErrorPanel({ message }: { message: string }) {
  return (
    <View style={conversationStyles.errorPanel}>
      <Text style={conversationStyles.errorPanelTitle}>Something went wrong</Text>
      <Text style={conversationStyles.errorPanelText}>{message}</Text>
    </View>
  );
}

function createWelcomeMessage(agent: SuperagentAgent): SuperagentMessage {
  return {
    id: 'welcome',
    role: 'assistant',
    content: `Tell ${agent.name || 'your Superagent'} what you want to get done.`,
  };
}

function getMessageAutoScrollId(message: SuperagentMessage, index: number) {
  return message.id ?? `${message.role}-${index}`;
}
