import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ActivityIndicator, Alert, Animated, Easing, Keyboard, Modal, Platform, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import {
  ArrowUp,
  AudioLines,
  Camera,
  ChevronDown,
  FileText,
  Image as ImageIcon,
  Mic,
  Paperclip,
  Plus,
  Square,
  X,
  type LucideIcon,
} from 'lucide-react-native';

import { AttachmentPreviewStrip } from '../attachments/AttachmentPreviewStrip';
import { ComposerSuggestions } from './ComposerSuggestions';
import { composerStyles } from './composerStyles';
import { MAX_ATTACHMENTS, normalizeMediaAttachments } from '../attachments/mediaUtils';
import { useAgentBi } from '../../analytics/mixpanelContext';
import { OutOfCreditsWidget } from '../credits/OutOfCreditsWidget';
import { useSuperagentMedia } from '../../runtime/runtimeContext';
import { createSyntheticLevelSource, formatRecordingDuration, levelToWaveHeight, smoothLevel } from '../../runtime/speechToTextUtils';
import { styles } from '../../styles';
import { themedColor, themedSurface } from '../../theme';
import type { SpeechToTextState } from '../../runtime/useSpeechToText';
import type {
  SuperagentComposerSuggestions,
  SuperagentLiveVoiceInput,
  SuperagentMediaActionContext,
  SuperagentMediaAttachment,
  SuperagentMediaPicker,
  SuperagentPromptSuggestion,
  SuperagentReplyTo,
} from '../../types';

const MAX_CHAT_WIDTH = 900;

// Native attach sources tracked on `Chat Attachment Added` (Drive is untracked —
// it never surfaces in the shell). Not web's Google-Drive-only `Chat File Import`.
type AttachmentSource = 'camera' | 'photos' | 'files';

// Temporarily hide the voice / Live Voice button in the composer while Live Voice is
// being fixed on the native app. All the voice wiring (handlers, props, runtime) is
// intentionally kept intact — flip this back to `true` to restore the button.
const SHOW_VOICE_BUTTON = false;

type VoiceState = 'idle' | 'processing';

