import os
from pathlib import Path

try:
    import tomllib  # Python >= 3.11 (stdlib)
except ModuleNotFoundError:  # pragma: no cover - Fallback fuer aeltere Interpreter
    import tomli as tomllib  # type: ignore

from pydantic.fields import FieldInfo
from pydantic_settings import (
    BaseSettings,
    PydanticBaseSettingsSource,
    SettingsConfigDict,
)

BASE_DIR = Path(__file__).resolve().parent.parent
ENV_FILE = BASE_DIR / ".env"
DEFAULT_CONFIG_FILE = BASE_DIR / "config" / "voice-assistant.toml"


def _setting_lookup(key: str) -> str | None:
    """Liest einen Steuer-Schluessel: echte Umgebung zuerst, dann die .env-Datei.

    Noetig fuer VA_PROFILE/VA_CONFIG_FILE, weil diese gebraucht werden, BEVOR
    pydantic-settings die .env laedt - und .env-Werte sonst nicht in os.environ stehen.
    """
    value = os.getenv(key)
    if value is not None:
        return value
    try:
        from dotenv import dotenv_values
    except ModuleNotFoundError:  # pragma: no cover
        return None
    if ENV_FILE.is_file():
        return dotenv_values(ENV_FILE).get(key)
    return None


def _config_file_path() -> Path:
    return Path(_setting_lookup("VA_CONFIG_FILE") or str(DEFAULT_CONFIG_FILE))


def active_profile() -> str | None:
    """Name des aktiven Profils (VA_PROFILE) aus Umgebung oder .env, falls gesetzt."""
    profile = _setting_lookup("VA_PROFILE")
    return profile.strip() or None if profile else None


def load_profile_config() -> dict:
    """Liest die zentrale TOML-Config und merged [defaults] + [profiles.<VA_PROFILE>].

    - Fehlt die Datei, gilt ein leeres dict (nur ENV/Defaults greifen) - kein Fehler,
      damit reine Cloud-Deployments ohne Datei (nur ENV) funktionieren.
    - Ein gesetztes, aber unbekanntes VA_PROFILE ist ein Konfigurationsfehler.
    """
    path = _config_file_path()
    if not path.is_file():
        return {}

    with path.open("rb") as handle:
        data = tomllib.load(handle)

    merged: dict = dict(data.get("defaults", {}))

    profile = active_profile()
    if profile:
        profiles = data.get("profiles", {})
        if profile not in profiles:
            raise ValueError(
                f"Unbekanntes VA_PROFILE {profile!r}. "
                f"Verfuegbar: {sorted(profiles)}"
            )
        merged.update(profiles[profile])

    return merged


class TomlProfileSource(PydanticBaseSettingsSource):
    """Settings-Quelle aus der zentralen TOML-Config (inkl. aktivem Profil).

    Liegt in der Praezedenz unter ENV/.env, aber ueber den eingebauten Defaults.
    Es werden nur Schluessel durchgereicht, die auch als Settings-Feld existieren.
    """

    def __init__(self, settings_cls):
        super().__init__(settings_cls)
        raw = load_profile_config()
        known = set(settings_cls.model_fields)
        self._values = {
            key.lower(): value
            for key, value in raw.items()
            if key.lower() in known
        }

    def get_field_value(self, field: FieldInfo, field_name: str):
        if field_name in self._values:
            return self._values[field_name], field_name, False
        return None, field_name, False

    def __call__(self) -> dict:
        return dict(self._values)


