"""Settings management: load, save, normalize, TTS stability helpers."""
from __future__ import annotations

import json
import re
from pathlib import Path

import requests

from core.constants import (
    CONFIG_DIR, CONFIG_FILE,
    _VOICES_DIR_DEFAULT, _OUTPUT_DIR_DEFAULT,
    _WHISPER_DEFAULT, _TTS_DEFAULT, _TTS_STREAM_DEFAULT,
    _CUSTOMVOICE_DEFAULT, _VOICE_DESIGN_DEFAULT,
    _NVIDIA_ROUTER_DEFAULT, _NVIDIA_TTS_DEFAULT, _NVIDIA_ASR_DEFAULT,
    _NVIDIA_CLONE_DEFAULT, _NVIDIA_ZEROSHOT_DEFAULT, _NVIDIA_FLOW_DEFAULT,
    _FASTER_WHISPER_DEFAULT, _WHISPER_CPP_DEFAULT,
    _KOKORO_DEFAULT, _VIBEVOICE_DEFAULT, _XTTS_DEFAULT, _FISHSPEECH_DEFAULT,
    _TTS_STREAM_DEFAULT,
)

# ── Settings key whitelist ────────────────────────────────────────────────────

_SETTINGS_KEYS = {
    "whisper_url", "tts_url", "tts_stream_url", "tts_stream_mode", "tts_backend",
    "output_dir", "voices_scan_dir", "voice_design_url", "customvoice_url",
    "nvidia_router_url", "nvidia_tts_url", "nvidia_asr_url", "nvidia_clone_url",
    "nvidia_zeroshot_url", "nvidia_flow_url",
    "faster_whisper_url", "whisper_cpp_url", "groq_api_key", "kokoro_url", "vibevoice_url", "xtts_url",
    "whisper_api_key", "tts_api_key", "voice_design_api_key", "elevenlabs_api_key",
    "tts_stability_enabled", "tts_extra_params", "tts_extra_params_by_backend",
    # Captures settings
    "stt_language", "stt_preferred_backend",
    "auto_refine", "refine_model",
    "refine_fillers", "refine_repetitions", "refine_corrections", "refine_punctuation",
    "captures_default_voice",
    "client_voice_bindings",
    "llm_url", "llm_model", "llm_api_key",
    # Browser-persistent UI state
    "refine_llm_url", "conv_llm_url", "seed_finder_text",
    "seed_finder_dir", "pt_dir", "audiobook_prompt",
}

# ── TTS stability defaults ────────────────────────────────────────────────────

_TTS_STABILITY_DEFAULT = {"temperature": 0.1, "top_p": 0.8, "seed": 0}
_TTS_STABILITY_BY_BACKEND_DEFAULT = {
    "voice_clone": dict(_TTS_STABILITY_DEFAULT),
    "streaming": dict(_TTS_STABILITY_DEFAULT),
    "customvoice": dict(_TTS_STABILITY_DEFAULT),
    "voice_design": dict(_TTS_STABILITY_DEFAULT),
    "nvidia_magpie": {},
    "nvidia_zeroshot": {},
    "nvidia_flow": {},
    "kokoro": {},
    "vibevoice": {},
    "xtts": {},
    "fishspeech": {},
}
_TTS_PAYLOAD_CORE_KEYS = {"model", "input", "voice", "response_format", "instruct", "language"}


def _settings_bool(value, default: bool = True) -> bool:
    if value is None:
        return default
    if isinstance(value, bool):
        return value
    return str(value).strip().lower() not in {"0", "false", "off", "no"}


def _clean_tts_param_dict(raw, fallback: dict | None = None) -> dict:
    if raw in (None, ""):
        raw = fallback or {}
    if isinstance(raw, str):
        try:
            raw = json.loads(raw)
        except Exception:
            raw = fallback or {}
    if not isinstance(raw, dict):
        return {}
    params = {}
    for key, value in raw.items():
        if key in _TTS_PAYLOAD_CORE_KEYS or str(key).startswith("_"):
            continue
        if isinstance(value, (str, int, float, bool)) or value is None:
            params[str(key)] = value
    return params