export function ConversationComposer({
  attachments,
  canQueue,
  canSend,
  context,
  creditSendAllowed = true,
  draft,
  isOutOfCredits,
  isQueueFull,
  isSending,
  modelLabel,
  onAddAttachments,
  onAppendTranscript,
  onChangeDraft,
  onClearReply,
  onImportFromDrive,
  onOpenModelSettings,
  onPickFiles,
  onPickPhotos,
  onRemoveAttachment,
  onSend,
  onSendTranscript,
  onStartLiveVoice,
  onStop,
  onTakePhoto,
  onViewPlans,
  replyTo,
  suggestions,
}: {
  attachments: SuperagentMediaAttachment[];
  // Enqueue behind the running turn is allowed (busy, queue available, not full).
  canQueue: boolean;
  canSend: boolean;
  context: SuperagentMediaActionContext;
  // Same credit gate `canSend` uses (usage loaded + not out of credits). Dictation
  // is disabled when false so out-of-credit users can't burn STT/provider capacity
  // on a transcript that can't be sent.
  creditSendAllowed?: boolean;
  draft: string;
  isOutOfCredits?: boolean;
  // The running turn plus queued follow-ups have reached the cap — enqueue is blocked.
  isQueueFull?: boolean;
  isSending: boolean;
  modelLabel?: string;
  onAddAttachments: (attachments: SuperagentMediaAttachment[]) => void;
  onAppendTranscript: (transcript: string) => void;
  onChangeDraft: (value: string) => void;
  onClearReply: () => void;
  onImportFromDrive?: SuperagentMediaPicker;
  onOpenModelSettings?: () => void;
  onPickFiles?: SuperagentMediaPicker;
  onPickPhotos?: SuperagentMediaPicker;
  onRemoveAttachment: (index: number) => void;
  onSend: () => void;
  onSendTranscript: (transcript: string) => void;
  onStartLiveVoice?: SuperagentLiveVoiceInput;
  onStop: () => void;
  onTakePhoto?: SuperagentMediaPicker;
  onViewPlans?: () => void;
  replyTo: SuperagentReplyTo | null;
  suggestions?: SuperagentComposerSuggestions;
}) {
  const bi = useAgentBi();
  const { speechToText } = useSuperagentMedia();
  const inputRef = useRef<TextInput>(null);
  const [liveVoiceState, setLiveVoiceState] = useState<VoiceState>('idle');
  const [mediaError, setMediaError] = useState<string | null>(null);
  const [isAttachmentMenuOpen, setIsAttachmentMenuOpen] = useState(false);

  const addFromPicker = useCallback(async (picker: SuperagentMediaPicker | undefined, source?: AttachmentSource) => {
    if (!picker || attachments.length >= MAX_ATTACHMENTS) return;
    setIsAttachmentMenuOpen(false);
    setMediaError(null);
    try {
      const next = normalizeMediaAttachments(await picker(context));
      if (next.length === 0) return; // cancelled/empty picker — nothing attached, don't track
      const added = next.slice(0, MAX_ATTACHMENTS - attachments.length);
      onAddAttachments(added);
      // Only bounded, non-PII fields — never the file name or url.
      if (source) void bi.trackEditor('Chat Attachment Added', {
        source,
        count: added.length,
        kinds: [...new Set(added.map((attachment) => attachment.kind ?? 'file'))],
        mime_types: [...new Set(added.map((attachment) => attachment.mimeType).filter(Boolean))],
      });
    } catch (error) {
      setMediaError(error instanceof Error ? error.message : 'Failed to attach media');
    }
  }, [attachments.length, context, bi, onAddAttachments]);

  // Tap-to-start / tap-to-stop dictation. The hook owns the state machine, upload,
  // and error copy; the composer just appends the returned transcript to the draft
  // (stop & review) or hands it to the send path (send now).
  const handleDictate = useCallback(async () => {
    setMediaError(null);
    const transcript = await speechToText.toggle();
    if (transcript) onAppendTranscript(transcript);
  }, [onAppendTranscript, speechToText.toggle]);

  const handleDictateSend = useCallback(async () => {
    setMediaError(null);
    const transcript = await speechToText.toggle();
    if (transcript) onSendTranscript(transcript);
  }, [onSendTranscript, speechToText.toggle]);

  const handleDictateCancel = useCallback(() => {
    void speechToText.cancel();
  }, [speechToText.cancel]);

  // Leaving the conversation unmounts the composer while the runtime (and its
  // recorder state) stay alive — discard any in-flight recording so the mic can't
  // keep running or get transcribed into the next conversation's draft.
  useEffect(() => {
    return () => { void speechToText.cancel(); };
  }, [speechToText.cancel]);

  const startLiveVoice = useCallback(async () => {
    if (!onStartLiveVoice || liveVoiceState !== 'idle') return;
    setMediaError(null);
    setLiveVoiceState('processing');
    try {
      await onStartLiveVoice(context);
    } catch (error) {
      Alert.alert(
        'Live Voice unavailable',
        error instanceof Error ? error.message : 'Failed to start Live Voice',
      );
    } finally {
      setLiveVoiceState('idle');
    }
  }, [context, liveVoiceState, onStartLiveVoice]);

  const canAttach = Boolean(onImportFromDrive || onPickFiles || onPickPhotos || onTakePhoto) && attachments.length < MAX_ATTACHMENTS;
  const isDraftEmpty = draft.trim().length === 0 && attachments.length === 0;
  // iOS can't present a native picker over the still-open Modal — defer it to
  // Modal.onDismiss (iOS-only); other platforms launch on tap.
  const pendingPickerRef = useRef<{ picker: SuperagentMediaPicker; source?: AttachmentSource } | undefined>(undefined);
  const openAttachmentOptions = useCallback(() => {
    if (!canAttach) return;
    Keyboard.dismiss(); // else the keyboard covers the sheet
    setIsAttachmentMenuOpen(true);
  }, [canAttach]);
  // Closing without a pick clears any deferred picker so onDismiss can't launch one.
  const closeAttachmentMenu = useCallback(() => {
    pendingPickerRef.current = undefined;
    setIsAttachmentMenuOpen(false);
  }, []);

  const selectPicker = useCallback((picker: SuperagentMediaPicker | undefined, source?: AttachmentSource) => {
    if (!picker || attachments.length >= MAX_ATTACHMENTS) return;
    if (Platform.OS === 'ios') {
      pendingPickerRef.current = { picker, source };
      setIsAttachmentMenuOpen(false);
      return;
    }
    setIsAttachmentMenuOpen(false);
    void addFromPicker(picker, source);
  }, [addFromPicker, attachments.length]);
  const handleDrawerDismiss = useCallback(() => {
    const pending = pendingPickerRef.current;
    pendingPickerRef.current = undefined;
    if (pending) void addFromPicker(pending.picker, pending.source);
  }, [addFromPicker]);

  // A controlled multiline TextInput on iOS can fail to visually repaint when its
  // value is set to '' from JS state while it holds focus (the send path clears the
  // draft this way). Force the native clear on the non-empty → empty transition so
  // the field always empties after a send; value='' agrees, so nothing re-asserts.
  const prevDraftRef = useRef(draft);
  useEffect(() => {
    if (prevDraftRef.current.length > 0 && draft.length === 0) inputRef.current?.clear();
    prevDraftRef.current = draft;
  }, [draft]);

  // Tapping a suggestion fills the composer (it does NOT auto-send, matching web) and
  // focuses the input; the suggestion state owner reports the click for analytics.
  const handleSelectSuggestion = useCallback((suggestion: SuperagentPromptSuggestion) => {
    suggestions?.onSelect(suggestion);
    onChangeDraft(suggestion.prompt);
    inputRef.current?.focus();
  }, [onChangeDraft, suggestions]);

  // Dictation errors flow through the hook; surface them in the same slot as
  // attachment/media errors, preferring a live dictation error when present.
  const composerError = speechToText.error ?? mediaError;

  return (
    <View style={composerStyles.wrap}>
      {composerError ? <Text style={composerStyles.errorText}>{composerError}</Text> : null}
      {isOutOfCredits ? <OutOfCreditsWidget onViewPlans={onViewPlans} /> : null}
      {replyTo ? <ReplyPreview replyTo={replyTo} onClear={onClearReply} /> : null}
      {suggestions ? (
        <ComposerSuggestions
          suggestions={suggestions.items}
          visible={suggestions.visible}
          showRestore={suggestions.showRestore}
          refreshing={suggestions.refreshing}
          onSelect={handleSelectSuggestion}
          onRefresh={suggestions.onRefresh}
          onDismiss={suggestions.onDismiss}
          onRestore={suggestions.onRestore}
        />
      ) : null}
      <AttachmentPreviewStrip attachments={attachments} onRemove={onRemoveAttachment} />
      <AttachmentDrawer
        canAttach={canAttach}
        onClose={closeAttachmentMenu}
        onDismiss={handleDrawerDismiss}
        onImportFromDrive={onImportFromDrive ? () => selectPicker(onImportFromDrive) : undefined}
        onPickFiles={onPickFiles ? () => selectPicker(onPickFiles, 'files') : undefined}
        onPickPhotos={onPickPhotos ? () => selectPicker(onPickPhotos, 'photos') : undefined}
        onTakePhoto={onTakePhoto ? () => selectPicker(onTakePhoto, 'camera') : undefined}
        visible={isAttachmentMenuOpen}
      />
      <View style={[composerStyles.shell, { maxWidth: MAX_CHAT_WIDTH }]}>
        {speechToText.state !== 'idle' ? (
          <DictationBar
            getLevel={speechToText.getLevel}
            onCancel={handleDictateCancel}
            onReview={handleDictate}
            onSendNow={handleDictateSend}
            state={speechToText.state}
          />
        ) : (
          <>
            <TextInput
              ref={inputRef}
              multiline
              onChangeText={onChangeDraft}
              onFocus={closeAttachmentMenu}
              placeholder="Type a message"
              placeholderTextColor={themedColor('#A1A1AA')}
              style={composerStyles.input}
              textAlignVertical="top"
              value={draft}
            />
            <View style={composerStyles.controlsRow}>
              <View style={composerStyles.controlsLeft}>
                <ToolButton
                  Icon={Plus}
                  active={isAttachmentMenuOpen}
                  disabled={!canAttach}
                  label="Add attachment"
                  onPress={openAttachmentOptions}
                />
                {onOpenModelSettings ? (
                  <ModelPickerButton label={modelLabel} onPress={onOpenModelSettings} />
                ) : null}
              </View>
              <View style={composerStyles.controlsRight}>
                <DictationButton
                  // Stays usable while a turn runs so the user can dictate a queued
                  // message — only blocked out of credits or once the queue is full
                  // (isQueueFull is already false when no turn is running).
                  disabled={!creditSendAllowed || isQueueFull}
                  onPress={handleDictate}
                />
                <ComposerActionButton
                  canQueue={canQueue}
                  canSend={canSend}
                  isDraftEmpty={isDraftEmpty}
                  isLiveVoiceActive={liveVoiceState === 'processing'}
                  isQueueFull={isQueueFull}
                  onSend={onSend}
                  onStartVoice={context.conversationId && onStartLiveVoice ? startLiveVoice : undefined}
                  onStop={onStop}
                  isSending={isSending}
                />
              </View>
            </View>
          </>
        )}
      </View>
    </View>
  );
}

