"""Darstellungshelfer: Icons, Formatierung, Template-Globals.

Die Icons sind Pfadfragmente eines 24x24-Rasters mit ``stroke="currentColor"``
-- kein Icon-Font, kein externes Sprite, keine zusaetzliche HTTP-Anfrage und
nichts, was die CSP aufweichen muesste.
"""

from __future__ import annotations

import math
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from flask import Flask, url_for
from markupsafe import Markup

ICONS: dict[str, str] = {
    "grid": '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>',
    "cpu": '<rect x="5" y="5" width="14" height="14" rx="2"/><rect x="9" y="9" width="6" height="6" rx="1"/><path d="M9 2v3M15 2v3M9 19v3M15 19v3M2 9h3M2 15h3M19 9h3M19 15h3"/>',
    "switch": '<rect x="2" y="7" width="20" height="10" rx="2"/><path d="M6 11v2M10 11v2M14 11v2M18 11v2"/>',
    "network": '<rect x="9" y="2" width="6" height="6" rx="1.5"/><rect x="2" y="16" width="6" height="6" rx="1.5"/><rect x="16" y="16" width="6" height="6" rx="1.5"/><path d="M12 8v4M5 16v-2h14v2"/>',
    "key": '<circle cx="8" cy="15" r="4"/><path d="m10.8 12.2 8.2-8.2M17 5l3 3M14 8l3 3"/>',
    "wrench": '<path d="M14.5 3.5a5 5 0 0 0-6.9 6.1L3 14.2V21h6.8l4.6-4.6a5 5 0 0 0 6.1-6.9l-3 3-3.9-.9-.9-3.9 3-3z"/>',
    "users": '<path d="M16 20v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 20v-2a4 4 0 0 0-3-3.9M16 3.1a4 4 0 0 1 0 7.8"/>',
    "shield-users": '<path d="M12 2 4 5v6c0 5 3.4 9.3 8 11 4.6-1.7 8-6 8-11V5l-8-3z"/><circle cx="12" cy="10" r="2.2"/><path d="M8.5 16c.7-1.6 2-2.4 3.5-2.4s2.8.8 3.5 2.4"/>',
    "folder": '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/>',
    "sliders": '<path d="M4 6h10M18 6h2M4 12h4M12 12h8M4 18h12M20 18h0"/><circle cx="16" cy="6" r="2"/><circle cx="10" cy="12" r="2"/><circle cx="18" cy="18" r="2"/>',
    "server": '<rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><path d="M7 7.5h.01M7 16.5h.01"/>',
    "trash": '<path d="M4 7h16M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2M6 7l1 13a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1l1-13M10 11v6M14 11v6"/>',
    "terminal": '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="m7 9 3 3-3 3M13 15h4"/>',
    "clock": '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
    "history": '<path d="M3 12a9 9 0 1 0 3-6.7M3 4v4h4"/><path d="M12 8v4l3 2"/>',
    "transfer": '<path d="M4 8h13l-3-3M20 16H7l3 3"/>',
    "shield": '<path d="M12 2 4 5v6c0 5 3.4 9.3 8 11 4.6-1.7 8-6 8-11V5l-8-3z"/>',
    "search": '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>',
    "menu": '<path d="M4 6h16M4 12h16M4 18h16"/>',
    "chevron-right": '<path d="m9 6 6 6-6 6"/>',
    "chevron-down": '<path d="m6 9 6 6 6-6"/>',
    "sun": '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5 19 19M19 5l-1.5 1.5M6.5 17.5 5 19"/>',
    "moon": '<path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a7 7 0 0 0 10.5 10.5z"/>',
    "monitor": '<rect x="2" y="4" width="20" height="13" rx="2"/><path d="M8 21h8M12 17v4"/>',
    "log-out": '<path d="M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3M10 8l-4 4 4 4M6 12h10"/>',
    "settings": '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.9.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.3-1.9l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.9-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/>',
    "plus": '<path d="M12 5v14M5 12h14"/>',
    "pencil": '<path d="M4 20h4l10.5-10.5a2.1 2.1 0 0 0-3-3L5 17v3z"/>',
    "x": '<path d="M6 6l12 12M18 6 6 18"/>',
    "check": '<path d="m5 13 4 4L19 7"/>',
    "alert": '<path d="M12 3 2 20h20L12 3z"/><path d="M12 9v5M12 17h.01"/>',
    "info": '<circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/>',
    "download": '<path d="M12 3v12M7 11l5 5 5-5M4 21h16"/>',
    "upload": '<path d="M12 21V9M7 13l5-5 5 5M4 4h16"/>',
    "refresh": '<path d="M20 12a8 8 0 1 1-2.3-5.7M20 4v4h-4"/>',
    "play": '<path d="M7 4l12 8-12 8V4z"/>',
    "power": '<path d="M12 3v9"/><path d="M18.4 6.6a9 9 0 1 1-12.8 0"/>',
    "lock": '<rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/>',
    "unlock": '<rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 0 1 7.5-2"/>',
    "mail": '<rect x="2" y="5" width="20" height="14" rx="2"/><path d="m3 7 9 6 9-6"/>',
    "building": '<rect x="4" y="3" width="16" height="18" rx="2"/><path d="M9 7h2M13 7h2M9 11h2M13 11h2M9 15h2M13 15h2M10 21v-3h4v3"/>',
    "ticket": '<path d="M3 9a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2 2 2 0 0 0 0 6 2 2 0 0 1-2 2H5a2 2 0 0 1-2-2 2 2 0 0 0 0-6z"/><path d="M9 7v10"/>',
    "file-text": '<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5z"/><path d="M14 3v5h5M9 13h6M9 17h4"/>',
    "activity": '<path d="M3 12h4l3-8 4 16 3-8h4"/>',
    "database": '<ellipse cx="12" cy="6" rx="8" ry="3"/><path d="M4 6v12c0 1.7 3.6 3 8 3s8-1.3 8-3V6M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3"/>',
    "eye": '<path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/>',
    "copy": '<rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h8"/>',
    "external": '<path d="M14 4h6v6M20 4 11 13M18 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4"/>',
    "filter": '<path d="M3 5h18l-7 8v6l-4-2v-4L3 5z"/>',
    "calendar": '<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/>',
    "link": '<path d="M10 13a5 5 0 0 0 7 0l2-2a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-2 2a5 5 0 0 0 7 7l1-1"/>',
    "puzzle": '<path d="M9 3h2a2 2 0 0 1 0 4h3v3a2 2 0 0 1 4 0v3h-3v3a2 2 0 0 1-4 0H8v-3H5a2 2 0 0 1 0-4h3V3z"/>',
    "credit-card": '<rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20M6 15h4"/>',
    "map-pin": '<path d="M12 21s7-6.3 7-11a7 7 0 1 0-14 0c0 4.7 7 11 7 11z"/><circle cx="12" cy="10" r="2.5"/>',
    "share": '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 13.5 6.8 4M15.4 6.5l-6.8 4"/>',
    "hard-drive": '<rect x="2" y="13" width="20" height="8" rx="2"/><path d="M5 13 8 4h8l3 9M6 17h.01M10 17h.01"/>',
}


