from __future__ import annotations

import json
import re
import subprocess
import sys
import time
from dataclasses import dataclass, field
from typing import Any, Callable, Optional

from .voice import VoiceEngine, normalize_spoken_text, similarity_score


@dataclass
class TranscriptEntry:
    role: str
    text: str
    source: str = 'typed'
    confidence: float = 1.0
    timestamp: float = field(default_factory=time.time)

    def line(self) -> str:
        stamp = time.strftime('%H:%M:%S', time.localtime(self.timestamp))
        return f'[{stamp}] {self.role.upper()}: {self.text}'


@dataclass
class AssistantCommand:
    name: str
    patterns: list[str] = field(default_factory=list)
    callback: Optional[Callable[[dict[str, Any]], Any]] = None
    dangerous: bool = False
    description: str = ''
    threshold: float = 0.68


class AssistantManager:
    def __init__(self, name: str = 'Nest', *, voice_engine: VoiceEngine | None = None, wake_word: str = 'nest', enabled: bool = True, listen_enabled: bool = True, voice_reply_enabled: bool = False, persona: str = 'assistant', voice_profile: str = 'nova') -> None:
        self.name = name
        self.voice_engine = voice_engine or VoiceEngine()
        self.enabled = enabled
        self.listen_enabled = listen_enabled
        self.voice_reply_enabled = voice_reply_enabled
        self.wake_word_enabled = True
        self.wake_word = wake_word
        self.persona = persona
        self.voice_profile = voice_profile
        self.window: Any = None
        self.commands: list[AssistantCommand] = []
        self.transcript: list[TranscriptEntry] = []
        self.last_heard = ''
        self.last_response = ''
        self.last_intent = 'idle'
        self.last_entities: dict[str, str] = {}
        self.last_confidence: float = 0.0
        self.language = 'mixed'
        self.available_voice_profiles = ['nova', 'aria', 'sage', 'echo', 'custom']
        self.register_default_commands()

    def bind_window(self, window: Any) -> 'AssistantManager':
        self.window = window
        return self

    def set_voice_profile(self, name: str) -> None:
        self.voice_profile = name or self.voice_profile

    def enable_voice_reply(self, enabled: bool) -> None:
        self.voice_reply_enabled = bool(enabled)

    def enable_listening(self, enabled: bool) -> None:
        self.listen_enabled = bool(enabled)

    def enable_wake_word(self, enabled: bool) -> None:
        self.wake_word_enabled = bool(enabled)

    def register_command(self, name: str, patterns: list[str] | tuple[str, ...], callback: Optional[Callable[[dict[str, Any]], Any]] = None, *, dangerous: bool = False, description: str = '', threshold: float = 0.68) -> AssistantCommand:
        command = AssistantCommand(name=name, patterns=[str(p) for p in patterns], callback=callback, dangerous=dangerous, description=description, threshold=threshold)
        self.commands.append(command)
        return command

    def register_default_commands(self) -> None:
        defaults = [
            ('open_game', ['open game *', 'launch game *', 'start game *', 'เปิดเกม *', 'เข้าเกม *'], 'Open a game/app target'),
            ('open_app', ['open app *', 'launch app *', 'เปิดแอป *', 'เปิดโปรแกรม *'], 'Open an app target'),
            ('open_tab', ['open tab *', 'switch to * tab', 'เปิดแท็บ *', 'ไปแท็บ *'], 'Switch a UI tab'),
            ('set_theme', ['set theme *', 'theme *', 'เปลี่ยนธีม *'], 'Switch the current theme'),
            ('show_logs', ['open logs', 'show logs', 'เปิดล็อก', 'ดูล็อก'], 'Open log area'),
            ('help', ['help', 'show help', 'ช่วยด้วย', 'คำสั่ง'], 'Show help'),
        ]
        for name, patterns, description in defaults:
            self.register_command(name, patterns, description=description)

    def _log(self, level: str, message: str) -> None:
        if self.window is not None:
            try:
                self.window.log(level, message)
            except Exception:
                pass

    def _notify(self, message: str, tone: str = 'info') -> None:
        if self.window is not None:
            try:
                self.window.notify(message, tone=tone)
            except Exception:
                pass

    def add_transcript(self, role: str, text: str, *, source: str = 'typed', confidence: float = 1.0) -> TranscriptEntry:
        entry = TranscriptEntry(role=role, text=text, source=source, confidence=confidence)
        self.transcript.append(entry)
        self.transcript[:] = self.transcript[-60:]
        return entry

    def transcript_lines(self, limit: int = 8) -> list[str]:
        return [entry.line() for entry in self.transcript[-limit:]] or ['(no transcript yet)']

    def capture(self, timeout: int = 5, phrase_time_limit: int = 8) -> dict[str, Any]:
        result = self.voice_engine.listen_best_effort(timeout=timeout, phrase_time_limit=phrase_time_limit)
        payload = {'text': result.text, 'provider': result.provider, 'error': result.error}
        if result.text:
            self.add_transcript('user', result.text, source=result.provider)
        return payload

    def speak(self, text: str) -> bool:
        if not self.voice_reply_enabled or not text.strip():
            return False
        if self._speak_pyttsx3(text):
            return True
        if sys.platform.startswith('win') and self._speak_windows(text):
            return True
        return False

    def _speak_pyttsx3(self, text: str) -> bool:
        try:
            import pyttsx3
            engine = pyttsx3.init()
            engine.say(text)
            engine.runAndWait()
            return True
        except Exception:
            return False

    def _speak_windows(self, text: str) -> bool:
        script = f"""
Add-Type -AssemblyName System.Speech
$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer
$speak.Speak({json.dumps(text)})
"""
        try:
            subprocess.Popen(['powershell', '-NoProfile', '-Command', script])
            return True
        except Exception:
            return False

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

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

    def _pattern_to_regex(self, pattern: str) -> str:
        escaped = re.escape(self._normalize_pattern(pattern))
        escaped = escaped.replace(r'\*', r'(?P<target>.+)')
        return '^' + escaped + '$'

    def _match_registered(self, text: str) -> tuple[AssistantCommand | None, dict[str, str], float]:
        normalized = normalize_spoken_text(text)
        best_command: AssistantCommand | None = None
        best_entities: dict[str, str] = {}
        best_score = 0.0
        for command in self.commands:
            for pattern in command.patterns:
                pattern_norm = self._normalize_pattern(pattern)
                if '*' in pattern_norm:
                    regex = re.compile(self._pattern_to_regex(pattern_norm))
                    match = regex.match(normalized)
                    if match:
                        target = (match.groupdict().get('target') or '').strip()
                        return command, ({'target': target} if target else {}), 0.97
                score = similarity_score(normalized, pattern_norm.replace('*', '').strip())
                if score >= command.threshold and score > best_score:
                    best_command = command
                    best_score = score
                    best_entities = {}
        return best_command, best_entities, best_score

    def _fallback_parse(self, text: str) -> tuple[str, dict[str, str], float]:
        normalized = normalize_spoken_text(text)
        rules = [
            (r'^(?:open|launch|start) game (?P<target>.+)$', 'open_game', 0.92),
            (r'^(?:open|launch|start) app (?P<target>.+)$', 'open_app', 0.9),
            (r'^เปิดเกม (?P<target>.+)$', 'open_game', 0.93),
            (r'^เปิดแอป (?P<target>.+)$', 'open_app', 0.9),
            (r'^เปิดโปรแกรม (?P<target>.+)$', 'open_app', 0.9),
            (r'^(?:open|switch to) tab (?P<target>.+)$', 'open_tab', 0.88),
            (r'^ไปแท็บ (?P<target>.+)$', 'open_tab', 0.88),
            (r'^(?:theme|set theme) (?P<target>.+)$', 'set_theme', 0.84),
            (r'^เปลี่ยนธีม (?P<target>.+)$', 'set_theme', 0.84),
            (r'^(show|open) logs$', 'show_logs', 0.82),
            (r'^(help|show help|ช่วยด้วย|คำสั่ง)$', 'help', 0.82),
        ]
        for pattern, intent, conf in rules:
            match = re.match(pattern, normalized)
            if match:
                data = {k: v for k, v in match.groupdict().items() if v}
                return intent, data, conf
        return 'unknown', {}, 0.0

    def _default_response(self, intent: str, entities: dict[str, str]) -> str:
        target = entities.get('target', '').strip()
        if intent == 'open_game' and target:
            return f'กำลังเปิดเกม {target}'
        if intent == 'open_app' and target:
            return f'กำลังเปิดโปรแกรม {target}'
        if intent == 'open_tab' and target:
            return f'กำลังไปที่แท็บ {target}'
        if intent == 'set_theme' and target:
            return f'กำลังเปลี่ยนธีมเป็น {target}'
        if intent == 'show_logs':
            return 'กำลังเปิดส่วน logs'
        if intent == 'help':
            return 'กำลังเปิดหน้าช่วยเหลือ'
        return 'รับคำสั่งแล้ว'

    def process(self, text: str, *, source: str = 'typed', confirm_handler: Optional[Callable[[str], bool]] = None, require_wake_word: bool = False) -> dict[str, Any]:
        text = (text or '').strip()
        if not self.enabled:
            return {'ok': False, 'reason': 'assistant-disabled', 'heard': text}
        if not text:
            return {'ok': False, 'reason': 'empty'}
        heard_wake, stripped = self._strip_wake_word(text)
        if require_wake_word and self.wake_word_enabled and not heard_wake:
            return {'ok': False, 'reason': 'wake-word', 'heard': text}
        if not any(entry.text == text and entry.role == 'user' for entry in self.transcript[-1:]):
            self.add_transcript('user', text, source=source)
        self.last_heard = stripped or text
        command, entities, confidence = self._match_registered(stripped or text)
        intent = command.name if command else 'unknown'
        if not command:
            intent, entities, confidence = self._fallback_parse(stripped or text)
        self.last_intent = intent
        self.last_entities = dict(entities)
        self.last_confidence = confidence
        response = self._default_response(intent, entities)
        action_payload = {
            'intent': intent,
            'heard': text,
            'normalized': stripped or text,
            'entities': dict(entities),
            'confidence': confidence,
            'response': response,
        }
        if command and command.dangerous and confirm_handler and not confirm_handler(intent):
            action_payload['ok'] = False
            action_payload['reason'] = 'not-confirmed'
            response = 'ยกเลิกคำสั่งแล้ว'
        else:
            action_payload['ok'] = intent != 'unknown'
            if command and command.callback:
                result = command.callback(action_payload)
                if isinstance(result, str) and result.strip():
                    response = result.strip()
        self.last_response = response
        self.add_transcript('assistant', response, source='assistant', confidence=confidence or 1.0)
        self._log('INFO', f'Assistant heard: {text}')
        self._log('INFO', f'Assistant intent: {intent} {entities if entities else ""}'.strip())
        self._notify(response, tone='success' if action_payload.get('ok') else 'warning')
        self.speak(response)
        action_payload['response'] = response
        return action_payload
