"""Erhebung 2026, Schritt 2: Meldungen nach Abschnitt 4 der Methodik klassifizieren.

Klare Faelle entscheidet ein Regelwerk, Grenzfaelle ein Sprachmodell. Beides
wird in der Spalte `entscheid_durch` festgehalten, damit der Bericht die
Anzahl der Grenzfaelle nennen kann (Methodik 4.5).

Eingabe:  erhebung-2026/rohdaten/meldungen.csv
Ausgabe:  erhebung-2026/auswertung/klassifikation.csv
          erhebung-2026/auswertung/modell-antworten.json  (Cache, spart Geld bei Wiederholung)
          erhebung-2026/auswertung/stichprobe-handpruefung.csv  (50 Meldungen fuer die Handpruefung)

Aufruf aus dem Ordner backend:  python scripts/erhebung_klassifikation.py
"""

from __future__ import annotations

import csv
import json
import random
import re
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

from anthropic import Anthropic

REPO = Path(__file__).resolve().parents[2]
ROHDATEN = REPO / "erhebung-2026" / "rohdaten" / "meldungen.csv"
AUSWERTUNG = REPO / "erhebung-2026" / "auswertung"
KLASSIFIKATION = AUSWERTUNG / "klassifikation.csv"
MODELL_CACHE = AUSWERTUNG / "modell-antworten.json"
STICHPROBE = AUSWERTUNG / "stichprobe-handpruefung.csv"

MODELL = "claude-opus-5"
STICHPROBEN_STARTWERT = 20260921  # Datum der Veroeffentlichung, im Bericht nennen

LABOR_WOERTER = r"zahntechn|dentallabor|dental-labor|dental labor|zahnlabor"
PRAXIS_WOERTER = (
    r"zahnarzt|zahnärzt|zahnmedizin|kieferorthop|dentalhygien|zahnklinik|"
    r"zahnbehandl|oralchirurg|implantolog|zahnheilkunde"
)
HANDEL_WOERTER = r"handel|vertrieb|herstellung|beteiligung|holding|immobilien|liegenschaft"
BEHANDLUNG_WOERTER = (
    r"zahnarztpraxis|zahnarzt|zahnärzt|kieferorthop|dentalhygien|zahnklinik|"
    r"zahnbehandl|betrieb einer praxis|betrieb von praxen|praxis für"
)
HANDKORREKTUREN = AUSWERTUNG / "handkorrekturen.csv"

SPALTEN_NEU = [
    "typ", "ereignis", "ausschluss", "zaehlt_praxis", "zaehlt_laborabgang",
    "entscheid_durch", "begruendung",
]

REGELN_FUER_MODELL = """Du klassifizierst eine Meldung aus dem Schweizerischen Handelsamtsblatt (SHAB)
nach diesen vorab veroeffentlichten Zaehlregeln. Halte dich wortgetreu daran.

4.2 Zahnarztpraxis: Eine Meldung zaehlt als Zahnarztpraxis, wenn Firmenname oder Zweck einen
zahnmedizinischen Behandlungsbezug haben. Reine Dentalhandels-, Vertriebs- und Beteiligungsfirmen
zaehlen nicht. Zahntechnische Labore zaehlen nicht als Praxis.

4.3 Ansprechbar: Eine Praxis gilt als ansprechbar, wenn eines von beidem publiziert wurde:
(a) Erstmalige Neueintragung (HR01) der Praxis im Handelsregister, oder
(b) Personenwechsel (HR02): eine Mutation, in der die Meldung mindestens eine ausgeschiedene
und mindestens eine neu eingetragene zeichnungsberechtigte Person nennt.
Ausdruecklich nicht ansprechbar:
- Rechtsformwechsel: Eine bestehende Einzelfirma wird in eine GmbH oder AG umgewandelt. Erkennbar an
  der Uebernahme von Geschaeft und Aktiven aus einer bestehenden Firma (Sacheinlage), an
  gleichbleibenden Personen oder an einer zeitgleichen Loeschung der Vorgaengerfirma.
- Sitzverlegung in einen anderen Kanton, die im Zuzugskanton als Neueintragung erscheint.
- Umfirmierung, Kapitalveraenderung, Adressaenderung, Aenderung der Revisionsstelle.
- Reine Zeichnungsrechts-Anpassungen ohne Personenwechsel. Eine Person, die nur ihre Funktion
  oder Unterschrift aendert (Eintrag mit "[bisher: ...]"), ist keine neu eingetragene Person.
- Zweigniederlassung: Eine Filiale einer bestehenden Praxis ist keine erstmalige Neueintragung
  der Praxis. Sie zaehlt nur, wenn der Hauptsitz erkennbar keine Zahnarztpraxis ist.

4.4 Zahntechnisches Labor: Eine Meldung zaehlt als Labor, wenn Firmenname oder Zweck zahntechnische
Fertigung bezeichnen. Als Abgang zaehlen Loeschung (HR03) und Konkurs (KK). Nicht als Abgang zaehlen
Loeschungen infolge Fusion, Uebernahme, Sitzverlegung oder Rechtsformwechsel.

Grundsatz: Die Regeln sind bewusst so gesetzt, dass Grenzfaelle die Zahl senken. Im Zweifel
zaehlt eine Meldung nicht. Entscheide nur anhand des vorliegenden Textes, ergaenze kein Wissen.
Antworte nur mit dem JSON-Objekt."""