function AttachmentDrawer({
  canAttach,
  onClose,
  onDismiss,
  onImportFromDrive,
  onPickFiles,
  onPickPhotos,
  onTakePhoto,
  visible,
}: {
  canAttach: boolean;
  onClose: () => void;
  onDismiss?: () => void;
  onImportFromDrive?: () => void;
  onPickFiles?: () => void;
  onPickPhotos?: () => void;
  onTakePhoto?: () => void;
  visible: boolean;
}) {
  return (
    <Modal animationType="slide" onDismiss={onDismiss} onRequestClose={onClose} transparent visible={visible}>
      <View style={composerStyles.attachmentDrawerBackdrop}>
        {/* Sibling (not wrapper) so screen readers reach each item, not one grouped button. */}
        <Pressable accessibilityLabel="Close attachment menu" accessibilityRole="button" onPress={onClose} style={StyleSheet.absoluteFill} />
        {/* No-op press so sheet taps don't reach the backdrop; accessible={false} keeps items focusable. */}
        <Pressable accessible={false} onPress={() => {}} style={composerStyles.attachmentDrawerSheet}>
          <View style={composerStyles.attachmentDrawerHandle} />
          <Text style={composerStyles.attachmentMenuSectionLabel}>Add attachment</Text>
          <AttachmentMenuItem
            Icon={Paperclip}
            disabled={!canAttach || !onPickFiles}
            label="Files"
            onPress={onPickFiles}
          />
          <AttachmentMenuItem
            Icon={Camera}
            disabled={!canAttach || !onTakePhoto}
            label="Camera"
            onPress={onTakePhoto}
          />
          <AttachmentMenuItem
            Icon={ImageIcon}
            disabled={!canAttach || !onPickPhotos}
            label="Photos"
            onPress={onPickPhotos}
          />
          {onImportFromDrive ? (
            <>
              <Text style={composerStyles.attachmentMenuSectionLabel}>Plugins</Text>
              <AttachmentMenuItem
                Icon={FileText}
                disabled={!canAttach}
                label="Google Drive"
                onPress={onImportFromDrive}
              />
            </>
          ) : null}
        </Pressable>
      </View>
    </Modal>
  );
}

