"""Anwendungsfabrik: verdrahtet alle Bausteine zu einer lauffaehigen Flask-App.

Jede der beiden Anwendungen ruft ``create_base_app()`` und ergaenzt danach nur
noch ihre eigenen Blueprints. Alles, was beide teilen -- Datenbank,
Migrationen, Sitzungen, CSRF, Header, Protokollierung, Rechtesystem,
Navigation, Benutzer-/Gruppenverwaltung, Kontoseite, Diagnose -- entsteht hier
genau einmal.
"""

from __future__ import annotations

import secrets
from datetime import datetime, timezone
from typing import Any, Sequence

from flask import Blueprint, Flask, g, render_template, request
from werkzeug.exceptions import HTTPException

from . import audit, logging_setup, ui
from .config import CoreConfig
from .core_schema import CORE_MIGRATIONS
from .db import Database
from .extension import TesmCore, close_db, core, db
from .keystore import Keystore, load_or_create_secret_key
from .licensing_client import LicenseManager
from .migrations import Migration
from .nav import NavTree
from .rbac import service as rbac
from .rbac.model import PermissionTree, RolePreset
from .security import csrf
from .security import sessions as session_store
from .security.sessions import SqliteSessionInterface
from .settings_store import SettingSpec, SettingsStore
from .sysops import SysOps
from .auth import directory
from .auth import routes as auth_routes
from .auth import service as auth_service
from .auth.models import ANONYMOUS, current_user
from .views import account as account_views
from .views import admin as admin_views
from .views import diagnostics as diagnostics_views
from .views import hostsettings as hostsettings_views
from .views import logs as logs_views
from .views import trash as trash_views

CORE_SETTINGS: tuple[SettingSpec, ...] = (
    SettingSpec("nav_order", [], "json", "Reihenfolge der Navigation", section="intern"),
    SettingSpec("nav_child_order", {}, "json", "Reihenfolge der Unterpunkte", section="intern"),
    SettingSpec(
        "trash_retention_days",
        30,
        "int",
        "Aufbewahrung im Papierkorb (Tage)",
        hint="Nach dieser Frist werden Eintraege beim naechsten Aufruf endgueltig entfernt.",
        section="system",
        minimum=1,
        maximum=3650,
    ),
    SettingSpec("timezone", "", "str", "Zeitzone des Hosts", section="system"),
    SettingSpec("hostname", "", "str", "Hostname", section="system"),
    SettingSpec(
        "log_rotate_interval",
        "weekly",
        "str",
        "Rotationsintervall der Protokolle",
        section="system",
        choices=(("daily", "taeglich"), ("weekly", "woechentlich"), ("monthly", "monatlich")),
    ),
    SettingSpec(
        "log_rotate_keep", 4, "int", "Aufbewahrte Rotationen", section="system", minimum=1, maximum=52
    ),
    # -- Webserver/TLS (Host-Ebene, in beiden Anwendungen gleich) ----------- #
    # Lizenzserver: eigenes Zertifikat statt abgeschalteter Pruefung.
    SettingSpec(
        "license_server_ca",
        "",
        "str",
        "Zertifikat des Lizenzservers (PEM)",
        section="license",
    ),
    SettingSpec("web_domain", "", "str", "Domain", section="web"),
    SettingSpec("web_https_enabled", False, "bool", "HTTPS aktiv", section="web"),
    SettingSpec("web_hsts_enabled", False, "bool", "HSTS senden", section="web"),
    SettingSpec("web_redirect_http", True, "bool", "HTTP auf HTTPS umleiten", section="web"),
    SettingSpec("web_cert_path", "", "str", "Zertifikat (Pfad)", section="web"),
    SettingSpec("web_key_path", "", "str", "Privater Schluessel (Pfad)", section="web"),
    SettingSpec("web_acme_email", "", "str", "E-Mail fuer Let's Encrypt", section="web"),
    SettingSpec("web_acme_webroot", "", "str", "ACME-Webroot", section="web"),
    # Abweichende Ports braucht nur eine Testinstanz neben der Produktion --
    # im Regelbetrieb bleibt es bei 80 und 443.
    SettingSpec(
        "web_http_port", 80, "int", "HTTP-Port", section="web", minimum=1, maximum=65535
    ),
    SettingSpec(
        "web_https_port", 443, "int", "HTTPS-Port", section="web", minimum=1, maximum=65535
    ),
    SettingSpec(
        "web_max_body_mb", 128, "int", "Maximale Anfragegroesse (MB)", section="web",
        minimum=1, maximum=8192,
    ),
    *directory.SETTINGS,
)