AUSGABE_SCHEMA = {
    "type": "object",
    "properties": {
        "typ": {"type": "string", "enum": ["praxis", "labor", "keins"]},
        "ereignis": {
            "type": "string",
            "enum": ["neueintragung", "personenwechsel", "loeschung", "konkurs", "sonstiges"],
        },
        "ausschluss": {
            "type": "string",
            "enum": [
                "keiner", "rechtsformwechsel", "sitzverlegung", "zweigniederlassung",
                "umfirmierung_kapital_adresse", "nur_zeichnungsrecht", "fusion_uebernahme",
                "kein_dentalbezug", "handel_vertrieb_beteiligung", "anderer",
            ],
        },
        "zaehlt_praxis": {"type": "boolean"},
        "zaehlt_laborabgang": {"type": "boolean"},
        "begruendung": {"type": "string"},
    },
    "required": ["typ", "ereignis", "ausschluss", "zaehlt_praxis", "zaehlt_laborabgang", "begruendung"],
    "additionalProperties": False,
}


def api_schluessel() -> str:
    for zeile in (REPO / ".env").read_text(encoding="utf-8").splitlines():
        if zeile.startswith("ANTHROPIC_API_KEY="):
            return zeile.split("=", 1)[1].strip().strip('"')
    raise SystemExit("ANTHROPIC_API_KEY fehlt in .env")


def typ_signale(m: dict) -> tuple[bool, bool, bool]:
    """labor: zahntechnisches Wort. praxis: ein Wort, das Behandlung bezeichnet
    (nur «Zahnmedizin» reicht nicht, das steht auch bei Software- und
    Weiterbildungsfirmen; solche Faelle gehen ans Modell). handel: Handelszweck."""
    t = f"{m['firma']} {m['zweck']} {m['titel']}".lower()
    labor = bool(re.search(LABOR_WOERTER, t))
    praxis = bool(re.search(BEHANDLUNG_WOERTER, t))
    handel = bool(re.search(HANDEL_WOERTER, m["zweck"].lower())) or (
        bool(re.search(PRAXIS_WOERTER, t)) and not praxis
    )
    return labor, praxis or bool(re.search(PRAXIS_WOERTER, t)), handel


def personenwechsel(text: str) -> bool | None:
    """True: ausgeschieden und neu. False: ausgeschieden, aber nur Funktionsaenderungen.
    None: keine Ausgeschiedenen oder keine Neueintragungen."""
    aus = re.search(r"Ausgeschiedene Personen[^:]*:(.*?)(?:Eingetragene Personen|$)", text, re.S)
    neu = re.search(r"Eingetragene Personen neu oder mutierend:(.*)$", text, re.S)
    if not aus or not neu:
        return None
    # Wer ohne Zeichnungsberechtigung ausscheidet, ist keine ausgeschiedene
    # zeichnungsberechtigte Person im Sinne von 4.3
    ausgeschieden = [e for e in aus.group(1).split(";") if e.strip() and "ohne Zeichnungsberechtigung" not in e]
    if not ausgeschieden:
        return None
    neue = [
        e for e in neu.group(1).split(";")
        if e.strip() and "[bisher" not in e and "ohne Zeichnungsberechtigung" not in e
    ]
    return len(neue) > 0


