"""
GOLS - OpenAI Language & Voice Intelligence
===========================================
Single integration point for all OpenAI usage across the platform.

Capabilities
------------
  • Script generation (role-play, interview, coaching, assessment, learning, custom)
  • Script rewrite / improve / summarize / translate / personalize
  • Language detection
  • Avatar-optimized rewriting (clean spoken text for TTS + facial delivery)
  • Gendered, multilingual Text-to-Speech (OpenAI TTS)

Design notes
------------
  • Lazy, cached singleton client — created once, reused.
  • Graceful degradation: if no OPENAI_API_KEY is configured, text helpers fall
    back to deterministic local transforms and TTS callers fall back to gTTS in
    the pipeline. Nothing crashes when the key is missing.
  • Response caching: identical (model, prompt) text requests are cached in-memory
    (LRU) to cut token spend on repeated calls (e.g. polling / re-renders).
  • All errors are wrapped in OpenAIServiceError with a clean message.
"""

from __future__ import annotations

import hashlib
import json
import os
import threading
from functools import lru_cache
from typing import Optional, List, Dict, Any

from app.config import cfg
from app.utils.logger import log


# ── Supported languages (ISO code -> human name) ──────────────────────────────
SUPPORTED_LANGUAGES: Dict[str, str] = {
    "en": "English",   "hi": "Hindi",     "mr": "Marathi",   "ta": "Tamil",
    "te": "Telugu",    "kn": "Kannada",   "ml": "Malayalam", "gu": "Gujarati",
    "bn": "Bengali",   "pa": "Punjabi",   "ur": "Urdu",      "es": "Spanish",
    "fr": "French",    "de": "German",    "ja": "Japanese",  "zh": "Chinese",
    "ar": "Arabic",    "pt": "Portuguese","ru": "Russian",   "it": "Italian",
    "ko": "Korean",    "id": "Indonesian","nl": "Dutch",     "tr": "Turkish",
}

# Content types the studio can request
CONTENT_TYPES = (
    "general", "roleplay", "interview", "coaching", "assessment", "learning",
    "sales", "onboarding", "announcement",
)


class OpenAIServiceError(Exception):
    """Raised for any recoverable OpenAI failure (clean, user-facing message)."""


# ── Client (lazy singleton) ───────────────────────────────────────────────────
_client = None
_client_lock = threading.Lock()


def _get_client():
    """Return a cached OpenAI client, or None if no key / SDK not installed."""
    global _client
    if _client is not None:
        return _client
    if not cfg.OPENAI_API_KEY:
        return None
    with _client_lock:
        if _client is None:
            try:
                from openai import OpenAI
                kwargs: Dict[str, Any] = {
                    "api_key": cfg.OPENAI_API_KEY,
                    "timeout": cfg.OPENAI_TIMEOUT,
                }
                if cfg.OPENAI_BASE_URL:
                    kwargs["base_url"] = cfg.OPENAI_BASE_URL
                _client = OpenAI(**kwargs)
                log.info("[openai] client initialised (model=%s, tts=%s)",
                         cfg.OPENAI_TEXT_MODEL, cfg.OPENAI_TTS_MODEL)
            except Exception as e:  # pragma: no cover - import/SDK issues
                log.error("[openai] failed to init client: %s", e)
                _client = None
    return _client


def is_enabled() -> bool:
    """True when OpenAI is configured and usable."""
    return _get_client() is not None


def lang_name(code: str) -> str:
    return SUPPORTED_LANGUAGES.get((code or "en").lower(), "English")


