import os
import sys
import base64
import argparse
import subprocess
import tempfile
import shutil
import re
import glob
from typing import List, Optional, Dict, Any, Tuple

class SalesHubAudioGenerator:
    """
    Standalone Audio Generator for Sales Hub.
    Uses Piper Neural TTS (the exact same TTS engine & ONNX model architecture used in Roleplay).
    Completely independent script with robust voice model resolution and fallback mechanisms.
    """

    def __init__(self, piper_binary_path: Optional[str] = None, model_path: Optional[str] = None):
        """
        Initialize the generator with automatic path resolution.

        Args:
            piper_binary_path: Optional path to the 'piper' binary executable.
            model_path: Optional path to a Piper ONNX voice model file (.onnx).
        """
        self.base_dir = os.path.dirname(os.path.abspath(__file__))
        self.piper_binary = piper_binary_path or self._resolve_piper_executable()
        
        model_env = model_path or os.getenv("PIPER_MODEL_PATH", "assets/models/en_US-lessac-medium.onnx")
        self.model_path = self._make_absolute(model_env)
        if not os.path.exists(self.model_path):
            self.model_path = self._find_default_model()

        self.config_path = self._get_config_path(self.model_path) if self.model_path else None

        # Print diagnostic check
        print("\n🎙️ Piper TTS Diagnostic Check:")
        if not self.piper_binary:
            print("   - Binary: ❌ NOT FOUND (Check PIPER_EXECUTABLE_PATH)")
        else:
            print(f"   - Binary: ✅ FOUND at {self.piper_binary}")

        if not self.model_path or not os.path.exists(self.model_path):
            print(f"   - Default Model: ❌ NOT FOUND (Checked: {self.model_path})")
        else:
            print(f"   - Default Model: ✅ FOUND at {self.model_path}\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 Ubuntu GTK conflict.
        """
        BLACKLIST = ["/usr/bin/piper", "/bin/piper"]

        # Strategy A: Environment Variable
        env_path = os.getenv("PIPER_EXECUTABLE_PATH")
        if env_path:
            if os.path.isabs(env_path):
                if env_path not in BLACKLIST and os.path.exists(env_path):
                    return env_path
            else:
                resolved = shutil.which(env_path)
                if resolved and not any(b in resolved for b in BLACKLIST):
                    return resolved

        # Strategy B: System PATH
        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: Standard Production / Windows Paths
        search_paths = [
            "/usr/local/bin/piper-tts",
            "/usr/local/bin/piper",
            "/opt/piper/piper",
            "/opt/piper/piper/piper",
            "C:\\piper\\piper.exe",
            "C:\\Program Files\\piper\\piper.exe"
        ]
        for path in search_paths:
            if os.path.exists(path) 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."""
        if not model_path:
            return None
        config_path = model_path + ".json"
        return config_path if os.path.exists(config_path) else None

    def _get_models_dirs(self) -> List[str]:
        """Finds all existing assets/models directories across local workspace and production paths."""
        dirs = [
            os.path.join(self.base_dir, "assets", "models"),
            os.path.join(os.path.dirname(self.base_dir), "testing", "assets", "models"),
            "D:\\testing\\assets\\models",
            "C:\\piper\\models"
        ]
        return [d for d in dirs if os.path.exists(d)]

    def _find_default_model(self) -> Optional[str]:
        """Finds any available ONNX model file from assets/models."""
        for models_dir in self._get_models_dirs():
            for file in os.listdir(models_dir):
                if file.endswith(".onnx"):
                    return os.path.join(models_dir, file)
        return None

    def _resolve_voice_model(self, voice: str) -> tuple:
        """
        Ultra-robust resolution for Piper voice models across all model directories.
        Guarantees gender matching for female vs male voice requests.
        """
        model_dirs = self._get_models_dirs()
        if not voice or not model_dirs:
            return self.model_path, self.config_path

        v_lower = voice.lower()
        is_female = "female" in v_lower
        is_male = "male" in v_lower and not is_female

        for mdir in model_dirs:
            # 1. Strict gender match check
            if is_female:
                for file in os.listdir(mdir):
                    if file.endswith(".onnx") and "female" in file.lower():
                        match_path = os.path.join(mdir, file)
                        print(f"🎙️ [Voice Match] Matched female model '{file}' for requested voice '{voice}'")
                        return match_path, self._get_config_path(match_path)

            if is_male:
                for file in os.listdir(mdir):
                    if file.endswith(".onnx") and "male" in file.lower() and "female" not in file.lower():
                        match_path = os.path.join(mdir, file)
                        print(f"🎙️ [Voice Match] Matched male model '{file}' for requested voice '{voice}'")
                        return match_path, self._get_config_path(match_path)

            # 2. Check exact or variant file matches
            variants = [
                voice,
                voice.replace("_", "-"),
                voice.replace("-", "_"),
            ]
            for v in variants:
                path = os.path.join(mdir, f"{v}.onnx")
                if os.path.exists(path):
                    return path, self._get_config_path(path)

            # 3. Glob search
            pattern = os.path.join(mdir, "*.onnx")
            matches = glob.glob(pattern)
            for match in matches:
                bname = os.path.basename(match).lower()
                if is_female and "female" in bname:
                    return match, self._get_config_path(match)
                if is_male and "male" in bname and "female" not in bname:
                    return match, self._get_config_path(match)

        # Fallback if specific model not matched
        if self.model_path and os.path.exists(self.model_path):
            print(f"⚠️ Piper voice '{voice}' fallback to default model: {os.path.basename(self.model_path)}")
            return self.model_path, self.config_path

        return None, None

    def generate_audio(
        self,
        text: str,
        output_filename: Optional[str] = "saleshub_output.wav",
        voice_model_path: Optional[str] = None,
        voice: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        Converts text input into audio speech (.wav/.mp3) and returns Base64 audio string.
        """
        if not text or not text.strip():
            return {
                "success": False,
                "message": "Error: Input text cannot be empty.",
                "voice_base64": None,
                "file_path": None,
                "engine": None,
                "piper_error": None
            }

        clean_text = text.replace('"', '').replace("'", "").strip()

        # Resolve active model and config
        if voice_model_path and os.path.exists(voice_model_path):
            active_model = voice_model_path
            active_config = self._get_config_path(active_model)
        elif voice:
            active_model, active_config = self._resolve_voice_model(voice)
        else:
            active_model = self.model_path
            active_config = self.config_path

        piper_error_msg = None

        # Check binary and model status for detailed diagnostics
        if not self.piper_binary:
            piper_error_msg = "Piper binary executable not found in PATH or standard locations."
            print(f"⚠️ [Piper TTS Warning] {piper_error_msg}")
        elif not active_model:
            piper_error_msg = "Piper ONNX voice model path is not specified and no default model found."
            print(f"⚠️ [Piper TTS Warning] {piper_error_msg}")
        elif not os.path.exists(active_model):
            piper_error_msg = f"Piper ONNX model file does not exist at path: '{active_model}'"
            print(f"⚠️ [Piper TTS Warning] {piper_error_msg}")

        # 1. Attempt generation with Piper Neural TTS (Roleplay Model Engine)
        if self.piper_binary and active_model and os.path.exists(active_model):
            try:
                temp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
                temp_wav_path = temp_wav.name
                temp_wav.close()

                cmd = [
                    self.piper_binary,
                    "--model", active_model,
                    "--output_file", temp_wav_path
                ]

                config_file = active_model + ".json"
                if os.path.exists(config_file):
                    cmd.extend(["--config", config_file])

                print(f"🎙️ [Piper Neural TTS] Synthesizing speech for text: \"{clean_text[:60]}...\"")
                process = subprocess.Popen(
                    cmd,
                    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 and os.path.exists(temp_wav_path) and os.path.getsize(temp_wav_path) > 0:
                    with open(temp_wav_path, "rb") as f:
                        audio_data = f.read()

                    voice_b64 = base64.b64encode(audio_data).decode("utf-8")

                    saved_path = None
                    if output_filename:
                        os.makedirs(os.path.dirname(os.path.abspath(output_filename)), exist_ok=True)
                        with open(output_filename, "wb") as out_f:
                            out_f.write(audio_data)
                        saved_path = os.path.abspath(output_filename)

                    os.remove(temp_wav_path)
                    return {
                        "success": True,
                        "message": "Audio generated successfully using Piper Neural TTS.",
                        "voice_base64": voice_b64,
                        "file_path": saved_path,
                        "engine": "Piper Neural TTS (ONNX Model)",
                        "piper_error": None
                    }

                # If process failed or output file is empty
                piper_error_msg = stderr.strip() if stderr else f"Piper process exited with return code {process.returncode}"
                print(f"❌ [Piper TTS Error] Process Failed! Code: {process.returncode} | Detail: {piper_error_msg}")

                if os.path.exists(temp_wav_path):
                    os.remove(temp_wav_path)
            except Exception as e:
                piper_error_msg = f"Piper execution exception: {str(e)}"
                print(f"❌ [Piper TTS Exception] {piper_error_msg}")

        # 2. Fallback 1: gTTS (Google Text-To-Speech API)
        try:
            from gtts import gTTS
            print(f"🎙️ [gTTS Engine] Synthesizing speech...")
            tts = gTTS(text=clean_text, lang='en')

            temp_mp3 = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
            temp_mp3_path = temp_mp3.name
            temp_mp3.close()

            tts.save(temp_mp3_path)

            with open(temp_mp3_path, "rb") as f:
                audio_data = f.read()

            voice_b64 = base64.b64encode(audio_data).decode("utf-8")

            saved_path = None
            if output_filename:
                out_path = output_filename if output_filename.endswith(".mp3") or output_filename.endswith(".wav") else output_filename + ".mp3"
                os.makedirs(os.path.dirname(os.path.abspath(out_path)), exist_ok=True)
                with open(out_path, "wb") as out_f:
                    out_f.write(audio_data)
                saved_path = os.path.abspath(out_path)

            os.remove(temp_mp3_path)
            return {
                "success": True,
                "message": "Audio generated successfully using gTTS.",
                "voice_base64": voice_b64,
                "file_path": saved_path,
                "engine": "gTTS (Google Text-To-Speech)",
                "piper_error": piper_error_msg
            }
        except ImportError:
            pass
        except Exception as e:
            print(f"⚠️ gTTS notice: {e}")

        # 3. Fallback 2: pyttsx3 (System Native Offline TTS)
        try:
            import pyttsx3
            print(f"🎙️ [pyttsx3 Engine] Synthesizing speech...")
            engine = pyttsx3.init()

            temp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
            temp_wav_path = temp_wav.name
            temp_wav.close()

            engine.save_to_file(clean_text, temp_wav_path)
            engine.runAndWait()

            if os.path.exists(temp_wav_path) and os.path.getsize(temp_wav_path) > 0:
                with open(temp_wav_path, "rb") as f:
                    audio_data = f.read()

                voice_b64 = base64.b64encode(audio_data).decode("utf-8")

                saved_path = None
                if output_filename:
                    os.makedirs(os.path.dirname(os.path.abspath(output_filename)), exist_ok=True)
                    with open(output_filename, "wb") as out_f:
                        out_f.write(audio_data)
                    saved_path = os.path.abspath(output_filename)

                os.remove(temp_wav_path)
                return {
                    "success": True,
                    "message": "Audio generated successfully using pyttsx3 offline engine.",
                    "voice_base64": voice_b64,
                    "file_path": saved_path,
                    "engine": "pyttsx3 (Offline Native TTS)",
                    "piper_error": piper_error_msg
                }
        except ImportError:
            pass
        except Exception as e:
            print(f"⚠️ pyttsx3 notice: {e}")

        return {
            "success": False,
            "message": "No active TTS engine found. Please install piper binary/models, gTTS ('pip install gtts'), or pyttsx3 ('pip install pyttsx3').",
            "voice_base64": None,
            "file_path": None,
            "engine": None,
            "piper_error": piper_error_msg
        }


_global_generator_instance = None

def get_audio_generator(model_path: Optional[str] = None) -> SalesHubAudioGenerator:
    """Returns a cached singleton instance of SalesHubAudioGenerator to avoid re-initialization overhead."""
    global _global_generator_instance
    if _global_generator_instance is None:
        _global_generator_instance = SalesHubAudioGenerator(model_path=model_path)
    return _global_generator_instance

# Direct function call interface
def generate_saleshub_audio(
    text: str,
    output_filename: str = "saleshub_output.wav",
    model_path: Optional[str] = None,
    voice: Optional[str] = None
) -> Dict[str, Any]:
    generator = get_audio_generator(model_path=model_path)
    return generator.generate_audio(text=text, output_filename=output_filename, voice=voice)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Sales Hub Audio Generator (Standalone)")
    parser.add_argument("--text", "-t", type=str, default="Welcome to Sales Hub! Here is your audio script.", help="Text script to generate audio from")
    parser.add_argument("--output", "-o", type=str, default="saleshub_output.wav", help="Target output audio filename (.wav)")
    parser.add_argument("--model", "-m", type=str, default=None, help="Optional path to Piper ONNX voice model file (.onnx)")
    parser.add_argument("--voice", "-v", type=str, default=None, help="Optional voice model name (e.g. en_US-hfc_female-medium)")

    args = parser.parse_args()

    print("\n--- Sales Hub Standalone Audio Generator ---")
    result = generate_saleshub_audio(text=args.text, output_filename=args.output, model_path=args.model, voice=args.voice)

    if result["success"]:
        print(f"\n✅ SUCCESS!")
        print(f"   Engine Used: {result['engine']}")
        print(f"   Message: {result['message']}")
        print(f"   Saved Audio File: {result['file_path']}")
        print(f"   Base64 Length: {len(result['voice_base64'])} characters")
    else:
        print(f"\n❌ FAILED!")
        print(f"   Reason: {result['message']}")
