"""TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen.

Diese Datei ist die Anwendungsfabrik. Sie tut drei Dinge und nicht mehr:
Konfiguration laden, das gemeinsame Geruest aus ``tesm_core`` aufbauen, die
eigenen Blueprints anmelden. Der Vorgaenger hatte an dieser Stelle eine
``app.py`` mit mehreren tausend Zeilen -- Routen, SQL, SSH, DHCP-Erzeugung und
Templates in einer Datei.
"""

from __future__ import annotations

import logging
from typing import Any

from flask import Flask, current_app
from tesm_core.app_factory import create_base_app
from tesm_core.config import load_core_config
from tesm_core.extension import core
from tesm_core.views import trash as trash_views

from .navigation import NAVIGATION
from .permissions import PERMISSIONS, ROLE_PRESETS
from .schema import TESM_MIGRATIONS
from .settings_spec import TESM_SETTINGS

__version__ = "2.0.0"

APP_KEY = "tesm"
APP_NAME = "TESM"
APP_SHORT = "TE"
APP_EDITION = "Geraeteverwaltung"
#: Funktionszeile unter der Wortmarke auf der Anmeldeseite. Uebernommen vom
#: Vorgaenger, wo sie Teil des Hintergrundmotivs war -- sie sagt in einer Zeile,
#: was die Anwendung tut, und das gehoert auf die erste Seite.
AUTH_TAGLINE = (
    "UEBERWACHEN · BOOTEN · ANBINDEN (DHCP) · "
    "BETRIEBSSYSTEM GEBEN (PXE)"
)
ENV_PREFIX = "TESM_"

logger = logging.getLogger(__name__)


def create_app(**overrides: Any) -> Flask:
    config = load_core_config(
        app_key=APP_KEY, app_name=APP_NAME, env_prefix=ENV_PREFIX
    )

    app = create_base_app(
        config=config,
        permissions=PERMISSIONS,
        nav=NAVIGATION,
        role_presets=ROLE_PRESETS,
        app_migrations={"tesm": TESM_MIGRATIONS},
        settings_specs=TESM_SETTINGS,
        import_name="tesm",
        app_short=APP_SHORT,
        app_edition=APP_EDITION,
        auth_tagline=AUTH_TAGLINE,
        app_version=__version__,
        enable_license=True,
    )

    _register_blueprints(app)
    _register_registry(app)
    _register_trash_providers(app)
    _startup_tasks(app)

    if overrides:
        app.config.update(overrides)
    return app


def _register_blueprints(app: Flask) -> None:
    from .blueprints import (
        backup,
        credentials,
        dashboard,
        devices,
        dhcp,
        fileshare,
        license as license_bp,
        logs,
        maintenance,
        settings,
        switches,
        hostkeys,
        terminal,
    )

    app.register_blueprint(dashboard.bp)
    app.register_blueprint(devices.bp)
    app.register_blueprint(switches.bp)
    app.register_blueprint(credentials.bp)
    app.register_blueprint(hostkeys.bp)
    app.register_blueprint(maintenance.bp)
    app.register_blueprint(dhcp.bp)
    app.register_blueprint(fileshare.bp)
    app.register_blueprint(logs.bp)  # tesmlogs: Kea + Neustarts
    app.register_blueprint(settings.bp)
    app.register_blueprint(license_bp.bp)
    app.register_blueprint(backup.bp)
    terminal.init_app(app)

    # Die Statusuebersicht ist die Startseite; ``index`` bleibt als Endpunkt
    # bestehen, weil der Kern (Weiterleitungen, Fehlerseiten) darauf verweist.
    app.add_url_rule("/", "index", dashboard.index_view)


