"""Konfiguration aus Umgebungsvariablen -- typisiert, an einer Stelle.

Im Vorgaengerprojekt waren Pfade und Konstanten quer durch eine mehrere
tausend Zeilen lange ``app.py`` verstreut. Hier gibt es genau ein Objekt, das
beim Start gebaut, validiert und danach nicht mehr veraendert wird.

Namensraum: jede Anwendung bekommt ein eigenes Env-Praefix (``TESM_`` bzw.
``TESM_LICENSE_``), damit beide Dienste auf demselben Host ohne Kollisionen
laufen koennen -- inklusive der Pfade, die im Vorgaenger kollidierten
(netplan-Override!).
"""

from __future__ import annotations

import os
import re
from dataclasses import dataclass, field, replace
from pathlib import Path


def _env(prefix: str, name: str, default: str = "") -> str:
    return os.environ.get(f"{prefix}{name}", default).strip()


def _env_bool(prefix: str, name: str, default: bool) -> bool:
    raw = _env(prefix, name)
    if not raw:
        return default
    return raw.lower() in {"1", "true", "yes", "on"}


def _env_int(prefix: str, name: str, default: int) -> int:
    raw = _env(prefix, name)
    try:
        return int(raw) if raw else default
    except ValueError:
        return default


@dataclass(frozen=True, slots=True)
class SecurityConfig:
    """Alles, was das Sicherheitsverhalten steuert. Defaults sind die sichere Wahl."""

    #: Cookie nur ueber HTTPS ausliefern. In Produktion immer True (nginx terminiert TLS).
    cookie_secure: bool = True
    cookie_samesite: str = "Lax"
    #: Leerlauf-Zeitfenster einer Sitzung.
    session_idle_minutes: int = 60
    #: Harte Obergrenze unabhaengig von Aktivitaet.
    session_absolute_hours: int = 12
    #: Wie lange eine erneute Passwortabfrage ("sudo-Modus") gilt.
    reauth_valid_minutes: int = 15
    #: Fehlversuche bis zur temporaeren Sperre eines Kontos.
    login_max_attempts: int = 8
    login_lockout_minutes: int = 15
    #: Fehlversuche pro IP im gleichen Zeitfenster (unabhaengig vom Konto).
    login_max_attempts_per_ip: int = 30
    #: Mindestlaenge lokaler Passwoerter.
    password_min_length: int = 12
    #: 2FA fuer Administratoren erzwingen.
    require_totp_for_admins: bool = False
    #: HSTS-Header setzen (nur sinnvoll, wenn TLS tatsaechlich terminiert wird).
    hsts_enabled: bool = False
    hsts_max_age: int = 15552000
    #: Vertrauenswuerdige Proxy-Hops fuer X-Forwarded-For (0 = Header ignorieren).
    trusted_proxy_hops: int = 1
    #: Maximale Groesse eines Uploads in Bytes (None = unbegrenzt).
    max_content_length: int | None = 64 * 1024 * 1024


@dataclass(frozen=True, slots=True)
class CoreConfig:
    """Basiskonfiguration, die beide Anwendungen teilen."""

    app_key: str
    """Technischer Kurzname, z. B. ``tesm`` -- Namensraum fuer Pfade und Units."""

    site_key: str
    """Name *dieser Installation* auf dem Host, z. B. ``tesm-opus``.

    Weicht vom ``app_key`` ab, sobald neben der Hauptinstallation eine zweite
    Instanz laeuft. Alles, was ausserhalb von ``/srv/<name>`` liegt und damit
    zwischen Instanzen kollidieren koennte -- nginx-Site, netplan-Datei,
    logrotate-Regel, Zertifikatsverzeichnis, Name des Sitzungscookies --, wird
    ueber diesen Schluessel benannt und **nicht** ueber den ``app_key``.

    Ohne diese Trennung wuerde eine Testinstanz beim Speichern der
    Webserver-Einstellungen die Site-Datei der Produktion ueberschreiben, und
    beide Instanzen wuerden sich auf demselben Host dasselbe Sitzungscookie
    teilen (Cookies unterscheiden keine Ports).
    """

    app_name: str
    """Anzeigename in der Oberflaeche."""

    env_prefix: str
    base_dir: Path
    data_dir: Path
    log_dir: Path
    db_path: Path
    secret_key_file: Path
    data_key_file: Path
    license_path: Path
    license_key_path: Path
    #: Adresse, auf der der Anwendungsserver lauscht -- exakt das, was nginx
    #: als ``proxy_pass``-Ziel braucht. Kommt aus derselben Umgebungsdatei wie
    #: der ``--bind``-Parameter von gunicorn, damit beide nie auseinanderlaufen.
    #: Vorher stand hier ein fest verdrahtetes ``127.0.0.1:5000``; eine zweite
    #: Instanz auf Port 5100 erzeugte damit eine nginx-Site, die auf die
    #: *andere* Installation zeigte.
    bind: str = "127.0.0.1:5000"
    debug: bool = False
    #: Instanz laeuft als echter Webprozess (gesetzt durch die systemd-Unit).
    web_process: bool = False
    security: SecurityConfig = field(default_factory=SecurityConfig)
    #: Zusaetzliche, app-spezifische Werte.
    extra: dict[str, str] = field(default_factory=dict)

    @property
    def is_production(self) -> bool:
        return not self.debug

    def path_in_data(self, name: str) -> Path:
        return self.data_dir / name