# ── Cached chat completion ────────────────────────────────────────────────────
@lru_cache(maxsize=256)
def _cached_chat(model: str, system: str, user: str,
                 temperature: float, max_tokens: int) -> str:
    """LRU-cached chat call. Args must be hashable (all strings/numbers)."""
    client = _get_client()
    if client is None:
        raise OpenAIServiceError("OpenAI is not configured (set OPENAI_API_KEY).")
    try:
        resp = client.chat.completions.create(
            model=model,
            messages=[
                {"role": "system", "content": system},
                {"role": "user", "content": user},
            ],
            temperature=temperature,
            max_tokens=max_tokens,
        )
        return (resp.choices[0].message.content or "").strip()
    except Exception as e:
        log.error("[openai] chat completion failed: %s", e)
        raise OpenAIServiceError(f"OpenAI request failed: {e}")


def _chat(system: str, user: str,
          temperature: Optional[float] = None,
          max_tokens: Optional[int] = None) -> str:
    return _cached_chat(
        cfg.OPENAI_TEXT_MODEL,
        system.strip(),
        user.strip(),
        cfg.OPENAI_TEMPERATURE if temperature is None else temperature,
        cfg.OPENAI_MAX_TOKENS if max_tokens is None else max_tokens,
    )


# ══════════════════════════════════════════════════════════════════════════════
# TEXT INTELLIGENCE
# ══════════════════════════════════════════════════════════════════════════════
_SYSTEM_BASE = (
    "You are a professional scriptwriter for AI talking-avatar videos used on a "
    "platform similar to HeyGen and Tavus. You write natural, human-sounding "
    "spoken dialogue meant to be read aloud by a realistic avatar. "
    "Rules: write ONLY the words the avatar will speak — no stage directions, "
    "no headings, no markdown, no bullet symbols, no speaker labels unless a "
    "dialogue is explicitly requested. Use short, breathable sentences and "
    "natural punctuation that helps text-to-speech pacing."
)


def _content_brief(content_type: str) -> str:
    briefs = {
        "general":    "Write a clear, engaging spoken message.",
        "roleplay":   "Write a realistic role-play scene the avatar performs, staying fully in character.",
        "interview":  "Write interview questions and natural framing the avatar asks one by one.",
        "coaching":   "Write a warm, motivating coaching session with practical, encouraging guidance.",
        "assessment": "Write assessment questions with brief spoken framing for each.",
        "learning":   "Write a concise, easy-to-follow micro-lesson that teaches the topic step by step.",
        "sales":      "Write a persuasive but trustworthy sales pitch focused on benefits.",
        "onboarding": "Write a friendly onboarding/welcome message that orients a new person.",
        "announcement": "Write a crisp, confident announcement.",
    }
    return briefs.get(content_type, briefs["general"])


def generate_script(
    prompt: str,
    *,
    content_type: str = "general",
    language: str = "en",
    tone: str = "professional",
    length: str = "medium",          # short | medium | long
    emotion: str = "natural",
) -> str:
    """Generate an avatar-ready spoken script from a topic/brief."""
    length_map = {"short": "about 40-70 words", "medium": "about 90-150 words",
                  "long": "about 200-300 words"}
    words = length_map.get(length, length_map["medium"])
    system = (
        f"{_SYSTEM_BASE}\n{_content_brief(content_type)}\n"
        f"Write the entire output in {lang_name(language)}. "
        f"Tone: {tone}. Emotional delivery: {emotion}. Target length: {words}."
    )
    return _chat(system, prompt)


def improve_script(script: str, *, language: str = "en",
                   tone: str = "professional") -> str:
    """Improve clarity, flow and naturalness while keeping meaning."""
    system = (
        f"{_SYSTEM_BASE}\nImprove the following script for a talking-avatar video: "
        f"fix grammar, improve flow and naturalness, keep the original meaning and "
        f"length similar. Output in {lang_name(language)}, tone {tone}. "
        f"Return only the improved spoken text."
    )
    return _chat(system, script)


def rewrite_script(script: str, instruction: str, *, language: str = "en") -> str:
    """Rewrite a script following a free-form instruction."""
    system = (
        f"{_SYSTEM_BASE}\nRewrite the script according to the user's instruction. "
        f"Output the spoken text only, in {lang_name(language)}."
    )
    return _chat(system, f"INSTRUCTION: {instruction}\n\nSCRIPT:\n{script}")