def _register_registry(app: Flask) -> None:
    """Haengt app-spezifische Bausteine in den Kern ein."""
    from .services import inventory, monitor

    extension = core(app)
    extension.registry["reencrypt_secrets"] = inventory.reencrypt_secrets
    # Angaben fuer die nginx-Vorlage im Kern.
    extension.registry["upstream"] = extension.config.bind
    extension.registry["static_root"] = str(extension.config.base_dir / "static")
    extension.registry["websocket_path"] = "/ws/"
    extension.registry["public_dashboard_url"] = "/" 
    extension.registry["live_log"] = monitor.LiveLog(extension.config.log_dir / "live.log")
    extension.registry["status_pill_builder"] = _status_pills
    extension.login_hooks.append(_mount_shares_after_login)
    extension.logout_hooks.append(_release_shares_on_logout)


def _mount_shares_after_login(conn: Any, user: Any, secret: str, group_dns: tuple) -> None:
    """Bindet nach der Anmeldung die Freigaben dieses Benutzers ein.

    Ohne diesen Haken blieb der ganze Freigabe-Teil Theorie: Zuordnungen liessen
    sich anlegen, es entstand aber nie eine Einbindung, und weil der Menuepunkt
    an vorhandenen Einbindungen haengt, war die Seite nie zu sehen.
    """
    from flask import session

    from .services import fileshare

    extension = core()
    if extension.license is not None and not extension.license.module_active("fileshare"):
        return

    with extension.database.transaction(conn):
        messages = fileshare.mount_for_login(
            conn,
            user_id=user.id,
            session_id=getattr(session, "sid", ""),
            username=user.username,
            password=secret,
            group_dns=group_dns,
        )
    for message in messages:
        current_app.logger.info("Dateifreigabe fuer %s -- %s", user.username, message)


def _release_shares_on_logout(conn: Any, user: Any) -> None:
    """Haengt die Freigaben dieses Benutzers beim Abmelden wieder aus.

    Eingebunden wird mit dem Konto des Benutzers. Bliebe die Einbindung nach
    dem Abmelden bestehen, waere sein Zugriff weiter offen -- bis irgendwann
    die Zeitgrenze greift. Das ist zu lange.
    """
    from .services import fileshare

    extension = core()
    with extension.database.transaction(conn):
        released = fileshare.release_all_for_user(conn, user.id)
    if released:
        current_app.logger.info(
            "Dateifreigaben fuer %s geloest: %d", user.username, released
        )


def _status_pills(user: Any) -> list[dict[str, str]]:
    """Zusaetzliche Anzeigen in der Topbar."""
    if not user.is_authenticated:
        return []
    from tesm_core.extension import db

    from .services import kea, monitor

    extension = core()
    pills: list[dict[str, str]] = []

    counts = monitor.dashboard_counts(db())
    if counts["offline"]:
        pills.append(
            {
                "label": f"{counts['offline']} offline",
                "css_class": "status-pill--danger",
                "dot": "1",
                "dot_class": "status-pill__dot--pulse",
                "title": "Geraete, die beim letzten Durchlauf nicht geantwortet haben.",
            }
        )

    if user.has_any(("dhcp.view", "dhcp.edit")) and extension.license is not None:
        if extension.license.module_active("dhcp") and extension.sysops is not None:
            state = extension.sysops.service_state(kea.SERVICE_UNIT)
            if state["installed"] == "ja":
                active = state["active"] == "active"
                pills.append(
                    {
                        "label": "DHCP aktiv" if active else "DHCP gestoppt",
                        "css_class": "" if active else "status-pill--warning",
                        "dot": "1",
                        "dot_class": "",
                        "title": f"kea-dhcp4-server: {state['active']}, {state['enabled']}",
                    }
                )
    return pills


