"""Einstellungen auf Host-Ebene: Netzwerk, Webserver/TLS, Verzeichnisdienst.

In beiden Anwendungen identisch -- darum genau einmal hier. Jede App bringt
zusaetzlich nur ihre eigene Systemseite mit.

Lizenzpflicht wird serverseitig durchgesetzt (``license.require``). Der
Vorgaenger pruefte Verzeichnisdienst und Webserver nur im Template; ein direkter
POST kam trotz fehlender Lizenz durch, obwohl die Dokumentation das Gegenteil
behauptete.
"""

from __future__ import annotations

import shutil
import threading
import time
from typing import Any

from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for

from .. import audit
from ..auth import directory
from ..auth.models import current_user, permission_required
from ..auth.service import client_ip
from ..db import Database
from ..extension import core, db, transaction
from ..licensing_client import LicenseError
from ..rbac import service as rbac
from ..system import hostnet, webserver

bp = Blueprint("hostsettings", __name__, url_prefix="/einstellungen")

PENDING_NETWORK_KEY = "pending_network_revert"


def _audit(action: str, target: str = "", detail: str = "", severity: str = "info") -> None:
    with transaction() as conn:
        audit.log(
            conn,
            actor=current_user.username,
            actor_id=current_user.id,
            action=action,
            target=target,
            detail=detail,
            ip=client_ip(),
            severity=severity,
        )


def _license_hint(feature: str) -> str:
    manager = core().license
    if manager is None:
        return ""
    try:
        manager.require(feature)
    except LicenseError as exc:
        return str(exc)
    return ""


# --------------------------------------------------------------------------- #
# Netzwerk
# --------------------------------------------------------------------------- #


@bp.get("/netzwerk")
@permission_required("settings_network.view")
def network():
    extension = core()
    sysops = extension.sysops
    return render_template(
        "tesm_core/settings_network.html",
        interfaces=hostnet.list_interfaces(sysops) if sysops else [],
        backend=hostnet.detect_backend(sysops) if sysops else hostnet.BACKEND_UNKNOWN,
        revert_seconds=hostnet.REVERT_SECONDS,
        pending=extension.registry.get(PENDING_NETWORK_KEY),
        sysops_available=bool(sysops and sysops.available),
        netplan_path=f"/etc/netplan/90-{extension.config.site_key}.yaml",
    )


@bp.post("/netzwerk")
@permission_required("settings_network.edit")
def network_apply():
    extension = core()
    sysops = extension.sysops
    if sysops is None or not sysops.available:
        flash("Systemaktionen sind in dieser Umgebung nicht verfuegbar.", "warning")
        return redirect(url_for("hostsettings.network"))

    interface = (request.form.get("interface") or "").strip()
    if interface not in {entry.name for entry in hostnet.list_interfaces(sysops)}:
        flash("Unbekanntes Interface.", "danger")
        return redirect(url_for("hostsettings.network"))

    dhcp = request.form.get("mode") == "dhcp"
    try:
        if dhcp:
            content = hostnet.render_netplan(interface=interface, dhcp=True)
        else:
            address, prefix, gateway, dns = hostnet.validate_static(
                address=request.form.get("address", ""),
                prefix=request.form.get("prefix", "24"),
                gateway=request.form.get("gateway", ""),
                dns=request.form.get("dns", ""),
            )
            content = hostnet.render_netplan(
                interface=interface,
                dhcp=False,
                address=address,
                prefix=prefix,
                gateway=gateway,
                dns=dns,
            )
    except ValueError as exc:
        flash(str(exc), "danger")
        return redirect(url_for("hostsettings.network"))

    previous = sysops.run("file-read", f"/etc/netplan/90-{extension.config.site_key}.yaml")
    write = sysops.write_netplan(content)
    if not write.ok:
        flash(f"Schreiben fehlgeschlagen: {write.message}", "danger")
        return redirect(url_for("hostsettings.network"))
    applied = sysops.apply_netplan()
    if not applied.ok:
        sysops.write_netplan(previous.stdout if previous.ok else "")
        sysops.apply_netplan()
        flash(
            f"Uebernahme fehlgeschlagen, alter Zustand wiederhergestellt: {applied.message}",
            "danger",
        )
        return redirect(url_for("hostsettings.network"))

    extension.registry[PENDING_NETWORK_KEY] = {
        "interface": interface,
        "previous": previous.stdout if previous.ok else "",
        "deadline": time.time() + hostnet.REVERT_SECONDS,
    }
    _start_revert_timer(extension)
    _audit("settings.network_applied", interface, severity="warning")
    flash(
        f"Uebernommen. Bitte innerhalb von {hostnet.REVERT_SECONDS} Sekunden bestaetigen -- "
        "sonst wird automatisch zurueckgerollt.",
        "warning",
    )
    return redirect(url_for("hostsettings.network"))


