"""Baut ``docs/updatebericht.md`` — den Bericht zur jeweils aktuellen Fassung.

Warum es diesen Bericht gibt
----------------------------
``CHANGELOG.md`` fuehrt die ganze Geschichte, und der Etikettentext im Git
fasst eine Fassung fuer Anwender zusammen. Beides erreicht aber niemanden, der
die Anwendung nur installiert hat: Der Changelog liegt im Quelltextarchiv, das
Etikett im Git. Dieser Bericht ist das Blatt, das man jemandem zusammen mit der
Installationsdatei in die Hand gibt.

Warum er erzeugt und nicht geschrieben wird
-------------------------------------------
Weil er sonst driftet. Genau das ist in diesem Projekt zweimal passiert - die
Sperrdatei stand zwei Fassungen lang still, und die Anzahl der Paketpruefungen
wanderte durch neun Dokumentstellen. Der Mittelteil dieses Berichts stammt
deshalb **Wort fuer Wort** aus dem Abschnitt der Fassung in ``CHANGELOG.md``;
die Fassungsnummer kommt aus ``app/package.json``. Von Hand geschrieben ist nur
der Rahmen, und der aendert sich zwischen Fassungen nicht.

``app/tests/dokumentation.test.ts`` haelt fest, dass der Bericht zur Fassung
passt und den Changelog-Abschnitt unveraendert enthaelt.

Aufruf
------
    python tools/updatebericht.py
    python tools/updatebericht.py --pruefen    # meldet nur, ob er aktuell ist
"""

from __future__ import annotations

import io
import json
import re
import sys
from pathlib import Path

WURZEL = Path(__file__).resolve().parent.parent
PAKET = WURZEL / 'app' / 'package.json'
CHANGELOG = WURZEL / 'CHANGELOG.md'
ZIEL = WURZEL / 'docs' / 'updatebericht.md'

#: Ueberschrift eines Fassungsabschnitts: "## [0.23.0] — 2026-08-29".
#: Der Gedankenstrich ist ein Halbgeviertstrich, kein Bindestrich.
FASSUNGSKOPF = re.compile(r'^## \[(?P<nummer>[^\]]+)\](?:\s*—\s*(?P<datum>\S+))?\s*$')


def fassung() -> str:
    return json.loads(PAKET.read_text(encoding='utf-8'))['version']


def abschnitt(nummer: str) -> tuple[str, list[str]]:
    """Der Changelog-Abschnitt einer Fassung: Datum und Zeilen ohne Ueberschrift."""
    zeilen = CHANGELOG.read_text(encoding='utf-8').split('\n')

    start = None
    datum = ''
    for i, zeile in enumerate(zeilen):
        treffer = FASSUNGSKOPF.match(zeile)
        if treffer is not None and treffer.group('nummer') == nummer:
            start = i
            datum = treffer.group('datum') or ''
            break
    if start is None:
        raise SystemExit(f'ABBRUCH: CHANGELOG.md hat keinen Abschnitt „## [{nummer}]“.')

    ende = len(zeilen)
    for i in range(start + 1, len(zeilen)):
        # Hinter der aeltesten Fassung steht ein HTML-Kommentar, der zu keiner
        # gehoert. Ohne diese Bedingung liefe der letzte Abschnitt bis zum
        # Dateiende und zoege ihn mit hinein. Heute trifft das nichts - erzeugt
        # wird immer die Fassung aus der package.json, und die ist nie die
        # aelteste. Die Bedingung steht hier, damit das so bleibt.
        if FASSUNGSKOPF.match(zeilen[i]) or zeilen[i].startswith('<!--'):
            ende = i
            break

    inhalt = zeilen[start + 1 : ende]
    while inhalt and not inhalt[0].strip():
        inhalt.pop(0)
    while inhalt and not inhalt[-1].strip():
        inhalt.pop()
    # Die Trennlinie vor der naechsten Fassung gehoert nicht zum Abschnitt.
    while inhalt and inhalt[-1].strip() == '---':
        inhalt.pop()
        while inhalt and not inhalt[-1].strip():
            inhalt.pop()

    return datum, [verweise_umhaengen(zeile) for zeile in inhalt]


