from app.schemas import AudioChunk, PipelineTrace
from app.pipeline.sentence_chunker import SentenceChunker
from app.metrics import timer, metrics


def _stage(name: str):
    return timer("stage_duration_seconds", {"stage": name})

# Festes Ausgabeformat der TTS-Stufe (s16le PCM, 24 kHz, mono).
TTS_AUDIO_FORMAT = "pcm"
TTS_SAMPLE_RATE = 24000
TTS_CHANNELS = 1


class Orchestrator:
    def __init__(self, stt, llm, tts, input_cleaner, spoken_adapter, tts_normalizer,
                 normalize_level: str = "full"):
        self.stt = stt
        self.llm = llm
        self.tts = tts
        self.input_cleaner = input_cleaner
        self.spoken_adapter = spoken_adapter
        self.tts_normalizer = tts_normalizer
        self.normalize_level = normalize_level

    async def _emit_to_output(self, audio: bytes, output) -> None:
        """Schreibt das synthetisierte Audio durch den gewaehlten Output-Endpunkt.

        Der HTTP-Stream bleibt davon unberuehrt (additiv). Bei lokalen Geraeten
        ist write_chunk heute ein no-op; LoopbackOutput sammelt die Chunks.
        """
        if output is None:
            return
        chunk = AudioChunk(
            data=audio,
            sample_rate=TTS_SAMPLE_RATE,
            channels=TTS_CHANNELS,
            format=TTS_AUDIO_FORMAT,
        )
        await output.open()
        try:
            await output.write_chunk(chunk)
            await output.flush()
        finally:
            await output.close()

    async def transcribe_only(
        self,
        audio_bytes: bytes,
        fmt: str,
        language: str | None = None,
        input=None,
    ):
        trace = PipelineTrace()
        # input dient hier nur der Validierung/Metadaten; das Audio kommt per Upload.
        if input is not None:
            await input.capabilities()
        with _stage("stt"):
            trace.raw_transcript = await self.stt.transcribe(
                audio_bytes,
                fmt=fmt,
                language=language,
            )
        trace.cleaned_transcript = await self.input_cleaner.run(
            trace.raw_transcript or ""
        )
        return trace

    async def speak_only(
        self,
        text: str,
        voice: str | None = None,
        language: str | None = None,
        output=None,
    ):
        spoken = await self.spoken_adapter.run(text, language=language)
        normalized = await self.tts_normalizer.run(
            spoken, language=language, level=self.normalize_level
        )
        with _stage("tts"):
            audio = await self.tts.synthesize(normalized, voice=voice, language=language)
        await self._emit_to_output(audio, output)
        return audio

    async def chat_text(
        self,
        text: str,
        language: str | None = None,
        voice: str | None = None,
        output=None,
        history: list[dict] | None = None,
        language_mode: str = "fix",
        detected_language: str | None = None,
        text_only: bool = False,
    ):
        trace = PipelineTrace()

        trace.raw_transcript = text
        trace.cleaned_transcript = await self.input_cleaner.run(text or "")

        # Flex: die erkannte Sprache bestimmt die Ausgabe. Gibt es keine (z. B. Text-Chat
        # ohne Audio), bleibt sie None -> das LLM antwortet von selbst in der Eingabesprache.
        # Fix: immer die konfigurierte Sprache (Whisper hat die Eingabe übersetzt).
        effective_language = detected_language if language_mode == "flex" else language

        with _stage("llm"):
            trace.semantic_response = await self.llm.complete(
                trace.cleaned_transcript or "",
                history=history,
                language=effective_language,
            )
        if not trace.semantic_response:
            raise RuntimeError("LLM returned an empty response")

        trace.spoken_response = await self.spoken_adapter.run(
            trace.semantic_response,
            language=effective_language,
        )
        trace.tts_ready_text = await self.tts_normalizer.run(
            trace.spoken_response,
            language=effective_language,
            level=self.normalize_level,
        )

        # text_only: Geräte-TTS (Web Speech API) übernimmt das Sprechen -> kein Server-Audio.
        if text_only:
            return trace, b""

        with _stage("tts"):
            audio = await self.tts.synthesize(
                trace.tts_ready_text,
                voice=voice,
                language=effective_language,
            )
        await self._emit_to_output(audio, output)
        return trace, audio

    async def chat_stream(
        self,
        text: str,
        language: str | None = None,
        voice: str | None = None,
        output=None,
        history: list[dict] | None = None,
        on_token=None,
        on_audio=None,
        language_mode: str = "fix",
        detected_language: str | None = None,
        text_only: bool = False,
    ):
        """Wie chat_text, aber gestreamt.

        `on_token(delta)` (async) wird pro LLM-Token-Delta aufgerufen.
        Ist `on_audio(chunk)` gesetzt, wird das Audio satzweise erzeugt (chunked TTS)
        und pro fertigem Satz ausgeliefert, statt erst am Ende komplett.
        """
        trace = PipelineTrace()
        trace.raw_transcript = text
        trace.cleaned_transcript = await self.input_cleaner.run(text or "")

        # Siehe chat_text: Flex folgt der erkannten Sprache (None -> LLM spiegelt Eingabe),
        # Fix nutzt die konfigurierte Sprache.
        effective_language = detected_language if language_mode == "flex" else language

        # text_only: kein Server-Audio (Geräte-TTS spricht selbst) -> kein Chunked-TTS.
        chunker = SentenceChunker() if (on_audio and not text_only) else None
        audio_parts: list[bytes] = []

        async def _emit_sentence(sentence: str) -> None:
            spoken = await self.spoken_adapter.run(sentence, language=effective_language)
            ready = await self.tts_normalizer.run(
                spoken, language=effective_language, level=self.normalize_level
            )
            if not ready.strip():
                return
            chunk = await self.tts.synthesize(ready, voice=voice, language=effective_language)
            audio_parts.append(chunk)
            await on_audio(chunk)

        parts: list[str] = []
        stream_fn = getattr(self.llm, "stream", None)
        if stream_fn is not None:
            async for delta in stream_fn(trace.cleaned_transcript or "", history=history, language=effective_language):
                parts.append(delta)
                if on_token:
                    await on_token(delta)
                if chunker:
                    for sentence in chunker.feed(delta):
                        await _emit_sentence(sentence)
        else:
            result = await self.llm.complete(trace.cleaned_transcript or "", history=history, language=effective_language)
            parts.append(result)
            if on_token:
                await on_token(result)
            if chunker:
                for sentence in chunker.feed(result):
                    await _emit_sentence(sentence)

        trace.semantic_response = "".join(parts)
        if not trace.semantic_response:
            raise RuntimeError("LLM returned an empty response")

        trace.spoken_response = await self.spoken_adapter.run(
            trace.semantic_response,
            language=effective_language,
        )
        trace.tts_ready_text = await self.tts_normalizer.run(
            trace.spoken_response,
            language=effective_language,
            level=self.normalize_level,
        )

        if text_only:
            return trace, b""

        if chunker:
            tail = chunker.flush()
            if tail:
                await _emit_sentence(tail)
            audio = b"".join(audio_parts)
        else:
            audio = await self.tts.synthesize(
                trace.tts_ready_text, voice=voice, language=effective_language
            )

        await self._emit_to_output(audio, output)
        return trace, audio