def _register_trash_providers(app: Flask) -> None:
    from .services import inventory

    with app.app_context():
        trash_views.register(
            trash_views.TrashProvider(
                key="devices",
                label="Clients",
                icon="cpu",
                list_fn=lambda conn: inventory.trash_entries(
                    conn, "devices", label_column="name", detail_columns=("mac", "ip")
                ),
                restore_fn=lambda conn, ident: inventory.restore_row(
                    conn, "devices", int(ident), unique_column="mac"
                ),
                purge_fn=lambda conn, ident: inventory.purge_row(conn, "devices", int(ident)),
                hint="Die MAC-Adresse bleibt reserviert, solange das Geraet hier liegt.",
            )
        )
        trash_views.register(
            trash_views.TrashProvider(
                key="switches",
                label="Switche",
                icon="switch",
                list_fn=lambda conn: inventory.trash_entries(
                    conn, "switches", label_column="hostname", detail_columns=("ip", "model")
                ),
                restore_fn=lambda conn, ident: inventory.restore_row(
                    conn, "switches", int(ident), unique_column="hostname"
                ),
                purge_fn=lambda conn, ident: inventory.purge_row(conn, "switches", int(ident)),
            )
        )
        trash_views.register(
            trash_views.TrashProvider(
                key="credentials",
                label="Zugangsdaten",
                icon="key",
                list_fn=lambda conn: inventory.trash_entries(
                    conn, "credentials", label_column="name", detail_columns=("username", "category")
                ),
                restore_fn=lambda conn, ident: inventory.restore_row(
                    conn, "credentials", int(ident), unique_column="name"
                ),
                purge_fn=lambda conn, ident: inventory.purge_row(conn, "credentials", int(ident)),
                hint="Endgueltiges Loeschen entfernt auch das gespeicherte Passwort.",
            )
        )


def _startup_tasks(app: Flask) -> None:
    """Einmalige Aufgaben beim Start des echten Webprozesses.

    Der Vorgaenger fuehrte solche Aufgaben bei **jedem** Modul-Import aus -- ein
    Hilfsskript, das ``app.py`` importierte, raeumte dadurch die
    Datei-Einbindungen fremder, aktiver Sitzungen ab. Darum laufen sie hier nur,
    wenn die systemd-Unit den Prozess ausdruecklich als Webprozess markiert hat.
    """
    extension = core(app)
    if not extension.config.web_process:
        logger.info(
            "Startaufgaben uebersprungen -- dieser Prozess ist nicht als Webprozess markiert."
        )
        return

    from .services import maintenance

    with extension.database.session() as conn, extension.database.transaction(conn):
        orphaned = maintenance.mark_orphaned_jobs(conn)
    if orphaned:
        logger.warning("%s unterbrochene Wartungsauftraege als unklar markiert", orphaned)

    if extension.license is not None:
        import socket

        extension.license.start_heartbeat(hostname=socket.gethostname())

    _start_fileshare_sweep(app)


#: Wie oft nach abgelaufenen Einbindungen gesehen wird.
FILESHARE_SWEEP_SECONDS = 300


def _start_fileshare_sweep(app: Flask) -> None:
    """Loest abgelaufene Freigabe-Einbindungen -- im Webprozess.

    Muss hier laufen und nicht im Ueberwachungsdienst: eingebunden wird im
    Mount-Namensraum dieses Dienstes, und ein anderer Dienst sieht diese
    Einbindungen nicht einmal. Ein ``umount`` von dort waere ein Aufruf ins
    Leere -- die Datenbankzeile waere als geloest markiert, die Freigabe aber
    weiter eingebunden.
    """
    import threading

    from .services import fileshare

    def schleife() -> None:
        while True:
            time.sleep(FILESHARE_SWEEP_SECONDS)
            try:
                with app.app_context():
                    geloest = fileshare.sweep_expired(core(app).database)
                if geloest:
                    logger.info("%s abgelaufene Freigabe-Einbindungen geloest", geloest)
            except Exception:  # noqa: BLE001 - der Webprozess darf nie sterben
                logger.exception("Aufraeumen der Dateifreigaben fehlgeschlagen")

    threading.Thread(target=schleife, name="fileshare-sweep", daemon=True).start()


# Fuer ``gunicorn tesm:app``
def _lazy_app() -> Flask:  # pragma: no cover - nur vom WSGI-Server benutzt
    return create_app()


app = None  # wird von wsgi.py gesetzt