def _clean_preview_backend(value: str) -> str:
    key = re.sub(r"[^a-z0-9]+", "_", str(value or "voice_clone").lower()).strip("_")
    aliases = {
        "clone": "voice_clone",
        "base": "voice_clone",
        "voiceclone": "voice_clone",
        "voice_clone_base": "voice_clone",
        "stream": "streaming",
        "tts_streaming": "streaming",
        "custom": "customvoice",
        "custom_voice": "customvoice",
        "voice_design": "voice_design",
        "voicedesign": "voice_design",
        "design": "voice_design",
        "nvidia": "nvidia_magpie",
        "magpie": "nvidia_magpie",
        "nvidia_tts": "nvidia_magpie",
        "nvidia_magpie_tts": "nvidia_magpie",
        "nvidia_clone": "nvidia_zeroshot",
        "nvidia_zeroshot_tts": "nvidia_zeroshot",
        "magpie_zeroshot": "nvidia_zeroshot",
        "zeroshot": "nvidia_zeroshot",
        "zero_shot": "nvidia_zeroshot",
        "nvidia_flow_tts": "nvidia_flow",
        "magpie_flow": "nvidia_flow",
        "flow": "nvidia_flow",
        "kokoro_fastapi": "kokoro",
        "kokoro_tts": "kokoro",
        "kokoro_local": "kokoro",
        "vibevoice_service": "vibevoice",
        "vibe_voice": "vibevoice",
        "vibetts": "vibevoice",
        "xtts_v2": "xtts",
        "xtts2": "xtts",
        "coqui_xtts": "xtts",
        "fish": "fishspeech",
        "fish_speech": "fishspeech",
        "fishaudio": "fishspeech",
        "openaudio": "fishspeech",
    }
    key = aliases.get(key, key)
    return key if key in {"voice_clone", "streaming", "customvoice", "voice_design", "nvidia_magpie", "nvidia_zeroshot", "nvidia_flow", "kokoro", "vibevoice", "xtts", "fishspeech"} else "voice_clone"


def _tts_extra_params(settings: dict, backend: str = "voice_clone") -> dict:
    if not _settings_bool(settings.get("tts_stability_enabled"), True):
        return {}
    backend = _clean_preview_backend(backend)
    by_backend = settings.get("tts_extra_params_by_backend")
    if isinstance(by_backend, str):
        try:
            by_backend = json.loads(by_backend)
        except Exception:
            by_backend = None
    if isinstance(by_backend, dict):
        fallback = _TTS_STABILITY_BY_BACKEND_DEFAULT.get(backend, _TTS_STABILITY_DEFAULT)
        if backend in by_backend:
            return _clean_tts_param_dict(by_backend.get(backend), fallback)
        return _clean_tts_param_dict(fallback, fallback)
    return _clean_tts_param_dict(settings.get("tts_extra_params", _TTS_STABILITY_BY_BACKEND_DEFAULT.get(backend, _TTS_STABILITY_DEFAULT)), _TTS_STABILITY_BY_BACKEND_DEFAULT.get(backend, _TTS_STABILITY_DEFAULT))


def _apply_tts_extra_params(payload: dict, settings: dict, backend: str = "voice_clone") -> dict:
    params = _tts_extra_params(settings, backend)
    if params:
        payload.update(params)
    return payload


def _strip_tts_extra_params(payload: dict) -> dict:
    return {k: v for k, v in payload.items() if k in _TTS_PAYLOAD_CORE_KEYS}


def _post_tts_with_fallback(endpoint: str, payload: dict, headers: dict, **kwargs) -> requests.Response:
    resp = requests.post(endpoint, json=payload, headers=headers, **kwargs)
    fallback = _strip_tts_extra_params(payload)
    if resp.status_code in {400, 404, 415, 422} and fallback != payload:
        try:
            resp.close()
        except Exception:
            pass
        import logging
        logging.getLogger("uvicorn.error").warning("TTS backend rejected extra generation params; retrying without them")
        return requests.post(endpoint, json=fallback, headers=headers, **kwargs)
    return resp


def _preview_backend_base_url(settings: dict, backend: str) -> str:
    backend = _clean_preview_backend(backend)
    if backend == "streaming":
        return settings.get("tts_stream_url") or settings.get("tts_url") or _TTS_STREAM_DEFAULT
    if backend == "customvoice":
        return settings.get("customvoice_url") or _CUSTOMVOICE_DEFAULT
    if backend == "voice_design":
        return settings.get("voice_design_url") or _VOICE_DESIGN_DEFAULT
    if backend == "nvidia_magpie":
        return settings.get("nvidia_tts_url") or settings.get("nvidia_router_url") or _NVIDIA_TTS_DEFAULT
    if backend == "nvidia_zeroshot":
        return settings.get("nvidia_zeroshot_url") or settings.get("nvidia_clone_url") or settings.get("nvidia_router_url") or _NVIDIA_ZEROSHOT_DEFAULT
    if backend == "nvidia_flow":
        return settings.get("nvidia_flow_url") or settings.get("nvidia_clone_url") or settings.get("nvidia_router_url") or _NVIDIA_FLOW_DEFAULT
    if backend == "kokoro":
        return settings.get("kokoro_url") or _KOKORO_DEFAULT
    if backend == "vibevoice":
        return settings.get("vibevoice_url") or _VIBEVOICE_DEFAULT
    if backend == "xtts":
        return settings.get("xtts_url") or _XTTS_DEFAULT
    if backend == "fishspeech":
        return settings.get("fishspeech_url") or _FISHSPEECH_DEFAULT
    return settings.get("tts_url") or _TTS_DEFAULT


