"""Sovereign voice loop — listen, think, speak.

A fully local voice interface for Timmy. No cloud, no network calls.
All processing happens on the user's machine:

    Mic → VAD/silence detection → Whisper (local STT) → Timmy chat → Piper TTS → Speaker

Usage:
    from timmy.voice_loop import VoiceLoop
    loop = VoiceLoop()
    loop.run()  # blocks, Ctrl-C to stop

Requires: sounddevice, numpy, whisper, piper-tts
"""

from __future__ import annotations

import asyncio
import logging
import sys
from dataclasses import dataclass
from pathlib import Path

from timmy.voice.activation import (
    EXIT_COMMANDS,
    WHISPER_HALLUCINATIONS,
    is_exit_command,
    is_hallucination,
)
from timmy.voice.audio_io import (
    DEFAULT_MAX_UTTERANCE,
    DEFAULT_MIN_UTTERANCE,
    DEFAULT_SAMPLE_RATE,
    DEFAULT_SILENCE_DURATION,
    DEFAULT_SILENCE_THRESHOLD,
)
from timmy.voice.helpers import _install_quiet_asyncgen_hooks, _suppress_mcp_noise
from timmy.voice.llm import LLMMixin
from timmy.voice.speech_engines import (
    DEFAULT_PIPER_VOICE,
    DEFAULT_WHISPER_MODEL,
)
from timmy.voice.stt import STTMixin
from timmy.voice.tts import TTSMixin

logger = logging.getLogger(__name__)

DEFAULT_SESSION_ID = "voice"


@dataclass
class VoiceConfig:
    """Configuration for the voice loop."""

    whisper_model: str = DEFAULT_WHISPER_MODEL
    piper_voice: Path = DEFAULT_PIPER_VOICE
    sample_rate: int = DEFAULT_SAMPLE_RATE
    silence_threshold: float = DEFAULT_SILENCE_THRESHOLD
    silence_duration: float = DEFAULT_SILENCE_DURATION
    min_utterance: float = DEFAULT_MIN_UTTERANCE
    max_utterance: float = DEFAULT_MAX_UTTERANCE
    session_id: str = DEFAULT_SESSION_ID
    # Set True to use macOS `say` instead of Piper
    use_say_fallback: bool = False
    # Piper speaking rate (default 1.0, lower = slower)
    speaking_rate: float = 1.0
    # Backend/model for Timmy inference
    backend: str | None = None
    model_size: str | None = None


class VoiceLoop(STTMixin, TTSMixin, LLMMixin):
    """Sovereign listen-think-speak loop.

    Everything runs locally:
    - STT: OpenAI Whisper (local model, no API)
    - LLM: Timmy via Ollama (local inference)
    - TTS: Piper (local ONNX model) or macOS `say`
    """

    # Class-level constants delegate to the activation module.
    _WHISPER_HALLUCINATIONS = WHISPER_HALLUCINATIONS
    _EXIT_COMMANDS = EXIT_COMMANDS

    def __init__(self, config: VoiceConfig | None = None) -> None:
        self.config = config or VoiceConfig()
        self._whisper_model = None
        self._running = False
        self._speaking = False
        self._interrupted = False
        self._loop: asyncio.AbstractEventLoop | None = None

    # ── Lazy initialization ─────────────────────────────────────────────

    def _ensure_piper(self) -> bool:
        """Check that Piper voice model exists."""
        if self.config.use_say_fallback:
            return True
        voice_path = self.config.piper_voice
        if not voice_path.exists():
            logger.warning("Piper voice not found at %s — falling back to `say`", voice_path)
            self.config.use_say_fallback = True
            return True
        return True

    # ── Main Loop ───────────────────────────────────────────────────────

    def _log_banner(self) -> None:
        """Log the startup banner with STT/TTS/LLM configuration."""
        tts_label = (
            "macOS say"
            if self.config.use_say_fallback
            else f"Piper ({self.config.piper_voice.name})"
        )
        logger.info(
            "\n" + "=" * 60 + "\n"
            "  🎙️  Timmy Voice — Sovereign Voice Interface\n" + "=" * 60 + "\n"
            f"  STT:  Whisper ({self.config.whisper_model})\n"
            f"  TTS:  {tts_label}\n"
            "  LLM:  Timmy (local Ollama)\n" + "=" * 60 + "\n"
            "  Speak naturally. Timmy will listen, think, and respond.\n"
            "  Press Ctrl-C to exit.\n" + "=" * 60
        )

    def _is_hallucination(self, text: str) -> bool:
        """Return True if *text* is a known Whisper hallucination."""
        return is_hallucination(text)

    def _is_exit_command(self, text: str) -> bool:
        """Return True if the user asked to stop the voice session."""
        return is_exit_command(text)

    def _process_turn(self, text: str) -> None:
        """Handle a single listen-think-speak turn after transcription."""
        sys.stdout.write(f"\n  👤 You: {text}\n")
        sys.stdout.flush()
        response = self._think(text)
        sys.stdout.write(f"  🤖 Timmy: {response}\n")
        sys.stdout.flush()
        self._speak(response)

    def run(self) -> None:
        """Run the voice loop. Blocks until Ctrl-C."""
        self._ensure_piper()
        _suppress_mcp_noise()
        _install_quiet_asyncgen_hooks()
        self._log_banner()
        self._running = True
        try:
            while self._running:
                audio = self._record_utterance()
                if audio is None:
                    continue
                text = self._transcribe(audio)
                if self._is_hallucination(text):
                    logger.debug("Ignoring likely Whisper hallucination: '%s'", text)
                    continue
                if self._is_exit_command(text):
                    logger.info("👋 Goodbye!")
                    break
                self._process_turn(text)
        except KeyboardInterrupt:
            logger.info("👋 Voice loop stopped.")
        finally:
            self._running = False
            self._cleanup_loop()

    def stop(self) -> None:
        """Stop the voice loop (from another thread)."""
        self._running = False
