import { useEffect, useLayoutEffect, useMemo, useRef, useState, useCallback } from 'react';
import { View, Text, FlatList, TouchableOpacity, Pressable, StyleSheet, Alert, Platform, Image, GestureResponderEvent, TextInput, useWindowDimensions, Modal } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { EventEmitter } from 'expo-modules-core';
import { Ionicons } from '@expo/vector-icons';
import { useTheme } from '../ui/ThemeProvider';
import { spacing, radius, Theme } from '../ui/theme';
import { useConfigContext } from '../store/config';
import { useSessionContext, SessionStore, normalizeSessionTitleText } from '../store/sessions';
import { useConnectionManager } from '../store/connectionManager';
import { useTunnelConnection } from '../store/tunnelConnection';
import { useProfile } from '../store/profile';
import { ConnectionStatusIndicator } from '../ui/ConnectionStatusIndicator';
import { AgentSelectorSheet } from '../ui/AgentSelectorSheet';
import { ChatMessage, AgentProgressUpdate } from '../lib/openaiClient';
import { SettingsApiClient } from '../lib/settingsApi';
import { SessionListItem, isStubSession } from '../types/session';
import { createButtonAccessibilityLabel, createMinimumTouchTargetStyle, createTextInputAccessibilityLabel } from '../lib/accessibility';
import { filterSessionSearchResults, type SessionSearchResult } from './session-list-search';

const darkSpinner = require('../../assets/loading-spinner.gif');
const lightSpinner = require('../../assets/light-spinner.gif');

interface Props {
  navigation: any;
}

