import { useCallback, useEffect, useRef, useState } from 'react';
import {
  View, Text, TextInput, FlatList, Pressable, Platform, Animated,
  Keyboard, KeyboardAvoidingView, StyleSheet, NativeSyntheticEvent,
  NativeScrollEvent, ActivityIndicator, AppState, TouchableOpacity, Linking,
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { Audio, InterruptionModeAndroid, InterruptionModeIOS } from 'expo-av';
// TODO(sdk54): migrate to new expo-file-system class-based API (File/Directory/Paths) — see Task #14
import * as FileSystem from 'expo-file-system/legacy';
import Constants from 'expo-constants';
import { useTranslation } from 'react-i18next';
import { RiveAvatar } from '../components/RiveAvatar';
import { apiFetch } from '../lib/api';
import { supabase } from '../lib/supabase';
import { useColors } from '../lib/theme';
import {
  type GameType, GAME_META, MemoryGame, TicTacToeGame, SnakeGame, TetrisGame,
} from '../components/urge/UrgeGames';
import { SosFeedbackModal, type SosFeedback } from '../components/urge/SosFeedbackModal';
import { ShareSuccessDrawer } from '../components/urge/ShareSuccessDrawer';
import { InlineRatingDrawer } from '../components/urge/InlineRatingDrawer';
import { BreathingDrawer } from '../components/urge/Breathing';
import GamePickerDrawer from '../components/urge/GamePickerDrawer';
import { VoiceBars } from '../components/urge/InlineIndicators';
import MessageRow, { GameHeader, type SosMsg } from '../components/urge/MessageRow';
import { SOS_BOOT } from '../lib/sosPrompts';
import { CHIP_SETS, BREATH_PHASES, type ChipSet } from '../lib/sosConstants';
import { parseLyraResponse, detectEmotion, type LyraEmotion, type ChipSpec } from '../lib/lyraResponse';
import { streamSosLyra, type CrisisLevel } from '../lib/sosStream';
import { SosTtsQueue } from '../lib/sosTtsQueue';
import { endpointForProvider, useTtsProvider, currentProvider } from '../lib/ttsProvider';
import { currentLlmProvider } from '../lib/llmProvider';
import { BenchSession } from '../lib/sosTtsBenchmark';

// ── Main Screen ───────────────────────────────────────────────────────────────

export default function SOSScreen() {
  const { t, i18n } = useTranslation();
  const router = useRouter();
  const insets = useSafeAreaInsets();
  const colors = useColors();
  const st = makeStyles(colors);
  const flatRef = useRef<FlatList>(null);

  const [messages, setMessages] = useState<SosMsg[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [isBreathing, setIsBreathing] = useState(false);
  const [input, setInput] = useState('');
  const [thinking, setThinking] = useState(false);
  const [emotion, setEmotion] = useState<LyraEmotion>('idle');
  const [isSpeaking, setIsSpeaking] = useState(false);
  const [isTtsLoading, setIsTtsLoading] = useState(false);
  const [soundEnabled, setSoundEnabled] = useState(true);
  const soundEnabledRef = useRef(true);
  const [chipSet, setChipSet] = useState<ChipSet>('start');
  const [dynamicChips, setDynamicChips] = useState<ChipSpec[]>([]);
  const [crisisChips, setCrisisChips] = useState<Array<{ label: string; action: string }>>([]);
  const [crisisLevel, setCrisisLevel] = useState<CrisisLevel>('none');
  const [userTurnCount, setUserTurnCount] = useState(0);

  // ——— Session-Tracking für DiGA ———
  const sessionStartRef = useRef<Date>(new Date());
  const breathingCountRef = useRef(0);
  const gamesPlayedRef = useRef<Array<{ game: string; score: number; durationSec: number }>>([]);
  const wasOvercomeRef = useRef(false);
  const sessionSavedRef = useRef(false);
  // Hint-Trigger: User klickt "weiter reden" nach Atmen/Spiel
  const requestStatsReminderRef = useRef(false);
  // Hint-Trigger: nach Atmen/Spiel → Lyra soll Check-in machen
  const requestCheckInRef = useRef<'after_breathing' | 'after_game' | null>(null);
  // Hint-Trigger: nach Überwinden → Lyra soll zu Share/Rate motivieren
  const requestOvercomeMotivationRef = useRef(false);
  // Game-Score-Tracking: PB vor dem aktuellen Spielstart (für Vergleich nach Spiel-Ende)
  const pbBeforeGameRef = useRef<number>(0);
  // Hint-Trigger: nach Spiel-Ende mit neuem Personal-Best → Lyra soll feiern
  const requestNewPbCelebrationRef = useRef<{ game: string; oldScore: number; newScore: number } | null>(null);

  // Exit-Feedback-Modal
  const [feedbackVisible, setFeedbackVisible] = useState(false);
  const exitingRef = useRef(false);
  // Inline-Feedback (Drawer in der Chat-Session) — wenn gegeben, kein Exit-Modal mehr
  const inlineFeedbackRef = useRef<SosFeedback | null>(null);
  const [ratingDrawerVisible, setRatingDrawerVisible] = useState(false);
  // Share-Success Drawer
  const [shareDrawerVisible, setShareDrawerVisible] = useState(false);
  const [shareDraft, setShareDraft] = useState('');
  const [shareGenerating, setShareGenerating] = useState(false);
  const sharePostedRef = useRef(false);
  const [isPickingGame, setIsPickingGame] = useState(false);
  const [breathingDone, setBreathingDone] = useState(false);
  const [playingGame, setPlayingGame] = useState<GameType | null>(null);
  const [keyboardHeight, setKeyboardHeight] = useState(0);
  const [showScrollBtn, setShowScrollBtn] = useState(false);

  const ttsRef = useRef<Audio.Sound | null>(null);
  const ttsAbortRef = useRef<AbortController | null>(null);
  // Phase B: sentence-level streaming TTS — eine Queue pro sendToLyra-Call.
  const ttsQueueRef = useRef<SosTtsQueue | null>(null);
  const emotionTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const isNearBottomRef = useRef(true);

  useEffect(() => { soundEnabledRef.current = soundEnabled; }, [soundEnabled]);

  // ttsProvider state: Änderungen invalidieren den Atemübungs-Audio-Cache unten.
  const [ttsProvider] = useTtsProvider();

  // Pre-cache der Atemübungs-Voice-Cues (Einatmen / Halten / Ausatmen).
  // Bei Phase-Wechsel im BreathingDrawer kommt das Audio sofort statt mit
  // ~600ms TTS-Roundtrip — so bleibt Voice synchron mit der Pulse-Animation.
  // Cache wird invalidiert wenn der TTS-Provider wechselt.
  const breathAudioCacheRef = useRef<Map<string, string>>(new Map());
  useEffect(() => {
    breathAudioCacheRef.current.clear();
    const phrases = BREATH_PHASES
      .map((p) => p.speakLine)
      .filter((s): s is string => Boolean(s));
    let cancelled = false;
    (async () => {
      for (const text of phrases) {
        if (cancelled) return;
        const audio = await fetchTtsAudio(text).catch(() => null);
        if (cancelled) return;
        if (audio) breathAudioCacheRef.current.set(text, audio.uri);
      }
    })();
    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [ttsProvider]);

  // Audio-Mode: bei SOS-Mount Audio-Session konfigurieren.
  // - playsInSilentModeIOS: Lyra spricht auch wenn iPhone auf "stumm"
  // - shouldDuckAndroid: andere Audio-Quellen (Spotify) leiser machen wenn Lyra spricht
  // - staysActiveInBackground: false → OS pausiert TTS automatisch wenn App im Background
  // Plus warm-up: Audio.Sound.createAsync hat auf Android ~500ms Cold-Start.
  // Wir feuern einen no-op-load + unload damit ExoPlayer warm ist bevor Lyra spricht.
  useEffect(() => {
    Audio.setAudioModeAsync({
      allowsRecordingIOS: false,
      playsInSilentModeIOS: true,
      staysActiveInBackground: false,
      interruptionModeIOS: InterruptionModeIOS.DoNotMix,
      shouldDuckAndroid: true,
      interruptionModeAndroid: InterruptionModeAndroid.DoNotMix,
      playThroughEarpieceAndroid: false,
    }).catch((err) => {
      console.warn('[sos-audio-mode] failed:', err);
    });
  }, []);

  // Cleanup-Effect: bei Component-Unmount (router.back, hardware-back, replace,
  // ...) ALLE TTS-Resources freigeben — damit Lyra nicht im Background weiter
  // redet wenn der User die SOS-Page verlässt. User-Bug 2026-05-04 auf A50.
  useEffect(() => {
    return () => {
      ttsAbortRef.current?.abort();
      ttsAbortRef.current = null;
      if (ttsRef.current) {
        const s = ttsRef.current;
        ttsRef.current = null;
        s.stopAsync().catch(() => {});
        s.unloadAsync().catch(() => {});
      }
      ttsQueueRef.current?.abort();
      ttsQueueRef.current = null;
      if (emotionTimer.current) {
        clearTimeout(emotionTimer.current);
        emotionTimer.current = null;
      }
    };
  }, []);

  // AppState: wenn App in den Background geht (Home-Button, App-Switcher,
  // Lock-Screen) → TTS stoppen. Sonst spricht Lyra durch's Lock-Screen weiter.
  useEffect(() => {
    const sub = AppState.addEventListener('change', (state) => {
      if (state === 'background' || state === 'inactive') {
        ttsAbortRef.current?.abort();
        ttsAbortRef.current = null;
        if (ttsRef.current) {
          const s = ttsRef.current;
          ttsRef.current = null;
          s.stopAsync().catch(() => {});
          s.unloadAsync().catch(() => {});
        }
        ttsQueueRef.current?.abort();
        ttsQueueRef.current = null;
        setIsSpeaking(false);
        setIsTtsLoading(false);
      }
    });
    return () => sub.remove();
  }, []);

  useEffect(() => {
    const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
    const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
    const show = Keyboard.addListener(showEvent, (e) => setKeyboardHeight(e.endCoordinates.height));
    const hide = Keyboard.addListener(hideEvent, () => setKeyboardHeight(0));
    return () => { show.remove(); hide.remove(); };
  }, []);

  useEffect(() => {
    if (messages.length === 0) return;
    if (isNearBottomRef.current) requestAnimationFrame(() => flatRef.current?.scrollToEnd({ animated: true }));
    else setShowScrollBtn(true);
  }, [messages.length, thinking, isBreathing]);

  function handleScroll(e: NativeSyntheticEvent<NativeScrollEvent>) {
    const { layoutMeasurement, contentOffset, contentSize } = e.nativeEvent;
    const dist = contentSize.height - contentOffset.y - layoutMeasurement.height;
    isNearBottomRef.current = dist < 80;
    if (isNearBottomRef.current) setShowScrollBtn(false);
  }

  function addMessage(msg: SosMsg) { setMessages((prev) => [...prev, msg]); }
  function scheduleEmotionReset(delay = 4000) {
    if (emotionTimer.current) clearTimeout(emotionTimer.current);
    emotionTimer.current = setTimeout(() => setEmotion('idle'), delay);
  }

  function stopSpeaking() {
    ttsAbortRef.current?.abort();
    ttsAbortRef.current = null;
    ttsRef.current?.stopAsync().catch(() => {});
    ttsRef.current?.unloadAsync().catch(() => {});
    ttsRef.current = null;
    // Phase B: laufende Sentence-Queue auch abbrechen
    ttsQueueRef.current?.abort();
    ttsQueueRef.current = null;
    setIsSpeaking(false); setIsTtsLoading(false); setEmotion('idle');
  }

  // Holt Audio-MP3 von speak-openai und speichert als temporäre Datei
  async function fetchTtsAudio(rawText: string): Promise<{ uri: string; controller: AbortController } | null> {
    if (!soundEnabledRef.current) return null;
    const text = rawText.replace(/\s+/g, ' ').trim();
    if (!text) return null;
    const controller = new AbortController();
    const session = (await supabase.auth.getSession()).data.session;
    if (controller.signal.aborted) return null;
    const apiBase = Constants.expoConfig?.extra?.apiUrl as string;
    const endpoint = '/api/coach/speak';
    const isGoogleCloud = false;
    const ttsRes = await fetch(`${apiBase}${endpoint}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...(session?.access_token ? { Authorization: `Bearer ${session.access_token}` } : {}),
      },
      body: JSON.stringify({ text, locale: i18n.language, mode: 'sos' }),
      signal: controller.signal,
    });
    if (!ttsRes.ok || controller.signal.aborted) return null;

    let base64: string;
    let ext: 'mp3' | 'wav';
    if (isGoogleCloud) {
      const json = (await ttsRes.json()) as { audio?: string };
      const dataUri = json.audio ?? '';
      const comma = dataUri.indexOf(',');
      if (comma === -1) return null;
      base64 = dataUri.slice(comma + 1);
      ext = 'mp3';
    } else {
      const buffer = await ttsRes.arrayBuffer();
      if (controller.signal.aborted || buffer.byteLength === 0) return null;
      const bytes = new Uint8Array(buffer);
      const chunks: string[] = [];
      const cs = 0x8000;
      for (let i = 0; i < bytes.length; i += cs)
        chunks.push(String.fromCharCode(...bytes.subarray(i, Math.min(i + cs, bytes.length))));
      base64 = btoa(chunks.join(''));
      ext = endpoint.endsWith('/speak-gemini') ? 'wav' : 'mp3';
    }

    const tmpPath = `${FileSystem.cacheDirectory}sos-tts-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
    await FileSystem.writeAsStringAsync(tmpPath, base64, { encoding: FileSystem.EncodingType.Base64 });
    if (controller.signal.aborted) return null;
    return { uri: tmpPath, controller };
  }

  // Spielt eine fertige Audio-Datei (oder fetcht sie wenn nicht vorhanden) und wartet auf didJustFinish
  async function playTtsAudio(rawText: string, prefetched: { uri: string; controller: AbortController } | null): Promise<void> {
    if (!soundEnabledRef.current) return;
    // Bestehenden Audio NICHT abbrechen — wir wollen nahtlos anschließen
    setIsTtsLoading(!prefetched);
    let audio = prefetched;
    if (!audio) {
      try { audio = await fetchTtsAudio(rawText); } catch { audio = null; }
    }
    if (!audio) { setIsTtsLoading(false); return; }
    ttsAbortRef.current = audio.controller;
    try {
      const { sound } = await Audio.Sound.createAsync({ uri: audio.uri }, { shouldPlay: true });
      if (audio.controller.signal.aborted) { sound.unloadAsync(); setIsTtsLoading(false); return; }
      setIsTtsLoading(false);
      ttsRef.current = sound;
      setIsSpeaking(true);
      await new Promise<void>((resolve) => {
        const aborted = () => { resolve(); };
        audio!.controller.signal.addEventListener('abort', aborted);
        sound.setOnPlaybackStatusUpdate((status) => {
          if (status.isLoaded && status.didJustFinish) {
            setIsSpeaking(false);
            scheduleEmotionReset(0);
            sound.unloadAsync().catch(() => {});
            audio!.controller.signal.removeEventListener('abort', aborted);
            resolve();
          }
        });
      });
    } catch (err: any) {
      if (err?.name === 'AbortError' || audio.controller.signal.aborted) { setIsTtsLoading(false); return; }
      console.warn('[sos-tts]', err);
      setIsTtsLoading(false);
      setIsSpeaking(false);
    }
  }

  // Legacy: einmaliger TTS-Call (für nicht-Streaming Pfade z.B. opening greeting)
  async function speakText(rawText: string): Promise<void> {
    if (!soundEnabledRef.current) return;
    ttsAbortRef.current?.abort();
    if (ttsRef.current) {
      ttsRef.current.stopAsync().catch(() => {});
      ttsRef.current.unloadAsync().catch(() => {});
      ttsRef.current = null;
      setIsSpeaking(false);
    }
    // Cache-hit (Atemübung-Phrasen): instant playback, kein API-roundtrip.
    const cleaned = rawText.replace(/\s+/g, ' ').trim();
    const cachedUri = breathAudioCacheRef.current.get(cleaned);
    if (cachedUri) {
      await playTtsAudio(cleaned, { uri: cachedUri, controller: new AbortController() });
      return;
    }
    const audio = await fetchTtsAudio(rawText).catch(() => null);
    if (!audio) return;
    await playTtsAudio(rawText, audio);
  }

  async function sendToLyra(userText: string) {
    if (thinking) return;
    if (isSpeaking) stopSpeaking();
    addMessage({ id: Date.now().toString(), role: 'user', content: userText, timestamp: new Date() });
    setUserTurnCount((n) => n + 1);
    setThinking(true); setEmotion('thinking');
    // Latenz-Benchmark — eine Session pro sendToLyra-Call. Marker werden in
    // stream/queue über onMetric gesammelt, gedruckt im onIdle (oder als
    // Fallback im finally bei Errors / sound-off).
    const bench = new BenchSession({ provider: currentProvider(), llm: currentLlmProvider(), label: 'send' });
    try {
      const visibleHistory = messages.filter((m) => !m.cardType).map((m) => ({ role: m.role, content: m.content }));

      // ----- Hint-Builder -----
      const hints: string[] = [];

      // (1) Check-in nach Atmen/Spiel
      const checkIn = requestCheckInRef.current;
      requestCheckInRef.current = null;
      if (checkIn === 'after_breathing') {
        hints.push('[SYSTEM-HINT] Der User hat gerade die Atemübung beendet. Frage warm nach, wie er sich JETZT fühlt. Biete 3-4 Chips: "🫁 Nochmal atmen", "🎮 Spiel", "💭 An was anderes denken", "😄 Erzähl mir einen Witz".');
      } else if (checkIn === 'after_game') {
        hints.push('[SYSTEM-HINT] Der User hat gerade das Spiel beendet. Frage warm, wie er sich JETZT fühlt. Biete 3-4 Chips: "🎮 Weiter spielen", "🫁 Atemübung", "💭 An was anderes denken", "😄 Erzähl mir einen Witz".');
      }

      // (1b) Personal-Best-Celebration — wenn User gerade neuen PB gemacht hat
      if (requestNewPbCelebrationRef.current) {
        const { game, oldScore, newScore } = requestNewPbCelebrationRef.current;
        requestNewPbCelebrationRef.current = null;
        const pbHint = oldScore > 0
          ? `[SYSTEM-HINT] NEUER REKORD! Der User hat sein bisheriges Personal-Best in ${game} (${oldScore}) auf ${newScore} verbessert. Feiere das warm in 1-2 Sätzen — nenne KONKRET die Zahlen ${oldScore} → ${newScore}. Mach klar: das ist sein eigener Sieg, ein echter Schritt. Dann der normale after_game-Check-in (siehe oben).`
          : `[SYSTEM-HINT] Erster Score! Der User hat seinen ALLERSTEN Score in ${game}: ${newScore}. Feiere den Erstversuch warm — der erste Score ist immer was Besonderes. Dann der normale after_game-Check-in.`;
        hints.push(pbHint);
      }

      // (2) Stats-Reminder bei "weiter reden" nach Aktion
      if (requestStatsReminderRef.current) {
        requestStatsReminderRef.current = false;
        const b = breathingCountRef.current;
        const g = gamesPlayedRef.current.length;
        const parts: string[] = [];
        if (b > 0) parts.push(`${b}x Atemübung`);
        if (g > 0) parts.push(`${g}x Spiel`);
        const stats = parts.join(' + ') || 'mehrere Bewältigungs-Tools';
        hints.push(`[SYSTEM-HINT] Erinnere den User WARM und KONKRET: Wir haben in dieser Session schon ${stats} gemacht. Das senkt wissenschaftlich nachweislich den Spielimpuls (z.B. Atemübungen aktivieren den Parasympathikus). Lobe ihn: er hat die Gambling-Industrie heute schon geschlagen. Dann frage, was er jetzt braucht. Chips: "❤️ Überwunden", "🫁 Nochmal atmen", "🎮 Spiel".`);
      }

      // (3) Nach 3+ Turns: Service-Angebot wenn Lyra noch nicht angeboten hat
      if ((userTurnCount + 1) >= 3 && breathingCountRef.current === 0 && gamesPlayedRef.current.length === 0) {
        hints.push('[SYSTEM-HINT] Ich habe jetzt schon mehrere Male geantwortet. Biete mir JETZT konkret eine Atemübung ODER ein Spiel an. Chips müssen "breathing" + "game_picker" enthalten, optional "send_text:Lass uns weiter reden".');
      }

      // (4) Nach Überwinden → Lyra motiviert zu Sharing + Bewertung
      if (requestOvercomeMotivationRef.current) {
        requestOvercomeMotivationRef.current = false;
        hints.push(
          '[SYSTEM-HINT] Der User hat den Impuls ÜBERWUNDEN. Feiere ihn warm und KURZ (2-3 Sätze max). Dann motiviere ihn zu ZWEI konkreten Aktionen: ' +
            '(a) Erfolg mit der Community teilen — das stärkt andere Betroffene und macht ihn stolz. ' +
            '(b) Diese Session bewerten + kurze Bemerkung geben — das hilft uns Lyra zu verbessern und die App für DiGA-Zertifizierung qualitativ besser zu machen. ' +
            'WICHTIG: Liefere GENAU diese Chips: ' +
            '[{"label":"✨ Erfolg teilen","action":"share_success"}, {"label":"⭐ Session bewerten","action":"rate_session"}, {"label":"✅ Fertig","action":"close"}]'
        );
      }

      const hintMsgs = hints.map((h) => ({ role: 'user' as const, content: h }));
      const apiMessages = [...SOS_BOOT, ...visibleHistory, { role: 'user' as const, content: userText }, ...hintMsgs];

      // ── SSE-Streaming via react-native-sse ──
      const session = (await supabase.auth.getSession()).data.session;
      const apiBase = Constants.expoConfig?.extra?.apiUrl as string;

      if (!session?.access_token) throw new Error('no token');

      const assistantId = (Date.now() + 1).toString();
      let assistantAdded = false;
      const ensureBubble = (text: string) => {
        if (!assistantAdded) {
          assistantAdded = true;
          addMessage({ id: assistantId, role: 'assistant', content: text, timestamp: new Date() });
        } else {
          setMessages((prev) => prev.map((m) => (m.id === assistantId ? { ...m, content: text } : m)));
        }
      };

      let visible = '';
      let parsedChips: Array<{ label: string; action: string }> = [];
      let streamError: any = null;
      // Hybrid-TTS v3 (Threshold 3): warte auf 3 Sätze bevor first-chunk
      // ans TTS geht. Mit prompt "max 3 Sätze" sind 99% aller Antworten
      // single-shot → NULL Voice-Boundary, 100% konsistente Stimme.
      // Trade-off: ~1s mehr First-Audio-Latenz im 3-Satz-Fall vs. v2.
      const firstChunkSentences: string[] = [];
      let firstChunkConsumed = false;
      let firstChunkText = '';

      // Hybrid-TTS-Queue: erster Satz live + Rest als ein Block. Pre-fetch in
      // der Queue startet sofort beim enqueue → läuft parallel zum Playback
      // des ersten Satzes → null Gap zwischen Satz-1 und Rest.
      ttsQueueRef.current?.abort();
      const ttsQueue = soundEnabledRef.current
        ? new SosTtsQueue({
            apiBase,
            accessToken: session.access_token,
            locale: i18n.language,
            endpoint: '/api/coach/speak',
            onStart: () => { setIsSpeaking(true); setIsTtsLoading(false); },
            onIdle: () => { setIsSpeaking(false); setIsTtsLoading(false); scheduleEmotionReset(0); bench.print(); },
            onError: (err, sentence) => {
              console.warn('[sos-tts-queue] segment failed:', sentence.slice(0, 50), err);
            },
            onMetric: bench.mark,
          })
        : null;
      ttsQueueRef.current = ttsQueue;
      if (ttsQueue) setIsTtsLoading(true);

      await new Promise<void>((resolve) => {
        let cancel: (() => void) | undefined;
        streamSosLyra({
          apiBase,
          token: session.access_token,
          messages: apiMessages,
          locale: i18n.language,
          llmProvider: currentLlmProvider(),
          onMetric: bench.mark,
          onCrisisLevel: (level) => setCrisisLevel(level),
          onCrisisChips: (chips) => setCrisisChips(chips),
          onTextUpdate: (full) => {
            visible = full;
            ensureBubble(full);
          },
          onChips: (chips) => {
            parsedChips = chips;
          },
          onSentence: (sentence) => {
            if (firstChunkConsumed) return;
            firstChunkSentences.push(sentence);
            // Trigger erst bei 3 vollständigen Sätzen (war v2: 2). Mit Server-
            // Prompt "max 3 Sätze" landen 99% aller Antworten als single-shot
            // im onDone (count<3) → NULL Boundary, 100% konsistente Stimme.
            // Boundary nur bei seltenen 4+-Satz-Antworten.
            if (firstChunkSentences.length >= 2) {
              firstChunkConsumed = true;
              firstChunkText = firstChunkSentences.join(' ');
              ttsQueue?.enqueue(firstChunkText);
            }
          },
          onDone: (full) => {
            visible = full || visible;
            ensureBubble(visible);
            if (ttsQueue) {
              if (firstChunkConsumed) {
                // Rest = full minus first-chunk. Fester Match via indexOf.
                const idx = full.indexOf(firstChunkText);
                const rest =
                  idx >= 0
                    ? full.slice(idx + firstChunkText.length).trim()
                    : full.replace(firstChunkText, '').trim();
                if (rest.length > 0) {
                  // Mode 'sos-continuation' → server gibt OpenAI explizite
                  // "no fresh-start"-Instructions → Boundary weicher.
                  ttsQueue.enqueue(rest, 'sos-continuation');
                }
              } else {
                // <2 Sätze detektiert (kurze Antwort) — kompletten Text als
                // single-shot. Voice 100% konsistent, kein Boundary überhaupt.
                const cleaned = (full || visible).trim();
                if (cleaned) ttsQueue.enqueue(cleaned);
              }
            }
            resolve();
          },
          onError: (err) => {
            streamError = err;
            resolve();
          },
        })
          .then((c) => {
            cancel = c;
          })
          .catch((err) => {
            streamError = err;
            resolve();
          });
      });

      // Fallback bei Stream-Fehler: alter non-streaming Endpoint
      if (streamError) {
        console.warn('[sos-stream] failed, fallback', streamError);
        // Queue abbrechen damit nicht halb-gefüllter Stream noch Audio spielt
        ttsQueueRef.current?.abort();
        ttsQueueRef.current = null;
        const res = await apiFetch<{ message: string }>('/api/coach/message', {
          method: 'POST',
          body: { messages: apiMessages, locale: i18n.language, sosMode: true },
        });
        const parsed = parseLyraResponse(res?.message ?? '');
        visible = parsed.message;
        parsedChips = parsed.chips;
        ensureBubble(visible);
        if (visible) speakText(visible);
      }
      // Wenn Stream erfolgreich war, Queue aber leer geblieben (z.B. Stream hat
      // 0 Sätze geliefert, Edge-Case bei sehr kurzer LLM-Antwort) → Loading-Flag
      // selbst zurücksetzen, sonst hängt der Spinner.
      if (!streamError && ttsQueue && !ttsQueue.isActive()) {
        setIsTtsLoading(false);
      }

      const e = detectEmotion(visible);

      // Fallback-Chips wenn Lyra keine liefert
      let finalChips = parsedChips;
      if (finalChips.length === 0) {
        if (checkIn === 'after_breathing') {
          finalChips = [
            { label: '🫁 Nochmal atmen', action: 'breathing' },
            { label: '🎮 Spiel', action: 'game_picker' },
            { label: '💭 An was anderes denken', action: 'send_text:Lenk mich bitte ab — erzähl mir was Schönes.' },
            { label: '❤️ Überwunden', action: 'overcome' },
          ];
        } else if (checkIn === 'after_game') {
          finalChips = [
            { label: '🎮 Weiter spielen', action: 'game_picker' },
            { label: '🫁 Atemübung', action: 'breathing' },
            { label: '😄 Erzähl mir einen Witz', action: 'send_text:Erzähl mir bitte einen kurzen Witz.' },
            { label: '❤️ Überwunden', action: 'overcome' },
          ];
        } else if ((userTurnCount + 1) >= 3) {
          finalChips = [
            { label: '🫁 Atemübung', action: 'breathing' },
            { label: '🎮 Spiel', action: 'game_picker' },
            { label: '💬 Weiter reden', action: 'send_text:Lass uns weiter reden.' },
          ];
        } else {
          finalChips = [
            { label: '💬 Mehr erzählen', action: 'send_text:Ich möchte mehr erzählen.' },
            { label: '🫁 Atemübung', action: 'breathing' },
            { label: '🎮 Ablenken', action: 'game_picker' },
          ];
        }
      }
      setDynamicChips(finalChips);
      setChipSet('none');
      setEmotion(e); scheduleEmotionReset(e === 'empathy' ? 6000 : 4000);
    } catch {
      addMessage({ id: (Date.now() + 1).toString(), role: 'assistant', content: t('coach.error'), timestamp: new Date() });
      setEmotion('idle');
    } finally {
      setThinking(false);
      // Fallback-Print NUR wenn keine TTS-Queue (mehr) aktiv ist. Sonst feuert
      // das finally bei kurzen Antworten zu früh — der TTS-Fetch läuft dann
      // gerade erst, headers kommen erst Sekunden später, und ein print()
      // hier würde alle TTS-Marker verwerfen. Im aktiven Fall übernimmt
      // ttsQueue.onIdle den Print.
      if (!ttsQueueRef.current?.isActive()) {
        bench.print('finally');
      }
    }
  }

  // Opening greeting on mount — nutzt gleichen Streaming-Pfad wie sendToLyra,
  // damit Hybrid-TTS auch beim Start greift (sonst dauerte es 4-5s bis Lyra
  // sprach, weil non-streaming /api/coach/message + dann separater TTS-Call).
  useEffect(() => {
    let cancelled = false;
    async function openGreeting() {
      setIsLoading(true); setEmotion('thinking'); setThinking(true);
      const greetingId = 'greeting-' + Date.now();
      let assistantAdded = false;
      const ensureBubble = (text: string) => {
        if (!assistantAdded) {
          assistantAdded = true;
          addMessage({ id: greetingId, role: 'assistant', content: text, timestamp: new Date() });
        } else {
          setMessages((prev) => prev.map((m) => (m.id === greetingId ? { ...m, content: text } : m)));
        }
      };
      try {
        const session = (await supabase.auth.getSession()).data.session;
        if (cancelled) return;
        if (!session?.access_token) throw new Error('no token');
        const apiBase = Constants.expoConfig?.extra?.apiUrl as string;

        // Latenz-Benchmark fürs Greeting — gleiches Pattern wie sendToLyra.
        const greetingBench = new BenchSession({ provider: currentProvider(), llm: currentLlmProvider(), label: 'greeting' });

        // Hybrid-TTS-Queue, gleiches Pattern wie sendToLyra
        const ttsQueue = soundEnabledRef.current
          ? new SosTtsQueue({
              apiBase,
              accessToken: session.access_token,
              locale: i18n.language,
              endpoint: '/api/coach/speak',
              onStart: () => { setIsSpeaking(true); setIsTtsLoading(false); },
              onIdle: () => { setIsSpeaking(false); setIsTtsLoading(false); scheduleEmotionReset(0); greetingBench.print(); },
              onError: (err, sentence) => {
                console.warn('[sos-tts-greeting] segment failed:', sentence.slice(0, 50), err);
              },
              onMetric: greetingBench.mark,
            })
          : null;
        ttsQueueRef.current?.abort();
        ttsQueueRef.current = ttsQueue;
        if (ttsQueue) setIsTtsLoading(true);

        const greetingChunkSentences: string[] = [];
        let greetingChunkConsumed = false;
        let greetingChunkText = '';
        let visible = '';
        let parsedChips: Array<{ label: string; action: string }> = [];

        await new Promise<void>((resolve) => {
          streamSosLyra({
            apiBase,
            token: session.access_token,
            messages: SOS_BOOT,
            locale: i18n.language,
            llmProvider: currentLlmProvider(),
            onMetric: greetingBench.mark,
            onCrisisLevel: (level) => { if (!cancelled) setCrisisLevel(level); },
            onCrisisChips: (chips) => { if (!cancelled) setCrisisChips(chips); },
            onTextUpdate: (full) => {
              if (cancelled) return;
              visible = full;
              ensureBubble(full);
            },
            onChips: (chips) => { parsedChips = chips; },
            onSentence: (s) => {
              if (greetingChunkConsumed) return;
              greetingChunkSentences.push(s);
              if (greetingChunkSentences.length >= 2) {
                greetingChunkConsumed = true;
                greetingChunkText = greetingChunkSentences.join(' ');
                ttsQueue?.enqueue(greetingChunkText);
              }
            },
            onDone: (full) => {
              if (cancelled) { resolve(); return; }
              visible = full || visible;
              ensureBubble(visible);
              if (ttsQueue) {
                if (greetingChunkConsumed) {
                  const idx = full.indexOf(greetingChunkText);
                  const rest = idx >= 0
                    ? full.slice(idx + greetingChunkText.length).trim()
                    : full.replace(greetingChunkText, '').trim();
                  if (rest.length > 0) ttsQueue.enqueue(rest, 'sos-continuation');
                } else {
                  const cleaned = (full || visible).trim();
                  if (cleaned) ttsQueue.enqueue(cleaned);
                }
              }
              resolve();
            },
            onError: (err) => {
              console.warn('[sos-greeting] stream failed:', err);
              resolve();
            },
          })
            .then(() => {})
            .catch(() => resolve());
        });

        if (cancelled) return;
        const e = detectEmotion(visible);
        // Fallback-Chips falls Lyra im Greeting keine liefert
        const greetingChips = parsedChips.length > 0 ? parsedChips : [
          { label: '😤 Wütend', action: 'feel:Ich bin gerade sehr wütend.' },
          { label: '😰 Ängstlich', action: 'feel:Ich bin ängstlich und nervos.' },
          { label: '😔 Traurig', action: 'feel:Ich bin traurig.' },
          { label: '🤔 Etwas anderes', action: 'need_help' },
        ];
        setDynamicChips(greetingChips);
        setChipSet('none');
        setEmotion(e); scheduleEmotionReset(e === 'empathy' ? 6000 : 4000);
      } catch (err) {
        if (cancelled) return;
        console.warn('[sos-greeting] failed, fallback message', err);
        addMessage({ id: 'greeting-err', role: 'assistant', content: 'Ich bin für dich da. Was ist gerade los?', timestamp: new Date() });
        setEmotion('empathy'); scheduleEmotionReset(5000);
      } finally { if (!cancelled) { setIsLoading(false); setThinking(false); } }
    }
    openGreeting();
    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  function startBreathing() {
    // Laufendes Lyra-TTS stoppen — sonst spricht sie in die Atemübung rein
    stopSpeaking();
    setIsBreathing(true);
    setChipSet('none');
  }

  async function handleBreathingDone() {
    setIsBreathing(false);
    setBreathingDone(true);
    breathingCountRef.current += 1;
    setChipSet('after_breathing');
    requestCheckInRef.current = 'after_breathing';
    await sendToLyra('Ich habe gerade die 4-7-8 Atemübung abgeschlossen.');
  }

  async function handleChip(action: string) {
    if (thinking) return;
    Keyboard.dismiss();
    if (action.startsWith('tel:')) {
      Linking.openURL(action).catch(() => {});
      return;
    }
    // Clear dynamic chips on any chip action — Lyra will provide new ones in her reply
    setDynamicChips([]);
    if (action.startsWith('feel:')) {
      const feelingText = action.replace('feel:', '');
      setChipSet('none');
      await sendToLyra(feelingText);
    } else if (action === 'need_help') {
      setChipSet('none');
      await sendToLyra('Ich möchte darüber reden, was gerade los ist.');
    } else if (action === 'just_play') {
      setChipSet('none');
      setIsPickingGame(true);
    } else if (action === 'breathing') {
      startBreathing();
    } else if (action === 'game_picker') {
      setChipSet('none');
      setIsPickingGame(true);
    } else if (action === 'overcome') {
      await finalizeOvercome();
    } else if (action === 'show_stats') {
      setChipSet('none');
      try {
        const res = await apiFetch<{ urges?: number; overcomeCnt?: number; streakDays?: number }>('/api/urge/stats');
        const msg = res
          ? `Du hast heute ${res.overcomeCnt ?? 1} von ${res.urges ?? 1} Impulsen widerstanden. Dein Streak: ${res.streakDays ?? 1} Tag(e). Ich bin stolz auf dich! 💪`
          : 'Jeder überwundene Impuls macht dich stärker. Ich bin stolz auf dich! 💪';
        addMessage({ id: 'stats-' + Date.now(), role: 'assistant', content: msg, timestamp: new Date() });
        speakText(msg);
      } catch {
        addMessage({ id: 'stats-err', role: 'assistant', content: 'Jeder überwundene Impuls zählt. Du machst das großartig! 💪', timestamp: new Date() });
      }
    } else if (action === 'close') {
      attemptExit();
    } else if (action === 'share_success') {
      setChipSet('none');
      openShareDrawer();
    } else if (action === 'rate_session') {
      setChipSet('none');
      setRatingDrawerVisible(true);
    } else if (action.startsWith('send_text:')) {
      setChipSet('none');
      const txt = action.replace('send_text:', '');
      // Wenn der User nach Atem/Spiel "weiter reden" klickt → Stats-Reminder triggern
      if ((breathingCountRef.current > 0 || gamesPlayedRef.current.length > 0) &&
          /weiter reden|weiterreden|reden|sprechen/i.test(txt)) {
        requestStatsReminderRef.current = true;
      }
      await sendToLyra(txt);
    }
  }

  async function handleGameSelect(game: GameType) {
    setIsPickingGame(false);
    setPlayingGame(game);
    const titles: Record<GameType, string> = { snake: 'Snake', tetris: 'Tetris', memory: 'Memory', tictactoe: 'Tic-Tac-Toe' };
    const title = titles[game];

    // Personal-Best fetchen (parallel zum Game-Start) — Lyra mentions PB im Pep-Talk.
    // Cache pbBeforeGame für späteren Vergleich nach Spiel-Ende (Rekord-Detection).
    let pbContext = '';
    pbBeforeGameRef.current = 0;
    try {
      const pbRes = await apiFetch<{ score: number; hasRecord: boolean }>(`/api/games/highscore?gameName=${encodeURIComponent(game)}`);
      const pb = pbRes?.score ?? 0;
      pbBeforeGameRef.current = pb;
      if (pbRes?.hasRecord && pb > 0) {
        pbContext = ` Sein bisheriger Personal-Best in ${title} ist ${pb}. Erwähne diesen PB konkret im Kommentar UND glaube an ihn dass er es heute schaffen kann.`;
      } else {
        pbContext = ` Es ist sein ALLERSTER ${title}-Versuch. Ermutige zum Erstversuch.`;
      }
    } catch {
      // Silent fallback — Lyra bekommt einfach keinen PB-Hint
    }

    // Fire-and-forget Lyra-Kommentar (UI launches game immediately)
    (async () => {
      try {
        const promptMsg = `[INTERN: Der User hat das Spiel "${title}" gewählt.${pbContext} Gib EINEN warmen Kommentar (1-2 Sätze) — bei Tetris nostalgisch, Snake spielerisch, Memory ermutigend, Tic-Tac-Toe entspannt. Antworte als reines JSON: {"message":"...","chips":[]}. Kein Markdown.]`;
        const visibleHistory = messages.filter((m) => !m.cardType).map((m) => ({ role: m.role, content: m.content }));
        const apiMessages = [...SOS_BOOT, ...visibleHistory, { role: 'user' as const, content: promptMsg }];
        const res = await apiFetch<{ message: string }>('/api/coach/message', {
          method: 'POST',
          body: { messages: apiMessages, locale: i18n.language, sosMode: true },
        });
        const parsed = parseLyraResponse(res?.message ?? '');
        const visible = parsed.message || `Viel Spaß mit ${title}!`;
        addMessage({ id: 'gamecomment-' + Date.now(), role: 'assistant', content: visible, timestamp: new Date() });
        setEmotion('happy'); scheduleEmotionReset(4000); speakText(visible);
      } catch {
        const fallback = pbBeforeGameRef.current > 0
          ? `Viel Spaß mit ${title}! Dein PB ist ${pbBeforeGameRef.current} — ich glaube du schaffst heute mehr.`
          : `Viel Spaß mit ${title}! Ich bin hier, wenn du fertig bist.`;
        addMessage({ id: 'gamecomment-err-' + Date.now(), role: 'assistant', content: fallback, timestamp: new Date() });
        speakText(fallback);
      }
    })();
  }

  async function handleGameComplete(score: number) {
    const gameName = GAME_META.find((g) => g.id === playingGame)?.id ?? 'das Spiel';
    const game = playingGame ?? 'unknown';
    gamesPlayedRef.current.push({ game, score, durationSec: 0 });

    // Score persistieren — server entscheidet ob's ein neuer PB ist (idempotent)
    let isNewBest = false;
    try {
      const res = await apiFetch<{ ok: boolean; isNewBest: boolean }>('/api/games/score', {
        method: 'POST',
        body: { gameName: game, score },
      });
      isNewBest = !!res?.isNewBest;
    } catch (err) {
      console.warn('[urge] score submit failed', err);
    }

    // Wenn Rekord: Lyra im nächsten Reply feiern lassen via Hint-Trigger
    const oldPb = pbBeforeGameRef.current;
    if (isNewBest && score > oldPb) {
      requestNewPbCelebrationRef.current = { game: gameName, oldScore: oldPb, newScore: score };
    }

    setPlayingGame(null); setChipSet('after_game');
    requestCheckInRef.current = 'after_game';
    await sendToLyra(`Ich habe ${gameName} gespielt und ${score} Punkte erreicht.`);
  }

  async function handleGameAbandon() {
    const game = playingGame;
    if (game) gamesPlayedRef.current.push({ game, score: 0, durationSec: 0 });
    setPlayingGame(null); setChipSet('after_game');
    requestCheckInRef.current = 'after_game';
    await sendToLyra('Ich habe das Spiel abgebrochen.');
  }

  async function finalizeOvercome() {
    if (thinking) return;
    setChipSet('none');
    wasOvercomeRef.current = true;
    try { await apiFetch('/api/urge', { method: 'POST', body: { emotion: 'other', wasOvercome: true, breathingDone } }); } catch {}
    // Lyra antwortet warm + motiviert zu Share/Rate
    requestOvercomeMotivationRef.current = true;
    await sendToLyra('Ich habe den Spielimpuls gerade überwunden – ich bin so erleichtert und stolz auf mich!');
    addMessage({ id: 'overcome-' + Date.now(), role: 'assistant', content: '', cardType: 'overcome', timestamp: new Date() });
    // Chips garantiert anzeigen — egal was Lyra zurückgibt (Hint kann verfehlt werden)
    setDynamicChips([
      { label: '✨ Erfolg teilen', action: 'share_success' },
      { label: '⭐ Session bewerten', action: 'rate_session' },
      { label: '✅ Fertig', action: 'close' },
    ]);
    setChipSet('none');
  }

  // ───── Share-Success Drawer ─────
  async function generateShareDraft() {
    setShareGenerating(true);
    try {
      const b = breathingCountRef.current;
      const g = gamesPlayedRef.current.length;
      const stats: string[] = [];
      if (b > 0) stats.push(`${b}x Atemübung`);
      if (g > 0) stats.push(`${g}x Mini-Spiel`);
      const statsLine = stats.length > 0 ? stats.join(' + ') : 'Lyras Begleitung';

      const promptMsg =
        `[INTERN: Schreibe einen kurzen, ehrlichen, warmen anonymen Community-Post (max 4 Sätze, ich-Form, Deutsch) über meinen heutigen Erfolg. ` +
        `Ich habe einen akuten Spielimpuls überwunden mit ${statsLine}. ` +
        `Inspiriere andere, ohne zu predigen. Kein Hashtag, keine Emojis-Spam (max 1 Emoji am Ende). ` +
        `Antworte als reines JSON: {"message":"<post-text>","chips":[]}. Kein Markdown.]`;
      const visibleHistory = messages
        .filter((m) => !m.cardType && m.content)
        .slice(-10)
        .map((m) => ({ role: m.role, content: m.content }));
      const apiMessages = [...SOS_BOOT, ...visibleHistory, { role: 'user' as const, content: promptMsg }];
      const res = await apiFetch<{ message: string }>('/api/coach/message', {
        method: 'POST',
        body: { messages: apiMessages, locale: i18n.language, sosMode: true },
      });
      const parsed = parseLyraResponse(res?.message ?? '');
      const draft =
        parsed.message?.trim() ||
        'Heute hatte ich einen heftigen Spielimpuls — und ich habe ihn überwunden. Es war hart, aber ich bin geblieben. 💪';
      setShareDraft(draft);
    } catch {
      setShareDraft(
        'Heute hatte ich einen Spielimpuls — und ich habe ihn überwunden. Schritt für Schritt. 💪',
      );
    } finally {
      setShareGenerating(false);
    }
  }

  async function openShareDrawer() {
    setShareDrawerVisible(true);
    setShareDraft('');
    await generateShareDraft();
  }

  async function submitSharePost(text: string) {
    if (sharePostedRef.current) return;
    sharePostedRef.current = true;
    try {
      await apiFetch('/api/community/post', {
        method: 'POST',
        body: { category: 'story', content: text },
      });
      addMessage({
        id: 'share-ack-' + Date.now(),
        role: 'assistant',
        content: 'Dein Erfolg ist geteilt — danke, dass du andere stärkst. 💛',
        timestamp: new Date(),
      });
    } catch {
      sharePostedRef.current = false;
      addMessage({
        id: 'share-err-' + Date.now(),
        role: 'assistant',
        content: 'Das Teilen hat gerade nicht geklappt. Versuch es später nochmal.',
        timestamp: new Date(),
      });
    } finally {
      setShareDrawerVisible(false);
    }
  }

  // ───── Inline-Rating Drawer ─────
  async function submitInlineRating(fb: SosFeedback) {
    inlineFeedbackRef.current = fb;
    setRatingDrawerVisible(false);
    addMessage({
      id: 'rate-ack-' + Date.now(),
      role: 'assistant',
      content: 'Danke für dein Feedback — das hilft mir, besser zu werden. 💛',
      timestamp: new Date(),
    });
  }


  async function handleSend() {
    const content = input.trim();
    if (!content || thinking) return;
    setInput('');
    if (chipSet === 'start') setChipSet('help');
    await sendToLyra(content);
  }

  // ───── Exit + Session-Persist ─────
  function hasInteracted(): boolean {
    const userMsgs = messages.filter((m) => m.role === 'user').length;
    return (
      userMsgs > 0 ||
      breathingCountRef.current > 0 ||
      gamesPlayedRef.current.length > 0 ||
      wasOvercomeRef.current
    );
  }

  function attemptExit() {
    if (exitingRef.current) return;
    if (isSpeaking) stopSpeaking();
    // Wenn User schon inline bewertet hat → direkt speichern, kein Modal
    if (inlineFeedbackRef.current) {
      exitingRef.current = true;
      void persistSession(inlineFeedbackRef.current).finally(() => router.back());
      return;
    }
    if (hasInteracted()) {
      setFeedbackVisible(true);
    } else {
      // Nur reingeschaut → kein Modal, kein DB-Save
      exitingRef.current = true;
      router.back();
    }
  }

  async function persistSession(feedback: SosFeedback | null) {
    const endedAt = new Date();
    const durationSec = Math.max(
      1,
      Math.round((endedAt.getTime() - sessionStartRef.current.getTime()) / 1000),
    );
    const payload = {
      startedAt: sessionStartRef.current.toISOString(),
      endedAt: endedAt.toISOString(),
      durationSec,
      messages: messages
        .filter((m) => !m.cardType && m.content)
        .map((m) => ({
          role: m.role,
          content: m.content,
          timestamp: m.timestamp.toISOString(),
        })),
      gamesPlayed: gamesPlayedRef.current,
      breathingCount: breathingCountRef.current,
      wasOvercome: wasOvercomeRef.current,
      feedbackBetter: feedback?.better ?? null,
      feedbackRating: feedback?.rating ?? null,
      feedbackText: feedback?.text || null,
      locale: i18n.language,
    };
    try {
      await apiFetch('/api/sos/session', { method: 'POST', body: payload });
    } catch (err) {
      console.warn('[sos-session-save]', err);
    }
  }

  async function handleFeedbackSubmit(feedback: SosFeedback) {
    setFeedbackVisible(false);
    exitingRef.current = true;
    await persistSession(feedback);
    router.back();
  }

  async function handleFeedbackSkip() {
    setFeedbackVisible(false);
    exitingRef.current = true;
    await persistSession(null);
    router.back();
  }

  const currentChips = dynamicChips.length > 0 ? dynamicChips : (CHIP_SETS[chipSet] ?? []);
  const topBarHeight = insets.top + 160;

  const renderMessage = useCallback(
    ({ item }: { item: SosMsg }) => (
      <MessageRow
        item={item}
        onGameSelect={handleGameSelect}
        onBreathingDone={() => {}}
      />
    ),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [],
  );

  const listData: SosMsg[] = messages;

  function renderItem({ item }: { item: SosMsg }) {
    return <MessageRow item={item} onGameSelect={handleGameSelect} onBreathingDone={() => {}} />;
  }

  return (
    <SafeAreaView style={st.container} edges={['top']}>
      <View style={[st.topBarBackdrop, { height: topBarHeight }]} pointerEvents="none" />

      {/* Header */}
      <View style={[st.topBar, { top: insets.top + 6 }]}>
        <Pressable style={st.actionBtn} onPress={attemptExit} hitSlop={12}>
          <Ionicons name="close" size={22} color={colors.textMuted} />
        </Pressable>
        <View style={st.avatarCenter}>
          <RiveAvatar emotion={emotion} size="md" />
          <View style={st.avatarMeta}>
            <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
              <Text style={st.avatarName}>Lyra</Text>
              <View
                style={{
                  paddingHorizontal: 6,
                  paddingVertical: 2,
                  borderRadius: 6,
                  backgroundColor: colors.error + '22',
                  borderWidth: 1,
                  borderColor: colors.error + '55',
                }}
              >
                <Text
                  style={{
                    fontSize: 10,
                    fontFamily: 'Nunito_700Bold',
                    color: colors.error,
                    letterSpacing: 0.5,
                  }}
                >
                  SOS
                </Text>
              </View>
            </View>
            {(thinking || isLoading) && !isSpeaking && (
              <View style={st.speakingRow}>
                <VoiceBars count={5} baseColor="#3b82f6" />
                <Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: '#3b82f6' }}>denkt nach …</Text>
              </View>
            )}
            {!thinking && !isLoading && isTtsLoading && !isSpeaking && (
              <View style={st.speakingRow}>
                <ActivityIndicator size="small" color={colors.brandOrange} />
                <Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: '#9ca3af' }}>spricht...</Text>
              </View>
            )}
            {isSpeaking && (
              <View style={st.speakingRow}>
                <VoiceBars count={5} baseColor={colors.brandOrange} />
                <Pressable style={st.stopBtn} onPress={stopSpeaking} hitSlop={6}>
                  <Ionicons name="square" size={10} color={colors.brandOrange} />
                </Pressable>
              </View>
            )}
          </View>
        </View>
        <Pressable style={st.actionBtn} onPress={() => setSoundEnabled((s) => !s)} hitSlop={12}>
          <Ionicons name={soundEnabled ? 'volume-high' : 'volume-mute-outline'} size={22} color={soundEnabled ? colors.brandOrange : '#9ca3af'} />
        </Pressable>
      </View>

      {playingGame ? (
        <View style={{ flex: 1 }}>
          <GameHeader game={playingGame} emotion={emotion} onBack={handleGameAbandon} />
          <View style={{ flex: 1, padding: 14 }}>
            {playingGame === 'memory' && <MemoryGame mode="sos" onComplete={handleGameComplete} onAbandon={handleGameAbandon} />}
            {playingGame === 'tictactoe' && <TicTacToeGame mode="sos" onComplete={handleGameComplete} onAbandon={handleGameAbandon} />}
            {playingGame === 'snake' && <SnakeGame mode="sos" onComplete={handleGameComplete} onAbandon={handleGameAbandon} />}
            {playingGame === 'tetris' && <TetrisGame mode="sos" onComplete={handleGameComplete} onAbandon={handleGameAbandon} />}
          </View>
        </View>
      ) : (
        <KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={0}>
          {(
            <View style={{ flex: 1 }}>
              <FlatList
                ref={flatRef}
                data={listData}
                renderItem={renderItem as any}
                keyExtractor={(item: any) => item.id}
                contentContainerStyle={[st.listContent, { paddingTop: topBarHeight + 10 }]}
                showsVerticalScrollIndicator={false}
                onScroll={handleScroll}
                scrollEventThrottle={100}
                onContentSizeChange={() => { if (isNearBottomRef.current) flatRef.current?.scrollToEnd({ animated: false }); }}
                ListFooterComponent={null}
              />
              {showScrollBtn && (
                <Pressable style={st.scrollDownBtn} onPress={() => { flatRef.current?.scrollToEnd({ animated: true }); setShowScrollBtn(false); }}>
                  <Ionicons name="chevron-down" size={18} color="#fff" />
                </Pressable>
              )}
            </View>
          )}

          {/* Crisis chips — permanent row, shown as soon as backend detects a crisis.
              Never replaced by LLM chips. Tel-actions open the dialer directly. */}
          {crisisChips.length > 0 && (
            <View style={st.crisisBar}>
              <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 8 }}>
                <Ionicons name="warning-outline" size={14} color="#dc2626" />
                <Text style={st.crisisBarLabel}>{t('coach.crisis_bar_label')}</Text>
              </View>
              <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
                {crisisChips.map((chip) => (
                  <TouchableOpacity
                    key={chip.action}
                    activeOpacity={0.7}
                    onPress={() => handleChip(chip.action)}
                    style={st.crisisChip}
                  >
                    <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
                      <Ionicons name="call-outline" size={14} color="#dc2626" />
                      <Text style={st.crisisChipText}>{chip.label}</Text>
                    </View>
                  </TouchableOpacity>
                ))}
              </View>
            </View>
          )}

          {/* Chips above input — only after Lyra has answered.
              Bei Standard-Actions (breathing/game/overcome/etc): Ionicons
              (native = SF Symbols iOS, Material Android) + Label OHNE Emoji.
              Bei custom-Actions ohne Mapping: Emoji aus chip.label (LLM-generiert).
              Verhindert "Lung-Emoji + leaf-Icon"-Doppelung. */}
          {currentChips.length > 0 && !isLoading && !thinking && (
            <View style={{ flexDirection: 'row', flexWrap: 'wrap', paddingHorizontal: 12, paddingVertical: 8, gap: 8 }}>
              {currentChips.map((chip) => {
                const isOvercome = chip.action === 'overcome';
                const isStats = chip.action === 'show_stats';
                const isFeel = chip.action.startsWith('feel:');
                const isBreathing = chip.action === 'breathing';
                const isGame = chip.action === 'game_picker' || chip.action === 'just_play';
                const isHelp = chip.action === 'need_help';
                const isClose = chip.action === 'close';

                const iconColor =
                  isBreathing ? '#0891b2' :
                  isGame ? '#9333ea' :
                  isOvercome ? '#16a34a' :
                  isStats ? '#2563eb' :
                  isHelp ? '#dc2626' :
                  isClose ? '#94a3b8' :
                  isFeel ? '#7c3aed' :
                  '#475569';

                const iconName: any =
                  isBreathing ? 'leaf-outline' :
                  isGame ? 'game-controller-outline' :
                  isOvercome ? 'checkmark-circle-outline' :
                  isStats ? 'stats-chart-outline' :
                  isHelp ? 'alert-circle-outline' :
                  isClose ? 'close-circle-outline' :
                  isFeel ? 'heart-outline' :
                  null;

                // Wenn Ionicons-Match: Emoji aus Label strippen (kein Doppel-Icon)
                const labelText = iconName
                  ? chip.label.replace(/^\s*[\p{Extended_Pictographic}\p{Emoji_Component}]+\s*/u, '')
                  : chip.label;

                return (
                  <Pressable
                    key={chip.action}
                    onPress={() => handleChip(chip.action)}
                    disabled={thinking}
                    style={[st.chip, thinking && { opacity: 0.4 }]}
                  >
                    <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
                      {iconName && <Ionicons name={iconName} size={15} color={iconColor} />}
                      <Text style={st.chipText}>{labelText}</Text>
                    </View>
                  </Pressable>
                );
              })}
            </View>
          )}

          {/* Input bar — natürliche Höhe, außerhalb flex:1 */}
          <View style={[st.inputBar, { paddingBottom: keyboardHeight > 0 ? 8 : Math.max(12, insets.bottom) }]}>
            <TextInput
              style={st.textInput}
              placeholder={t('coach.placeholder')}
              placeholderTextColor="#a3a3a3"
              value={input}
              onChangeText={setInput}
              multiline
              maxLength={1000}
              returnKeyType="send"
              onSubmitEditing={handleSend}
              editable={!thinking}
            />
            {input.trim() !== '' && (
              <Pressable style={[st.sendBtn, thinking && { opacity: 0.4 }]} onPress={handleSend} disabled={thinking || !input.trim()}>
                <Ionicons name="send" size={16} color="#fff" />
              </Pressable>
            )}
          </View>
        </KeyboardAvoidingView>
      )}

      {/* Breathing drawer — absolute, slides up over input */}
      {isBreathing && (
        <BreathingDrawer
          onDone={handleBreathingDone}
          onSpeak={speakText}
        />
      )}

      {/* Game picker drawer — absolute, slides up over input */}
      {isPickingGame && !playingGame && (
        <GamePickerDrawer
          onSelect={handleGameSelect}
          onClose={() => setIsPickingGame(false)}
        />
      )}

      {/* Inline-Rating Drawer (Alternative zum Exit-Modal) */}
      {ratingDrawerVisible && (
        <InlineRatingDrawer
          onSubmit={submitInlineRating}
          onClose={() => setRatingDrawerVisible(false)}
        />
      )}

      {/* Share-Success Drawer */}
      {shareDrawerVisible && (
        <ShareSuccessDrawer
          initialText={shareDraft}
          generating={shareGenerating}
          onShare={submitSharePost}
          onClose={() => setShareDrawerVisible(false)}
          onRegenerate={generateShareDraft}
        />
      )}

      {/* Exit-Feedback-Modal */}
      <SosFeedbackModal
        visible={feedbackVisible}
        onSubmit={handleFeedbackSubmit}
        onSkip={handleFeedbackSkip}
      />
    </SafeAreaView>
  );
}

