import os
import base64
import subprocess
import tempfile
import shutil
import re
import glob
from typing import Optional, List, Tuple
from dotenv import load_dotenv

load_dotenv()

class TTSService:
    def __init__(self):
        """
        Initializes the Piper TTS service with automatic path correction and absolute resolution.
        """
        # Get base directory of the project (parent of 'services' folder)
        self.base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

        # 1. Resolve Piper Executable
        self.piper_path = self._resolve_piper_executable()

        # 2. Resolve Model and Config Paths (Try relative to project root if not absolute)
        model_env = os.getenv("PIPER_MODEL_PATH", "assets/models/en_US-lessac-medium.onnx")
        config_env = os.getenv("PIPER_CONFIG_PATH", "assets/models/en_US-lessac-medium.onnx.json")

        self.model_path = self._make_absolute(model_env)
        self.config_path = self._make_absolute(config_env)

        self.available = False

        print("\n🔍 Piper TTS Diagnostic Check:")
        if not self.piper_path:
            print("   - Binary: ❌ NOT FOUND (Check PIPER_EXECUTABLE_PATH)")
        else:
            print(f"   - Binary: ✅ FOUND at {self.piper_path}")

        if not self.model_path or not os.path.exists(self.model_path):
            print(f"   - Model:  ❌ NOT FOUND (Checked: {self.model_path})")
        else:
            print(f"   - Model:  ✅ FOUND at {self.model_path}")
            self.available = True if self.piper_path else False

        if self.available:
            print("🚀 Piper TTS Service Fully Active\n")
        else:
            print("⚠️ Piper TTS Service Disabled (Missing requirements)\n")

    def _make_absolute(self, path: str) -> str:
        """Converts relative paths to absolute paths based on project root."""
        if not path: return ""
        if os.path.isabs(path): return path
        return os.path.join(self.base_dir, path)

    def _resolve_piper_executable(self) -> Optional[str]:
        """
        Strategically searches for the piper binary in common production paths.
        STRICTLY blocks /usr/bin/piper to avoid the Ubuntu GTK conflict.
        """
        BLACKLIST = ["/usr/bin/piper", "/bin/piper"]

        # Strategy A: Environment Variable (Explicit mapping)
        env_path = os.getenv("PIPER_EXECUTABLE_PATH")
        if env_path:
            # If absolute path provided, check it
            if os.path.isabs(env_path):
                if env_path not in BLACKLIST and os.path.exists(env_path) and os.access(env_path, os.X_OK):
                    return env_path
            else:
                # If name provided, resolve it
                resolved = shutil.which(env_path)
                if resolved and not any(b in resolved for b in BLACKLIST):
                    return resolved

        # Strategy B: System PATH (Try 'piper-tts' first)
        for name in ["piper-tts", "piper"]:
            path = shutil.which(name)
            if path and not any(b in path for b in BLACKLIST):
                return path

        # Strategy C: Common Ubuntu Deployment Paths (Production defaults)
        # We check /usr/local/bin first as it's the standard place for user-installed binaries
        for path in ["/usr/local/bin/piper-tts", "/usr/local/bin/piper", "/opt/piper/piper", "/opt/piper/piper/piper"]:
            if os.path.exists(path) and os.access(path, os.X_OK) and path not in BLACKLIST:
                return path

        return None

    def _get_config_path(self, model_path: str) -> Optional[str]:
        """Returns the .json config path for a model if it exists."""
        config_path = model_path + ".json"
        return config_path if os.path.exists(config_path) else None

    def _resolve_voice_model(self, voice: str) -> tuple:
        """
        Ultra-robust resolution for Piper voice models.
        Tries exact match, naming variants, and globbing to find the best possible file.
        """
        models_dir = os.path.join(self.base_dir, "assets", "models")
        if not os.path.exists(models_dir):
            return None, None

        # 1. Variants to try
        variants = [
            voice,                          # Exact: en_US-hfc_female-medium
            voice.replace("_", "-"),        # Dashes: en-US-hfc-female-medium
            voice.replace("-", "_"),        # Underscores: en_US_hfc_female_medium
        ]

        # Add simpler base name variants if voice is long (e.g. hfc_female-medium -> hfc_female)
        parts = re.split(r'[-_]', voice)
        if len(parts) > 2:
            variants.append("-".join(parts[-2:])) # e.g. female-medium
            variants.append("_".join(parts[-2:])) # e.g. female_medium

        # Deduplicate while preserving order
        variants = list(dict.fromkeys(variants))

        for v in variants:
            path = os.path.join(models_dir, f"{v}.onnx")
            if os.path.exists(path):
                return path, self._get_config_path(path)

        # 2. Glob searching (finding any file that contains the requested voice name)
        # We try to match the most unique part of the voice name
        search_terms = [voice] + parts
        # Remove common terms like 'en', 'US', 'medium', 'low', 'high'
        stopwords = {'en', 'us', 'gb', 'in', 'medium', 'low', 'high', 'low-poly', 'voice'}
        search_terms = [t for t in search_terms if t.lower() not in stopwords and len(t) > 1]

        for term in search_terms:
            pattern = os.path.join(models_dir, f"*{term}*.onnx")
            matches = glob.glob(pattern)
            if matches:
                # Use the first match
                match = matches[0]
                print(f"🎙️ Fuzzy match found for '{voice}': {os.path.basename(match)}")
                return match, self._get_config_path(match)

        # 3. Fallback to default model
        if self.model_path and os.path.exists(self.model_path):
            print(f"⚠️ Piper voice '{voice}' not found (tried {len(variants)} variants + glob). Falling back to default: {os.path.basename(self.model_path)}")
            return self.model_path, self.config_path

        # 4. Final resort: ANY model
        available_models = glob.glob(os.path.join(models_dir, "*.onnx"))
        if available_models:
            fallback = available_models[0]
            print(f"⚠️ Using emergency fallback: {os.path.basename(fallback)}")
            return fallback, self._get_config_path(fallback)

        return None, None

    def generate_voice_base64(self, text: str, voice: Optional[str] = None) -> Optional[str]:
        """
        Generates high-quality audio base64 string using the specified Piper voice model.
        Thread-safe and optimized using temporary file isolation.

        Args:
            text: The text to synthesize.
            voice: Optional Piper voice model name (e.g. "en_US-hfc_female-medium").
                   If provided, uses that model; otherwise uses the default model.
        """
        if not self.available:
            print("⚠️ TTS skipped: Service not available (Check binary/model paths).")
            return None

        if not text or not text.strip():
            print("⚠️ TTS skipped: Empty text provided.")
            return None

        # Resolve model/config paths — use requested voice or fall back to default
        if voice:
            active_model, active_config = self._resolve_voice_model(voice)
            if not active_model:
                print(f"⚠️ TTS skipped: Voice model '{voice}' not found in assets/models/.")
                return None
        else:
            active_model = self.model_path
            active_config = self.config_path

        # Clean text for better TTS performance
        clean_text = text.replace('"', '').replace("'", "").strip()

        temp_wav_path = None
        try:
            # Create isolated temporary file for this specific request
            with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_wav:
                temp_wav_path = temp_wav.name

            command = [
                self.piper_path,
                "--model", active_model,
                "--output_file", temp_wav_path
            ]

            if active_config and os.path.exists(active_config):
                command.extend(["--config", active_config])

            print(f"🎙️ Generating voice ({voice or 'default'}) for text: {clean_text[:50]}...")

            # Execute with high priority and captured streams
            process = subprocess.Popen(
                command,
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                encoding='utf-8'
            )

            stdout, stderr = process.communicate(input=clean_text, timeout=30)

            if process.returncode != 0:
                print(f"❌ Piper Execution Error (Code {process.returncode}): {stderr}")
                return None

            # Efficiently read and encode
            if os.path.exists(temp_wav_path) and os.path.getsize(temp_wav_path) > 0:
                with open(temp_wav_path, "rb") as audio_file:
                    b64 = base64.b64encode(audio_file.read()).decode("utf-8")
                    print(f"✅ Voice generated successfully ({len(b64)} chars)")
                    return b64

            print("❌ Piper generated an empty file.")
            return None

        except subprocess.TimeoutExpired:
            print("❌ Piper process timed out (30s limit).")
            return None
        except Exception as e:
            print(f"❌ Exception in Piper TTS processing: {str(e)}")
            return None
        finally:
            if temp_wav_path and os.path.exists(temp_wav_path):
                try:
                    os.remove(temp_wav_path)
                except Exception:
                    pass

