#!/usr/bin/env python3
"""LehrPuls Forschungs-Kit: Beispielanalyse in Python (pandas, matplotlib).

Liest den Rohdaten-Export (Langformat, eine Zeile je Antwort) und rechnet
  1. Schwierigkeit P je Frage (alles-oder-nichts, nur Runde 1)
  2. Trennschärfe je Frage: wie LehrPuls (obere/untere 27 % je Session, Mittel
     über Sessions) und zusätzlich punktbiserial
  3. Peer-Instruction-Gewinn Runde 1 → Runde 2 (wie LehrPuls und gepaart je Person)
  4. Längsschnitt derselben Frage über Semester
  5. Antwortzeit-Verteilungen
und schreibt kennzahlen-fragen.csv, laengsschnitt.csv und analyse-Python.png.

Aufruf:
  python3 analyse.py                                   # Beispieldatensatz
  python3 analyse.py pfad/zum/export.csv               # eigener Datei-Export (CSV)
  LEHRPULS_SCHLUESSEL=lp_… python3 analyse.py schnittstelle "kurs=Physik 1&tage=120"
      # direkt aus der Lese-Schnittstelle (Handbuch 10.9); Schlüssel nie ins Skript
Ohne installierte Pakete: uv run --with pandas --with matplotlib analyse.py

Datei-Export (deutsche Oberfläche): Strichpunkt, Dezimalkomma, UTF-8 mit BOM.
Schnittstelle (?format=csv): Komma, Dezimalpunkt. Beide landen in derselben Tabelle.
"""
import io
import os
import sys
import urllib.parse
import urllib.request
from pathlib import Path

import numpy as np
import pandas as pd

HIER = Path(__file__).resolve().parent
AUSGABE = HIER / "ausgabe"
AUSGABE.mkdir(exist_ok=True)


# ---- Laden ------------------------------------------------------------------
def lade_datei(pfad: Path) -> pd.DataFrame:
    with open(pfad, encoding="utf-8-sig") as f:
        f.readline()
        zweite = f.readline()
    dezimal = "," if ";" in zweite and any(t.replace(",", "").isdigit() and "," in t for t in zweite.split(";")) else "."
    return pd.read_csv(pfad, sep=";", decimal=dezimal, encoding="utf-8-sig", low_memory=False)


def lade_schnittstelle(eingrenzung: str = "") -> pd.DataFrame:
    basis = os.environ.get("LEHRPULS_ADRESSE", "http://127.0.0.1:8716/api/v1")
    schluessel = os.environ.get("LEHRPULS_SCHLUESSEL")
    if not schluessel:
        sys.exit("Umgebungsvariable LEHRPULS_SCHLUESSEL fehlt")
    adresse = f"{basis}/antworten?format=csv"
    if eingrenzung:
        adresse += "&" + urllib.parse.quote(eingrenzung, safe="=&")
    anfrage = urllib.request.Request(adresse, headers={"Authorization": f"Bearer {schluessel}"})
    with urllib.request.urlopen(anfrage, timeout=60) as antwort:
        text = antwort.read().decode("utf-8")
    return pd.read_csv(io.StringIO(text), sep=",", decimal=".")


if len(sys.argv) > 1 and sys.argv[1] == "schnittstelle":
    antworten = lade_schnittstelle(sys.argv[2] if len(sys.argv) > 2 else "")
else:
    pfad = Path(sys.argv[1]) if len(sys.argv) > 1 else HIER / ".." / "beispieldaten" / "antworten.csv"
    antworten = lade_datei(pfad)
print(f"Geladen: {len(antworten)} Antworten, {antworten.shape[1]} Spalten, "
      f"{antworten.session_id.nunique()} Sessions, {antworten.frage_id.nunique()} Fragen")

# ---- Analysebasis: wie die Auswertungen von LehrPuls -----------------------
basis = antworten[(antworten.ausgeschlossen == 0) & (antworten.gruppe == 0)].copy()
bewertbar = basis[basis.korrekt.notna()].copy()
bewertbar["korrekt"] = bewertbar.korrekt.astype(int)
runde1 = bewertbar[bewertbar.runde == 1].copy()

# ---- 1. Schwierigkeit P (nur Runde 1) ---------------------------------------
p_je_frage = (runde1.groupby(["frage_id", "frage_titel", "fragetyp"])["korrekt"]
              .agg(n_runde1="size", schwierigkeit_p="mean").reset_index())
p_je_frage["schwierigkeit_p"] = p_je_frage.schwierigkeit_p.round(3)


