#!/usr/bin/env python3

# Sensitivitaetsanalyse.py
"""
Kapitel LP: Schattenpreis- und Sensitivitätsanalyse mit SciPy und HiGHS.

Achtung: Ohne Vorzeichenumkehr der Dualwerte waere die Handlungsempfehlung
strukturell immer "kein Zukauf noetig" - selbst bei harten Engpaessen. Dieses
Programm zeigt die korrekte Vorzeichenbehandlung.
"""

import numpy as np
from scipy.optimize import linprog

# --- Modell: Maximiere Deckungsbeitrag aus 3 Produkten ---------------------
#   max 40*x1 + 30*x2 + 50*x3
DECKUNGSBEITRAG = np.array([40.0, 30.0, 50.0])

# Ressourcenverbrauch je Produkt (Zeile = Ressource, Spalte = Produkt)
VERBRAUCH = np.array([
    [2.0, 1.0, 3.0],      # Montagezeit
    [1.0, 2.0, 1.0],      # Lackierzeit
    [1.0, 0.5, 2.0],      # Qualitätsprüfung
])
KAPAZITAET = np.array([120.0, 80.0, 50.0])          # Stunden
RESSOURCEN = ["Montage", "Lackieren", "Qualitätsprüfung"]
PRODUKTE = ["Produkt 1", "Produkt 2", "Produkt 3"]

ANGEBOTSPREIS_PRUEFSTUNDE = 18.0                     # EUR/h - lohnt sich der Zukauf?


def analysiere():
    # linprog MINIMIERT -> Zielfunktion negieren
    ergebnis = linprog(c=-DECKUNGSBEITRAG, A_ub=VERBRAUCH, b_ub=KAPAZITAET,
                       bounds=[(0, None)] * len(DECKUNGSBEITRAG), method="highs")
    if not ergebnis.success:
        raise SystemExit(f"Kein Optimum gefunden: {ergebnis.message}")

    max_gewinn = -ergebnis.fun
    mengen = ergebnis.x
    schlupf = ergebnis.slack

    # ------------------------------------------------------------------
    # DER ENTSCHEIDENDE PUNKT:
    # Weil wir zur Maximierung negiert haben, sind die Dualwerte aus
    # linprog fuer "<="-Bedingungen <= 0. Zurueckdrehen!
    # ------------------------------------------------------------------
    schattenpreise = -ergebnis.ineqlin.marginals

    print("=" * 74)
    print("      PRIMALE UND DUALE ERGEBNISANALYSE (SENSITIVITAET)")
    print("=" * 74)
    print(f"Maximaler Deckungsbeitrag: {max_gewinn:,.2f} EUR\n")

    print("--- Primalloesung: optimale Produktionsmengen ---")
    for name, menge in zip(PRODUKTE, mengen):
        print(f"  * {name}: {menge:8.2f} Stueck")

    print("\n--- Duale Analyse: Schattenpreise und Auslastung ---")
    for i, name in enumerate(RESSOURCEN):
        kapazitaet = KAPAZITAET[i]
        genutzt = kapazitaet - schlupf[i]
        auslastung = genutzt / kapazitaet * 100
        preis = schattenpreise[i]
        bindend = abs(schlupf[i]) < 1e-9

        print(f"\nRessource '{name}':")
        print(f"  Auslastung:    {genutzt:6.1f} / {kapazitaet:6.1f} h ({auslastung:5.1f} %)"
              f"  -> {'ENGPASS' if bindend else 'Reserve: %.1f h' % schlupf[i]}")
        print(f"  Schattenpreis: {preis:6.2f} EUR je zusaetzlicher Stunde")

        if preis > 1e-9:
            print(f"  >> Zusaetzliche Stunden lohnen sich bis zu einem Preis von "
                  f"{preis:.2f} EUR/h.")
        else:
            print(f"  >> Kein Zukauf noetig - die Kapazitaet ist nicht erschoepft.")

    # --- Konkrete Kaufentscheidung ---------------------------------------
    preis_pruefung = schattenpreise[RESSOURCEN.index("Qualitätsprüfung")]
    marge = preis_pruefung - ANGEBOTSPREIS_PRUEFSTUNDE
    print("\n" + "-" * 74)
    print(f"ENTSCHEIDUNG: Pruefstunden werden fuer "
          f"{ANGEBOTSPREIS_PRUEFSTUNDE:.2f} EUR/h angeboten.")
    print(f"  Schattenpreis:  {preis_pruefung:6.2f} EUR/h")
    print(f"  Angebotspreis:  {ANGEBOTSPREIS_PRUEFSTUNDE:6.2f} EUR/h")
    print(f"  Marge:          {marge:+6.2f} EUR je zugekaufter Stunde")
    print(f"  >> {'ZUKAUFEN' if marge > 0 else 'NICHT ZUKAUFEN'}")

    # --- Numerische Gegenprobe: Kapazitaet wirklich um 1 erhoehen --------
    kapazitaet_plus = KAPAZITAET.copy()
    kapazitaet_plus[RESSOURCEN.index("Qualitätsprüfung")] += 1.0
    gegenprobe = linprog(c=-DECKUNGSBEITRAG, A_ub=VERBRAUCH, b_ub=kapazitaet_plus,
                         bounds=[(0, None)] * 3, method="highs")
    tatsaechlicher_zuwachs = -gegenprobe.fun - max_gewinn
    print("\n--- Gegenprobe: Modell mit +1 Pruefstunde neu geloest ---")
    print(f"  Vorhergesagt (Schattenpreis): {preis_pruefung:8.4f} EUR")
    print(f"  Tatsaechlich gemessen:        {tatsaechlicher_zuwachs:8.4f} EUR")
    assert abs(tatsaechlicher_zuwachs - preis_pruefung) < 1e-6, \
        "Schattenpreis stimmt nicht mit der Messung ueberein!"
    print("  -> Der Schattenpreis ist bestaetigt.")

    # --- Komplementaerer Schlupf pruefen --------------------------------
    for s, y in zip(schlupf, schattenpreise):
        assert abs(s * y) < 1e-6, "Komplementaerer Schlupf verletzt!"
    print("\nPruefung: komplementaerer Schlupf fuer alle Ressourcen erfuellt.")
    print("=" * 74)


if __name__ == "__main__":
    analysiere()