#: Verweise im Changelog gelten vom Wurzelverzeichnis aus, der Bericht liegt in
#: ``docs/``. Ohne Umhaengen zeigte ``docs/stand.md`` von dort auf
#: ``docs/docs/stand.md`` - ein toter Verweis in einem erzeugten Dokument, den
#: niemand bemerkt, weil ihn niemand von Hand geschrieben hat.
VERWEIS = re.compile(r'\]\((?P<ziel>[^)]+)\)')


def verweise_umhaengen(zeile: str) -> str:
    """Rechnet relative Verweise vom Wurzelverzeichnis nach ``docs/`` um."""

    def ersetzen(treffer: re.Match[str]) -> str:
        ziel = treffer.group('ziel')
        # Adressen und reine Sprungmarken bleiben, wie sie sind.
        if ziel.startswith(('http://', 'https://', 'mailto:', '#')):
            return treffer.group(0)
        if ziel.startswith('docs/'):
            neues_ziel = ziel[len('docs/') :]
        else:
            # Alles Uebrige liegt im Wurzelverzeichnis, also eine Ebene hoeher.
            neues_ziel = '../' + ziel
        return '](' + neues_ziel + ')'

    return VERWEIS.sub(ersetzen, zeile)


RAHMEN_KOPF = """# Updatebericht zur Fassung {nummer}

**Stand {datum}.** Was sich geändert hat, wie Sie aktualisieren und was diese
Fassung ausdrücklich nicht leistet.

> Dieser Bericht ist **erzeugt, nicht von Hand geschrieben**. Der Abschnitt
> „Was sich geändert hat“ stammt Wort für Wort aus
> [CHANGELOG.md](../CHANGELOG.md), die Fassungsnummer aus `app/package.json`.
> Neu erzeugen mit `python tools/updatebericht.py`. Die vollständige
> Geschichte aller Fassungen steht im Änderungsverlauf; hier steht eine.

---

## Was sich geändert hat

"""