function AttachmentMenuItem({
  Icon,
  disabled,
  label,
  onPress,
}: {
  Icon: LucideIcon;
  disabled?: boolean;
  label: string;
  onPress?: () => void;
}) {
  return (
    <Pressable
      accessibilityLabel={label}
      accessibilityRole="button"
      disabled={disabled}
      onPress={onPress}
      style={({ pressed }) => [
        composerStyles.attachmentMenuItem,
        disabled && composerStyles.disabledButton,
        pressed && styles.pressed,
      ]}
    >
      <View style={composerStyles.attachmentMenuIcon}>
        <Icon color={disabled ? themedColor('#8E8E93') : themedColor('#F4F4F5')} size={20} strokeWidth={2} />
      </View>
      <Text style={[composerStyles.attachmentMenuItemText, disabled && composerStyles.attachmentMenuItemTextDisabled]}>
        {label}
      </Text>
    </Pressable>
  );
}

function ModelPickerButton({ label, onPress }: { label?: string; onPress: () => void }) {
  const displayLabel = label ?? 'Auto';
  return (
    <Pressable
      accessibilityLabel={`Model: ${displayLabel}. Open agent settings`}
      accessibilityRole="button"
      onPress={onPress}
      style={({ pressed }) => [composerStyles.modelButton, pressed && styles.pressed]}
    >
      <Text numberOfLines={1} style={composerStyles.modelButtonText}>{displayLabel}</Text>
      <ChevronDown color={themedColor('#A1A1AA')} size={14} strokeWidth={2.4} />
    </Pressable>
  );
}

