#!/usr/bin/env python3

# Infeasibility_Diagnose.py
"""
Kapitel Praxisfallen: Aus INFEASIBLE einen Notfallplan machen.

Demonstriert die hierarchische Relaxation an einem Dienstplan, der in der
harten Fassung unloesbar ist, und zeigt, wie der Solver die Ursache benennt.
"""

from ortools.sat.python import cp_model

SLOTS = ["Mo frueh", "Mo spaet", "Di frueh", "Di spaet"]
FAECHER = ["Mathematik", "Physik", "Mathematik", "Chemie"]   # Chemie kann niemand!
PERSONAL = ["Alice", "Bob", "Carla"]
QUALIFIKATION = {
    "Alice": {"Mathematik", "Physik"},
    "Bob":   {"Mathematik"},
    "Carla": {"Physik", "Mathematik"},
}
MAX_PRO_PERSON = 2
STRAFE_UNBESETZT = 10_000          # sehr teuer, aber nicht unmoeglich
STRAFE_UEBERLAST = 500             # teuer, aber billiger als Ausfall


def plane(harte_fassung: bool):
    """harte_fassung=True: klassisch (kann INFEASIBLE werden).
       harte_fassung=False: mit Schlupfvariablen (immer loesbar)."""
    modell = cp_model.CpModel()
    x = {(p, s): modell.NewBoolVar(f"x_{p}_{s}")
         for p in PERSONAL for s in range(len(SLOTS))}

    strafen = []
    unbesetzt = {}
    ueberlast = {}

    for s in range(len(SLOTS)):
        if harte_fassung:
            modell.AddExactlyOne(x[p, s] for p in PERSONAL)
        else:
            # Schlupfvariable: der Slot DARF unbesetzt bleiben - gegen hohe Strafe
            unbesetzt[s] = modell.NewBoolVar(f"unbesetzt_{s}")
            modell.AddExactlyOne([x[p, s] for p in PERSONAL] + [unbesetzt[s]])
            strafen.append(unbesetzt[s] * STRAFE_UNBESETZT)

    # Qualifikation bleibt IMMER hart - fachfremder Unterricht ist keine Option
    for s, fach in enumerate(FAECHER):
        for p in PERSONAL:
            if fach not in QUALIFIKATION[p]:
                modell.Add(x[p, s] == 0)

    for p in PERSONAL:
        last = sum(x[p, s] for s in range(len(SLOTS)))
        if harte_fassung:
            modell.Add(last <= MAX_PRO_PERSON)
        else:
            # Ueberlast erlaubt - aber teuer
            ueberlast[p] = modell.NewIntVar(0, len(SLOTS), f"ueberlast_{p}")
            modell.Add(last <= MAX_PRO_PERSON + ueberlast[p])
            strafen.append(ueberlast[p] * STRAFE_UEBERLAST)

    if strafen:
        modell.Minimize(sum(strafen))

    loeser = cp_model.CpSolver()
    loeser.parameters.max_time_in_seconds = 5.0
    status = loeser.Solve(modell)
    return loeser, status, x, unbesetzt, ueberlast


if __name__ == "__main__":
    print("=" * 78)
    print("  VARIANTE A: KLASSISCH MIT LAUTER HARTEN BEDINGUNGEN")
    print("=" * 78)
    loeser, status, *_ = plane(harte_fassung=True)
    print(f"Solver-Status: {loeser.StatusName(status)}")
    if status == cp_model.INFEASIBLE:
        print("Das System kann keinen Plan liefern. Der Anwender erfaehrt NICHT,")
        print("welche Regel das Problem verursacht - nur, dass es nicht geht.\n")

    print("=" * 78)
    print("  VARIANTE B: MIT HIERARCHISCHER RELAXATION")
    print("=" * 78)
    loeser, status, x, unbesetzt, ueberlast = plane(harte_fassung=False)
    print(f"Solver-Status: {loeser.StatusName(status)} | "
          f"Strafkosten: {loeser.ObjectiveValue():.0f}\n")

    print(f"{'Slot':<12} {'Fach':<12} {'Zuweisung':<18} {'Bemerkung'}")
    print("-" * 78)
    for s, slot in enumerate(SLOTS):
        if loeser.Value(unbesetzt[s]):
            print(f"{slot:<12} {FAECHER[s]:<12} {'-- UNBESETZT --':<18} "
                  f"Kein qualifiziertes Personal verfuegbar")
        else:
            person = next(p for p in PERSONAL if loeser.Value(x[p, s]))
            print(f"{slot:<12} {FAECHER[s]:<12} {person:<18}")

    print("\n--- Diagnose ---")
    for s, slot in enumerate(SLOTS):
        if loeser.Value(unbesetzt[s]):
            fach = FAECHER[s]
            qualifiziert = [p for p in PERSONAL if fach in QUALIFIKATION[p]]
            print(f"  '{slot}' ({fach}): {len(qualifiziert)} qualifizierte Personen "
                  f"{qualifiziert if qualifiziert else '-> URSACHE: niemand kann dieses Fach'}")
    for p in PERSONAL:
        if loeser.Value(ueberlast[p]):
            print(f"  {p} arbeitet {loeser.Value(ueberlast[p])} Stunden ueber der Grenze.")

    print("\nDer Anwender bekommt jetzt einen Plan PLUS eine konkrete Ursache -")
    print("und kann handeln: Vertretung von aussen holen, Stunde verlegen,")
    print("oder die Klasse zusammenlegen.")
    print("=" * 78)