RAHMEN_FUSS = """
---

## Wie Sie aktualisieren

Es gibt **keine automatische Aktualisierung**. Die Anwendung sucht von sich aus
nie nach einer neuen Fassung — sie stellt überhaupt keine Verbindung her.

1. Die Datei `Waffensachkunde Lernsoftware-{nummer}-Setup-x64.exe` starten.
2. Windows meldet sich, weil die Datei nicht signiert ist: „Weitere
   Informationen“ → „Trotzdem ausführen“. Das kommt bei **jeder** neuen
   Fassung wieder, auch wenn Sie den Hinweis schon einmal weggeklickt haben —
   jede Setup-Datei ist für das Betriebssystem eine neue, unbekannte Datei.
   Warum das so ist, steht in [installation.md](installation.md).
3. Der Assistent installiert über die vorhandene Fassung. Eine vorherige
   Deinstallation ist nicht nötig.

**Ihr Lernstand bleibt.** Er liegt nicht im Programmverzeichnis, sondern unter
`%APPDATA%\\Waffensachkunde Lernsoftware`, und das Installationsprogramm fasst
dieses Verzeichnis nicht an — auch beim Deinstallieren nicht
(`deleteAppDataOnUninstall: false`). Bringt eine Fassung eine neue Form der
Datenbank mit, wird der vorhandene Lernstand beim ersten Öffnen **nur
ergänzt**: Es kommen Tabellen und Spalten dazu, nichts wird geändert oder
gelöscht. Geprüft ist das für jeden Schritt von der ersten Form bis zur
heutigen.

**Der Weg zurück ist versperrt.** Sobald eine neuere Fassung Ihren Lernstand
einmal angefasst hat, öffnet ihn eine ältere nicht mehr; sie meldet
stattdessen „Der Lernstand wurde mit einer neueren Programmversion angelegt“.
Das ist Absicht: Eine ältere Fassung kennt die neuen Felder nicht, und was sie
schriebe, wäre unvollständig, ohne dass es jemand bemerkte. Dieselbe Sperre
gilt für Sicherungen — eine Sicherung aus einer neueren Fassung lässt sich in
eine ältere nicht einspielen.

**Deshalb vor dem Aktualisieren sichern**, wenn Sie sich die Rückkehr
offenhalten wollen: Startbildschirm → „Lernstand sichern und übertragen“ →
„Sicherung speichern …“. Diese Sicherung stammt noch aus der alten Fassung und
lässt sich dort auch wieder einspielen.

## Was diese Fassung nicht leistet

- **Sie ist nicht signiert.** Windows SmartScreen meldet sich beim ersten
  Start. Das sagt nichts darüber, ob die Anwendung schädlich ist — nur, dass
  das Betriebssystem den Herausgeber nicht kennt.
- **Es gibt keine Fassung für macOS.** Der Bau ist vorbereitet, aber kein Paket
  ist je entstanden, und nichts an dieser Software lief je auf einem Mac.
- **Barrierefreiheit ist gebaut, aber nicht nachgewiesen.** Was automatisch
  prüfbar ist, wird bei jedem Lauf geprüft; die Szenarien am echten
  Screenreader stehen aus. Einzelheiten in
  [stand.md](stand.md), Abschnitt 5.
- **Über das Bestehen der Prüfung entscheidet allein der zuständige
  Prüfungsausschuss.** Diese Anwendung übt den schriftlichen Teil; den
  praktischen Teil nach § 2 Abs. 3 AWaffV bildet sie nicht ab.

## Woran Sie die Angaben nachprüfen können

- Die installierte Fassung steht in der Anwendung: Startbildschirm →
  **Systemzustand** → „Programmversion“. Daneben steht der Baustand; trägt er
  ein `+`, stammt das Paket aus einem unfertigen Arbeitsbaum.
- Der vollständige Änderungsverlauf: [CHANGELOG.md](../CHANGELOG.md).
- Wo die Software steht — auch mit ihren Lücken: [stand.md](stand.md).
- Welche Daten entstehen und wo sie liegen:
  [datenschutz.md](datenschutz.md).
- Der Quelltext steht unter der European Union Public Licence 1.2 und ist auf
  Anfrage erhältlich; ein öffentliches Archiv gibt es bislang nicht.
"""


def bericht() -> str:
    nummer = fassung()
    datum, inhalt = abschnitt(nummer)
    if not datum:
        raise SystemExit(f'ABBRUCH: Der Abschnitt „## [{nummer}]“ nennt kein Datum.')

    return (
        RAHMEN_KOPF.format(nummer=nummer, datum=datum)
        + '\n'.join(inhalt)
        + '\n'
        + RAHMEN_FUSS.format(nummer=nummer)
    )


def main() -> int:
    neu = bericht()

    if '--pruefen' in sys.argv:
        alt = ZIEL.read_text(encoding='utf-8') if ZIEL.exists() else ''
        if alt == neu:
            print(f'{ZIEL.relative_to(WURZEL)} ist aktuell.')
            return 0
        print(f'{ZIEL.relative_to(WURZEL)} ist NICHT aktuell — neu erzeugen mit:')
        print('  python tools/updatebericht.py')
        return 1

    io.open(ZIEL, 'w', encoding='utf-8', newline='').write(neu)
    print(f'Geschrieben: {ZIEL.relative_to(WURZEL)} ({len(neu)} Zeichen, Fassung {fassung()}).')
    return 0


if __name__ == '__main__':
    raise SystemExit(main())