def summarize_script(script: str, *, language: str = "en") -> str:
    system = (
        f"{_SYSTEM_BASE}\nSummarize the following into a short spoken version "
        f"that keeps the key points. Output only spoken text in {lang_name(language)}."
    )
    return _chat(system, script, temperature=0.3)


def translate_script(script: str, target_language: str) -> str:
    """Translate to a target language, preserving spoken/natural style."""
    system = (
        f"{_SYSTEM_BASE}\nTranslate the script into {lang_name(target_language)}. "
        f"Keep it natural and idiomatic for a spoken avatar, not a literal translation. "
        f"Return only the translated spoken text."
    )
    return _chat(system, script, temperature=0.3)


def personalize_script(script: str, audience: str, *, language: str = "en") -> str:
    system = (
        f"{_SYSTEM_BASE}\nPersonalize the script for this audience: {audience}. "
        f"Adjust examples, vocabulary and tone accordingly. "
        f"Output only spoken text in {lang_name(language)}."
    )
    return _chat(system, script)


def optimize_for_speech(script: str, *, language: str = "en",
                        emotion: str = "natural") -> str:
    """
    Clean a raw script into avatar/TTS-optimized spoken text:
    expand abbreviations, add natural pauses (commas), remove markup/emoji.
    Used automatically by the pipeline before TTS when OpenAI is enabled.
    """
    system = (
        f"{_SYSTEM_BASE}\nRewrite the text so it is optimal for text-to-speech and "
        f"realistic avatar delivery with a {emotion} emotional tone: expand "
        f"abbreviations and numbers into spoken words, add natural commas for "
        f"breathing, remove any emojis, markdown, URLs or symbols. Keep the meaning "
        f"and language ({lang_name(language)}). Return only the cleaned spoken text."
    )
    return _chat(system, script, temperature=0.2)


def detect_language(text: str) -> str:
    """Return a best-effort ISO 639-1 code for the text's language."""
    if not is_enabled():
        return _heuristic_detect(text)
    system = (
        "Detect the language of the user's text. Respond with ONLY the lowercase "
        "ISO 639-1 two-letter code (e.g. en, hi, mr, ta). No other words."
    )
    try:
        code = _chat(system, text[:600], temperature=0, max_tokens=5).strip().lower()
        code = code[:2]
        return code if code in SUPPORTED_LANGUAGES else "en"
    except OpenAIServiceError:
        return _heuristic_detect(text)


def _heuristic_detect(text: str) -> str:
    """Offline fallback: detect a few Indian scripts by Unicode range."""
    ranges = {
        "hi": (0x0900, 0x097F),  # Devanagari (also mr)
        "bn": (0x0980, 0x09FF),
        "pa": (0x0A00, 0x0A7F),
        "gu": (0x0A80, 0x0AFF),
        "ta": (0x0B80, 0x0BFF),
        "te": (0x0C00, 0x0C7F),
        "kn": (0x0C80, 0x0CFF),
        "ml": (0x0D00, 0x0D7F),
        "ur": (0x0600, 0x06FF),
        "ar": (0x0600, 0x06FF),
        "zh": (0x4E00, 0x9FFF),
        "ja": (0x3040, 0x30FF),
        "ko": (0xAC00, 0xD7AF),
    }
    for ch in text:
        o = ord(ch)
        for code, (lo, hi) in ranges.items():
            if lo <= o <= hi:
                return code
    return "en"