def icon(name: str, *, size: int = 18, extra_class: str = "") -> Markup:
    """Rendert ein Icon als Inline-SVG."""
    path = ICONS.get(name) or ICONS["info"]
    classes = f"icon icon--{name}" + (f" {extra_class}" if extra_class else "")
    return Markup(
        f'<svg class="{classes}" width="{size}" height="{size}" viewBox="0 0 24 24" '
        'fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" '
        f'stroke-linejoin="round" aria-hidden="true">{path}</svg>'
    )


# --------------------------------------------------------------------------- #
# Formatierung
# --------------------------------------------------------------------------- #


def format_bytes(value: Any) -> str:
    try:
        size = float(value)
    except (TypeError, ValueError):
        return "-"
    if size < 1024:
        return f"{int(size)} B"
    units = ("KB", "MB", "GB", "TB")
    index = min(int(math.log(size, 1024)) - 1, len(units) - 1)
    return f"{size / (1024 ** (index + 1)):.1f} {units[index]}".replace(".", ",")


def format_datetime(value: Any, *, with_time: bool = True) -> str:
    if not value:
        return "-"
    if isinstance(value, datetime):
        moment = value
    else:
        text = str(value).strip()
        if text.endswith("Z"):
            text = text[:-1] + "+00:00"
        try:
            moment = datetime.fromisoformat(text)
        except ValueError:
            return str(value)
    if moment.tzinfo is None:
        moment = moment.replace(tzinfo=timezone.utc)
    local = moment.astimezone()
    return local.strftime("%d.%m.%Y %H:%M") if with_time else local.strftime("%d.%m.%Y")


