from __future__ import annotations

import re
import subprocess
import sys
import time
from dataclasses import dataclass, field
from difflib import SequenceMatcher
from typing import Callable, Optional


def normalize_spoken_text(text: str) -> str:
    text = (text or '').strip().lower()
    text = re.sub(r'[^\w\s-]', ' ', text)
    text = re.sub(r'\s+', ' ', text)
    return text


def similarity_score(left: str, right: str) -> float:
    left = normalize_spoken_text(left)
    right = normalize_spoken_text(right)
    if not left or not right:
        return 0.0
    if left == right:
        return 1.0
    if left in right or right in left:
        shorter = min(len(left), len(right))
        longer = max(len(left), len(right))
        return max(shorter / max(1, longer), 0.86)
    return SequenceMatcher(None, left, right).ratio()


def find_best_phrase(text: str, phrases: list[str]) -> tuple[str, float]:
    best_phrase = ''
    best_score = 0.0
    for phrase in phrases:
        score = similarity_score(text, phrase)
        if score > best_score:
            best_score = score
            best_phrase = phrase
    return best_phrase, best_score


LANGUAGE_PHRASE_PACKS = {
    'en': {
        'confirm': ['confirm', 'yes do it', 'yes', 'okay'],
        'cancel': ['cancel', 'stop that', 'no', 'abort'],
        'help': ['help', 'show commands'],
        'open_game': ['open game *', 'launch game *', 'start game *'],
    },
    'th': {
        'confirm': ['ยืนยัน', 'ตกลง', 'โอเค'],
        'cancel': ['ยกเลิก', 'หยุด', 'ไม่'],
        'help': ['ช่วยด้วย', 'คำสั่ง', 'แสดงคำสั่ง'],
        'open_game': ['เปิดเกม *', 'เข้าเกม *', 'เล่นเกม *'],
    },
}


@dataclass
class VoiceCaptureResult:
    text: str = ''
    provider: str = 'none'
    error: str = ''


@dataclass
class VoiceEngine:
    language: str = 'en-US'
    available: bool = field(init=False, default=False)
    last_error: str = ''
    last_provider: str = 'none'

    def __post_init__(self) -> None:
        self.available = bool(self.available_providers())

    def available_providers(self) -> list[str]:
        providers: list[str] = []
        try:
            import speech_recognition  # noqa: F401
            providers.append('speech_recognition')
        except Exception:
            pass
        if sys.platform.startswith('win'):
            providers.append('windows_system_speech')
        return providers

    def status_text(self) -> str:
        providers = ', '.join(self.available_providers()) or 'typed fallback only'
        return f'available={self.available} providers={providers}'

    def listen_best_effort(self, timeout: int = 5, phrase_time_limit: int = 8) -> VoiceCaptureResult:
        result = self._listen_speech_recognition(timeout, phrase_time_limit)
        if result.text:
            self.available = True
            self.last_provider = result.provider
            self.last_error = ''
            return result
        if sys.platform.startswith('win'):
            result = self._listen_windows_system_speech(timeout)
            if result.text:
                self.available = True
                self.last_provider = result.provider
                self.last_error = ''
                return result
        self.last_error = result.error or self.last_error
        self.last_provider = result.provider or 'none'
        return result

    def listen(self, timeout: int = 5, phrase_time_limit: int = 8) -> str:
        result = self.listen_best_effort(timeout=timeout, phrase_time_limit=phrase_time_limit)
        return result.text.strip()

    def _listen_speech_recognition(self, timeout: int = 5, phrase_time_limit: int = 8) -> VoiceCaptureResult:
        try:
            import speech_recognition as sr
        except Exception as exc:
            return VoiceCaptureResult(provider='speech_recognition', error=str(exc))
        recognizer = sr.Recognizer()
        try:
            with sr.Microphone() as source:
                recognizer.adjust_for_ambient_noise(source, duration=0.35)
                audio = recognizer.listen(source, timeout=timeout, phrase_time_limit=phrase_time_limit)
            text = recognizer.recognize_google(audio, language=self.language)
            return VoiceCaptureResult(text=text.strip(), provider='speech_recognition')
        except Exception as exc:  # pragma: no cover
            return VoiceCaptureResult(provider='speech_recognition', error=str(exc))

    def _listen_windows_system_speech(self, timeout: int = 5) -> VoiceCaptureResult:
        script = f"""
Add-Type -AssemblyName System.Speech
$rec = New-Object System.Speech.Recognition.SpeechRecognitionEngine
$rec.SetInputToDefaultAudioDevice()
$rec.LoadGrammar((New-Object System.Speech.Recognition.DictationGrammar))
$result = $rec.Recognize([TimeSpan]::FromSeconds({max(2, int(timeout))}))
if ($result) {{ $result.Text }}
"""
        try:
            completed = subprocess.run(
                ['powershell', '-NoProfile', '-Command', script],
                capture_output=True,
                text=True,
                timeout=max(4, timeout + 3),
            )
            text = (completed.stdout or '').strip()
            if text:
                return VoiceCaptureResult(text=text, provider='windows_system_speech')
            error = (completed.stderr or '').strip() or 'No speech captured.'
            return VoiceCaptureResult(provider='windows_system_speech', error=error)
        except Exception as exc:
            return VoiceCaptureResult(provider='windows_system_speech', error=str(exc))