@bp.post("/netzwerk/bestaetigen")
@permission_required("settings_network.edit")
def network_confirm():
    if core().registry.pop(PENDING_NETWORK_KEY, None) is None:
        flash("Es liegt keine Aenderung zur Bestaetigung vor.", "info")
    else:
        _audit("settings.network_confirmed", severity="notice")
        flash("Netzwerkeinstellungen bestaetigt.", "success")
    return redirect(url_for("hostsettings.network"))


def _start_revert_timer(extension: Any) -> None:
    def worker() -> None:
        time.sleep(hostnet.REVERT_SECONDS + 2)
        pending = extension.registry.pop(PENDING_NETWORK_KEY, None)
        if not pending:
            return
        sysops = extension.sysops
        if sysops is not None:
            sysops.write_netplan(pending.get("previous") or "")
            sysops.apply_netplan()
        with extension.database.session() as conn, extension.database.transaction(conn):
            audit.log(
                conn,
                actor="system",
                action="settings.network_auto_reverted",
                detail="Keine Bestaetigung innerhalb der Frist -- automatisch zurueckgerollt.",
                severity="warning",
            )

    threading.Thread(target=worker, name="network-revert", daemon=True).start()


# --------------------------------------------------------------------------- #
# Webserver und TLS
# --------------------------------------------------------------------------- #


def _web_config() -> webserver.WebConfig:
    extension = core()
    conn = db()
    values = extension.settings.get_many(
        conn,
        [
            "web_domain",
            "web_https_enabled",
            "web_hsts_enabled",
            "web_redirect_http",
            "web_cert_path",
            "web_key_path",
            "web_acme_webroot",
            "web_max_body_mb",
            "web_http_port",
            "web_https_port",
        ],
    )
    return webserver.WebConfig(
        app_key=extension.config.site_key,
        upstream=str(extension.registry.get("upstream", "127.0.0.1:5000")),
        static_root=str(extension.registry.get("static_root", extension.config.base_dir / "static")),
        domain=str(values["web_domain"] or ""),
        https_enabled=bool(values["web_https_enabled"]),
        hsts_enabled=bool(values["web_hsts_enabled"]),
        redirect_http=bool(values["web_redirect_http"]),
        cert_path=str(values["web_cert_path"] or ""),
        key_path=str(values["web_key_path"] or ""),
        acme_webroot=str(values["web_acme_webroot"] or f"/var/www/{extension.config.site_key}-acme"),
        websocket_path=str(extension.registry.get("websocket_path", "")),
        max_body_mb=int(values["web_max_body_mb"] or 128),
        http_port=int(values["web_http_port"] or 80),
        https_port=int(values["web_https_port"] or 443),
    )


@bp.get("/webserver")
@permission_required("settings_web.view")
def web():
    extension = core()
    config = _web_config()
    return render_template(
        "tesm_core/settings_web.html",
        config=config,
        preview=webserver.render(config),
        certificate=webserver.certificate_info(config.cert_path),
        acme_email=str(extension.settings.get(db(), "web_acme_email") or ""),
        license_hint=_license_hint("nginx"),
        certbot_available=bool(shutil.which("certbot")),
        sysops_available=bool(extension.sysops and extension.sysops.available),
        local_names=webserver.local_names(),
    )


@bp.post("/webserver")
@permission_required("settings_web.edit")
def web_save():
    problem = _license_hint("nginx")
    if problem:
        flash(problem, "danger")
        return redirect(url_for("hostsettings.web"))

    extension = core()
    conn = db()
    try:
        domain = webserver.validate_domain(request.form.get("web_domain", ""))
    except ValueError as exc:
        flash(str(exc), "danger")
        return redirect(url_for("hostsettings.web"))

    https_enabled = request.form.get("web_https_enabled") == "1"
    cert_path = (request.form.get("web_cert_path") or "").strip()
    key_path = (request.form.get("web_key_path") or "").strip()
    if https_enabled and not (cert_path and key_path):
        flash("Fuer HTTPS werden Zertifikat und privater Schluessel gebraucht.", "danger")
        return redirect(url_for("hostsettings.web"))

    values = {
        "web_domain": domain,
        "web_https_enabled": https_enabled,
        "web_hsts_enabled": request.form.get("web_hsts_enabled") == "1" and https_enabled,
        "web_redirect_http": request.form.get("web_redirect_http") == "1",
        "web_cert_path": cert_path,
        "web_key_path": key_path,
        "web_acme_email": (request.form.get("web_acme_email") or "").strip(),
        "web_acme_webroot": (request.form.get("web_acme_webroot") or "").strip()
        or f"/var/www/{extension.config.site_key}-acme",
    }
    with transaction():
        changed = extension.settings.set_many(conn, values, actor=current_user.username)
    _audit("settings.web_saved", domain, ", ".join(changed), severity="notice")
    flash("Gespeichert. Uebernehmen Sie die Konfiguration, damit sie wirksam wird.", "success")
    return redirect(url_for("hostsettings.web"))


