"""ElevenLabs TTS – natives Hochdeutsch für VidPuls (Premium-Stimmen)."""

from __future__ import annotations

import json
import os
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
SECRETS = ROOT / "data" / "jami-secrets.local.json"

# Beste Modelle für akzentfreies Deutsch
MODEL_NARRATOR = "eleven_multilingual_v2"
MODEL_DIALOGUE = "eleven_multilingual_v2"
MODEL_FALLBACKS = ("eleven_turbo_v2_5", "eleven_v3")
LANGUAGE_CODE = "de"
OUTPUT_FORMAT = "mp3_44100_128"
MAX_CHARS = 4800

# Premade-Stimmen – sehr gut auf Deutsch (language_code=de)
VOICE_FEMALE_DEFAULT = "XrExE9yKIg1WjnnlVkGX"  # Matilda – Sarah
VOICE_MALE_DEFAULT = "onwK4e9ZLuTAKqWW03F9"  # Daniel
VOICE_LAURA_DEFAULT = "EXAVITQu4vr4xnSDxMaL"  # Sarah (EL) – Laura

# Weniger Stability = weniger roboterhaft; Style etwas höher = lebendiger
VOICE_SETTINGS_NARRATOR = {
    "stability": 0.34,
    "similarity_boost": 0.80,
    "style": 0.38,
    "use_speaker_boost": True,
}
VOICE_SETTINGS_DIALOGUE = {
    "stability": 0.26,
    "similarity_boost": 0.78,
    "style": 0.58,
    "use_speaker_boost": True,
}

SPEED_NARRATOR = 0.98
SPEED_DIALOGUE = 0.96

_runtime_voice_overrides: dict[str, str] = {}


def set_runtime_voice_overrides(mapping: dict[str, str] | None) -> None:
    global _runtime_voice_overrides
    _runtime_voice_overrides = {k.lower(): v for k, v in (mapping or {}).items() if v}


def list_voices() -> list[dict[str, str]]:
    """ElevenLabs-Stimmen für UI-Dropdowns."""
    fallback = [
        {"voice_id": VOICE_FEMALE_DEFAULT, "name": "Matilda (Standard Sarah)", "labels": "de"},
        {"voice_id": VOICE_LAURA_DEFAULT, "name": "Laura (Standard Laura)", "labels": "de"},
        {"voice_id": VOICE_MALE_DEFAULT, "name": "Daniel (Mann)", "labels": "de"},
        {"voice_id": "21m00Tcm4TlvDq8ikWAM", "name": "Rachel", "labels": "en"},
        {"voice_id": "pNInz6obpgDQGcFmaJgB", "name": "Adam", "labels": "en"},
    ]
    key = api_key()
    if not key:
        return fallback
    req = urllib.request.Request(
        "https://api.elevenlabs.io/v1/voices",
        headers={"xi-api-key": key, "Accept": "application/json"},
        method="GET",
    )
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            data = json.loads(resp.read().decode())
        out: list[dict[str, str]] = []
        for v in data.get("voices") or []:
            vid = str(v.get("voice_id") or "")
            name = str(v.get("name") or vid[:8])
            if not vid:
                continue
            labels = v.get("labels") or {}
            lang = str(labels.get("language") or labels.get("accent") or "")
            out.append({"voice_id": vid, "name": name, "labels": lang})
        out.sort(key=lambda x: x["name"].lower())
        return out or fallback
    except (urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError, OSError):
        return fallback


def _load_secrets() -> dict:
    if not SECRETS.is_file():
        return {}
    try:
        return json.loads(SECRETS.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}


