#!/usr/bin/env python3

# Erklaerbarkeit.py
"""
Kapitel Praxisfallen: Ein Constraint-Trace, der die drei Fragen der Faustregel fuer
Erklaerbarkeit (Abschnitt 'Die fuenf typischen Praxisfallen') fuer eine
einzelne Zuweisung beantwortet:
Warum diese Loesung? Warum nicht die Alternative? Was wuerde sie verbessern?
"""

from ortools.sat.python import cp_model

PERSONAL = ["Anna", "Ben", "Clara"]
BEREITS_STUNDEN = {"Anna": 1, "Ben": 2, "Clara": 0}   # diese Woche schon geleistet
MAX_STUNDEN = 2                                        # gesetzliche Obergrenze
STRAFE_VORBELASTUNG = 50                                # pro bereits geleisteter Stunde
STRAFKOSTEN_WUNSCH = {"Anna": 0, "Ben": 30, "Clara": 100}


def loese(max_stunden: int):
    modell = cp_model.CpModel()
    x = {p: modell.NewBoolVar(f"x_{p}") for p in PERSONAL}
    modell.AddExactlyOne(x.values())

    kosten = []
    for p in PERSONAL:
        if BEREITS_STUNDEN[p] + 1 > max_stunden:
            modell.Add(x[p] == 0)                       # hartes Ausschlusskriterium
        strafe = BEREITS_STUNDEN[p] * STRAFE_VORBELASTUNG + STRAFKOSTEN_WUNSCH[p]
        kosten.append(strafe * x[p])

    modell.Minimize(sum(kosten))
    loeser = cp_model.CpSolver()
    status = loeser.Solve(modell)
    if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
        return None
    gewinner = next(p for p in PERSONAL if loeser.Value(x[p]))
    return gewinner, loeser.ObjectiveValue()


def gesamtkosten(p: str) -> int:
    return BEREITS_STUNDEN[p] * STRAFE_VORBELASTUNG + STRAFKOSTEN_WUNSCH[p]


if __name__ == "__main__":
    gewinner, kosten_opt = loese(MAX_STUNDEN)

    print("=" * 78)
    print("  CONSTRAINT-TRACE: Warum bekommt", gewinner, "die Vertretungsstunde?")
    print("=" * 78)

    print("\n1. WARUM DIESE LOESUNG?")
    v, w = BEREITS_STUNDEN[gewinner] * STRAFE_VORBELASTUNG, STRAFKOSTEN_WUNSCH[gewinner]
    print(f"   {gewinner}: Vorbelastung {v} Strafpunkte + Wunsch {w} Strafpunkte "
          f"= {v + w} Strafpunkte insgesamt (Minimum).")

    print("\n2. WARUM NICHT DIE NAHELIEGENDEN ALTERNATIVEN?")
    for p in PERSONAL:
        if p == gewinner:
            continue
        if BEREITS_STUNDEN[p] + 1 > MAX_STUNDEN:
            print(f"   {p}: AUSGESCHLOSSEN - hat bereits {BEREITS_STUNDEN[p]} Stunden, "
                  f"eine dritte würde die Obergrenze von {MAX_STUNDEN} Stunden verletzen "
                  f"(harte Bedingung, nicht verhandelbar).")
        else:
            differenz = gesamtkosten(p) - kosten_opt
            print(f"   {p}: zulässig, aber {differenz:.0f} Strafpunkte teurer als "
                  f"{gewinner} ({gesamtkosten(p):.0f} statt {kosten_opt:.0f}).")

    print("\n3. WAS WUERDE DIE LOESUNG VERBESSERN?")
    gewinner_gelockert, kosten_gelockert = loese(MAX_STUNDEN + 1)
    if kosten_gelockert < kosten_opt:
        print(f"   Obergrenze auf {MAX_STUNDEN + 1} Stunden gelockert: {gewinner_gelockert} "
              f"uebernimmt jetzt zu {kosten_gelockert:.0f} Strafpunkten "
              f"({kosten_opt - kosten_gelockert:.0f} weniger als heute).")
        print("   -> Diese Bedingung hat tatsaechlich einen Preis.")
    else:
        print(f"   Obergrenze auf {MAX_STUNDEN + 1} Stunden gelockert: Ergebnis bleibt bei "
              f"{gewinner} mit {kosten_gelockert:.0f} Strafpunkten (unveraendert).")
        blockiert = ", ".join(p for p in PERSONAL
                               if BEREITS_STUNDEN[p] + 1 > MAX_STUNDEN and p != gewinner)
        print(f"   -> Die Obergrenze blockiert zwar {blockiert}, kostet aber gerade NICHTS:")
        print("   Selbst ohne sie waere die Loesung dieselbe.")
        print("   Blockierend und kostenrelevant sind zwei verschiedene Dinge.")
    print("=" * 78)