class Settings(BaseSettings):
    app_env: str = "dev"
    host: str = "0.0.0.0"
    port: int = 8080
    log_level: str = "info"
    openrouter_api_key: str = ""
    openrouter_stt_model: str = "openai/whisper-large-v3"
    openrouter_tts_model: str = "openai/gpt-4o-mini-tts"
    openrouter_tts_voice: str = "alloy"
    openrouter_llm_model: str = "openai/gpt-4.1-mini"
    default_language: str = "de"
    default_input_endpoint: str = "local-default"
    default_output_endpoint: str = "local-default"
    default_stt_provider: str = "openrouter"
    default_llm_provider: str = "local-openai-compatible"
    default_tts_provider: str = "openrouter"
    # Lokaler llama.cpp-Server (OpenAI-kompatibel), siehe scripts/llm-server/.
    local_llm_base_url: str = "http://127.0.0.1:8001/v1"
    local_llm_api_key: str = "dummy"
    local_llm_model: str = "va_llm"  # = --alias des llama.cpp-Servers
    # Sprach-Assistent: knappe, vorlesbare Antworten + Reasoning aus = deutlich schneller.
    local_llm_system_prompt: str = (
        "Du bist ein gesprochener Sprachassistent. Antworte kurz und natuerlich "
        "(in der Regel 1-3 Saetze), in reinem Fliesstext ohne Markdown, ohne "
        "Aufzaehlungen, ohne Emojis. Formuliere so, wie man es laut vorliest."
    )
    local_llm_disable_reasoning: bool = True  # Qwen3 /no_think: spart die Denkphase
    local_llm_max_tokens: int = 0             # 0 = serverseitiges Limit (-n)
    local_llm_temperature: float = 0.3
    local_llm_top_p: float = 0.9              # Nucleus-Sampling (0.0–1.0)
    faster_whisper_model: str = "small"       # tiny|base|small|medium|large-v3
    faster_whisper_device: str = "auto"       # auto|cpu|cuda
    faster_whisper_compute_type: str = "int8"     # default|int8|float16|int8_float16
    stt_force_language: bool = True           # STT mit fester Zielsprache (robust); False = Auto-Detect + Übersetzung
    # --- Lokales TTS (piper) -------------------------------------------------
    piper_bin: str = "piper"                  # Pfad/Name des piper-Binaries
    piper_voices_dir: str = str(Path.home() / ".local" / "share" / "piper" / "voices")
    piper_voice: str = "de_DE-thorsten-high"  # Stimmmodell-Name (ohne .onnx) oder voller Pfad
    tts_sample_rate: int = 24000              # Ziel-Sample-Rate (das Gateway erwartet 24000 Hz)
    # --- Chatterbox-TTS (hohe Qualitaet + Voice-Cloning, eigener HTTP-Dienst) -
    # --- Cartesia TTS (Cloud, sehr niedrige Latenz) -------------------------
    cartesia_api_key: str = ""
    cartesia_voice_id: str = ""          # UUID aus cartesia.ai/voices (Fallback)
    cartesia_tts_model: str = "sonic-turbo"
    cartesia_voices: str = ""              # "lang:gender:uuid:name" je Zeile, # = Kommentar  # sonic-2 | sonic-multilingual
    chatterbox_base_url: str = "http://127.0.0.1:9999"
    chatterbox_voice: str = ""                # Pfad zu Referenz-WAV (Voice-Cloning) oder leer
    chatterbox_lang: str = "de"
    chatterbox_speed: float = 1.0
    chatterbox_timeout: int = 180
    # Verzeichnis mit nativen Referenz-WAVs je Sprache (Konvention <lang>.wav, z. B. fr.wav).
    # Greift cross-lingual: pro Sprache eine muttersprachliche Stimme. Leer -> nur chatterbox_voice.
    chatterbox_voices_dir: str = str(BASE_DIR / "config" / "voices")
    db_path: str = str(BASE_DIR / "data" / "voice-assistant.db")
    admin_api_key: str = ""
    auth_enabled: bool = True
    # --- Forward-/Trusted-Header-Auth (Reverse-Proxy / YunoHost-SSO) ---------
    # Ist trusted_auth_header gesetzt UND die Quell-IP in trusted_proxy_ips, wird die
    # Identitaet aus diesem Header gelesen (SSO-User) und ein interner Nutzer
    # automatisch angelegt. Sonst gilt die normale Token-/Anonymous-Auth.
    trusted_auth_header: str = ""
    # Alternativ zur Header-Variante: Identitaet aus einem (signierten) JWT-Cookie lesen.
    # YunoHost reicht den Usernamen nicht als Header durch, sondern im Cookie
    # "yunohost.portal" (JWT, Claim "user"). Nur von der Proxy-IP akzeptiert.
    trusted_auth_cookie: str = ""        # Cookie-Name (z. B. yunohost.portal)
    trusted_auth_cookie_claim: str = "user"  # JWT-Claim mit dem Usernamen
    trusted_auth_jwt_secret: str = ""    # optional: HS256-Secret -> Signatur pruefen
    trusted_proxy_ips: str = ""        # kommasepariert; IP(s) des Reverse-Proxys
    admin_users: str = ""             # kommaseparierte SSO-Usernamen mit Admin-Rechten
    sso_logout_url: str = ""          # Logout-Link fuers Frontend (SSO-Portal)
    # Login-/Portal-URL: unauthentifizierte Seitenaufrufe werden hierhin umgeleitet
    # (Defense-in-Depth zusaetzlich zu SSOwat). Leer -> stattdessen HTTP 401.
    sso_login_url: str = ""
    history_max_messages: int = 10
    # Automatische Erinnerungs-Extraktion: das LLM destilliert dauerhafte Fakten
    # aus dem Gespraech und legt sie als Nutzer-Erinnerungen ab (best-effort,
    # nicht-blockierend). Leerer Provider = Default-LLM-Provider.
    memory_extraction_enabled: bool = True
    memory_extraction_every_n_turns: int = 3
    memory_extraction_max: int = 50
    memory_extraction_provider: str = ""
    audio_stream_default: bool = True  # satzweises TTS als Default (Admin kann abschalten)
    # TTS-Text-Normalisierung: auto|full|light|off. "auto" = piper -> full, Cloud -> light.
    tts_normalize_level: str = "auto"
    stt_fallback: str = ""  # kommaseparierte Provider-Namen (Fallback-Kette)
    llm_fallback: str = ""
    tts_fallback: str = ""
    daily_request_limit: int = 0  # 0 = unbegrenzt; Anfragen pro Nutzer pro Tag
    # Notruf-Benachrichtigung per E-Mail.
    emergency_contact_email: str = "dieter.schlueter@linix.de"  # Default-Kontaktperson
    smtp_host: str = ""        # ohne Host wird KEINE E-Mail versendet
    smtp_port: int = 587
    smtp_user: str = ""
    smtp_password: str = ""
    smtp_from: str = ""        # Absenderadresse; leer -> smtp_user
    smtp_starttls: bool = True

    # Notruf-Eskalation per SMS/Anruf: provider-agnostischer Webhook. Bei einem Notruf
    # geht ein JSON-POST an emergency_webhook_url (Kanäle, Telefonnummern, fertige
    # SMS-/Anruf-Texte). Den konkreten Gateway (Twilio, seven.io, …) verdrahtet der
    # Empfänger dahinter. Ohne URL wird nichts gesendet.
    emergency_webhook_url: str = ""
    emergency_webhook_token: str = ""   # optional -> Authorization: Bearer <token>
    emergency_contact_phone: str = ""   # Default-Telefonnummer(n), CSV, E.164 (+49…)
    emergency_webhook_channels: str = "sms,call"  # angefragte Kanäle (CSV)
    model_config = SettingsConfigDict(
        env_file=ENV_FILE, case_sensitive=False, extra="ignore"
    )

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls,
        init_settings,
        env_settings,
        dotenv_settings,
        file_secret_settings,
    ):
        # Praezedenz (frueher = hoeher): init > ENV > .env > TOML/Profil > Defaults
        return (
            init_settings,
            env_settings,
            dotenv_settings,
            TomlProfileSource(settings_cls),
            file_secret_settings,
        )


settings = Settings()