def konsistent(m: dict) -> None:
    """Zaehlregeln gelten unabhaengig davon, wer entschieden hat: Eine Praxis zaehlt nur
    bei Neueintragung oder Personenwechsel ohne Ausschluss, ein Laborabgang nur bei
    Loeschung (HR03) oder Konkurs (KK)."""
    m["zaehlt_praxis"] = (
        m["typ"] == "praxis"
        and m["ereignis"] in ("neueintragung", "personenwechsel")
        and m["ausschluss"] == "keiner"
        and m["unterrubrik"] in ("HR01", "HR02")
    )
    m["zaehlt_laborabgang"] = (
        m["typ"] == "labor"
        and m["ausschluss"] == "keiner"
        and (m["unterrubrik"] == "HR03" or m["rubrik"] == "KK")
    )


def regel(m: dict) -> dict | None:
    """Entscheidung per Regel, oder None wenn Grenzfall (dann Sprachmodell)."""
    labor, praxis, handel = typ_signale(m)
    text = m["publikationstext"]
    sub = m["unterrubrik"]
    rubrik = m["rubrik"]

    def klar(typ, ereignis, ausschluss, zp, zl, grund):
        return {
            "typ": typ, "ereignis": ereignis, "ausschluss": ausschluss,
            "zaehlt_praxis": zp, "zaehlt_laborabgang": zl,
            "entscheid_durch": "regel", "begruendung": grund,
        }

    # Konkurse: nur Firmenname (Methodik 8), nur Labore relevant
    if rubrik == "KK":
        if labor and not praxis:
            return klar("labor", "konkurs", "keiner", False, True, "Laborbegriff im Namen, Konkursrubrik")
        if praxis and not labor:
            return klar("praxis", "konkurs", "keiner", False, False, "Praxis-Konkurs wird nicht gezaehlt")
        return None

    # Typ unklar: beides, nichts, oder Praxiswort neben Handelszweck
    if (labor and praxis) or (not labor and not praxis) or (praxis and handel):
        return None

    if labor:
        if sub == "HR03":
            if re.search(r"Fusion|Sitz|Umwandlung|übernommen|Zweigniederlassung", text):
                return None
            return klar("labor", "loeschung", "keiner", False, True, "Loeschung eines Labors")
        return klar("labor", "sonstiges", "keiner", False, False, "Labor, kein Abgang")

    # ab hier: Praxis
    if sub == "HR01":
        if re.search(r"Sacheinlage|Aktiven und Passiven|übernimmt|bisher in", text):
            return None
        if re.search(r"Angaben zur Zweigniederlassung|schweizerische Zweigniederlassung", text):
            return None
        return klar("praxis", "neueintragung", "keiner", True, False, "Erstmalige Neueintragung")
    if sub == "HR02":
        if re.search(r"Umwandlung|Fusion", text):
            return None
        wechsel = personenwechsel(text)
        if wechsel is True:
            return klar("praxis", "personenwechsel", "keiner", True, False,
                        "Mindestens eine Person ausgeschieden und eine neu eingetragen")
        if wechsel is None:
            return klar("praxis", "sonstiges", "nur_zeichnungsrecht" if "Ausgeschiedene" in text
                        else "umfirmierung_kapital_adresse", False, False,
                        "Kein Personenwechsel nach 4.3")
        return None
    if sub == "HR03":
        return klar("praxis", "loeschung", "keiner", False, False, "Praxis-Loeschung wird nicht gezaehlt")
    return None


def frage_modell(client: Anthropic, m: dict) -> dict:
    inhalt = (
        f"Rubrik: {m['rubrik']} / {m['unterrubrik']}\n"
        f"Titel: {m['titel']}\n"
        f"Firma: {m['firma']}\nSitz: {m['sitz']}\nKantone: {m['kantone']}\n"
        f"Zweck: {m['zweck']}\n\nPublikationstext:\n{m['publikationstext']}"
    )
    antwort = client.messages.create(
        model=MODELL,
        max_tokens=2000,
        system=REGELN_FUER_MODELL,
        messages=[{"role": "user", "content": inhalt}],
        output_config={"effort": "medium", "format": {"type": "json_schema", "schema": AUSGABE_SCHEMA}},
    )
    if antwort.stop_reason != "end_turn":
        raise RuntimeError(f"Modell hat nicht sauber geantwortet: {antwort.stop_reason}")
    text = next(b.text for b in antwort.content if b.type == "text")
    ergebnis = json.loads(text)
    ergebnis["entscheid_durch"] = "modell"
    return ergebnis