def _normalize_settings(s: dict) -> dict:
    scan_dir = Path(s.get("voices_scan_dir", _VOICES_DIR_DEFAULT))
    output_dir = Path(s.get("output_dir", _OUTPUT_DIR_DEFAULT))
    legacy_output_dirs = {
        Path("/voices/speakers"),
        scan_dir / "speakers",
    }
    if output_dir in legacy_output_dirs:
        s["output_dir"] = str(scan_dir / "active_voices")
    if s.get("tts_stream_mode") not in {"auto", "streaming", "buffered"}:
        s["tts_stream_mode"] = "auto"
    if not isinstance(s.get("engine_local_urls"), dict):
        s["engine_local_urls"] = {}
    if not isinstance(s.get("engine_container_names"), dict):
        s["engine_container_names"] = {}
    if not isinstance(s.get("custom_engine_cards"), list):
        s["custom_engine_cards"] = []
    return s


_settings_cache: dict | None = None
_settings_cache_mtime: float = -1.0


def _load_settings() -> dict:
    global _settings_cache, _settings_cache_mtime
    mtime = CONFIG_FILE.stat().st_mtime if CONFIG_FILE.exists() else 0.0
    if _settings_cache is not None and mtime == _settings_cache_mtime:
        return dict(_settings_cache)
    defaults = {
        "whisper_url":          _WHISPER_DEFAULT,
        "tts_url":              _TTS_DEFAULT,
        "tts_stream_url":       _TTS_STREAM_DEFAULT,
        "tts_stream_mode":      "auto",
        "tts_backend":          "openai",
        "output_dir":           _OUTPUT_DIR_DEFAULT,
        "voices_scan_dir":      _VOICES_DIR_DEFAULT,
        "voice_design_url":     _VOICE_DESIGN_DEFAULT,
        "customvoice_url":       _CUSTOMVOICE_DEFAULT,
        "nvidia_router_url":     _NVIDIA_ROUTER_DEFAULT,
        "nvidia_tts_url":        _NVIDIA_TTS_DEFAULT,
        "nvidia_asr_url":        _NVIDIA_ASR_DEFAULT,
        "nvidia_clone_url":      _NVIDIA_CLONE_DEFAULT,
        "nvidia_zeroshot_url":   _NVIDIA_ZEROSHOT_DEFAULT,
        "nvidia_flow_url":       _NVIDIA_FLOW_DEFAULT,
        "faster_whisper_url":    _FASTER_WHISPER_DEFAULT,
        "whisper_cpp_url":       _WHISPER_CPP_DEFAULT,
        "groq_api_key":          "",
        "kokoro_url":            _KOKORO_DEFAULT,
        "vibevoice_url":         _VIBEVOICE_DEFAULT,
        "xtts_url":              _XTTS_DEFAULT,
        "fishspeech_url":        _FISHSPEECH_DEFAULT,
        "whisper_api_key":      "",
        "tts_api_key":          "",
        "voice_design_api_key": "",
        "tts_stability_enabled": True,
        "tts_extra_params": _TTS_STABILITY_DEFAULT,
        "tts_extra_params_by_backend": _TTS_STABILITY_BY_BACKEND_DEFAULT,
        # Captures
        "stt_language":           "",
        "stt_preferred_backend":  "",
        "auto_refine":            "off",
        "refine_model":           "",
        "refine_fillers":         True,
        "refine_repetitions":     True,
        "refine_corrections":     True,
        "refine_punctuation":     True,
        "captures_default_voice": "",
        "client_voice_bindings":  {},
        "llm_url":                "http://localhost:11434/v1",
        "llm_api_key":            "",
        "llm_model":              "",
        "engine_local_urls":      {},
        "engine_container_names": {},
        "custom_engine_cards":    [],
        "refine_llm_url":         "",
        "conv_llm_url":           "",
        "seed_finder_text":       "",
        "seed_finder_dir":        "",
        "pt_dir":                 "",
    }
    if CONFIG_FILE.exists():
        try:
            saved = json.loads(CONFIG_FILE.read_text())
            defaults.update({k: v for k, v in saved.items() if k in _SETTINGS_KEYS})
        except Exception:
            pass
    result = _normalize_settings(defaults)
    _settings_cache = result
    _settings_cache_mtime = mtime
    return dict(result)


def _save_settings(s: dict) -> None:
    global _settings_cache, _settings_cache_mtime
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    CONFIG_FILE.write_text(json.dumps(s, indent=2))
    _settings_cache = s
    _settings_cache_mtime = CONFIG_FILE.stat().st_mtime