function ReplyPreview({ onClear, replyTo }: { onClear: () => void; replyTo: SuperagentReplyTo }) {
  return (
    <View style={composerStyles.replyPreview}>
      <View style={composerStyles.replyPreviewMarker} />
      <Text numberOfLines={2} style={composerStyles.replyPreviewText}>{replyTo.content}</Text>
      <Pressable accessibilityLabel="Clear reply" accessibilityRole="button" onPress={onClear} style={composerStyles.replyPreviewClose}>
        <X color={themedColor('#F4F4F5')} size={16} strokeWidth={2.6} />
      </Pressable>
    </View>
  );
}

function ToolButton({
  Icon,
  active,
  disabled,
  label,
  onPress,
}: {
  Icon: LucideIcon;
  active?: boolean;
  disabled?: boolean;
  label: string;
  onPress: () => void;
}) {
  const iconColor = themedColor(disabled ? '#8E8E93' : '#F4F4F5');

  return (
    <Pressable accessibilityLabel={label} accessibilityRole="button" disabled={disabled} onPress={onPress} style={({ pressed }) => [
      composerStyles.iconButton,
      active && composerStyles.activeButton,
      disabled && composerStyles.disabledButton,
      pressed && styles.pressed,
    ]}>
      <Icon color={iconColor} size={16} strokeWidth={2.4} />
    </Pressable>
  );
}

// Idle mic button (mirrors the web builder's standalone mic button). Recording
// and processing visuals live in DictationBar, which replaces the whole composer
// while dictation is active — this button only ever renders idle.
function DictationButton({ disabled, onPress }: { disabled?: boolean; onPress: () => void }) {
  return (
    <Pressable accessibilityLabel="Dictate a message" accessibilityRole="button" disabled={disabled} onPress={onPress} style={({ pressed }) => [
      composerStyles.iconButton,
      disabled && composerStyles.disabledButton,
      pressed && styles.pressed,
    ]}>
      <Mic color={themedColor(disabled ? '#8E8E93' : '#F4F4F5')} size={16} strokeWidth={2.4} />
    </Pressable>
  );
}

// Live voice waveform: a conveyor of level bars scrolling right→left. Each tick
// samples the mic level (recorder metering via `getLevel`; a synthetic source
// when the host can't meter), runs it through fast-attack/slow-decay smoothing,
// appends a bar at the right edge, and slides the strip over one bar-slot with a
// linear animation — so the motion is continuous, not stepped. Bars are a ring
// buffer with stable keys, so per tick React mounts one bar and leaves the other
// 63 untouched instead of restyling the whole strip. Loud bars render dark,
// quiet ones gray. Bar geometry (width/gap) lives here, applied inline, because
// the slide distance must stay equal to one rendered bar slot.
const WAVE_BAR_COUNT = 64;
const WAVE_TICK_MS = 70;
const WAVE_BAR_WIDTH = 3;
const WAVE_BAR_GAP = 3;
const WAVE_MIN_HEIGHT = 4;
const WAVE_MAX_HEIGHT = 26;
const WAVE_LOUD_LEVEL = 0.35; // bars at/above this level (0..1) render "loud" (dark)