def create_base_app(
    *,
    config: CoreConfig,
    permissions: PermissionTree,
    nav: NavTree,
    role_presets: Sequence[RolePreset],
    app_migrations: dict[str, Sequence[Migration]] | None = None,
    settings_specs: Sequence[SettingSpec] = (),
    import_name: str = "tesm_core",
    app_short: str = "TESM",
    app_edition: str = "",
    #: Zeile unter der Wortmarke auf den Anmeldeseiten. Leer = kein
    #: Seitenhintergrund. Der Lizenzserver setzt sie bewusst nicht: die
    #: Funktionsliste von TESM waere dort schlicht falsch.
    auth_tagline: str = "",
    app_version: str = "2.0.0",
    built_in_trusted_keys: tuple[str, ...] = (),
    enable_license: bool = True,
    template_folder: str | None = None,
    static_folder: str | None = None,
) -> Flask:
    app = Flask(
        import_name,
        template_folder=template_folder or "templates",
        static_folder=static_folder or "static",
        static_url_path="/assets",
    )

    # Templates und Design-System des Kerns liegen im Paket ``tesm_core`` und
    # werden ueber einen eigenen Blueprint bereitgestellt. Dadurch kann jede
    # Anwendung zusaetzlich ihre eigenen templates/static mitbringen, ohne dass
    # sich Pfade in die Quere kommen.
    core_bp = Blueprint(
        "tesm_core",
        "tesm_core",
        template_folder="templates",
        static_folder="static/tesm_core",
        static_url_path="/core-assets",
    )
    app.register_blueprint(core_bp)

    config.data_dir.mkdir(parents=True, exist_ok=True)
    config.log_dir.mkdir(parents=True, exist_ok=True)

    app.config.update(
        SECRET_KEY=load_or_create_secret_key(config.secret_key_file),
        # site_key statt app_key: Cookies unterscheiden keine Ports. Zwei
        # Instanzen derselben Anwendung auf einem Host wuerden sich sonst
        # gegenseitig die Sitzung ueberschreiben.
        SESSION_COOKIE_NAME=f"{config.site_key.replace('-', '_')}_session",
        SESSION_COOKIE_HTTPONLY=True,
        SESSION_COOKIE_SECURE=config.security.cookie_secure,
        SESSION_COOKIE_SAMESITE=config.security.cookie_samesite,
        MAX_CONTENT_LENGTH=config.security.max_content_length,
        JSON_SORT_KEYS=False,
        TRAP_HTTP_EXCEPTIONS=False,
        PREFERRED_URL_SCHEME="https" if config.security.cookie_secure else "http",
        TEMPLATES_AUTO_RELOAD=config.debug,
        EXPLAIN_TEMPLATE_LOADING=False,
    )
    app.debug = config.debug

    logging_setup.init_app(app, log_dir=config.log_dir, debug=config.debug)

    database = Database(config.db_path)
    settings = SettingsStore([*CORE_SETTINGS, *settings_specs])
    keystore = Keystore(config.data_key_file)

    extension = TesmCore(
        config=config,
        database=database,
        keystore=keystore,
        permissions=permissions,
        nav=nav,
        settings=settings,
        role_presets=tuple(role_presets),
        migrations={"core": CORE_MIGRATIONS, **(app_migrations or {})},
        sysops=SysOps(app_key=config.app_key, site_key=config.site_key),
    )
    if enable_license:
        extension.license = LicenseManager(
            database=database,
            settings=settings,
            license_path=config.license_path,
            key_path=config.license_key_path,
            app_key=config.app_key,
            built_in_trusted_keys=built_in_trusted_keys,
        )
    app.extensions["tesm"] = extension

    app.session_interface = SqliteSessionInterface(
        database,
        cookie_name=app.config["SESSION_COOKIE_NAME"],
        idle_minutes=config.security.session_idle_minutes,
        absolute_hours=config.security.session_absolute_hours,
    )

    # -- Schema und Startzustand ------------------------------------------ #
    applied = extension.run_migrations()
    with database.session() as conn, database.transaction(conn):
        created = rbac.ensure_presets(conn, permissions, extension.role_presets)
        if applied or created:
            audit.log(
                conn,
                actor="system",
                action="system.startup_migration",
                detail="; ".join([*applied, *(f"Gruppe {name} angelegt" for name in created)]),
                severity="notice",
            )
        session_store.prune(conn)
    if extension.license is not None:
        with database.session() as conn:
            extension.license.reload(conn)

    # -- Request-Lebenszyklus --------------------------------------------- #
    csrf.init_app(app)
    ui.init_app(app)
    app.teardown_appcontext(close_db)

    @app.before_request
    def _prepare_request() -> None:
        g.csp_nonce = secrets.token_urlsafe(16)
        auth_service.load_request_user()

    @app.after_request
    def _finish_request(response):  # type: ignore[no-untyped-def]
        hsts = ""
        if config.security.hsts_enabled:
            hsts = f"max-age={config.security.hsts_max_age}; includeSubDomains"
        return csrf.apply_security_headers(
            response, nonce=getattr(g, "csp_nonce", ""), hsts=hsts
        )

    # -- Template-Kontext -------------------------------------------------- #
    from .rbac.model import ACTION_META

    @app.context_processor
    def _inject_context() -> dict[str, Any]:
        user = getattr(g, "user", ANONYMOUS)
        conn = db()
        order = settings.get(conn, "nav_order") or nav.default_order()
        child_order = settings.get(conn, "nav_child_order") or {}
        pills: list[dict[str, str]] = []
        if extension.license is not None and user.is_authenticated:
            pill = extension.license.topbar_pill()
            if pill:
                pills.append(pill)
        pills.extend(extension.registry.get("status_pill_builder", lambda _u: [])(user))

        nav_items = nav.resolve(user, order=order, child_order=child_order)
        palette = [
            {**entry, "url": _safe_url(entry["endpoint"])}
            for entry in nav.searchable(user)
            if entry["endpoint"]
        ]
        return {
            "app_name": config.app_name,
            "app_short": app_short,
            "app_edition": app_edition,
            "auth_tagline": auth_tagline,
            "app_version": app_version,
            "current_year": datetime.now(timezone.utc).year,
            "current_user": current_user,
            "nav_items": nav_items,
            "palette_entries": palette,
            "status_pills": pills,
            "csp_nonce": getattr(g, "csp_nonce", ""),
            "permission_tree": permissions,
            "action_meta": ACTION_META,
            "license_state": extension.license.state if extension.license else None,
            "license_active": extension.license.active() if extension.license else True,
            "module_active": (
                extension.license.module_active if extension.license else (lambda _n: True)
            ),
            "feature_allowed": (
                extension.license.feature_allowed if extension.license else (lambda _n: True)
            ),
            "migration_warning": bool(extension.registry.get("migration_warning")),
        }

    # -- Fehlerseiten ------------------------------------------------------ #
    @app.errorhandler(HTTPException)
    def _http_error(error: HTTPException):  # type: ignore[no-untyped-def]
        if _wants_json():
            return {
                "error": error.name,
                "message": error.description,
                "request_id": getattr(g, "request_id", ""),
            }, error.code or 500
        return (
            render_template(
                "tesm_core/error.html",
                code=error.code or 500,
                name=error.name,
                description=error.description,
                request_id=getattr(g, "request_id", ""),
                back_url=request.referrer or "/",
            ),
            error.code or 500,
        )

    @app.errorhandler(Exception)
    def _unhandled(error: Exception):  # type: ignore[no-untyped-def]
        app.logger.exception("Unbehandelter Fehler: %s", error)
        if _wants_json():
            return {
                "error": "Interner Fehler",
                "message": "Der Vorgang konnte nicht abgeschlossen werden.",
                "request_id": getattr(g, "request_id", ""),
            }, 500
        return (
            render_template(
                "tesm_core/error.html",
                code=500,
                name="Interner Fehler",
                description=(
                    "Der Vorgang konnte nicht abgeschlossen werden. Der Fehler wurde "
                    "protokolliert."
                ),
                request_id=getattr(g, "request_id", ""),
                back_url=request.referrer or "/",
            ),
            500,
        )

    # -- Gemeinsame Blueprints -------------------------------------------- #
    app.register_blueprint(auth_routes.bp)
    app.register_blueprint(admin_views.bp)
    app.register_blueprint(account_views.bp)
    app.register_blueprint(diagnostics_views.bp)
    app.register_blueprint(trash_views.bp)
    app.register_blueprint(hostsettings_views.bp)
    app.register_blueprint(logs_views.bp)
    with app.app_context():
        trash_views.register_core_providers()

    app.add_url_rule("/gesundheit", "health", _health, methods=["GET"])

    return app


def _safe_url(endpoint: str) -> str:
    from flask import url_for

    try:
        return url_for(endpoint)
    except Exception:  # noqa: BLE001 - Endpunkt existiert in dieser App nicht
        return ""


def _wants_json() -> bool:
    if request.path.startswith("/api/"):
        return True
    accept = request.accept_mimetypes
    return bool(accept.accept_json and not accept.accept_html)


def _health() -> tuple[dict[str, Any], int]:
    """Gesundheitsendpunkt fuer install.sh und Monitoring.

    Bewusst ohne Anmeldung, aber ohne jede verwertbare Information: nur
    "laeuft die App und antwortet die Datenbank".
    """
    extension = core()
    try:
        with extension.database.session() as conn:
            conn.execute("SELECT 1").fetchone()
    except Exception:  # noqa: BLE001
        return {"status": "degraded"}, 503
    return {"status": "ok", "version": extension.config.app_key}, 200