export default function SessionListScreen({ navigation }: Props) {
  const { theme, isDark } = useTheme();
  const { height: screenHeight } = useWindowDimensions();
  const styles = useMemo(() => createStyles(theme, screenHeight), [theme, screenHeight]);
  const { config } = useConfigContext();
  const connectionManager = useConnectionManager();
  const { connectionInfo, isInitialized } = useTunnelConnection();
  const { currentProfile } = useProfile();
  const [agentSelectorVisible, setAgentSelectorVisible] = useState(false);
  const [searchQuery, setSearchQuery] = useState('');
  const [sessionListMode, setSessionListMode] = useState<'active' | 'archived'>('active');
  const [renameSession, setRenameSession] = useState<SessionListItem | null>(null);
  const [renameTitleDraft, setRenameTitleDraft] = useState('');
  const [isRenameSaving, setIsRenameSaving] = useState(false);

  // ── Rapid Fire voice state ─────────────────────────────────────────────────
  const [rfListening, setRfListening] = useState(false);
  const [rfTranscript, setRfTranscript] = useState('');
  const [rfStatus, setRfStatus] = useState<
    'idle' | 'listening' | 'sending' | 'sent' | 'empty' | 'permissionDenied' | 'unavailable' | 'error'
  >('idle');
  const rfListeningRef = useRef(false);
  const rfStartingRef = useRef(false);
  const rfStoppingRef = useRef(false);
  const rfFinalRef = useRef('');
  const rfLiveRef = useRef('');
  const rfSrEmitterRef = useRef<any>(null);
  const rfSrSubsRef = useRef<any[]>([]);
  const rfGrantTimeRef = useRef(0);
  const rfUserReleasedRef = useRef(false);
  const rfWebRecRef = useRef<any>(null);
  const rfButtonRef = useRef<View>(null);
  const rfPressInSeenRef = useRef(false);
  const rfStopAndFireRef = useRef<(() => Promise<void>) | null>(null);
  const rfInFlightSessionIdsRef = useRef<Set<string>>(new Set());
  const rfMinHoldMs = 200;
  const sessionStoreRef = useRef<SessionStore | null>(null);
  const rfStatusTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  const rfLog = useCallback((msg: string, extra?: any) => {
    if (!__DEV__) return;
    if (typeof extra !== 'undefined') console.log(`[RapidFireDebug] ${msg}`, extra);
    else console.log(`[RapidFireDebug] ${msg}`);
  }, []);

  const normalizeVoiceText = useCallback((t?: string) => (t || '').replace(/\s+/g, ' ').trim(), []);
  const mergeVoiceText = useCallback((base?: string, live?: string) => {
    const a = normalizeVoiceText(base);
    const b = normalizeVoiceText(live);
    if (!a) return b;
    if (!b) return a;
    if (a === b) return a;
    if (b.startsWith(a)) return b;
    if (a.startsWith(b)) return a;
    const bWordBoundary = new RegExp(`(?:^|\\s)${b.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\s|$)`);
    if (bWordBoundary.test(a)) return a;
    const aWordBoundary = new RegExp(`(?:^|\\s)${a.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\s|$)`);
    if (aWordBoundary.test(b)) return b;
    const aWords = a.split(' ');
    const bWords = b.split(' ');
    const maxOverlap = Math.min(aWords.length, bWords.length);
    for (let k = maxOverlap; k > 0; k--) {
      const aSuffix = aWords.slice(-k).join(' ');
      const bPrefix = bWords.slice(0, k).join(' ');
      if (aSuffix === bPrefix) {
        const prefix = aWords.slice(0, aWords.length - k).join(' ');
        return normalizeVoiceText(`${prefix} ${b}`);
      }
    }
    return normalizeVoiceText(`${a} ${b}`);
  }, [normalizeVoiceText]);

  const rfBuildMessagesFromHistory = useCallback((history: any[]): ChatMessage[] => {
    if (!history || history.length === 0) return [];

    let currentTurnStartIndex = 0;
    for (let i = 0; i < history.length; i++) {
      if (history[i]?.role === 'user') {
        currentTurnStartIndex = i;
      }
    }

    const messages: ChatMessage[] = [];
    for (let i = currentTurnStartIndex + 1; i < history.length; i++) {
      const historyMsg = history[i];
      if (!historyMsg) continue;

      // Merge tool results into the preceding assistant message, matching ChatScreen behavior.
      if (historyMsg.role === 'tool' && messages.length > 0) {
        const lastMessage = messages[messages.length - 1];
        if (lastMessage.role === 'assistant' && lastMessage.toolCalls && lastMessage.toolCalls.length > 0) {
          const hasToolResults = historyMsg.toolResults && historyMsg.toolResults.length > 0;
          if (hasToolResults) {
            lastMessage.toolResults = [
              ...(lastMessage.toolResults || []),
              ...(historyMsg.toolResults || []),
            ];
            continue;
          }
        }
      }

      messages.push({
        role: historyMsg.role === 'tool' ? 'assistant' : historyMsg.role,
        content: historyMsg.content || '',
        toolCalls: historyMsg.toolCalls,
        toolResults: historyMsg.toolResults,
      });
    }

    return messages;
  }, []);

  const rfRunBackgroundSend = useCallback(async (sessionId: string, userText: string) => {
    const initialMessages: ChatMessage[] = [{ role: 'user', content: userText }];
    const ss = sessionStoreRef.current;
    if (!ss) return;

    const requestId = Date.now();

    rfInFlightSessionIdsRef.current.add(sessionId);
    connectionManager.setLatestRequestId(sessionId, requestId);
    connectionManager.incrementActiveRequests(sessionId);

    let streamingText = '';
    let persistTimer: ReturnType<typeof setTimeout> | null = null;
    let pendingMessages: ChatMessage[] | null = null;
    let finalized = false;

    const isLatestForSession = () => connectionManager.getLatestRequestId(sessionId) === requestId;

    const schedulePersist = (messages: ChatMessage[]) => {
      pendingMessages = messages;
      if (persistTimer) return;
      persistTimer = setTimeout(() => {
        persistTimer = null;
        const toPersist = pendingMessages;
        pendingMessages = null;
        if (!toPersist || !isLatestForSession()) return;
        const latestStore = sessionStoreRef.current;
        if (!latestStore) return;
        void latestStore.setMessagesForSession(sessionId, toPersist);
      }, 180);
    };

    const flushPersist = async () => {
      if (persistTimer) {
        clearTimeout(persistTimer);
        persistTimer = null;
      }
      const toPersist = pendingMessages;
      pendingMessages = null;
      if (!toPersist || !isLatestForSession()) return;
      const latestStore = sessionStoreRef.current;
      if (!latestStore) return;
      await latestStore.setMessagesForSession(sessionId, toPersist);
    };

    const onToken = (tok: string) => {
      if (finalized) return;
      if (!isLatestForSession()) return;
      if (tok.startsWith(streamingText) && tok.length >= streamingText.length) {
        streamingText = tok;
      } else {
        streamingText += tok;
      }
      schedulePersist([...initialMessages, { role: 'assistant', content: streamingText }]);
    };

    const onProgress = (update: AgentProgressUpdate) => {
      if (finalized) return;
      if (!isLatestForSession()) return;
      if (update.conversationHistory && update.conversationHistory.length > 0) {
        const assistantMessages = rfBuildMessagesFromHistory(update.conversationHistory as any[]);
        schedulePersist([...initialMessages, ...assistantMessages]);
        return;
      }
      const fullText = update.streamingContent?.text;
      if (fullText) {
        streamingText = fullText;
        schedulePersist([...initialMessages, { role: 'assistant', content: streamingText }]);
      }
    };

    try {
      const connection = connectionManager.getOrCreateConnection(sessionId);
      const client = connection.client;
      rfLog('backgroundSend:start', { sessionId, requestId });
      const response = await client.chat(initialMessages, onToken, onProgress, undefined);
      if (!isLatestForSession()) return;
      finalized = true;

      if (response.conversationId) {
        await ss.setServerConversationIdForSession(sessionId, response.conversationId);
      }

      let finalMessages: ChatMessage[];
      if (response.conversationHistory && response.conversationHistory.length > 0) {
        finalMessages = [...initialMessages, ...rfBuildMessagesFromHistory(response.conversationHistory as any[])];
      } else {
        const finalText = (response.content || streamingText).trim();
        finalMessages = finalText
          ? [...initialMessages, { role: 'assistant', content: finalText }]
          : initialMessages;
      }

      const authoritativeFinalText = (response.content || streamingText).trim();
      if (authoritativeFinalText) {
        let lastAssistantIndex = -1;
        for (let i = finalMessages.length - 1; i >= 0; i--) {
          if (finalMessages[i].role === 'assistant') {
            lastAssistantIndex = i;
            break;
          }
        }
        if (lastAssistantIndex >= 0) {
          finalMessages[lastAssistantIndex] = {
            ...finalMessages[lastAssistantIndex],
            content: authoritativeFinalText,
          };
        } else {
          finalMessages = [...finalMessages, { role: 'assistant', content: authoritativeFinalText }];
        }
      }

      pendingMessages = finalMessages;
      await flushPersist();
      rfLog('backgroundSend:done', { sessionId, requestId, messageCount: finalMessages.length });
    } catch (err) {
      finalized = true;
      rfLog('backgroundSend:error', { sessionId, requestId, error: (err as any)?.message || String(err) });
      const errorContent = (err as any)?.message
        ? `Error: ${(err as any).message}`
        : 'Error: Failed to get response.';
      pendingMessages = [...initialMessages, { role: 'assistant', content: errorContent }];
      await flushPersist();
    } finally {
      if (persistTimer) {
        clearTimeout(persistTimer);
      }
      rfInFlightSessionIdsRef.current.delete(sessionId);
      connectionManager.decrementActiveRequests(sessionId);
    }
  }, [connectionManager, rfBuildMessagesFromHistory, rfLog]);

  const rfSetListening = useCallback((v: boolean) => {
    rfListeningRef.current = v;
    setRfListening(v);
  }, []);

  const rfCleanupSubs = useCallback(() => {
    for (const sub of rfSrSubsRef.current) {
      try { sub.remove(); } catch {}
    }
    rfSrSubsRef.current = [];
  }, []);

  const rfSetTransientStatus = useCallback((
    status: 'sent' | 'empty' | 'permissionDenied' | 'unavailable' | 'error',
    clearAfterMs = 2500
  ) => {
    if (rfStatusTimeoutRef.current) {
      clearTimeout(rfStatusTimeoutRef.current);
      rfStatusTimeoutRef.current = null;
    }
    setRfStatus(status);
    rfStatusTimeoutRef.current = setTimeout(() => {
      setRfStatus('idle');
      rfStatusTimeoutRef.current = null;
    }, clearAfterMs);
  }, []);

  // On web, capture raw DOM release events in case RN Pressable misses onPressOut on long-press.
  useEffect(() => {
    if (Platform.OS !== 'web' || !rfButtonRef.current) return;

    // @ts-ignore - React Native Web ref resolves to a DOM element at runtime
    const domNode = rfButtonRef.current as any;
    if (!domNode || typeof domNode.addEventListener !== 'function') return;

    const summarizeDomEvent = (e: any) => ({
      type: e?.type,
      cancelable: !!e?.cancelable,
      defaultPrevented: !!e?.defaultPrevented,
      touches: typeof e?.touches?.length === 'number' ? e.touches.length : undefined,
      changedTouches: typeof e?.changedTouches?.length === 'number' ? e.changedTouches.length : undefined,
      pointerType: e?.pointerType,
      targetTag: e?.target?.tagName,
    });

    const stopFromDomFallback = (source: string, e: any) => {
      const details = summarizeDomEvent(e);
      if (!rfPressInSeenRef.current || !rfListeningRef.current || rfUserReleasedRef.current) {
        rfLog(`web:dom:${source} (no-op)`, {
          ...details,
          pressInSeen: rfPressInSeenRef.current,
          listening: rfListeningRef.current,
          userReleased: rfUserReleasedRef.current,
        });
        return;
      }
      const dt = Date.now() - rfGrantTimeRef.current;
      const delay = Math.max(0, rfMinHoldMs - dt);
      rfLog(`web:dom:${source} -> fallback stop`, { ...details, dt, delay });
      const maybeStop = () => {
        if (!rfListeningRef.current || rfUserReleasedRef.current) return;
        rfPressInSeenRef.current = false;
        void rfStopAndFireRef.current?.();
      };
      if (delay > 0) setTimeout(maybeStop, delay);
      else maybeStop();
    };

    const handleTouchStart = (e: any) => {
      rfLog('web:dom:touchstart', summarizeDomEvent(e));
      if (e.cancelable) e.preventDefault();
    };
    const handleTouchEnd = (e: any) => stopFromDomFallback('touchend', e);
    const handleTouchCancel = (e: any) => stopFromDomFallback('touchcancel', e);
    const handlePointerUp = (e: any) => stopFromDomFallback('pointerup', e);
    const handlePointerCancel = (e: any) => stopFromDomFallback('pointercancel', e);
    const handleContextMenu = (e: any) => {
      rfLog('web:dom:contextmenu', summarizeDomEvent(e));
      e.preventDefault();
    };

    rfLog('web:dom listeners attached', { nodeTag: domNode?.tagName });
    domNode.addEventListener('touchstart', handleTouchStart, { passive: false });
    domNode.addEventListener('touchend', handleTouchEnd, { passive: false });
    domNode.addEventListener('touchcancel', handleTouchCancel, { passive: false });
    domNode.addEventListener('pointerup', handlePointerUp, { passive: true });
    domNode.addEventListener('pointercancel', handlePointerCancel, { passive: true });
    domNode.addEventListener('contextmenu', handleContextMenu, { passive: false });
    document.addEventListener('touchend', handleTouchEnd, { passive: false });
    document.addEventListener('touchcancel', handleTouchCancel, { passive: false });
    document.addEventListener('pointerup', handlePointerUp, { passive: true });
    document.addEventListener('pointercancel', handlePointerCancel, { passive: true });

    return () => {
      rfLog('web:dom listeners removed');
      domNode.removeEventListener('touchstart', handleTouchStart);
      domNode.removeEventListener('touchend', handleTouchEnd);
      domNode.removeEventListener('touchcancel', handleTouchCancel);
      domNode.removeEventListener('pointerup', handlePointerUp);
      domNode.removeEventListener('pointercancel', handlePointerCancel);
      domNode.removeEventListener('contextmenu', handleContextMenu);
      document.removeEventListener('touchend', handleTouchEnd);
      document.removeEventListener('touchcancel', handleTouchCancel);
      document.removeEventListener('pointerup', handlePointerUp);
      document.removeEventListener('pointercancel', handlePointerCancel);
    };
  }, [rfLog]);

  const rfStartRecording = useCallback(async (e?: GestureResponderEvent) => {
    rfLog('startRecording called', {
      starting: rfStartingRef.current,
      listening: rfListeningRef.current,
      pageX: e?.nativeEvent?.pageX,
      pageY: e?.nativeEvent?.pageY,
    });
    if (rfStartingRef.current || rfListeningRef.current) return;
    rfStartingRef.current = true;
    rfStoppingRef.current = false;
    rfUserReleasedRef.current = false;
    rfFinalRef.current = '';
    rfLiveRef.current = '';
    setRfTranscript('');
    setRfStatus('listening');
    rfGrantTimeRef.current = Date.now();
    rfSetListening(true);
    try {
      if (Platform.OS !== 'web') {
        const SR: any = await import('expo-speech-recognition');
        if (SR?.ExpoSpeechRecognitionModule?.start) {
          if (!rfSrEmitterRef.current) {
            rfSrEmitterRef.current = new EventEmitter(SR.ExpoSpeechRecognitionModule);
          }
          rfCleanupSubs();
          const subResult = rfSrEmitterRef.current.addListener('result', (event: any) => {
            const t = event?.results?.[0]?.transcript ?? event?.text ?? event?.transcript ?? '';
            if (event?.isFinal && t) {
              rfFinalRef.current = mergeVoiceText(rfFinalRef.current, t);
            }
            const preview = mergeVoiceText(rfFinalRef.current, event?.isFinal ? '' : t);
            rfLiveRef.current = preview;
            setRfTranscript(preview);
            rfLog('native:onresult', {
              isFinal: !!event?.isFinal,
              chunk: normalizeVoiceText(t),
              preview,
            });
          });
          const subError = rfSrEmitterRef.current.addListener('error', (event: any) => {
            console.error('[RapidFire] SR error:', JSON.stringify(event));
            rfSetTransientStatus('error');
          });
          const subEnd = rfSrEmitterRef.current.addListener('end', async () => {
            // If user hasn't released, SR ended spuriously – try to restart
            if (!rfUserReleasedRef.current && !rfStoppingRef.current) {
              try {
                const SRInner: any = await import('expo-speech-recognition');
                if (SRInner?.ExpoSpeechRecognitionModule?.start) {
                  SRInner.ExpoSpeechRecognitionModule.start({
                    lang: 'en-US', interimResults: true, continuous: true,
                    volumeChangeEventOptions: { enabled: false, intervalMillis: 250 },
                  });
                  return; // restarted – stay in listening state
                }
              } catch {}
            }
            rfSetListening(false);
          });
          rfSrSubsRef.current.push(subResult, subError, subEnd);
          try {
            const perm = await SR.ExpoSpeechRecognitionModule.getPermissionsAsync();
            if (!perm?.granted) {
              const req = await SR.ExpoSpeechRecognitionModule.requestPermissionsAsync();
              if (!req?.granted) {
                rfSetListening(false);
                rfStartingRef.current = false;
                rfSetTransientStatus('permissionDenied', 4000);
                Alert.alert(
                  'Microphone Permission Required',
                  'Rapid Fire needs microphone permission. Enable it in system settings and try again.',
                  [{ text: 'OK' }]
                );
                return;
              }
            }
          } catch {}
          SR.ExpoSpeechRecognitionModule.start({ lang: 'en-US', interimResults: true, continuous: true,
            volumeChangeEventOptions: { enabled: false, intervalMillis: 250 } });
          rfStartingRef.current = false;
          return;
        }
      }
      // Web fallback – use Web Speech API
      if (Platform.OS === 'web') {
        const SRClass = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
        if (SRClass) {
          const rec = new SRClass();
          rec.lang = 'en-US';
          rec.interimResults = true;
          rec.continuous = true;
          rec.onresult = (ev: any) => {
            let interim = '';
            let finalText = '';
            for (let i = ev.resultIndex; i < ev.results.length; i++) {
              const res = ev.results[i];
              const txt = res[0]?.transcript || '';
              if (res.isFinal) finalText += txt;
              else interim += txt;
            }
            if (finalText) {
              rfFinalRef.current = mergeVoiceText(rfFinalRef.current, finalText);
            }
            const preview = mergeVoiceText(rfFinalRef.current, interim);
            rfLiveRef.current = preview;
            setRfTranscript(preview);
            rfLog('web:onresult', {
              interim: normalizeVoiceText(interim),
              finalChunk: normalizeVoiceText(finalText),
              preview,
            });
          };
          rec.onerror = (ev: any) => {
            console.error('[RapidFire] Web SR error:', ev?.error || ev);
            rfLog('web:onerror', ev?.error || ev);
            rfSetTransientStatus('error');
          };
          rec.onend = () => {
            rfLog('web:onend', {
              userReleased: rfUserReleasedRef.current,
              stopping: rfStoppingRef.current,
              hasRec: !!rfWebRecRef.current,
              final: rfFinalRef.current,
              live: rfLiveRef.current,
            });
            // If user hasn't released, SR ended spuriously – try to restart
            if (!rfUserReleasedRef.current && !rfStoppingRef.current && rfWebRecRef.current) {
              try { rfWebRecRef.current.start(); return; } catch {}
            }
            rfSetListening(false);
          };
          rfWebRecRef.current = rec;
          rec.start();
          rfStartingRef.current = false;
          return;
        }
      }
    } catch (err) {
      console.warn('[RapidFire] SR unavailable:', (err as any)?.message || err);
      rfSetTransientStatus('unavailable', 4000);
    }
    rfSetListening(false);
    rfStartingRef.current = false;
  }, [mergeVoiceText, normalizeVoiceText, rfCleanupSubs, rfLog, rfSetListening, rfSetTransientStatus]);

  const rfStopAndFire = useCallback(async () => {
    rfLog('stopAndFire called', {
      stopping: rfStoppingRef.current,
      listening: rfListeningRef.current,
      final: rfFinalRef.current,
      live: rfLiveRef.current,
    });
    if (rfStoppingRef.current) return;
    rfStoppingRef.current = true;
    rfUserReleasedRef.current = true;
    rfPressInSeenRef.current = false;
    try {
      if (Platform.OS !== 'web') {
        const SR: any = await import('expo-speech-recognition');
        if (SR?.ExpoSpeechRecognitionModule?.stop) SR.ExpoSpeechRecognitionModule.stop();
      }
      if (Platform.OS === 'web' && rfWebRecRef.current) {
        try { rfWebRecRef.current.stop(); } catch {}
        rfWebRecRef.current = null;
      }
    } catch {}
    rfCleanupSubs();
    rfSetListening(false);
    setRfStatus('sending');
    const finalText = mergeVoiceText(rfFinalRef.current, rfLiveRef.current).trim();
    rfFinalRef.current = '';
    rfLiveRef.current = '';
    setRfTranscript('');
    if (finalText) {
      // Create a new session and persist transcript, but keep user on Sessions screen.
      const ss = sessionStoreRef.current;
      if (ss) {
        try {
          // Save previous currentSessionId — createNewSession() will switch it,
          // but we need to restore it so the (possibly mounted) ChatScreen doesn't
          // race-load the new session while it still has 0 messages.
          const prevSessionId = ss.currentSessionId;
          const newSession = ss.createNewSession();
          // Restore immediately so ChatScreen's useEffect doesn't prematurely load
          // the new empty session. When the user later taps the session,
          // setCurrentSession(newId) will properly trigger the load with messages.
          ss.setCurrentSession(prevSessionId);
          await ss.setMessagesForSession(newSession.id, [{ role: 'user', content: finalText }]);
          setRfTranscript(finalText);
          // Fire the agent request in the background so the Sessions list updates live.
          void rfRunBackgroundSend(newSession.id, finalText);
          rfSetTransientStatus('sent');
        } catch (err) {
          console.error('[RapidFire] Failed to persist transcript:', err);
          rfSetTransientStatus('error');
        }
      } else {
        rfSetTransientStatus('error');
      }
    } else {
      rfSetTransientStatus('empty');
    }
    rfStoppingRef.current = false;
  }, [mergeVoiceText, rfCleanupSubs, rfLog, rfRunBackgroundSend, rfSetListening, rfSetTransientStatus]);

  rfStopAndFireRef.current = rfStopAndFire;
  // ── end Rapid Fire ─────────────────────────────────────────────────────────

  useLayoutEffect(() => {
    return () => {
      rfCleanupSubs();
      if (rfWebRecRef.current) {
        try { rfWebRecRef.current.stop(); } catch {}
        rfWebRecRef.current = null;
      }
      if (rfStatusTimeoutRef.current) {
        clearTimeout(rfStatusTimeoutRef.current);
      }
    };
  }, [rfCleanupSubs]);
  const insets = useSafeAreaInsets();
  const sessionStore = useSessionContext();
  sessionStoreRef.current = sessionStore;
  const isConnected = connectionInfo.state === 'connected';
  const hasConfiguredConnection = Boolean(config.baseUrl && config.apiKey);
  const hasActiveSearch = searchQuery.trim().length > 0;

  const handleCreateSession = useCallback(() => {
    sessionStore.createNewSession();
    navigation.navigate('Chat');
  }, [navigation, sessionStore]);

  const handleOpenSettings = useCallback(() => {
    navigation.navigate('Settings');
  }, [navigation]);

  const handleOpenSplitView = useCallback(() => {
    navigation.navigate('SplitChat');
  }, [navigation]);

  const handleOpenConnectionSettings = useCallback((openScanner = false) => {
    if (openScanner) {
      navigation.navigate('ConnectionSettings', { openScanner: true });
      return;
    }

    navigation.navigate('ConnectionSettings');
  }, [navigation]);

  useLayoutEffect(() => {
    navigation?.setOptions?.({
      headerTitle: () => (
        <TouchableOpacity
          style={{ flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}
          onPress={() => setAgentSelectorVisible(true)}
          accessibilityRole="button"
          accessibilityLabel={`Current agent: ${currentProfile?.name || 'Default'}. Tap to change.`}
          accessibilityHint="Opens agent selection menu"
        >
          <Text style={{ fontSize: 17, fontWeight: '600', color: theme.colors.foreground }}>Chats</Text>
          <View style={{
            flexDirection: 'row',
            alignItems: 'center',
            gap: 3,
            backgroundColor: theme.colors.primary + '33',
            paddingHorizontal: 8,
            paddingVertical: 2,
            borderRadius: 10,
            marginTop: 2,
          }}>
            <Text style={{
              fontSize: 11,
              color: theme.colors.primary,
              fontWeight: '500',
            }}>
              {currentProfile?.name || 'Default'}
            </Text>
            <Ionicons name="chevron-down" size={10} color={theme.colors.primary} />
          </View>
        </TouchableOpacity>
      ),
      headerRight: () => (
        <View style={{ flexDirection: 'row', alignItems: 'center' }}>
          <ConnectionStatusIndicator
            state={connectionInfo.state}
            retryCount={connectionInfo.retryCount}
            compact
          />
          <TouchableOpacity
            onPress={handleOpenSplitView}
            style={styles.headerSettingsButton}
            accessibilityRole="button"
            accessibilityLabel={createButtonAccessibilityLabel('Open split view')}
            accessibilityHint="Opens two chats at once for comparison"
          >
            <Ionicons name="git-compare-outline" size={18} color={theme.colors.foreground} />
          </TouchableOpacity>
          <TouchableOpacity
            onPress={handleCreateSession}
            style={styles.headerNewChatButton}
            accessibilityRole="button"
            accessibilityLabel={createButtonAccessibilityLabel('New chat')}
            accessibilityHint="Creates and opens a new chat."
          >
            <Text style={styles.headerNewChatButtonText}>+ New Chat</Text>
          </TouchableOpacity>
          <TouchableOpacity
            onPress={handleOpenSettings}
            style={styles.headerSettingsButton}
            accessibilityRole="button"
            accessibilityLabel={createButtonAccessibilityLabel('Open settings')}
            accessibilityHint="Opens app settings."
          >
            <Ionicons name="settings-outline" size={20} color={theme.colors.foreground} />
          </TouchableOpacity>
        </View>
      ),
    });
  }, [navigation, styles, theme, connectionInfo.state, connectionInfo.retryCount, currentProfile, setAgentSelectorVisible, handleCreateSession, handleOpenSettings, handleOpenSplitView]);

  if (!sessionStore.ready || !isInitialized) {
    return (
      <View style={[styles.container, styles.loadingContainer]}>
        <Image
          source={isDark ? darkSpinner : lightSpinner}
          style={styles.spinner}
          resizeMode="contain"
        />
        <Text style={styles.loadingText}>Loading chats...</Text>
      </View>
    );
  }

  const handleSelectSession = async (sessionId: string) => {
    const selectedSession = sessionStore.sessions.find(s => s.id === sessionId) || null;
    const rfBackgroundInFlight = rfInFlightSessionIdsRef.current.has(sessionId);
    const pendingUserOnlyText = selectedSession && selectedSession.messages.length === 1
      && selectedSession.messages[0].role === 'user'
      && !selectedSession.serverConversationId
      && !rfBackgroundInFlight
      ? (selectedSession.messages[0].content || '').trim()
      : '';

    // Fallback only: if a user-only stub exists and no background rapid-fire request
    // is running, auto-send when opening chat.
    if (pendingUserOnlyText) {
      rfLog('selectSession -> pending user-only session detected', { sessionId, pendingUserOnlyText });
      try {
        await sessionStore.setMessagesForSession(sessionId, []);
      } catch (err) {
        console.warn('[RapidFire] Failed to clear pending user-only session before auto-send:', err);
      }
      sessionStore.setCurrentSession(sessionId);
      navigation.navigate('Chat', { initialMessage: pendingUserOnlyText });
      return;
    }

    sessionStore.setCurrentSession(sessionId);
    navigation.navigate('Chat');
  };

  // Create a settings client for syncing pin/archive/delete state to server
  const settingsClient = useMemo(() => {
    if (config.baseUrl && config.apiKey) {
      return new SettingsApiClient(config.baseUrl, config.apiKey);
    }
    return undefined;
  }, [config.baseUrl, config.apiKey]);

  const handleDeleteSession = useCallback((session: SessionListItem) => {
    const doDelete = async () => {
      const serverConversationId = sessionStore.sessions.find(item => item.id === session.id)?.serverConversationId;

      try {
        if (settingsClient && serverConversationId) {
          await settingsClient.deleteConversation(serverConversationId);
        }
        connectionManager.removeConnection(session.id);
        await sessionStore.deleteSession(session.id);
      } catch (error: any) {
        Alert.alert('Delete Failed', error?.message || 'Failed to delete this chat');
      }
    };

    if (Platform.OS === 'web') {
      if (window.confirm(`Delete "${session.title}"?`)) {
        void doDelete();
      }
    } else {
      Alert.alert(
        'Delete Session',
        `Are you sure you want to delete "${session.title}"?`,
        [
          { text: 'Cancel', style: 'cancel' },
          { text: 'Delete', style: 'destructive', onPress: () => { void doDelete(); } },
        ]
      );
    }
  }, [connectionManager, sessionStore, settingsClient]);

  const handleOpenRenameSession = useCallback((session: SessionListItem) => {
    setRenameSession(session);
    setRenameTitleDraft(session.title);
  }, []);

  const handleCloseRenameSession = useCallback(() => {
    if (isRenameSaving) return;
    setRenameSession(null);
    setRenameTitleDraft('');
  }, [isRenameSaving]);

  const handleSaveRenameSession = useCallback(async () => {
    if (!renameSession || isRenameSaving) return;

    const nextTitle = normalizeSessionTitleText(renameTitleDraft);
    const previousTitle = normalizeSessionTitleText(renameSession.title);
    if (nextTitle === previousTitle) {
      handleCloseRenameSession();
      return;
    }

    setIsRenameSaving(true);
    try {
      let updatedTitle = nextTitle;
      const serverConversationId = sessionStore.sessions.find(item => item.id === renameSession.id)?.serverConversationId;
      if (settingsClient && serverConversationId) {
        const updatedConversation = await settingsClient.updateConversation(serverConversationId, { title: nextTitle });
        updatedTitle = updatedConversation.title || nextTitle;
      }
      await sessionStore.renameSessionTitle(renameSession.id, updatedTitle);
      setRenameSession(null);
      setRenameTitleDraft('');
    } catch (error: any) {
      Alert.alert('Rename Failed', error?.message || 'Failed to rename this chat');
    } finally {
      setIsRenameSaving(false);
    }
  }, [
    handleCloseRenameSession,
    isRenameSaving,
    renameSession,
    renameTitleDraft,
    sessionStore,
    settingsClient,
  ]);

  const handleToggleSessionPinned = useCallback(async (sessionId: string) => {
    await sessionStore.toggleSessionPinned(sessionId, settingsClient);
  }, [sessionStore, settingsClient]);

  const handleToggleSessionArchived = useCallback(async (sessionId: string) => {
    await sessionStore.toggleSessionArchived(sessionId, settingsClient);
  }, [sessionStore, settingsClient]);

  const handleSessionLongPress = useCallback((session: SessionListItem) => {
    const isPinned = session.isPinned;
    const isArchived = session.isArchived;
    const pinLabel = isPinned ? 'Unpin' : 'Pin';
    const archiveLabel = isArchived ? 'Unarchive' : 'Archive';

    if (Platform.OS === 'web') {
      // On web, fall back to a simple confirm for delete
      const action = window.prompt(`Choose action for "${session.title}":\n1. Rename\n2. ${pinLabel}\n3. ${archiveLabel}\n4. Delete\n\nEnter 1, 2, 3, or 4:`);
      if (action === '1') handleOpenRenameSession(session);
      else if (action === '2') void handleToggleSessionPinned(session.id);
      else if (action === '3') void handleToggleSessionArchived(session.id);
      else if (action === '4') handleDeleteSession(session);
    } else {
      Alert.alert(
        session.title,
        undefined,
        [
          { text: 'Rename', onPress: () => handleOpenRenameSession(session) },
          { text: pinLabel, onPress: () => void handleToggleSessionPinned(session.id) },
          { text: archiveLabel, onPress: () => void handleToggleSessionArchived(session.id) },
          { text: 'Delete', style: 'destructive', onPress: () => handleDeleteSession(session) },
          { text: 'Cancel', style: 'cancel' },
        ]
      );
    }
  }, [handleToggleSessionPinned, handleToggleSessionArchived, handleDeleteSession, handleOpenRenameSession]);

  const formatDate = (timestamp: number) => {
    const date = new Date(timestamp);
    const now = new Date();
    const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));

    if (diffDays === 0) {
      return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    } else if (diffDays === 1) {
      return 'Yesterday';
    } else if (diffDays < 7) {
      return date.toLocaleDateString([], { weekday: 'short' });
    }
    return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
  };

  const disconnectedTitle = connectionInfo.state === 'connecting' || connectionInfo.state === 'reconnecting'
    ? 'Connecting to your desktop'
    : connectionInfo.state === 'failed'
      ? 'Reconnect to DotAgents'
      : hasConfiguredConnection
        ? 'Reconnect to your chats'
        : 'Connect DotAgents to get started';

  const disconnectedSubtitle = connectionInfo.state === 'connecting' || connectionInfo.state === 'reconnecting'
    ? 'We are pairing with your desktop. Your conversation history will appear here as soon as the connection is ready.'
    : connectionInfo.state === 'failed'
      ? 'Open connection settings or scan a fresh QR code to restore syncing and get back into your conversations.'
      : hasConfiguredConnection
        ? 'Your mobile app is set up, but it is not currently connected. Reconnect to load your recent conversations.'
        : 'Scan the QR code from the desktop app to pair this device and bring your conversations into mobile.';

  // Build a set of stub session IDs for display purposes
  const stubSessionIds = useMemo(() => {
    const ids = new Set<string>();
    for (const s of sessionStore.sessions) {
      if (isStubSession(s)) ids.add(s.id);
    }
    return ids;
  }, [sessionStore.sessions]);

  const filteredSessions = useMemo(() => {
    const results = filterSessionSearchResults(sessionStore.sessions, searchQuery);
    return results.filter((s) => sessionListMode === 'archived' ? !!s.isArchived : !s.isArchived);
  }, [sessionListMode, sessionStore.sessions, searchQuery]);

  const archivedSessionCount = useMemo(
    () => sessionStore.sessions.filter((session) => !!session.isArchived).length,
    [sessionStore.sessions],
  );

  const renderSession = ({ item }: { item: SessionSearchResult }) => {
    const isActive = item.id === sessionStore.currentSessionId;
    const isStub = stubSessionIds.has(item.id);
    const rawPreview = (item.searchPreview ?? item.preview) || 'No messages yet';
    const sessionPreviewText = rawPreview.startsWith('tool: [') || rawPreview.includes('{"success":')
      ? 'Used a tool'
      : rawPreview.includes('{"')
        ? rawPreview.replace(/\{.*\}/g, '{...}').trim()
        : rawPreview;
    const sessionMetaLabel = `${item.messageCount} message${item.messageCount !== 1 ? 's' : ''}${isStub ? ' · from desktop' : ''}`;

    return (
      <Pressable
        style={({ pressed }) => [
          styles.sessionItem,
          isActive && styles.sessionItemActive,
          pressed && styles.sessionItemPressed,
        ]}
        onPress={() => handleSelectSession(item.id)}
        onLongPress={() => handleSessionLongPress(item)}
        accessibilityRole="button"
        accessibilityLabel={`${item.isPinned ? 'Pinned, ' : ''}${item.isArchived ? 'Archived, ' : ''}${item.title}, ${item.messageCount} message${item.messageCount !== 1 ? 's' : ''}`}
      >
        <View style={styles.sessionHeader}>
          <View style={styles.sessionTitleRow}>
            <Text style={styles.sessionTitle} numberOfLines={1}>
              {item.title}
            </Text>
          </View>
          <View style={styles.sessionHeaderMeta}>
            <Text style={styles.sessionDate}>{formatDate(item.updatedAt)}</Text>
            <Pressable
              style={[styles.sessionPinButton, item.isPinned && styles.sessionPinButtonActive]}
              onPress={(event: GestureResponderEvent) => {
                event.stopPropagation();
                void handleToggleSessionPinned(item.id);
              }}
              accessibilityRole="button"
              accessibilityLabel={createButtonAccessibilityLabel(item.isPinned ? `Unpin ${item.title}` : `Pin ${item.title}`)}
              accessibilityHint={item.isPinned
                ? 'Removes this chat from the pinned chats section.'
                : 'Keeps this chat at the top of the chats list.'}
            >
              <Text style={[styles.sessionPinButtonText, item.isPinned && styles.sessionPinButtonTextActive]}>
                {item.isPinned ? 'Pinned' : 'Pin'}
              </Text>
            </Pressable>
            {item.isArchived ? (
              <Pressable
                style={[styles.sessionArchiveButton, styles.sessionArchiveButtonActive]}
                onPress={(event: GestureResponderEvent) => {
                  event.stopPropagation();
                  void handleToggleSessionArchived(item.id);
                }}
                accessibilityRole="button"
                accessibilityLabel={createButtonAccessibilityLabel(`Unarchive ${item.title}`)}
                accessibilityHint="Moves this chat back to the active chats list."
              >
                <Text style={[styles.sessionArchiveButtonText, styles.sessionArchiveButtonTextActive]}>
                  Unarchive
                </Text>
              </Pressable>
            ) : null}
          </View>
        </View>
        <Text style={styles.sessionPreview} numberOfLines={1}>
          <Text style={styles.sessionPreviewMeta}>{sessionMetaLabel}</Text>
          {` · ${sessionPreviewText}`}
        </Text>
      </Pressable>
    );
  };

  const EmptyState = () => (
    <View style={styles.emptyState}>
      <View style={styles.emptyStateTextGroup}>
        <Text style={styles.emptyTitle}>{sessionListMode === 'archived' ? 'No archived chats' : 'No chats yet'}</Text>
        <Text style={styles.emptySubtitle}>
          {sessionListMode === 'archived'
            ? 'Archived chats will show up here after you archive them from the chat actions menu.'
            : 'Start your first chat so recent conversations show up here.'}
        </Text>
      </View>
      {sessionListMode === 'archived' ? (
        <TouchableOpacity
          style={[styles.emptySecondaryButton, styles.sessionActionTouchTarget, styles.emptyStateButton]}
          onPress={() => setSessionListMode('active')}
          accessibilityRole="button"
          accessibilityLabel={createButtonAccessibilityLabel('Show active chats')}
          accessibilityHint="Returns to the active chats list."
        >
          <Text style={styles.emptySecondaryButtonText}>Show active chats</Text>
        </TouchableOpacity>
      ) : (
        <TouchableOpacity
          style={[styles.newButton, styles.sessionActionTouchTarget, styles.emptyStateButton]}
          onPress={handleCreateSession}
          accessibilityRole="button"
          accessibilityLabel={createButtonAccessibilityLabel('Start first chat')}
          accessibilityHint="Creates and opens your first chat."
        >
          <Text style={styles.emptyStateButtonText}>Start first chat</Text>
        </TouchableOpacity>
      )}
    </View>
  );

  const DisconnectedState = () => (
    <View style={styles.disconnectedState}>
      <View style={styles.disconnectedCard}>
        <View style={styles.disconnectedStatusRow}>
          <ConnectionStatusIndicator
            state={connectionInfo.state}
            retryCount={connectionInfo.retryCount}
          />
        </View>
        <Text style={styles.disconnectedTitle}>{disconnectedTitle}</Text>
        <Text style={styles.disconnectedSubtitle}>{disconnectedSubtitle}</Text>
        {!!config.baseUrl && (
          <Text style={styles.disconnectedMeta} numberOfLines={2}>
            {config.baseUrl}
          </Text>
        )}
        <TouchableOpacity
          style={[styles.newButton, styles.sessionActionTouchTarget, styles.disconnectedPrimaryButton]}
          onPress={() => handleOpenConnectionSettings(true)}
          accessibilityRole="button"
          accessibilityLabel={createButtonAccessibilityLabel('Scan QR Code')}
          accessibilityHint="Opens the QR scanner to pair this mobile app with your desktop."
        >
          <Text style={styles.emptyStateButtonText}>Scan QR Code</Text>
        </TouchableOpacity>
        <TouchableOpacity
          style={[styles.disconnectedSecondaryButton, styles.sessionActionTouchTarget]}
          onPress={() => handleOpenConnectionSettings(false)}
          accessibilityRole="button"
          accessibilityLabel={createButtonAccessibilityLabel('Connection Settings')}
          accessibilityHint="Opens connection settings and pairing details."
        >
          <Text style={styles.disconnectedSecondaryButtonText}>Connection Settings</Text>
        </TouchableOpacity>
      </View>
    </View>
  );

  const SearchEmptyState = () => (
    <View style={styles.emptyState}>
      <View style={styles.emptyStateTextGroup}>
        <Text style={styles.emptyTitle}>No matching chats</Text>
        <Text style={styles.emptySubtitle}>
          Try a different keyword or clear search.
        </Text>
      </View>
      <TouchableOpacity
        style={[styles.newButton, styles.sessionActionTouchTarget, styles.emptyStateButton]}
        onPress={() => setSearchQuery('')}
        accessibilityRole="button"
        accessibilityLabel={createButtonAccessibilityLabel('Clear chat search')}
        accessibilityHint="Clears the current chat search query."
      >
        <Text style={styles.emptyStateButtonText}>Clear search</Text>
      </TouchableOpacity>
    </View>
  );

  const rfHintText = rfStatus === 'listening'
    ? 'Release to send...'
    : rfStatus === 'sending'
      ? 'Sending...'
      : rfStatus === 'sent'
        ? 'Sent to a new chat. Tap it to open.'
        : rfStatus === 'empty'
          ? 'No speech detected. Try again.'
          : rfStatus === 'permissionDenied'
            ? 'Microphone permission denied. Enable it in settings.'
            : rfStatus === 'unavailable'
              ? 'Speech recognition unavailable on this build/device.'
              : rfStatus === 'error'
                ? 'Rapid Fire failed. Try again.'
                : 'Hold to talk (Rapid Fire)';

  if (!isConnected) {
    return (
      <View style={[styles.container, styles.disconnectedContainer, { paddingBottom: insets.bottom }] }>
        <DisconnectedState />
      </View>
    );
  }

  return (
    <View style={[styles.container, { paddingBottom: insets.bottom }]}>
      <View style={styles.searchSection}>
        <View style={styles.searchInputRow}>
          <TextInput
            style={styles.searchInput}
            value={searchQuery}
            onChangeText={setSearchQuery}
            placeholder='Search chats...'
            placeholderTextColor={theme.colors.mutedForeground}
            accessibilityLabel={createTextInputAccessibilityLabel('Chat search')}
            accessibilityHint="Search chat titles, previews, and loaded message text."
            autoCapitalize='none'
            autoCorrect={false}
            returnKeyType='search'
          />
          {hasActiveSearch && (
            <TouchableOpacity
              style={styles.searchClearButton}
              onPress={() => setSearchQuery('')}
              accessibilityRole="button"
              accessibilityLabel={createButtonAccessibilityLabel('Clear chat search')}
              accessibilityHint="Clears the current chat search query."
            >
              <Text style={styles.searchClearButtonText}>Clear</Text>
            </TouchableOpacity>
          )}
        </View>
        <View
          style={styles.archiveFilterRow}
          accessibilityRole="tablist"
          accessibilityLabel="Chat archive filter"
        >
          {([
            ['active', 'Active'],
            ['archived', archivedSessionCount > 0 ? `Archived (${archivedSessionCount})` : 'Archived'],
          ] as const).map(([mode, label]) => {
            const selected = sessionListMode === mode;
            return (
              <TouchableOpacity
                key={mode}
                style={[styles.archiveFilterButton, selected && styles.archiveFilterButtonActive]}
                onPress={() => setSessionListMode(mode)}
                accessibilityRole="button"
                accessibilityState={{ selected }}
                accessibilityLabel={createButtonAccessibilityLabel(`Show ${mode} chats`)}
              >
                <Text style={[styles.archiveFilterButtonText, selected && styles.archiveFilterButtonTextActive]}>
                  {label}
                </Text>
              </TouchableOpacity>
            );
          })}
        </View>
      </View>

      <FlatList
        data={filteredSessions}
        renderItem={renderSession}
        keyExtractor={(item) => item.id}
        contentContainerStyle={filteredSessions.length === 0 ? styles.emptyList : styles.list}
        ListEmptyComponent={hasActiveSearch ? SearchEmptyState : EmptyState}
        keyboardShouldPersistTaps="handled"
      />

      {/* Rapid Fire hold-to-speak button */}
      <View style={styles.rfContainer}>
        {(rfListening || rfStatus === 'sent') && rfTranscript ? (
          <Text style={styles.rfTranscript} numberOfLines={2}>{rfTranscript}</Text>
        ) : null}
        <Text style={styles.rfHint}>
          {rfHintText}
        </Text>
        <Pressable
          ref={rfButtonRef}
          accessibilityRole="button"
          accessibilityLabel={rfListening ? 'Release to send' : 'Hold to talk, Rapid Fire'}
          style={({ pressed }) => [
            styles.rfButton,
            rfListening && styles.rfButtonOn,
            // @ts-ignore - Web-only CSS to disable long-press selection/callouts
            Platform.OS === 'web' && { userSelect: 'none', WebkitUserSelect: 'none', WebkitTouchCallout: 'none', touchAction: 'manipulation' },
            pressed && !rfListening && { opacity: 0.8 },
          ]}
          onPressIn={(e: GestureResponderEvent) => {
            rfGrantTimeRef.current = Date.now();
            rfPressInSeenRef.current = true;
            rfLog('mic:onPressIn', {
              listening: rfListeningRef.current,
              starting: rfStartingRef.current,
              pageX: e.nativeEvent.pageX,
              pageY: e.nativeEvent.pageY,
            });
            if (!rfListeningRef.current) { void rfStartRecording(e); }
          }}
          onPressOut={() => {
            rfPressInSeenRef.current = false;
            const dt = Date.now() - rfGrantTimeRef.current;
            const delay = Math.max(0, rfMinHoldMs - dt);
            rfLog('mic:onPressOut', { listening: rfListeningRef.current, dt, delay });
            if (delay > 0) {
              setTimeout(() => {
                rfLog('mic:onPressOut -> delayed stop fired', { listening: rfListeningRef.current });
                if (rfListeningRef.current) { void rfStopAndFire(); }
              }, delay);
            } else {
              if (rfListeningRef.current) { void rfStopAndFire(); }
            }
          }}
        >
          <Text style={styles.rfButtonText}>{rfListening ? '\uD83C\uDF99\uFE0F' : '\uD83C\uDFA4'}</Text>
          <Text style={styles.rfButtonLabel}>
            {rfListening ? '...' : (rfStatus === 'sending' ? 'Sending' : 'Hold')}
          </Text>
        </Pressable>
      </View>
      <Modal
        visible={!!renameSession}
        transparent
        animationType="fade"
        onRequestClose={handleCloseRenameSession}
      >
        <View style={styles.renameModalOverlay}>
          <View style={styles.renameModalContent}>
            <Text style={styles.renameModalTitle}>Rename chat</Text>
            <TextInput
              style={styles.renameInput}
              value={renameTitleDraft}
              onChangeText={setRenameTitleDraft}
              placeholder="Chat title"
              placeholderTextColor={theme.colors.mutedForeground}
              autoFocus
              returnKeyType="done"
              onSubmitEditing={() => { void handleSaveRenameSession(); }}
              accessibilityLabel={createTextInputAccessibilityLabel('Chat title')}
            />
            <View style={styles.renameActions}>
              <TouchableOpacity
                style={[styles.renameActionButton, styles.renameCancelButton]}
                onPress={handleCloseRenameSession}
                accessibilityRole="button"
                accessibilityLabel={createButtonAccessibilityLabel('Cancel rename')}
                disabled={isRenameSaving}
              >
                <Text style={styles.renameCancelText}>Cancel</Text>
              </TouchableOpacity>
              <TouchableOpacity
                style={[
                  styles.renameActionButton,
                  styles.renameSaveButton,
                  isRenameSaving && styles.renameSaveButtonDisabled,
                ]}
                onPress={() => { void handleSaveRenameSession(); }}
                accessibilityRole="button"
                accessibilityLabel={createButtonAccessibilityLabel('Save chat title')}
                disabled={isRenameSaving}
              >
                <Text style={styles.renameSaveText}>{isRenameSaving ? 'Saving' : 'Save'}</Text>
              </TouchableOpacity>
            </View>
          </View>
        </View>
      </Modal>
      <AgentSelectorSheet
        visible={agentSelectorVisible}
        onClose={() => setAgentSelectorVisible(false)}
      />
    </View>
  );
}