@bp.post("/webserver/uebernehmen")
@permission_required("settings_web.execute")
def web_apply():
    problem = _license_hint("nginx")
    if problem:
        flash(problem, "danger")
        return redirect(url_for("hostsettings.web"))
    sysops = core().sysops
    if sysops is None or not sysops.available:
        flash("Systemaktionen sind in dieser Umgebung nicht verfuegbar.", "warning")
        return redirect(url_for("hostsettings.web"))

    result = webserver.apply_config(sysops, webserver.render(_web_config()))
    _audit(
        "settings.web_applied",
        detail=result.get("message", ""),
        severity="notice" if result["ok"] else "warning",
    )
    flash(
        result["message"] + (f" {result.get('detail', '')}" if not result["ok"] else ""),
        "success" if result["ok"] else "danger",
    )
    return redirect(url_for("hostsettings.web"))


@bp.post("/webserver/zertifikat")
@permission_required("settings_web.execute")
def web_certificate():
    problem = _license_hint("nginx")
    if problem:
        flash(problem, "danger")
        return redirect(url_for("hostsettings.web"))
    extension = core()
    sysops = extension.sysops
    conn = db()
    if sysops is None or not sysops.available:
        flash("Systemaktionen sind in dieser Umgebung nicht verfuegbar.", "warning")
        return redirect(url_for("hostsettings.web"))

    config = _web_config()
    result = webserver.request_certificate(
        sysops,
        domain=config.domain,
        email=str(extension.settings.get(conn, "web_acme_email") or ""),
        webroot=config.acme_webroot,
    )
    if result["ok"]:
        with transaction():
            extension.settings.set_many(
                conn,
                {
                    "web_cert_path": result["cert_path"],
                    "web_key_path": result["key_path"],
                    "web_https_enabled": True,
                },
                actor=current_user.username,
            )
        _audit("settings.certificate_issued", config.domain, severity="notice")
    flash(result["message"], "success" if result["ok"] else "danger")
    return redirect(url_for("hostsettings.web"))


@bp.post("/webserver/selbstsigniert")
@permission_required("settings_web.execute")
def web_self_signed():
    """Erzeugt ein selbstsigniertes Zertifikat -- fuer Netze ohne Domain.

    Danach wird HTTPS aktiviert, aber **nicht** automatisch uebernommen: der
    Administrator soll die erzeugte Konfiguration erst sehen. Ein Reload, der
    die eigene Sitzung ueber einen anderen Port wegwirft, kommt nie ungefragt.
    """
    problem = _license_hint("nginx")
    if problem:
        flash(problem, "danger")
        return redirect(url_for("hostsettings.web"))
    extension = core()
    sysops = extension.sysops
    conn = db()
    if sysops is None or not sysops.available:
        flash("Systemaktionen sind in dieser Umgebung nicht verfuegbar.", "warning")
        return redirect(url_for("hostsettings.web"))

    config = _web_config()
    suggested = webserver.local_names()
    common_name = config.domain or (suggested[0] if suggested else "")
    result = webserver.create_self_signed(
        sysops, common_name=common_name, alt_names=[*suggested, "localhost"]
    )
    if result["ok"]:
        with transaction():
            extension.settings.set_many(
                conn,
                {
                    "web_cert_path": result["cert_path"],
                    "web_key_path": result["key_path"],
                    "web_https_enabled": True,
                },
                actor=current_user.username,
            )
        _audit("settings.self_signed_issued", common_name, severity="notice")
    flash(
        result["message"] + (f" {result.get('detail', '')}" if not result["ok"] else ""),
        "success" if result["ok"] else "danger",
    )
    return redirect(url_for("hostsettings.web"))


# --------------------------------------------------------------------------- #
# Verzeichnisdienst
# --------------------------------------------------------------------------- #


@bp.get("/verzeichnisdienst")
@permission_required("settings_directory.view")
def directory_settings():
    extension = core()
    conn = db()
    bind_username, _ = directory.bind_credentials(conn)
    return render_template(
        "tesm_core/settings_directory.html",
        values=extension.settings.get_many(conn, [spec.key for spec in directory.SETTINGS]),
        specs={spec.key: spec for spec in directory.SETTINGS},
        bind_username=bind_username,
        mappings=Database.all(
            conn,
            "SELECT m.*, g.name AS group_name FROM ldap_group_mappings m "
            "LEFT JOIN groups g ON g.id = m.app_group_id ORDER BY m.ad_group_name",
        ),
        groups=rbac.list_groups(conn),
        license_hint=_license_hint("directory"),
    )