# ══════════════════════════════════════════════════════════════════════════════
# CONVERSATIONAL CONTEXT (multi-turn)
# ══════════════════════════════════════════════════════════════════════════════
def converse(messages: List[Dict[str, str]], *, language: str = "en",
             system: Optional[str] = None) -> str:
    """
    Maintain conversational context. `messages` is a list of
    {"role": "user"|"assistant", "content": "..."}.
    """
    client = _get_client()
    if client is None:
        raise OpenAIServiceError("OpenAI is not configured (set OPENAI_API_KEY).")
    sys = system or (
        f"{_SYSTEM_BASE}\nYou are having a natural conversation that will be spoken "
        f"by an avatar. Reply in {lang_name(language)} with spoken text only."
    )
    try:
        resp = client.chat.completions.create(
            model=cfg.OPENAI_TEXT_MODEL,
            messages=[{"role": "system", "content": sys}, *messages],
            temperature=cfg.OPENAI_TEMPERATURE,
            max_tokens=cfg.OPENAI_MAX_TOKENS,
        )
        return (resp.choices[0].message.content or "").strip()
    except Exception as e:
        log.error("[openai] converse failed: %s", e)
        raise OpenAIServiceError(f"OpenAI request failed: {e}")


# ══════════════════════════════════════════════════════════════════════════════
# TEXT-TO-SPEECH (gendered, multilingual)
# ══════════════════════════════════════════════════════════════════════════════
def voice_for_gender(gender: str, override: Optional[str] = None) -> str:
    """Map a gender ('male'/'female') to an OpenAI TTS voice id."""
    if override:
        return override
    return cfg.OPENAI_VOICE_MALE if (gender or "").lower() == "male" \
        else cfg.OPENAI_VOICE_FEMALE


def synthesize_speech(
    text: str,
    out_path: str,
    *,
    gender: str = "female",
    voice: Optional[str] = None,
    language: str = "en",
    emotion: str = "natural",
    speed: float = 1.0,
) -> bool:
    """
    Generate speech audio (mp3) with OpenAI TTS. Returns True on success,
    False if OpenAI is unavailable (so the pipeline can fall back to gTTS).
    Raises OpenAIServiceError only on a hard API error.
    """
    client = _get_client()
    if client is None:
        return False

    chosen = voice_for_gender(gender, voice)
    instructions = (
        f"Speak in {lang_name(language)} with a {emotion}, natural human tone "
        f"suitable for a realistic talking-avatar video. Clear articulation, "
        f"appropriate emotional inflection and pacing."
    )
    try:
        spd = max(0.5, min(2.0, float(speed)))
    except Exception:
        spd = 1.0

    # Try the configured model first, then fall back to the universally-available
    # 'tts-1' (covers accounts without access to the newer gpt-4o tts models).
    models_to_try = []
    for m in (cfg.OPENAI_TTS_MODEL, "tts-1"):
        if m and m not in models_to_try:
            models_to_try.append(m)

    last_err = None
    for model in models_to_try:
        try:
            kwargs: Dict[str, Any] = dict(
                model=model, voice=chosen, input=text,
                response_format="mp3", speed=spd,
            )
            if "gpt-4o" in model:        # only the gpt-4o tts models accept this
                kwargs["instructions"] = instructions
            with client.audio.speech.with_streaming_response.create(**kwargs) as resp:
                resp.stream_to_file(out_path)
            if os.path.getsize(out_path) > 1000:
                log.info("[openai] TTS ok (model=%s, voice=%s, lang=%s) -> %s",
                         model, chosen, language, out_path)
                return True
        except Exception as e:
            last_err = e
            log.warning("[openai] TTS model '%s' failed (%s) — trying next", model, e)

    log.error("[openai] All OpenAI TTS attempts failed (%s) — pipeline falls back to gTTS",
              last_err)
    return False


# ── Cache management ──────────────────────────────────────────────────────────
def clear_cache() -> None:
    _cached_chat.cache_clear()


def cache_stats() -> Dict[str, Any]:
    info = _cached_chat.cache_info()
    return {"hits": info.hits, "misses": info.misses,
            "size": info.currsize, "maxsize": info.maxsize}