def load_core_config(
    *,
    app_key: str,
    app_name: str,
    env_prefix: str,
    default_base: Path | None = None,
    default_bind: str = "127.0.0.1:5000",
) -> CoreConfig:
    """Liest die Konfiguration aus der Umgebung.

    Ohne gesetzte Env-Vars laeuft alles in einem Unterverzeichnis
    ``instance/<app_key>`` relativ zum aktuellen Arbeitsverzeichnis -- damit ist
    die Anwendung ohne jede Vorbereitung lokal startbar, ohne je nach
    ``/srv``/``/var/log`` zu greifen.
    """
    base = Path(_env(env_prefix, "BASE_DIR") or (default_base or Path.cwd() / "instance" / app_key))
    base = base.expanduser().resolve()
    data_dir = Path(_env(env_prefix, "DATA_DIR") or base / "data").expanduser()
    log_dir = Path(_env(env_prefix, "LOG_DIR") or base / "logs").expanduser()

    security = SecurityConfig(
        cookie_secure=_env_bool(env_prefix, "COOKIE_SECURE", True),
        cookie_samesite=_env(env_prefix, "COOKIE_SAMESITE") or "Lax",
        session_idle_minutes=_env_int(env_prefix, "SESSION_IDLE_MINUTES", 60),
        session_absolute_hours=_env_int(env_prefix, "SESSION_ABSOLUTE_HOURS", 12),
        reauth_valid_minutes=_env_int(env_prefix, "REAUTH_VALID_MINUTES", 15),
        login_max_attempts=_env_int(env_prefix, "LOGIN_MAX_ATTEMPTS", 8),
        login_lockout_minutes=_env_int(env_prefix, "LOGIN_LOCKOUT_MINUTES", 15),
        login_max_attempts_per_ip=_env_int(env_prefix, "LOGIN_MAX_ATTEMPTS_PER_IP", 30),
        password_min_length=_env_int(env_prefix, "PASSWORD_MIN_LENGTH", 12),
        require_totp_for_admins=_env_bool(env_prefix, "REQUIRE_TOTP_FOR_ADMINS", False),
        hsts_enabled=_env_bool(env_prefix, "HSTS_ENABLED", False),
        hsts_max_age=_env_int(env_prefix, "HSTS_MAX_AGE", 15552000),
        trusted_proxy_hops=_env_int(env_prefix, "TRUSTED_PROXY_HOPS", 1),
        max_content_length=_env_int(env_prefix, "MAX_CONTENT_LENGTH", 64 * 1024 * 1024) or None,
    )

    debug = _env_bool(env_prefix, "DEBUG", False)
    if debug:
        # Lokale Entwicklung laeuft ueber http://127.0.0.1 -- ein Secure-Cookie
        # wuerde dort nie gesetzt und die Anmeldung schlicht nicht funktionieren.
        # dataclasses.replace, nicht __dict__: SecurityConfig hat slots und
        # damit gar kein __dict__ -- der alte Ausdruck warf beim Start einen
        # AttributeError, und zwar genau im Entwicklungsmodus, den das README
        # empfiehlt.
        security = replace(
            security, cookie_secure=_env_bool(env_prefix, "COOKIE_SECURE", False)
        )

    bind = (_env(env_prefix, "BIND") or default_bind).strip()
    if not re.fullmatch(r"[0-9A-Za-z_.\[\]:-]{1,80}:\d{1,5}", bind):
        raise ValueError(f"Ungueltige Bind-Adresse: {bind!r}")

    site_key = (_env(env_prefix, "SITE_KEY") or app_key).strip().lower()
    if not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,39}", site_key):
        raise ValueError(f"Ungueltiger SITE_KEY: {site_key!r}")

    return CoreConfig(
        app_key=app_key,
        site_key=site_key,
        bind=bind,
        app_name=_env(env_prefix, "APP_NAME") or app_name,
        env_prefix=env_prefix,
        base_dir=base,
        data_dir=data_dir,
        log_dir=log_dir,
        db_path=Path(_env(env_prefix, "DB_PATH") or data_dir / "app.db").expanduser(),
        secret_key_file=Path(
            _env(env_prefix, "SECRET_KEY_FILE") or data_dir / "secret.key"
        ).expanduser(),
        data_key_file=Path(_env(env_prefix, "DATA_KEY_FILE") or data_dir / "data.keys").expanduser(),
        license_path=Path(_env(env_prefix, "LICENSE_PATH") or data_dir / "license.json").expanduser(),
        license_key_path=Path(
            _env(env_prefix, "LICENSE_KEY_PATH") or data_dir / "license_key.json"
        ).expanduser(),
        debug=debug,
        web_process=_env(env_prefix, "WEB_PROCESS") == "1",
        security=security,
    )
