#!/usr/bin/env python3
"""Zeigt den Betriebszustand des Geraets — laufende Wurzel, Fassung, Selbstprueflauf.

Bewusst **nur Standardbibliothek**. Jede Abhaengigkeit im Grund-OS ist eine Zeile mehr in
der Stueckliste, ein Posten mehr im Schwachstellenprozess und damit eine Verpflichtung nach
dem Cyber Resilience Act. Eine Statusseite ist das nicht wert.

Es wird nichts gemerkt: Jede Auskunft entsteht beim Abruf aus der Wirklichkeit. Ein
gemerkter Zustand kann nach einem Ruecksprung falsch sein, ein gemessener nie.
"""

from __future__ import annotations

import json
import subprocess
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

PROTOKOLL = Path("/bestand/xinionos/protokoll/selbstpruefung.jsonl")
LETZTE = Path("/run/xinionos-selbstpruefung.json")
FASSUNG = Path("/etc/xinionos/fassung")
EINTRAEGE = Path("/boot/efi/loader/entries")
ANSCHLUSS = 8080
UNBEKANNT = "unbekannt"


def _wurzel() -> str:
    try:
        return subprocess.run(
            ["xinionos-wurzeln", "aktiv"], capture_output=True, text=True, timeout=5, check=True
        ).stdout.strip()
    except (OSError, subprocess.SubprocessError):
        return UNBEKANNT


def _startzaehler(wurzel: str) -> tuple[int | None, bool]:
    """Der Zaehler steht im Namen des Starteintrags: 'xinionos-a+2-1.conf'."""
    if not EINTRAEGE.is_dir() or wurzel == UNBEKANNT:
        return None, False
    for eintrag in EINTRAEGE.glob(f"xinionos-{wurzel}*.conf"):
        rest = eintrag.stem.removeprefix(f"xinionos-{wurzel}")
        if not rest:
            return None, True
        if rest.startswith("+"):
            offen = rest[1:].split("-", 1)[0]
            return (int(offen) if offen.isdigit() else None), False
    return None, False


def _lies(pfad: Path) -> str:
    try:
        return pfad.read_text(encoding="utf-8").strip()
    except OSError:
        return ""


def _verlauf(anzahl: int = 20) -> list[dict]:
    roh = _lies(PROTOKOLL)
    if not roh:
        return []
    saetze, puffer = [], ""
    for zeile in roh.splitlines(keepends=True):
        if zeile.startswith("{") and puffer:
            saetze.append(puffer)
            puffer = ""
        puffer += zeile
    if puffer:
        saetze.append(puffer)
    ergebnis = []
    for satz in reversed(saetze[-anzahl:]):
        try:
            ergebnis.append(json.loads(satz))
        except json.JSONDecodeError:
            continue
    return ergebnis


def zustand() -> dict:
    wurzel = _wurzel()
    offen, bestaetigt = _startzaehler(wurzel)
    try:
        pruefung = json.loads(_lies(LETZTE) or "{}")
    except json.JSONDecodeError:
        pruefung = {}
    return {
        "wurzel": {
            "aktiv": wurzel,
            "fassung": _lies(FASSUNG) or UNBEKANNT,
            "versuche_offen": offen,
            "bestaetigt": bestaetigt,
        },
        "selbstpruefung": pruefung,
    }


_STIL = (
    ":root{color-scheme:light dark}body{font-family:system-ui,sans-serif;max-width:60rem;"
    "margin:2rem auto;padding:0 1rem;line-height:1.5}table{border-collapse:collapse;width:100%;"
    "margin:1rem 0}th,td{text-align:left;padding:.4rem .6rem;"
    "border-bottom:1px solid rgba(128,128,128,.35)}th{width:16rem}"
    ".gut{color:#1a7f37;font-weight:600}.schlecht{color:#b42318;font-weight:600}"
    ".offen{color:#9a6700;font-weight:600}"
)


def _klasse(wert: str) -> str:
    return {"bestanden": "gut", "durchgefallen": "schlecht"}.get(wert, "offen")


def seite() -> str:
    from html import escape as e

    z = zustand()
    w = z["wurzel"]
    if w["bestaetigt"]:
        stand = '<span class="gut">bestätigt</span>'
    elif w["versuche_offen"] is not None:
        stand = f'<span class="offen">auf Probe — {w["versuche_offen"]} Startversuche offen</span>'
    else:
        stand = '<span class="offen">unbekannt</span>'
    ergebnis = str(z["selbstpruefung"].get("ergebnis", UNBEKANNT))
    schritte = "".join(
        f"<tr><th>{e(k)}</th><td class='{_klasse(str(v))}'>{e(str(v))}</td></tr>"
        for k, v in z["selbstpruefung"].get("schritte", {}).items()
    ) or "<tr><td>Kein Ergebnis vorhanden.</td></tr>"
    laeufe = "".join(
        f"<tr><td>{e(str(v.get('zeit','')))}</td><td>Wurzel {e(str(v.get('wurzel','?')))}</td>"
        f"<td>{e(str(v.get('fassung','?')))}</td>"
        f"<td class='{_klasse(str(v.get('ergebnis','')))}'>{e(str(v.get('ergebnis','')))}</td></tr>"
        for v in _verlauf()
    ) or "<tr><td colspan='4'>Noch kein Lauf aufgezeichnet.</td></tr>"
    return f"""<!doctype html><html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>XinionOS — Betriebszustand</title><style>{_STIL}</style></head><body>
<h1>Betriebszustand</h1>
<table>
<tr><th>Laufende Wurzel</th><td><strong>{e(w["aktiv"])}</strong></td></tr>
<tr><th>Fassung</th><td>{e(w["fassung"])}</td></tr>
<tr><th>Stand dieser Fassung</th><td>{stand}</td></tr>
<tr><th>Letzter Selbstprüflauf</th><td class="{_klasse(ergebnis)}">{e(ergebnis)}</td></tr>
</table>
<h2>Die Schritte des letzten Laufs</h2><table>{schritte}</table>
<h2>Verlauf</h2>
<p>Ein Rücksprung wird hier sichtbar: ein durchgefallener Lauf auf der einen Wurzel, danach ein
bestandener auf der anderen. Das Protokoll liegt im Bestandsbereich und überlebt den
Fassungswechsel.</p>
<table><tr><th>Zeitpunkt</th><th>Wurzel</th><th>Fassung</th><th>Ergebnis</th></tr>{laeufe}</table>
</body></html>"""


class Griff(BaseHTTPRequestHandler):
    server_version = "XinionOS"

    def do_GET(self) -> None:  # noqa: N802
        if self.path.startswith("/zustand"):
            leib, art = json.dumps(zustand(), ensure_ascii=False).encode(), "application/json"
        elif self.path in ("/", "/index.html"):
            leib, art = seite().encode(), "text/html; charset=utf-8"
        else:
            self.send_error(404)
            return
        self.send_response(200)
        self.send_header("Content-Type", art)
        self.send_header("Content-Length", str(len(leib)))
        self.end_headers()
        self.wfile.write(leib)

    def log_message(self, *_: object) -> None:
        pass  # das Journal fuehrt systemd


if __name__ == "__main__":
    ThreadingHTTPServer(("0.0.0.0", ANSCHLUSS), Griff).serve_forever()