def main() -> None:
    AUSWERTUNG.mkdir(parents=True, exist_ok=True)
    meldungen = list(csv.DictReader(ROHDATEN.open(encoding="utf-8")))
    cache: dict[str, dict] = {}
    if MODELL_CACHE.is_file():
        cache = json.loads(MODELL_CACHE.read_text(encoding="utf-8"))

    grenzfaelle = []
    for m in meldungen:
        entscheid = regel(m)
        if entscheid:
            m.update(entscheid)
        elif m["id"] in cache:
            m.update(cache[m["id"]])
        else:
            grenzfaelle.append(m)
    print(f"{len(meldungen)} Meldungen, {len(grenzfaelle)} Grenzfaelle ans Modell ({MODELL})")

    if grenzfaelle:
        client = Anthropic(api_key=api_schluessel(), max_retries=4)

        def bearbeite(m: dict) -> None:
            ergebnis = frage_modell(client, m)
            m.update(ergebnis)
            cache[m["id"]] = ergebnis
            print(f"  {m['titel'][:60]:<60} -> {ergebnis['typ']}/{ergebnis['ereignis']}"
                  f" zaehlt={ergebnis['zaehlt_praxis'] or ergebnis['zaehlt_laborabgang']}")

        with ThreadPoolExecutor(max_workers=4) as pool:
            list(pool.map(bearbeite, grenzfaelle))
        MODELL_CACHE.write_text(json.dumps(cache, ensure_ascii=False, indent=1), encoding="utf-8")

    for m in meldungen:
        konsistent(m)

    # Handkorrekturen aus der Pruefung nach 4.5 gelten zuletzt
    if HANDKORREKTUREN.is_file():
        korrekturen = {k["id"]: k for k in csv.DictReader(HANDKORREKTUREN.open(encoding="utf-8"))}
        for m in meldungen:
            k = korrekturen.get(m["id"])
            if k:
                m["typ"] = k["typ"]
                m["ausschluss"] = k["ausschluss"]
                m["entscheid_durch"] = "hand"
                m["begruendung"] = k["begruendung"]
                konsistent(m)
        print(f"Handkorrekturen angewendet: {len(korrekturen)}")

    spalten = list(meldungen[0].keys())
    with KLASSIFIKATION.open("w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=spalten)
        w.writeheader()
        w.writerows(meldungen)

    # Stichprobe fuer die Handpruefung: 25 zaehlend, 25 nicht zaehlend.
    # Wird nur einmal gezogen; die ausgefuellte Datei darf nicht ueberschrieben werden.
    if STICHPROBE.is_file():
        print(f"Stichprobe besteht schon, bleibt unveraendert: {STICHPROBE}")
        zusammenfassung(meldungen)
        return
    zufall = random.Random(STICHPROBEN_STARTWERT)
    zaehlend = [m for m in meldungen if m["zaehlt_praxis"] or m["zaehlt_laborabgang"]]
    nicht = [m for m in meldungen if not (m["zaehlt_praxis"] or m["zaehlt_laborabgang"])]
    probe = zufall.sample(zaehlend, 25) + zufall.sample(nicht, 25)
    with STICHPROBE.open("w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=[
            "id", "unterrubrik", "publikationsdatum", "kantone", "titel", "typ", "ereignis",
            "ausschluss", "zaehlt_praxis", "zaehlt_laborabgang", "entscheid_durch", "begruendung",
            "url", "handpruefung_korrekt", "handpruefung_bemerkung",
        ], extrasaction="ignore")
        w.writeheader()
        for m in probe:
            w.writerow({**m, "handpruefung_korrekt": "", "handpruefung_bemerkung": ""})

    print(f"Stichprobe: {STICHPROBE}")
    zusammenfassung(meldungen)


def zusammenfassung(meldungen: list[dict]) -> None:
    praxen = [m for m in meldungen if m["zaehlt_praxis"]]
    abgaenge = [m for m in meldungen if m["zaehlt_laborabgang"]]
    print(f"\nAnsprechbare Praxen: {len(praxen)}")
    print(f"  Neueintragung:  {sum(1 for m in praxen if m['ereignis'] == 'neueintragung')}")
    print(f"  Personenwechsel: {sum(1 for m in praxen if m['ereignis'] == 'personenwechsel')}")
    print(f"Laborabgaenge (Meldungen, noch nicht je Betrieb dedupliziert): {len(abgaenge)}")
    print(f"Entschieden durch: regel {sum(1 for m in meldungen if m['entscheid_durch'] == 'regel')}, "
          f"modell {sum(1 for m in meldungen if m['entscheid_durch'] == 'modell')}, "
          f"hand {sum(1 for m in meldungen if m['entscheid_durch'] == 'hand')}")


if __name__ == "__main__":
    main()