function makeStyles(colors: ReturnType<typeof useColors>) {
  return StyleSheet.create({
    container: { flex: 1, backgroundColor: colors.bg },
    topBar: { position: 'absolute', left: 0, right: 0, zIndex: 10, flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', paddingHorizontal: 12 },
    topBarBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 9, backgroundColor: colors.bg },
    actionBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: colors.surface, alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.08, shadowRadius: 6, elevation: 4 },
    avatarCenter: { flex: 1, alignItems: 'center', gap: 4 },
    avatarMeta: { alignItems: 'center', gap: 2 },
    avatarName: { fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.text },
    speakingRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
    stopBtn: { width: 18, height: 18, borderRadius: 9, backgroundColor: colors.surfaceElevated, alignItems: 'center', justifyContent: 'center' },
    listContent: { paddingHorizontal: 12, paddingBottom: 4 },
    scrollDownBtn: { position: 'absolute', bottom: 8, right: 16, width: 36, height: 36, borderRadius: 18, backgroundColor: '#374151', alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 4, elevation: 4 },
    chip: {
      borderRadius: 14,
      borderWidth: 1.5,
      borderColor: colors.border,
      backgroundColor: colors.bg,
      paddingHorizontal: 16,
      paddingVertical: 11,
      shadowColor: '#000',
      shadowOffset: { width: 0, height: 2 },
      shadowOpacity: 0.12,
      shadowRadius: 6,
      elevation: 3,
    },
    chipPressed: {
      backgroundColor: colors.surfaceElevated,
      borderColor: colors.textMuted,
      transform: [{ scale: 0.97 }],
      shadowOpacity: 0.05,
    },
    chipText: { fontFamily: 'Nunito_600SemiBold', fontSize: 14, color: colors.textMuted },
    inputBar: { flexDirection: 'row', alignItems: 'flex-end', paddingHorizontal: 12, paddingTop: 8, borderTopWidth: 1, borderTopColor: colors.border, backgroundColor: colors.bg, gap: 8 },
    textInput: { flex: 1, minHeight: 40, maxHeight: 120, backgroundColor: colors.surfaceElevated, borderRadius: 20, paddingHorizontal: 14, paddingVertical: 10, fontSize: 15, fontFamily: 'Nunito_400Regular', color: colors.text },
    sendBtn: { width: 38, height: 38, borderRadius: 19, backgroundColor: '#007AFF', alignItems: 'center', justifyContent: 'center' },
    crisisBar: {
      marginHorizontal: 12,
      marginBottom: 4,
      marginTop: 8,
      backgroundColor: '#fef2f2',
      borderRadius: 12,
      borderWidth: 1,
      borderColor: '#fca5a5',
      paddingHorizontal: 12,
      paddingVertical: 10,
    },
    crisisBarLabel: { fontSize: 11, fontFamily: 'Nunito_700Bold', color: '#dc2626', textTransform: 'uppercase', letterSpacing: 0.5 },
    crisisChip: {
      borderRadius: 12,
      borderWidth: 1.5,
      borderColor: '#fca5a5',
      backgroundColor: '#fff1f2',
      paddingHorizontal: 12,
      paddingVertical: 8,
    },
    crisisChipText: { fontFamily: 'Nunito_600SemiBold', fontSize: 13, color: '#dc2626' },
  });
}