function createStyles(theme: Theme, screenHeight: number) {
  const rfButtonHeight = Math.round(screenHeight * 0.18);
  return StyleSheet.create({
    container: {
      flex: 1,
      backgroundColor: theme.colors.background,
    },
    loadingContainer: {
      justifyContent: 'center',
      alignItems: 'center',
    },
    spinner: {
      width: 48,
      height: 48,
    },
    loadingText: {
      ...theme.typography.body,
      color: theme.colors.mutedForeground,
      marginTop: spacing.md,
    },
    newButton: {
      backgroundColor: theme.colors.primary,
      borderRadius: radius.lg,
    },
    headerNewChatButton: {
      ...createMinimumTouchTargetStyle({
        horizontalPadding: spacing.sm,
        verticalPadding: spacing.xs,
        horizontalMargin: 0,
      }),
      backgroundColor: theme.colors.primary,
      borderRadius: radius.lg,
      marginLeft: spacing.xs,
    },
    headerNewChatButtonText: {
      color: theme.colors.primaryForeground,
      fontSize: 12,
      fontWeight: '600',
    },
    sessionActionTouchTarget: {
      ...createMinimumTouchTargetStyle({
        horizontalPadding: spacing.md,
        verticalPadding: spacing.sm,
        horizontalMargin: 0,
      }),
    },
    newButtonText: {
      color: theme.colors.primaryForeground,
      fontWeight: '600',
    },
    headerSettingsButton: {
      ...createMinimumTouchTargetStyle({
        horizontalPadding: spacing.sm,
        verticalPadding: spacing.sm,
        horizontalMargin: 0,
      }),
      marginLeft: spacing.xs,
    },
    searchSection: {
      paddingHorizontal: spacing.md,
      paddingTop: spacing.sm,
      paddingBottom: spacing.xs,
      gap: spacing.xs,
    },
    searchInputRow: {
      flexDirection: 'row',
      alignItems: 'center',
      gap: spacing.sm,
    },
    searchInput: {
      ...theme.input,
      flex: 1,
    },
    searchClearButton: {
      ...createMinimumTouchTargetStyle({
        horizontalPadding: spacing.sm,
        verticalPadding: spacing.xs,
        horizontalMargin: 0,
      }),
      borderRadius: radius.lg,
    },
    searchClearButtonText: {
      color: theme.colors.primary,
      fontWeight: '600',
    },
    archiveFilterRow: {
      flexDirection: 'row',
      alignItems: 'center',
      gap: spacing.xs,
      marginTop: spacing.xs,
    },
    archiveFilterButton: {
      ...createMinimumTouchTargetStyle({
        horizontalPadding: spacing.sm,
        verticalPadding: spacing.xs,
        horizontalMargin: 0,
      }),
      borderRadius: radius.lg,
      borderWidth: 1,
      borderColor: theme.colors.border,
      backgroundColor: theme.colors.card,
    },
    archiveFilterButtonActive: {
      borderColor: theme.colors.primary,
      backgroundColor: theme.colors.primary + '14',
    },
    archiveFilterButtonText: {
      ...theme.typography.caption,
      color: theme.colors.mutedForeground,
      fontWeight: '600',
    },
    archiveFilterButtonTextActive: {
      color: theme.colors.primary,
    },
    list: {
      padding: spacing.md,
    },
    disconnectedContainer: {
      justifyContent: 'center',
      padding: spacing.md,
    },
    disconnectedState: {
      width: '100%' as const,
      alignItems: 'center',
    },
    disconnectedCard: {
      width: '100%' as const,
      maxWidth: 420,
      backgroundColor: theme.colors.card,
      borderRadius: radius.xl,
      borderWidth: 1,
      borderColor: theme.colors.border,
      padding: spacing.xl,
      gap: spacing.md,
    },
    disconnectedStatusRow: {
      alignItems: 'flex-start',
    },
    disconnectedTitle: {
      ...theme.typography.h2,
    },
    disconnectedSubtitle: {
      ...theme.typography.body,
      color: theme.colors.mutedForeground,
    },
    disconnectedMeta: {
      ...theme.typography.caption,
      color: theme.colors.mutedForeground,
    },
    disconnectedPrimaryButton: {
      alignSelf: 'center',
      minWidth: 200,
    },
    disconnectedSecondaryButton: {
      alignSelf: 'center',
      minWidth: 200,
      borderRadius: radius.lg,
      borderWidth: 1,
      borderColor: theme.colors.border,
      alignItems: 'center',
    },
    disconnectedSecondaryButtonText: {
      color: theme.colors.foreground,
      fontWeight: '600',
      textAlign: 'center',
    },
    emptyList: {
      flex: 1,
      justifyContent: 'center',
      alignItems: 'center',
    },
    sessionItem: {
      backgroundColor: theme.colors.card,
      borderRadius: radius.xl,
      padding: spacing.md,
      marginBottom: spacing.sm,
      borderWidth: 1,
      borderColor: theme.colors.border,
    },
    sessionItemActive: {
      borderColor: theme.colors.primary,
      borderWidth: 2,
    },
    sessionItemPressed: {
      opacity: 0.92,
    },
    sessionHeader: {
      flexDirection: 'row',
      justifyContent: 'space-between',
      marginBottom: 0,
    },
    sessionTitleRow: {
      flexDirection: 'row',
      alignItems: 'center',
      flex: 1,
      minWidth: 0,
      marginRight: 8,
    },
    sessionHeaderMeta: {
      flexDirection: 'row',
      alignItems: 'center',
      gap: spacing.xs,
      marginLeft: spacing.sm,
    },
    sessionTitle: {
      fontSize: 15,
      lineHeight: 20,
      fontWeight: '600',
      color: theme.colors.foreground,
      flex: 1,
      minWidth: 0,
    },
    sessionPinButton: {
      ...createMinimumTouchTargetStyle({
        horizontalPadding: spacing.xs,
        verticalPadding: 4,
        horizontalMargin: 0,
      }),
      borderRadius: radius.lg,
      borderWidth: 1,
      borderColor: theme.colors.border,
      backgroundColor: theme.colors.background,
    },
    sessionPinButtonActive: {
      borderColor: theme.colors.primary,
      backgroundColor: theme.colors.primary + '12',
    },
    sessionPinButtonText: {
      ...theme.typography.caption,
      color: theme.colors.mutedForeground,
      fontWeight: '600',
    },
    sessionPinButtonTextActive: {
      color: theme.colors.primary,
    },
    sessionArchiveButton: {
      ...createMinimumTouchTargetStyle({
        horizontalPadding: spacing.xs,
        verticalPadding: 4,
        horizontalMargin: 0,
      }),
      borderRadius: radius.lg,
      borderWidth: 1,
      borderColor: theme.colors.border,
      backgroundColor: theme.colors.background,
    },
    sessionArchiveButtonActive: {
      borderColor: theme.colors.primary,
      backgroundColor: theme.colors.primary + '12',
    },
    sessionArchiveButtonText: {
      ...theme.typography.caption,
      color: theme.colors.mutedForeground,
      fontWeight: '600',
    },
    sessionArchiveButtonTextActive: {
      color: theme.colors.primary,
    },
    sessionDate: {
      ...theme.typography.caption,
      color: theme.colors.mutedForeground,
    },
    sessionPreview: {
      ...theme.typography.caption,
      color: theme.colors.mutedForeground,
    },
    sessionPreviewMeta: {
      ...theme.typography.caption,
      color: theme.colors.mutedForeground,
      fontWeight: '500',
    },
    emptyState: {
      alignItems: 'center',
      width: '100%' as const,
      maxWidth: 360,
      padding: spacing.xl,
    },
    emptyStateTextGroup: {
      alignItems: 'center',
      marginBottom: spacing.lg,
    },
    emptyTitle: {
      ...theme.typography.h2,
      marginBottom: spacing.sm,
      textAlign: 'center',
    },
    emptySubtitle: {
      ...theme.typography.body,
      color: theme.colors.mutedForeground,
      textAlign: 'center',
    },
    emptyStateButton: {
      width: '100%' as const,
      maxWidth: 280,
    },
    emptyStateButtonText: {
      color: theme.colors.primaryForeground,
      fontWeight: '600',
      textAlign: 'center',
    },
    emptySecondaryButton: {
      borderRadius: radius.lg,
      borderWidth: 1,
      borderColor: theme.colors.border,
      backgroundColor: theme.colors.background,
    },
    emptySecondaryButtonText: {
      color: theme.colors.foreground,
      fontWeight: '600',
      textAlign: 'center',
    },
    renameModalOverlay: {
      flex: 1,
      backgroundColor: 'rgba(0, 0, 0, 0.5)',
      justifyContent: 'center',
      alignItems: 'center',
      padding: spacing.lg,
    },
    renameModalContent: {
      width: '100%' as const,
      maxWidth: 420,
      backgroundColor: theme.colors.background,
      borderRadius: radius.lg,
      borderWidth: 1,
      borderColor: theme.colors.border,
      padding: spacing.lg,
      gap: spacing.md,
    },
    renameModalTitle: {
      ...theme.typography.h2,
    },
    renameInput: {
      ...theme.input,
    },
    renameActions: {
      flexDirection: 'row',
      justifyContent: 'flex-end',
      alignItems: 'center',
      gap: spacing.sm,
      flexWrap: 'wrap',
    },
    renameActionButton: {
      ...createMinimumTouchTargetStyle({
        minSize: 44,
        horizontalPadding: spacing.md,
        verticalPadding: spacing.sm,
        horizontalMargin: 0,
      }),
      borderRadius: radius.md,
      alignItems: 'center',
      minWidth: 96,
    },
    renameCancelButton: {
      borderWidth: 1,
      borderColor: theme.colors.border,
      backgroundColor: theme.colors.background,
    },
    renameCancelText: {
      color: theme.colors.foreground,
      fontWeight: '600',
    },
    renameSaveButton: {
      backgroundColor: theme.colors.primary,
    },
    renameSaveButtonDisabled: {
      opacity: 0.6,
    },
    renameSaveText: {
      color: theme.colors.primaryForeground,
      fontWeight: '600',
    },
    rfContainer: {
      borderTopWidth: theme.hairline,
      borderTopColor: theme.colors.border,
      backgroundColor: theme.colors.card,
      paddingHorizontal: spacing.sm,
      paddingTop: spacing.xs,
      paddingBottom: spacing.sm,
      alignItems: 'center',
    },
    rfHint: {
      ...theme.typography.caption,
      color: theme.colors.mutedForeground,
      marginBottom: spacing.xs,
      textAlign: 'center',
    },
    rfTranscript: {
      ...theme.typography.body,
      color: theme.colors.foreground,
      textAlign: 'center',
      marginBottom: spacing.xs,
      paddingHorizontal: spacing.md,
    },
    rfButton: {
      width: '100%' as any,
      height: rfButtonHeight,
      borderRadius: radius.xl,
      borderWidth: 1.5,
      borderColor: theme.colors.border,
      backgroundColor: theme.colors.background,
      alignItems: 'center',
      justifyContent: 'center',
    },
    rfButtonOn: {
      backgroundColor: theme.colors.primary,
      borderColor: theme.colors.primary,
    },
    rfButtonText: {
      fontSize: 36,
    },
    rfButtonLabel: {
      fontSize: 13,
      color: theme.colors.mutedForeground,
      marginTop: 4,
      fontWeight: '600',
    },
  });
}