function LiveWaveform({ getLevel }: { getLevel?: () => number }) {
  const [bars, setBars] = useState(() =>
    Array.from({ length: WAVE_BAR_COUNT }, (_, index) => ({ key: index, level: 0 })),
  );
  const nextKeyRef = useRef(WAVE_BAR_COUNT);
  const translate = useRef(new Animated.Value(0)).current;
  const [syntheticLevel] = useState(() => createSyntheticLevelSource());

  useEffect(() => {
    let active = true;
    const step = () => {
      if (!active) return;
      const sample = getLevel ? getLevel() : syntheticLevel();
      setBars((previous) => [
        ...previous.slice(1),
        { key: nextKeyRef.current++, level: smoothLevel(previous[previous.length - 1].level, sample) },
      ]);
      // New bar starts one slot past the right edge (clipped) and slides in.
      translate.setValue(WAVE_BAR_WIDTH + WAVE_BAR_GAP);
      Animated.timing(translate, {
        duration: WAVE_TICK_MS,
        easing: Easing.linear,
        toValue: 0,
        useNativeDriver: Platform.OS !== 'web', // rn-web lacks the native animated module
      }).start(({ finished }) => {
        if (finished) step();
      });
    };
    step();
    return () => {
      active = false;
      translate.stopAnimation();
    };
  }, [getLevel, syntheticLevel, translate]);

  const loudColor = themedSurface('#F4F4F5'); // actionPrimary — tracks the send button's inverse fill
  const quietColor = themedSurface('#505050'); // chipFill
  return (
    <View style={composerStyles.dictationWaveTrack}>
      <Animated.View
        style={[composerStyles.dictationWaveStrip, { gap: WAVE_BAR_GAP, transform: [{ translateX: translate }] }]}
      >
        {bars.map(({ key, level }) => (
          <View
            key={key}
            style={[
              composerStyles.dictationWaveBar,
              {
                backgroundColor: level >= WAVE_LOUD_LEVEL ? loudColor : quietColor,
                height: levelToWaveHeight(level, WAVE_MIN_HEIGHT, WAVE_MAX_HEIGHT),
                width: WAVE_BAR_WIDTH,
              },
            ]}
          />
        ))}
      </Animated.View>
    </View>
  );
}

// Action button for the dictation bar — rounded square matching the composer's
// send button. `disabled` blocks interaction; `dimmed` applies the faded look
// separately, so the button showing the in-flight spinner can stay fully opaque
// while still being non-interactive.
function DictationActionButton({
  children,
  dimmed,
  disabled,
  inverse,
  label,
  onPress,
}: {
  children: React.ReactNode;
  dimmed?: boolean;
  disabled?: boolean;
  inverse?: boolean;
  label: string;
  onPress: () => void;
}) {
  return (
    <Pressable
      accessibilityLabel={label}
      accessibilityRole="button"
      disabled={disabled}
      onPress={onPress}
      style={({ pressed }) => [
        inverse ? composerStyles.dictationActionButtonInverse : composerStyles.dictationActionButton,
        dimmed && composerStyles.disabledButton,
        pressed && styles.pressed,
      ]}
    >
      {children}
    </Pressable>
  );
}

function useRecordingSeconds(running: boolean) {
  const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    setSeconds(0);
    if (!running) return;
    const id = setInterval(() => setSeconds((current) => current + 1), 1000);
    return () => clearInterval(id);
  }, [running]);
  return seconds;
}

// Recording bar — takes over the composer while dictating. Top row: scrolling
// waveform with the elapsed timer at its right edge. Bottom row: a gray discard
// (✕) button on the left; on the right a gray stop (⏹ → transcribe into the
// composer for review) button next to an inverse send-now (↑ → transcribe and
// send) button. While transcribing, the wave row disappears, a small status label
// sits between the buttons, and the tapped action's button shows a spinner.
function DictationBar({
  getLevel,
  onCancel,
  onReview,
  onSendNow,
  state,
}: {
  getLevel?: () => number;
  onCancel: () => void;
  onReview: () => void;
  onSendNow: () => void;
  state: SpeechToTextState;
}) {
  const isRecording = state === 'recording';
  const seconds = useRecordingSeconds(isRecording);
  const [pendingAction, setPendingAction] = useState<'review' | 'send' | null>(null);

  const handleReview = useCallback(() => {
    setPendingAction('review');
    onReview();
  }, [onReview]);

  const handleSendNow = useCallback(() => {
    setPendingAction('send');
    onSendNow();
  }, [onSendNow]);

  return (
    <View style={composerStyles.dictationBar}>
      {isRecording ? (
        <View style={composerStyles.dictationWaveRow}>
          <LiveWaveform getLevel={getLevel} />
          <Text style={composerStyles.dictationTimerText}>{formatRecordingDuration(seconds)}</Text>
        </View>
      ) : null}
      <View style={composerStyles.dictationControlsRow}>
        <DictationActionButton dimmed={!isRecording} disabled={!isRecording} label="Discard recording" onPress={onCancel}>
          <X color={themedColor('#F4F4F5')} size={16} strokeWidth={2.4} />
        </DictationActionButton>
        {!isRecording ? <Text style={composerStyles.dictationStatusText}>Transcribing…</Text> : null}
        <View style={composerStyles.dictationControlsRight}>
          <DictationActionButton
            dimmed={!isRecording && pendingAction !== 'review'}
            disabled={!isRecording}
            label="Stop and review transcript"
            onPress={handleReview}
          >
            {pendingAction === 'review' && !isRecording ? (
              <ActivityIndicator color={themedColor('#F4F4F5')} size="small" />
            ) : (
              <Square color={themedColor('#F4F4F5')} fill={themedColor('#F4F4F5')} size={12} strokeWidth={2} />
            )}
          </DictationActionButton>
          <DictationActionButton
            dimmed={!isRecording && pendingAction !== 'send'}
            disabled={!isRecording}
            inverse
            label="Stop recording and send"
            onPress={handleSendNow}
          >
            {pendingAction === 'send' && !isRecording ? (
              <ActivityIndicator color={themedColor('#111111')} size="small" />
            ) : (
              <ArrowUp color={themedColor('#111111')} size={18} strokeWidth={2.8} />
            )}
          </DictationActionButton>
        </View>
      </View>
    </View>
  );
}