def format_date(value: Any) -> str:
    return format_datetime(value, with_time=False)


def format_duration(seconds: Any) -> str:
    try:
        total = int(seconds)
    except (TypeError, ValueError):
        return "-"
    if total < 60:
        return f"{total} s"
    minutes, secs = divmod(total, 60)
    if minutes < 60:
        return f"{minutes} min {secs} s" if secs else f"{minutes} min"
    hours, minutes = divmod(minutes, 60)
    if hours < 24:
        return f"{hours} h {minutes} min" if minutes else f"{hours} h"
    days, hours = divmod(hours, 24)
    return f"{days} d {hours} h" if hours else f"{days} d"


def format_number(value: Any) -> str:
    try:
        return f"{int(value):n}".replace(",", ".")
    except (TypeError, ValueError):
        return str(value)


def yes_no(value: Any) -> str:
    return "ja" if value else "nein"


def from_json(value: Any) -> Any:
    """Parst einen JSON-String aus der Datenbank -- fuer Modul-/Rechtelisten.

    Wirft nie: kaputte Daten fuehren zu einer leeren Liste, nicht zu einer
    Fehlerseite.
    """
    if value in (None, ""):
        return []
    if isinstance(value, (list, dict)):
        return value
    import json as _json

    try:
        return _json.loads(str(value))
    except ValueError:
        return []


# --------------------------------------------------------------------------- #
# Registrierung
# --------------------------------------------------------------------------- #


def asset_url(app: Flask) -> Any:
    """``asset_url('css/base.css')`` mit Cache-Buster aus der Dateizeit."""
    cache: dict[str, str] = {}

    def build(filename: str, blueprint: str = "tesm_core") -> str:
        if filename in cache:
            return cache[filename]
        endpoint = f"{blueprint}.static" if blueprint else "static"
        try:
            url = url_for(endpoint, filename=filename)
        except Exception:  # noqa: BLE001 - Blueprint ohne static-Ordner
            url = url_for("static", filename=filename)
        version = ""
        try:
            static_folder = app.blueprints[blueprint].static_folder if blueprint else app.static_folder
            if static_folder:
                path = Path(static_folder) / filename
                if path.is_file():
                    version = str(int(path.stat().st_mtime))
        except (KeyError, OSError):
            version = ""
        result = f"{url}?v={version}" if version else url
        if not app.debug:
            cache[filename] = result
        return result

    return build


def init_app(app: Flask) -> None:
    app.jinja_env.globals.update(
        icon=icon,
        asset_url=asset_url(app),
        # Kleine Python-Builtins, die in Templates immer wieder gebraucht werden.
        # Jinja bringt sie nicht mit; ohne sie muesste jedes Template den Umweg
        # ueber eigene Filter gehen.
        bool=bool,
        int=int,
        str=str,
        len=len,
        min=min,
        max=max,
        sorted=sorted,
        dict=dict,
        list=list,
        set=set,
        enumerate=enumerate,
    )
    app.jinja_env.filters.update(
        bytes=format_bytes,
        dt=format_datetime,
        date=format_date,
        duration=format_duration,
        number=format_number,
        yes_no=yes_no,
        from_json=from_json,
    )
    app.jinja_env.trim_blocks = True
    app.jinja_env.lstrip_blocks = True
