"""Kokoro TTS – offline (Englisch-Stimmen, für Deutsch nicht empfohlen)."""

from __future__ import annotations

import json
import subprocess
from pathlib import Path

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

# Deutsch ohne britischen Akzent – neutrale US-Stimmen mit lang=de-de
VOICE_FEMALE_DEFAULT = "af_sarah"
VOICE_MALE_DEFAULT = "am_adam"
LANGUAGE = "de-de"
MAX_CHARS = 900

# Gesprächstempo – leicht langsamer wirkt menschlicher
_SPEED_FEMALE = 0.96
_SPEED_MALE = 0.94
_SPEED_SHORT_REPLY = 1.03

_pipelines: dict[str, object] = {}


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


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 voice_id(style: str) -> str:
    cfg = _load_config()
    sec = _load_secrets()
    if style == "male":
        return str(
            sec.get("kokoro_voice_male")
            or cfg.get("kokoro_voice_male")
            or VOICE_MALE_DEFAULT
        )
    return str(
        sec.get("kokoro_voice_female")
        or cfg.get("kokoro_voice_female")
        or VOICE_FEMALE_DEFAULT
    )


def _ffmpeg() -> str | None:
    try:
        from jam.tiktok_remix import _find_ffmpeg

        return _find_ffmpeg()
    except ImportError:
        return None


def _import_error() -> str | None:
    try:
        import pykokoro  # noqa: F401
        import soundfile  # noqa: F401
    except ImportError as e:
        return f"pykokoro fehlt: pip install pykokoro soundfile ({e})"
    try:
        import spacy

        try:
            spacy.load("de_core_news_md")
        except OSError:
            try:
                spacy.load("de_core_news_sm")
            except OSError:
                return "spaCy DE-Modell fehlt: python -m spacy download de_core_news_md"
    except ImportError:
        return "spaCy fehlt: pip install spacy && python -m spacy download de_core_news_md"
    return None


def configured() -> bool:
    return _import_error() is None


def missing_message() -> str:
    err = _import_error()
    if err:
        return err + " · Setup: python scripts/setup_kokoro_tts.py"
    return ""


def _generation_config(style: str, *, speed: float | None = None) -> object:
    from pykokoro.generation_config import GenerationConfig

    base = _SPEED_SHORT_REPLY if speed is not None else (
        _SPEED_FEMALE if style == "female" else _SPEED_MALE
    )
    spd = speed if speed is not None else base
    return GenerationConfig(
        lang=LANGUAGE,
        speed=max(0.85, min(1.08, spd)),
        pause_clause=0.22,
        pause_sentence=0.42,
        pause_paragraph=0.65,
        pause_variance=0.1,
    )


def _get_pipeline(style: str, *, speed: float | None = None):
    cache_key = f"{style}:{speed}" if speed is not None else style
    if cache_key in _pipelines:
        return _pipelines[cache_key]
    from pykokoro import KokoroPipeline, PipelineConfig

    vid = voice_id(style)
    cfg = PipelineConfig(voice=vid, generation=_generation_config(style, speed=speed))
    pipe = KokoroPipeline(cfg)
    _pipelines[cache_key] = pipe
    return pipe


def _write_mp3(audio, sample_rate: int, out: Path) -> bool:
    out.parent.mkdir(parents=True, exist_ok=True)
    wav_tmp = out.with_suffix(".wav")
    try:
        import numpy as np
        import soundfile as sf

        sf.write(str(wav_tmp), np.asarray(audio), int(sample_rate))
    except (ImportError, OSError, ValueError):
        return False

    ff = _ffmpeg()
    if ff and wav_tmp.is_file():
        try:
            subprocess.run(
                [ff, "-y", "-i", str(wav_tmp), "-c:a", "libmp3lame", "-q:a", "2", str(out)],
                capture_output=True,
                timeout=120,
                check=True,
            )
            wav_tmp.unlink(missing_ok=True)
            return out.is_file() and out.stat().st_size > 500
        except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError):
            pass

    # Fallback: WAV behalten wenn kein ffmpeg
    if wav_tmp.is_file() and wav_tmp.stat().st_size > 500:
        out.write_bytes(wav_tmp.read_bytes())
        return True
    return False


def synthesize_text(
    text: str,
    out: Path,
    *,
    style: str = "female",
    speed: float | None = None,
) -> tuple[bool, str]:
    err = _import_error()
    if err:
        return False, err
    line = text.strip()
    if not line:
        return False, "Leerer Text"
    try:
        pipe = _get_pipeline(style, speed=speed)
        res = pipe.run(line)
        if _write_mp3(res.audio, res.sample_rate, out):
            return True, f"kokoro-{voice_id(style)}"
    except Exception as e:
        return False, str(e)[:160]
    return False, "Kokoro TTS fehlgeschlagen"


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

    import re

    split = re.compile(r"(?<=[.!?…])\s+")
    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))

    work = out.parent / "_kokoro_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"
        ok, info = synthesize_text(block, part, style=style)
        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, "Kokoro: Zusammenfügen fehlgeschlagen"