def _load_config() -> dict:
    for name in ("vidpuls-config.json", "jampulse-config.json", "clipwelle-config.json"):
        p = ROOT / "data" / name
        if not p.is_file():
            continue
        try:
            return json.loads(p.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            pass
    return {}


def api_key() -> str:
    key = os.environ.get("ELEVENLABS_API_KEY", "").strip()
    if key:
        return key
    data = _load_secrets()
    return str(
        data.get("elevenlabs_api_key")
        or data.get("ELEVENLABS_API_KEY")
        or data.get("elevenlabs_key")
        or ""
    ).strip()


_permission_ok: bool | None = None


def configured() -> bool:
    if not api_key():
        return False
    return _permission_ok is not False


def permission_denied() -> bool:
    return _permission_ok is False


def missing_message() -> str:
    return (
        "ElevenLabs API-Key fehlt. In data/jami-secrets.local.json eintragen:\n"
        '  "elevenlabs_api_key": "xi-..."\n'
        "Key: https://elevenlabs.io/app/settings/api-keys · "
        "Setup: python scripts/setup_elevenlabs_tts.py"
    )


def model_id(*, dialogue: bool = False) -> str:
    cfg = _load_config()
    sec = _load_secrets()
    if dialogue:
        return str(
            sec.get("elevenlabs_model_dialogue")
            or cfg.get("elevenlabs_model_dialogue")
            or MODEL_DIALOGUE
        )
    return str(
        sec.get("elevenlabs_model")
        or cfg.get("elevenlabs_model")
        or MODEL_NARRATOR
    )


def _model_chain(*, dialogue: bool) -> list[str]:
    primary = model_id(dialogue=dialogue)
    chain = [primary]
    for mid in MODEL_FALLBACKS:
        if mid not in chain:
            chain.append(mid)
    return chain


def voice_id(style: str) -> str:
    return voice_id_for_speaker(style)


def voice_id_for_speaker(speaker: str) -> str:
    sp = (speaker or "female").strip().lower()
    if _runtime_voice_overrides:
        for key in (sp, "sarah" if sp in ("female", "lisa") else "", "laura" if sp == "laura" else ""):
            if key and key in _runtime_voice_overrides:
                return str(_runtime_voice_overrides[key])
        if sp in ("female", "sarah", "lisa") and "voice1" in _runtime_voice_overrides:
            return str(_runtime_voice_overrides["voice1"])
        if sp in ("laura",) and "voice2" in _runtime_voice_overrides:
            return str(_runtime_voice_overrides["voice2"])
    cfg = _load_config()
    sec = _load_secrets()
    sp = (speaker or "female").strip().lower()
    overrides = {
        "sarah": (
            sec.get("elevenlabs_voice_sarah")
            or cfg.get("elevenlabs_voice_sarah")
            or sec.get("elevenlabs_voice_female")
            or cfg.get("elevenlabs_voice_female")
            or VOICE_FEMALE_DEFAULT
        ),
        "laura": (
            sec.get("elevenlabs_voice_laura")
            or cfg.get("elevenlabs_voice_laura")
            or VOICE_LAURA_DEFAULT
        ),
        "male": (
            sec.get("elevenlabs_voice_male")
            or cfg.get("elevenlabs_voice_male")
            or VOICE_MALE_DEFAULT
        ),
        "female": (
            sec.get("elevenlabs_voice_female")
            or cfg.get("elevenlabs_voice_female")
            or VOICE_FEMALE_DEFAULT
        ),
    }
    if sp in overrides:
        return str(overrides[sp])
    return str(overrides["female"])


def _speech(
    text: str,
    out: Path,
    *,
    voice: str,
    dialogue: bool = False,
    previous_text: str = "",
    next_text: str = "",
) -> tuple[bool, str]:
    key = api_key()
    if not key:
        return False, missing_message()

    settings = VOICE_SETTINGS_DIALOGUE if dialogue else VOICE_SETTINGS_NARRATOR
    speed = SPEED_DIALOGUE if dialogue else SPEED_NARRATOR
    last_err = ""

    for mid in _model_chain(dialogue=dialogue):
        q = urllib.parse.urlencode({"output_format": OUTPUT_FORMAT})
        url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice}?{q}"
        body: dict = {
            "text": text,
            "model_id": mid,
            "language_code": LANGUAGE_CODE,
            "voice_settings": settings,
            "speed": speed,
        }
        if "v3" not in mid.lower():
            if previous_text.strip():
                body["previous_text"] = previous_text.strip()[:250]
            if next_text.strip():
                body["next_text"] = next_text.strip()[:250]

        req = urllib.request.Request(
            url,
            data=json.dumps(body).encode("utf-8"),
            headers={
                "xi-api-key": key,
                "Content-Type": "application/json",
                "Accept": "audio/mpeg",
            },
            method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=240) as resp:
                data = resp.read()
            if len(data) < 500:
                last_err = f"{mid}: leere Antwort"
                continue
            out.write_bytes(data)
            return True, mid
        except urllib.error.HTTPError as e:
            try:
                err_body = json.loads(e.read().decode())
                detail = err_body.get("detail")
                if isinstance(detail, dict):
                    last_err = str(detail.get("message", detail))[:180]
                elif isinstance(detail, list) and detail:
                    last_err = str(detail[0].get("msg", detail[0]))[:180]
                else:
                    last_err = str(err_body)[:180]
            except (json.JSONDecodeError, OSError, UnicodeDecodeError):
                last_err = str(e)[:180]
            if "text_to_speech" in last_err or "permission" in last_err.lower():
                global _permission_ok
                _permission_ok = False
        except (urllib.error.URLError, OSError, TimeoutError) as e:
            last_err = str(e)[:180]

    return False, last_err or "ElevenLabs TTS fehlgeschlagen"