function ComposerActionButton({
  canQueue,
  canSend,
  isDraftEmpty,
  isLiveVoiceActive,
  isQueueFull,
  isSending,
  onSend,
  onStartVoice,
  onStop,
}: {
  canQueue: boolean;
  canSend: boolean;
  isDraftEmpty: boolean;
  isLiveVoiceActive: boolean;
  isQueueFull?: boolean;
  isSending: boolean;
  onSend: () => void;
  onStartVoice?: () => void;
  onStop: () => void;
}) {
  if (isSending) {
    // Keep Stop as the primary action, but expose a Send button beside it whenever
    // there's a draft — tapping it queues the message behind the running turn. It's
    // disabled (muted) once the queue is full.
    return (
      <View style={composerStyles.actionButtonRow}>
        {!isDraftEmpty ? (
          <Pressable
            accessibilityLabel={isQueueFull ? 'Message queue full' : 'Queue message'}
            accessibilityRole="button"
            disabled={!canQueue}
            onPress={onSend}
            style={({ pressed }) => [
              composerStyles.sendButton,
              !canQueue && composerStyles.sendButtonDisabled,
              pressed && styles.pressed,
            ]}
          >
            <ArrowUp color={canQueue ? themedColor('#111111') : themedColor('#8E8E93')} size={18} strokeWidth={2.8} />
          </Pressable>
        ) : null}
        <Pressable accessibilityLabel="Stop Superagent" accessibilityRole="button" onPress={onStop} style={composerStyles.stopButton}>
          <Square color={themedColor('#111111')} fill={themedColor('#111111')} size={14} strokeWidth={2.2} />
        </Pressable>
      </View>
    );
  }

  if (isDraftEmpty && SHOW_VOICE_BUTTON) {
    const disabled = !onStartVoice;
    return (
      <Pressable accessibilityLabel="Start Live Voice" accessibilityRole="button" disabled={disabled} onPress={onStartVoice} style={({ pressed }) => [
        composerStyles.sendButton,
        composerStyles.liveActionButton,
        isLiveVoiceActive && composerStyles.activeButton,
        disabled && composerStyles.sendButtonDisabled,
        pressed && styles.pressed,
      ]}>
        <AudioLines color={disabled ? themedColor('#8E8E93') : themedColor('#F4F4F5')} size={16} strokeWidth={2.4} />
      </Pressable>
    );
  }

  return (
    <Pressable accessibilityLabel="Send message" accessibilityRole="button" disabled={!canSend} onPress={onSend} style={({ pressed }) => [
      composerStyles.sendButton,
      !canSend && composerStyles.sendButtonDisabled,
      pressed && styles.pressed,
    ]}>
      <ArrowUp color={canSend ? themedColor('#111111') : themedColor('#8E8E93')} size={18} strokeWidth={2.8} />
    </Pressable>
  );
}