# ---- 2. Trennschärfe ---------------------------------------------------------
def trennschaerfe_lehrpuls(d: pd.DataFrame) -> pd.Series:
    """Wie stats.rs: je Session Score je Person (Runde 1), obere/untere Gruppe
    round(0,27 n) in [1, n/2], Differenz der Anteile richtig, Mittel über Sessions."""
    werte = []
    for sid, s in d.groupby("session_id"):
        score = s.groupby("teilnehmer_id")["korrekt"].sum().sort_values(ascending=False)
        if len(score) < 2:
            continue
        g = int(np.clip(round(0.27 * len(score)), 1, len(score) // 2))
        oben, unten = set(score.index[:g]), set(score.index[-g:])
        for fid, f in s.groupby("frage_id"):
            o = f[f.teilnehmer_id.isin(oben)].korrekt
            u = f[f.teilnehmer_id.isin(unten)].korrekt
            if len(o) and len(u):
                werte.append((fid, o.mean() - u.mean()))
    w = pd.DataFrame(werte, columns=["frage_id", "d"])
    return w.groupby("frage_id")["d"].mean().round(3).rename("trennschaerfe")


ts = trennschaerfe_lehrpuls(runde1)

# punktbiserial (Forschungsvariante): Antwort gegen Rest-Score derselben Person in der Session
runde1["rest"] = runde1.groupby(["session_id", "teilnehmer_id"])["korrekt"].transform("sum") - runde1.korrekt
rpb = (runde1.groupby("frage_id")
       .apply(lambda f: f.korrekt.corr(f.rest) if f.korrekt.std() > 0 and f.rest.std() > 0 else np.nan,
              include_groups=False)
       .round(3).rename("r_punktbiserial"))

# ---- 3. Peer-Instruction-Gewinn ---------------------------------------------
# a) wie LehrPuls: P je Runde über alle Antworten der Frage, nur Fragen mit beiden Runden
pi_app = (bewertbar.groupby(["frage_id", "frage_titel", "runde"])["korrekt"].agg(["size", "mean"])
          .unstack("runde"))
pi_app = pi_app[pi_app[("size", 2)].notna()]
pi_app = pd.DataFrame({
    "n1": pi_app[("size", 1)].astype(int), "n2": pi_app[("size", 2)].astype(int),
    "pi_p_runde1": pi_app[("mean", 1)].round(3), "pi_p_runde2": pi_app[("mean", 2)].round(3),
}).reset_index()
# b) gepaart je Person: nur Sessions mit zweiter Runde; Wechsel zählen (McNemar)
schl = ["session_id", "frage_id", "teilnehmer_id"]
paare = (bewertbar[bewertbar.runde == 1][schl + ["korrekt"]]
         .merge(bewertbar[bewertbar.runde == 2][schl + ["korrekt"]], on=schl, suffixes=("_r1", "_r2")))


def mcnemar_p(b: int, c: int) -> float:
    """Exakter McNemar-Test (Binomial, zweiseitig) ohne scipy."""
    from math import comb
    b, c, n = int(b), int(c), int(b + c)
    if n == 0:
        return np.nan
    k = min(b, c)
    p = sum(comb(n, i) for i in range(k + 1)) / 2 ** n * 2
    return round(min(p, 1.0), 4)


pi_paare = paare.groupby("frage_id").apply(lambda f: pd.Series({
    "paare": len(f),
    "falsch_zu_richtig": int(((f.korrekt_r1 == 0) & (f.korrekt_r2 == 1)).sum()),
    "richtig_zu_falsch": int(((f.korrekt_r1 == 1) & (f.korrekt_r2 == 0)).sum()),
    "gewinn_gepaart": round(f.korrekt_r2.mean() - f.korrekt_r1.mean(), 3),
}), include_groups=False).reset_index()
pi_paare["mcnemar_p"] = [mcnemar_p(b, c) for b, c in zip(pi_paare.falsch_zu_richtig, pi_paare.richtig_zu_falsch)]
pi = pi_app.merge(pi_paare, on="frage_id")
print(f"\nPeer Instruction: {len(pi)} Fragen mit zweiter Runde, mittlerer gepaarter Gewinn {pi.gewinn_gepaart.mean():.3f}")

# ---- 4. Längsschnitt derselben Frage über Semester --------------------------
def semester_schluessel(s: str) -> float:
    import re
    jahr = int(re.search(r"(20\d{2})", s).group(1))
    return jahr + (0.75 if s.startswith("WS") else 0.25)


laengs = (runde1.groupby(["frage_id", "frage_titel", "semester"])["korrekt"]
          .agg(n="size", p="mean").reset_index())
laengs["p"] = laengs.p.round(3)
semester = sorted(laengs.semester.unique(), key=semester_schluessel)
breit = laengs.pivot(index="frage_id", columns="semester", values="p")[semester]
breit.columns = [f"P {s}" for s in breit.columns]
breit["spannweite"] = (breit.max(axis=1) - breit.min(axis=1)).round(3)
breit.sort_values("spannweite", ascending=False).to_csv(AUSGABE / "laengsschnitt.csv")

# ---- 5. Antwortzeiten --------------------------------------------------------
zeit = basis[basis.dauer_s.notna()]
print("\nMedian der Bearbeitungszeit (s) je Fragetyp:")
print(zeit.groupby("fragetyp")["dauer_s"].median().round(1).to_string())
zr = zeit[(zeit.fragetyp == "mc_single") & zeit.korrekt.notna()]
print("\nMedian (s) Single Choice nach Runde und Richtigkeit:")
print(zr.groupby(["runde", "korrekt"])["dauer_s"].median().round(1).unstack().to_string())

# ---- Kennzahlen-Tabelle ------------------------------------------------------
kenn = (p_je_frage.merge(ts, on="frage_id", how="left").merge(rpb, on="frage_id", how="left")
        .merge(pi[["frage_id", "pi_p_runde1", "pi_p_runde2", "gewinn_gepaart", "mcnemar_p"]], on="frage_id", how="left"))


def hinweis(z) -> str:
    if z.n_runde1 < 10:
        return "unter Mindestbasis n=10"
    if pd.notna(z.trennschaerfe) and z.trennschaerfe <= 0:
        return "Trennschärfe <= 0: Lösung prüfen"
    if z.schwierigkeit_p > 0.9:
        return "zu leicht"
    if z.schwierigkeit_p < 0.3:
        return "zu schwer"
    return ""


kenn["hinweis"] = kenn.apply(hinweis, axis=1)
kenn = kenn.sort_values("trennschaerfe")
kenn.to_csv(AUSGABE / "kennzahlen-fragen.csv", index=False)
print("\nFragen mit Hinweis:")
print(kenn[kenn.hinweis != ""][["frage_id", "frage_titel", "n_runde1", "schwierigkeit_p", "trennschaerfe", "hinweis"]].to_string(index=False))

# ---- Grafik ------------------------------------------------------------------
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

PETROL, GRAU, ROT, GOLD = "#0e8a80", "#56666b", "#b5484a", "#d9a441"
fig, ax = plt.subplots(2, 2, figsize=(12, 9.5), dpi=150)
# a) Gütekarte
a = ax[0, 0]
farbe = np.where(kenn.trennschaerfe <= 0, ROT, PETROL)
a.scatter(kenn.schwierigkeit_p, kenn.trennschaerfe, c=farbe, s=28)
for v in (0.3, 0.9):
    a.axvline(v, ls=":", c=GRAU, lw=1)
a.axhline(0, ls=":", c=GRAU, lw=1)
for _, z in kenn[kenn.hinweis != ""].iterrows():
    a.annotate(str(z.frage_id), (z.schwierigkeit_p, z.trennschaerfe), textcoords="offset points", xytext=(0, 6), ha="center", fontsize=7, color=GRAU)
a.set(xlim=(0, 1), ylim=(-0.5, 1), xlabel="Schwierigkeit P (Runde 1)", ylabel="Trennschärfe (obere − untere 27 %)", title="Gütekarte je Frage")
# b) Peer Instruction Hantel
b = ax[0, 1]
po = pi.sort_values("pi_p_runde1").reset_index(drop=True)
y = np.arange(len(po))
b.hlines(y, po.pi_p_runde1, po.pi_p_runde2, color=GRAU, lw=1)
b.scatter(po.pi_p_runde1, y, c=GRAU, s=24, label="Runde 1", zorder=3)
b.scatter(po.pi_p_runde2, y, c=PETROL, s=24, label="Runde 2", zorder=3)
b.set_yticks(y, po.frage_titel, fontsize=7)
b.set(xlim=(0, 1), xlabel="Anteil richtig", title="Peer Instruction: Runde 1 → Runde 2")
b.legend(loc="lower right", frameon=False, fontsize=8)
# c) Längsschnitt der vier Fragen mit größter Spannweite
c = ax[1, 0]
top = breit.sort_values("spannweite", ascending=False).index[:4]
for fid, f in zip(top, (PETROL, ROT, GRAU, GOLD)):
    l = laengs[laengs.frage_id == fid].set_index("semester").reindex(semester)
    titel = kenn.loc[kenn.frage_id == fid, "frage_titel"].iloc[0]
    c.plot(range(len(semester)), l.p, "-o", color=f, lw=2, ms=4, label=f"{fid} {titel}")
c.set_xticks(range(len(semester)), semester)
c.set(ylim=(0, 1), xlabel="Semester", ylabel="Schwierigkeit P", title="Längsschnitt derselben Frage")
c.legend(frameon=False, fontsize=7, loc="lower left")
# d) Antwortzeiten Single Choice Runde 1 gegen Runde 2
d = ax[1, 1]
bins = np.logspace(np.log10(max(zr.dauer_s.min(), 1)), np.log10(zr.dauer_s.max()), 40)
d.hist(zr[zr.runde == 1].dauer_s, bins=bins, density=True, color=GRAU, alpha=0.5, label="Runde 1")
d.hist(zr[zr.runde == 2].dauer_s, bins=bins, density=True, color=PETROL, alpha=0.6, label="Runde 2")
d.set_xscale("log")
d.set_xticks([3, 10, 30, 100, 300], ["3", "10", "30", "100", "300"])
d.set(xlabel="Bearbeitungszeit (s, log-Skala)", title="Antwortzeiten Single Choice")
d.legend(frameon=False, fontsize=8)
fig.tight_layout()
fig.savefig(AUSGABE / "analyse-Python.png")
print(f"\nGeschrieben: {AUSGABE / 'kennzahlen-fragen.csv'}, {AUSGABE / 'laengsschnitt.csv'}, {AUSGABE / 'analyse-Python.png'}")