def synthesize_text(
    text: str,
    out: Path,
    *,
    style: str = "female",
    dialogue: bool = False,
    previous_text: str = "",
    next_text: str = "",
) -> tuple[bool, str]:
    out.parent.mkdir(parents=True, exist_ok=True)
    vid = voice_id(style)
    ok, mid = _speech(
        text,
        out,
        voice=vid,
        dialogue=dialogue,
        previous_text=previous_text,
        next_text=next_text,
    )
    if ok:
        return True, f"elevenlabs-{mid}-{voice_id(style)[:6]}"
    return False, mid


def _chunk_text(text: str, max_chars: int = MAX_CHARS) -> list[str]:
    import re

    split = re.compile(r"(?<=[.!?…])\s+")
    if len(text) <= max_chars:
        return [text]
    sents = [p.strip() for p in split.split(text) if p.strip()] or [text]
    blocks: list[str] = []
    cur: list[str] = []
    cur_len = 0
    for s in sents:
        add = len(s) + (1 if cur else 0)
        if cur and cur_len + add > max_chars:
            blocks.append(" ".join(cur))
            cur = [s]
            cur_len = len(s)
        else:
            cur.append(s)
            cur_len += add
    if cur:
        blocks.append(" ".join(cur))
    return blocks or [text]


def synthesize_long(
    text: str,
    out: Path,
    *,
    style: str = "female",
    dialogue: bool = False,
    concat_fn,
) -> tuple[bool, str]:
    if len(text) <= MAX_CHARS:
        return synthesize_text(text, out, style=style, dialogue=dialogue)

    blocks = _chunk_text(text)
    work = out.parent / "_elevenlabs_parts"
    work.mkdir(parents=True, exist_ok=True)
    parts: list[Path] = []
    model_used = ""

    for i, block in enumerate(blocks):
        part = work / f"blk_{i:03d}.mp3"
        prev = blocks[i - 1] if i > 0 else ""
        nxt = blocks[i + 1] if i + 1 < len(blocks) else ""
        ok, info = synthesize_text(
            block, part, style=style, dialogue=dialogue,
            previous_text=prev, next_text=nxt,
        )
        if not ok:
            return False, info
        model_used = info
        parts.append(part)

    if len(parts) == 1:
        out.write_bytes(parts[0].read_bytes())
        return True, model_used
    if concat_fn(parts, out):
        return True, model_used
    return False, "ElevenLabs: Zusammenfügen fehlgeschlagen"


def synthesize_dialogue_turns(
    turns: list[tuple[str, str]],
    out: Path,
    *,
    concat_fn,
    pauses: list[float],
) -> tuple[bool, str]:
    """Dialog mit Kontext (previous/next) – fließenderes Gespräch."""
    work = out.parent / "_dialogue_elevenlabs"
    work.mkdir(parents=True, exist_ok=True)
    texts = [t.strip() for _, t in turns if t.strip()]
    parts: list[Path] = []
    model_used = ""

    idx = 0
    for i, (speaker, text) in enumerate(turns):
        line = text.strip()
        if not line:
            continue
        sp = speaker.strip().lower()
        prev = texts[idx - 1] if idx > 0 else ""
        nxt = texts[idx + 1] if idx + 1 < len(texts) else ""
        part = work / f"turn_{i:03d}.mp3"
        ok, info = synthesize_text(
            line, part, style=sp, dialogue=True,
            previous_text=prev, next_text=nxt,
        )
        if not ok:
            return False, info
        model_used = info
        parts.append(part)
        idx += 1

    if not parts:
        return False, "Leerer Dialog"
    if len(parts) == 1:
        out.write_bytes(parts[0].read_bytes())
        return True, model_used
    dlg_pauses = pauses[: max(len(parts) - 1, 0)]
    if concat_fn(parts, out, pauses=dlg_pauses):
        return True, model_used
    return False, "ElevenLabs: Dialog-Zusammenfügen fehlgeschlagen"