@dataclass
class VoiceRule:
    name: str
    phrases: list[str] = field(default_factory=list)
    callback: Optional[Callable[[str, str], None]] = None
    description: str = ''
    confirm: bool = False
    threshold: float = 0.68

    def all_phrases(self) -> list[str]:
        values = [self.name] + list(self.phrases)
        seen: list[str] = []
        for value in values:
            value = value.strip()
            if value and value not in seen:
                seen.append(value)
        return seen


class VoiceRouter:
    def __init__(self, wake_word: str = 'nest', cooldown_seconds: float = 0.0, language_pack: str = 'en') -> None:
        self.wake_word = wake_word
        self.cooldown_seconds = cooldown_seconds
        self.language_pack = language_pack
        self.rules: list[VoiceRule] = []
        self.last_trigger = 0.0
        self.confirm_handler: Callable[[str], bool] | None = None
        self.free_handler: Callable[[str], None] | None = None

    def register(self, name: str, *phrases: str, callback: Optional[Callable[[str, str], None]] = None, description: str = '', confirm: bool = False, threshold: float = 0.68) -> VoiceRule:
        rule = VoiceRule(name=name, phrases=[p for p in phrases if p], callback=callback, description=description, confirm=confirm, threshold=threshold)
        self.rules.append(rule)
        return rule

    def strip_wake_word(self, text: str) -> tuple[bool, str]:
        normalized = normalize_spoken_text(text)
        wake = normalize_spoken_text(self.wake_word)
        if not wake:
            return True, normalized
        if normalized.startswith(wake + ' '):
            return True, normalized[len(wake):].strip()
        if normalized == wake:
            return True, ''
        return False, normalized

    def commands(self) -> list[str]:
        return [rule.name for rule in self.rules]

    def _find_rule(self, text: str) -> tuple[VoiceRule | None, str, float]:
        normalized_to_rule: dict[str, VoiceRule] = {}
        phrases: list[str] = []
        for rule in self.rules:
            for phrase in rule.all_phrases():
                normalized = normalize_spoken_text(phrase)
                if normalized:
                    normalized_to_rule[normalized] = rule
                    phrases.append(normalized)
        if not phrases:
            return None, '', 0.0
        best_phrase, score = find_best_phrase(text, phrases)
        rule = normalized_to_rule.get(best_phrase)
        if not rule or score < rule.threshold:
            return None, best_phrase, score
        return rule, best_phrase, score

    def execute(self, text: str) -> dict[str, object]:
        now = time.time()
        if self.cooldown_seconds and now - self.last_trigger < self.cooldown_seconds:
            return {'ok': False, 'reason': 'cooldown'}
        heard_wake, command = self.strip_wake_word(text)
        if self.wake_word and not heard_wake:
            return {'ok': False, 'reason': 'wake-word'}
        self.last_trigger = now
        if ' then ' in command:
            parts = [p.strip() for p in command.split(' then ') if p.strip()]
            results = [self.execute(self.wake_word + ' ' + part if self.wake_word else part) for part in parts]
            return {'ok': True, 'chained': True, 'results': results}
        rule, phrase, score = self._find_rule(command)
        if rule:
            if rule.confirm and self.confirm_handler and not self.confirm_handler(rule.name):
                return {'ok': False, 'reason': 'not-confirmed', 'command': rule.name}
            if rule.callback:
                rule.callback(rule.name, command)
            return {'ok': True, 'matched': rule.name, 'phrase': phrase, 'score': score}
        if self.free_handler:
            self.free_handler(command)
            return {'ok': True, 'matched': None, 'command': command}
        return {'ok': False, 'reason': 'no-match', 'command': command}