@bp.post("/verzeichnisdienst")
@permission_required("settings_directory.edit")
def directory_save():
    problem = _license_hint("directory")
    if problem:
        flash(problem, "danger")
        return redirect(url_for("hostsettings.directory_settings"))

    extension = core()
    conn = db()
    values: dict[str, Any] = {}
    for spec in directory.SETTINGS:
        if spec.kind == "bool":
            values[spec.key] = request.form.get(spec.key) == "1"
        elif spec.key in request.form:
            values[spec.key] = request.form.get(spec.key)

    if values.get("ldap_security") == "none" and not values.get("ldap_allow_insecure"):
        flash(
            "Eine unverschluesselte Verbindung muss ausdruecklich erlaubt werden -- das Passwort "
            "geht dabei im Klartext ueber das Netz.",
            "danger",
        )
        return redirect(url_for("hostsettings.directory_settings"))

    with transaction():
        changed = extension.settings.set_many(conn, values, actor=current_user.username)
        if current_user.has("settings_directory.secrets"):
            directory.store_bind_credentials(
                conn,
                request.form.get("bind_username", ""),
                request.form.get("bind_password") or None,
            )
        audit.log(
            conn,
            actor=current_user.username,
            actor_id=current_user.id,
            action="settings.directory_saved",
            detail=", ".join(changed),
            ip=client_ip(),
            severity="notice",
        )
    flash("Verzeichniseinstellungen gespeichert.", "success")
    return redirect(url_for("hostsettings.directory_settings"))


@bp.post("/verzeichnisdienst/test")
@permission_required("settings_directory.execute")
def directory_test():
    result = directory.test_connection(db())
    flash(result["message"], "success" if result["ok"] else "danger")
    return redirect(url_for("hostsettings.directory_settings"))


@bp.get("/verzeichnisdienst/gruppensuche")
@permission_required("settings_directory.edit", "fileshare.edit", mode="any")
def directory_group_search():
    """Sucht Gruppen im Verzeichnis -- fuer die Auswahlfelder.

    Auch mit ``fileshare.edit`` erreichbar: wer Freigaben zuordnen darf, muss
    die Gruppen finden koennen, ohne zugleich den Verzeichnisdienst
    konfigurieren zu duerfen. Gelesen wird nur, und nur mit dem hinterlegten
    Dienstkonto -- es entsteht kein Zugriff, den der Server nicht ohnehin hat.
    """
    return jsonify(
        {"results": directory.search_directory_groups(db(), request.args.get("q") or "")}
    )


@bp.post("/verzeichnisdienst/zuordnung")
@permission_required("settings_directory.edit")
def directory_mapping_add():
    conn = db()
    dn = (request.form.get("ad_group_dn") or "").strip()
    name = (request.form.get("ad_group_name") or "").strip()
    group_id = request.form.get("app_group_id")
    if not dn or not group_id:
        flash("Verzeichnisgruppe und Zielgruppe sind beide erforderlich.", "danger")
        return redirect(url_for("hostsettings.directory_settings"))
    with transaction():
        conn.execute(
            "INSERT INTO ldap_group_mappings (ad_group_dn, ad_group_name, app_group_id, created_at) "
            "VALUES (?,?,?,datetime('now')) ON CONFLICT(ad_group_dn) DO UPDATE SET "
            "ad_group_name=excluded.ad_group_name, app_group_id=excluded.app_group_id",
            (dn, name or dn, int(group_id)),
        )
        audit.log(
            conn,
            actor=current_user.username,
            actor_id=current_user.id,
            action="settings.directory_mapping_added",
            target=name or dn,
            ip=client_ip(),
            severity="notice",
        )
    flash("Zuordnung gespeichert.", "success")
    return redirect(url_for("hostsettings.directory_settings"))


@bp.post("/verzeichnisdienst/zuordnung/<int:mapping_id>/loeschen")
@permission_required("settings_directory.edit")
def directory_mapping_delete(mapping_id: int):
    with transaction() as conn:
        conn.execute("DELETE FROM ldap_group_mappings WHERE id=?", (mapping_id,))
        audit.log(
            conn,
            actor=current_user.username,
            actor_id=current_user.id,
            action="settings.directory_mapping_deleted",
            target=str(mapping_id),
            ip=client_ip(),
            severity="notice",
        )
    flash("Zuordnung entfernt.", "success")
    return redirect(url_for("hostsettings.directory_settings"))
