import os
import re
from difflib import SequenceMatcher
from typing import Optional

try:
    import speech_recognition as sr
except ModuleNotFoundError:
    sr = None


class VoiceEngine:
    def __init__(
        self,
        microphone_index: Optional[int] = None,
        debug: bool = False,
        language: Optional[str] = None,
    ) -> None:
        if sr is None:
            raise RuntimeError(
                "Missing dependency 'SpeechRecognition'. Run: pip install -r requirements.txt"
            )

        self.debug = debug
        self.language = (language or os.getenv("VOICE_LANGUAGE") or "en-US").strip() or "en-US"
        self.wake_words = (
            "hey spotify",
            "hello spotify",
            "ok spotify",
            "okay spotify",
            "hey spotty",
        )

        mic_names = self.list_microphones()
        if not mic_names:
            raise RuntimeError(
                "No input device detected. Connect a microphone, grant terminal mic access, "
                "then run: python main.py --list-mics"
            )

        if microphone_index is None:
            env_index = os.getenv("MIC_DEVICE_INDEX")
            if env_index:
                try:
                    microphone_index = int(env_index)
                except ValueError as exc:
                    raise RuntimeError(
                        f"Invalid MIC_DEVICE_INDEX='{env_index}'. It must be an integer."
                    ) from exc
            else:
                microphone_index = self._choose_default_mic_index(mic_names)

        if microphone_index is not None:
            if microphone_index < 0 or microphone_index >= len(mic_names):
                raise RuntimeError(
                    f"Invalid microphone index {microphone_index}. Run: python main.py --list-mics"
                )

        self.recognizer = sr.Recognizer()
        self.recognizer.dynamic_energy_threshold = True
        self.recognizer.pause_threshold = 0.8
        self.recognizer.non_speaking_duration = 0.5
        self.recognizer.phrase_threshold = 0.25

        selected_index = microphone_index
        self.selected_mic_name = (
            mic_names[selected_index] if selected_index is not None else "system default"
        )

        try:
            self.microphone = sr.Microphone(device_index=selected_index)
        except Exception as exc:
            raise RuntimeError(f"No usable microphone found: {exc}") from exc

        with self.microphone as source:
            self.recognizer.adjust_for_ambient_noise(source, duration=1)

    @staticmethod
    def list_microphones() -> list[str]:
        if sr is None:
            return []
        try:
            return sr.Microphone.list_microphone_names()
        except Exception:
            return []

    @staticmethod
    def _choose_default_mic_index(mic_names: list[str]) -> int:
        ranked_keywords = ("microphone", "mic", "input", "headset", "airpods")
        for index, name in enumerate(mic_names):
            lowered = name.lower()
            if any(keyword in lowered for keyword in ranked_keywords):
                return index
        return 0

    @staticmethod
    def _normalize(text: str) -> str:
        cleaned = re.sub(r"[^a-z0-9\s]", " ", text.lower())
        return " ".join(cleaned.split())

    @staticmethod
    def _is_spotify_token(token: str) -> bool:
        known_aliases = {"spotify", "spotty", "spotifly", "spotifi", "spotfy"}
        if token in known_aliases:
            return True
        return SequenceMatcher(None, token, "spotify").ratio() >= 0.72

    def _extract_wake_remainder(self, transcript: str) -> tuple[bool, Optional[str]]:
        normalized = self._normalize(transcript)
        if not normalized:
            return False, None

        for wake_word in self.wake_words:
            wake_normalized = self._normalize(wake_word)
            if wake_normalized in normalized:
                remainder = normalized.split(wake_normalized, 1)[1].strip()
                return True, remainder or None

        tokens = normalized.split()
        if not tokens:
            return False, None

        greeting_tokens = {"hey", "hello", "hi", "ok", "okay"}
        spotify_index: Optional[int] = None
        for index, token in enumerate(tokens):
            if self._is_spotify_token(token):
                spotify_index = index
                break

        if spotify_index is None:
            return False, None

        has_greeting = any(token in greeting_tokens for token in tokens[: spotify_index + 1])
        starts_with_spotify = spotify_index == 0
        if not has_greeting and not starts_with_spotify:
            return False, None

        remainder_tokens = tokens[spotify_index + 1 :]
        remainder = " ".join(remainder_tokens).strip()
        return True, remainder or None

    def _transcribe_once(
        self,
        timeout: Optional[float] = None,
        phrase_time_limit: float = 4,
    ) -> Optional[str]:
        with self.microphone as source:
            try:
                audio = self.recognizer.listen(
                    source,
                    timeout=timeout,
                    phrase_time_limit=phrase_time_limit,
                )
            except sr.WaitTimeoutError:
                return None

        try:
            text = self.recognizer.recognize_google(audio, language=self.language)
        except sr.UnknownValueError:
            return None
        except sr.RequestError:
            return None

        return text.lower().strip()

    def listen_for_wake_event(self) -> tuple[bool, Optional[str]]:
        transcript = self._transcribe_once(timeout=None, phrase_time_limit=4.5)
        if self.debug and transcript:
            print(f"[voice-debug] wake-listen heard: {transcript}")
        if not transcript:
            return False, None

        return self._extract_wake_remainder(transcript)

    def listen_for_wake_word(self) -> bool:
        detected, _ = self.listen_for_wake_event()
        return detected

    def get_command(self) -> Optional[str]:
        transcript = self._transcribe_once(timeout=5, phrase_time_limit=8)
        if self.debug and transcript:
            print(f"[voice-debug] command heard: {transcript}")
        return transcript
