#!/usr/bin/env python3

# Stochastische_Optimierung.py
"""
Kapitel Unsicherheit: Two-Stage Stochastic Programming mit CVXPY.

Eigenschaften:
  * Die Analyse am Ende wird BERECHNET statt fest verdrahtet
  * Vergleich gegen drei Alternativen: Mittelwert, Worst Case, perfekte Voraussicht
  * Kennzahl EVPI (Wert perfekter Information) wird ausgewiesen
"""

import cvxpy as cp
import numpy as np
import pandas as pd

SZENARIEN = ["Ruhig", "Volatil", "Crash"]
WAHRSCHEINLICHKEIT = np.array([0.50, 0.30, 0.20])
BEDARF = np.array([100.0, 250.0, 500.0])

KOSTEN_VORAB = 40.0
KOSTEN_SPOT = 120.0
KOSTEN_LEERLAUF = 5.0

S = len(SZENARIEN)


def loese_stochastisch():
    """Zweistufiges Modell: eine Vorabentscheidung, szenarioabhaengige Korrektur."""
    x = cp.Variable(nonneg=True, name="Basiskapazitaet")        # Stufe 1
    y_spot = cp.Variable(S, nonneg=True, name="Spot_Zukauf")    # Stufe 2
    y_leer = cp.Variable(S, nonneg=True, name="Leerlauf")       # Stufe 2

    # Kopplung: Basis + Zukauf - Leerlauf == Bedarf (je Szenario)
    nebenbedingungen = [x + y_spot[s] - y_leer[s] == BEDARF[s] for s in range(S)]

    erwartete_korrektur = sum(
        WAHRSCHEINLICHKEIT[s] * (KOSTEN_SPOT * y_spot[s] + KOSTEN_LEERLAUF * y_leer[s])
        for s in range(S))

    problem = cp.Problem(cp.Minimize(KOSTEN_VORAB * x + erwartete_korrektur),
                         nebenbedingungen)
    problem.solve()
    return problem, x, y_spot, y_leer


def kosten_bei(kapazitaet):
    """Erwartete Gesamtkosten fuer eine fest vorgegebene Kapazitaet."""
    unter = np.maximum(BEDARF - kapazitaet, 0.0)
    ueber = np.maximum(kapazitaet - BEDARF, 0.0)
    je_szenario = KOSTEN_VORAB * kapazitaet + KOSTEN_SPOT * unter + KOSTEN_LEERLAUF * ueber
    return float(WAHRSCHEINLICHKEIT @ je_szenario)


if __name__ == "__main__":
    problem, x, y_spot, y_leer = loese_stochastisch()
    kapazitaet = float(x.value)
    mittelwert = float(WAHRSCHEINLICHKEIT @ BEDARF)

    print("=" * 82)
    print("     STOCHASTISCHE TWO-STAGE OPTIMIERUNG (KAPAZITAETSPLANUNG)")
    print("=" * 82)
    print(f"Status: {problem.status}")
    print(f"Erwarteter Bedarf (Mittelwert):   {mittelwert:.1f} Einheiten")
    print(f"Optimale Stufe-1-Kapazitaet x*:   {kapazitaet:.1f} Einheiten")
    print(f"Minimale erwartete Gesamtkosten:  {problem.value:,.2f} EUR\n")

    tabelle = pd.DataFrame({
        "Szenario": SZENARIEN,
        "Wahrsch.": [f"{p*100:.0f} %" for p in WAHRSCHEINLICHKEIT],
        "Bedarf": BEDARF,
        "Basis genutzt": [min(kapazitaet, b) for b in BEDARF],
        "Spot-Zukauf": np.round(y_spot.value, 1),
        "Leerlauf": np.round(y_leer.value, 1),
        "Kosten (EUR)": [f"{KOSTEN_VORAB*kapazitaet + KOSTEN_SPOT*y_spot.value[s] + KOSTEN_LEERLAUF*y_leer.value[s]:,.0f}"
                         for s in range(S)],
    })
    print(tabelle.to_string(index=False))

    # --- Vergleich mit Alternativstrategien (berechnet, nicht behauptet) --
    print("\n" + "-" * 82)
    print("Vergleich verschiedener Planungsstrategien:")
    print(f"{'Strategie':<34} {'Kapazitaet':>11} {'Erw. Kosten':>14} {'Mehrkosten':>13}")
    print("-" * 82)

    optimal = problem.value
    strategien = [
        ("Stochastisch optimal", kapazitaet),
        ("Naiv: Mittelwert einsetzen", mittelwert),
        ("Vorsichtig: Worst Case abdecken", float(BEDARF.max())),
        ("Optimistisch: Bestfall", float(BEDARF.min())),
    ]
    for name, kap in strategien:
        kosten = kosten_bei(kap)
        print(f"{name:<34} {kap:>11.1f} {kosten:>14,.0f} "
              f"{kosten - optimal:>+13,.0f}")

    # --- EVPI: Was waere perfekte Voraussicht wert? ----------------------
    # Bei perfekter Information wuerde man je Szenario genau den Bedarf kaufen.
    kosten_perfekt = float(WAHRSCHEINLICHKEIT @ (KOSTEN_VORAB * BEDARF))
    evpi = optimal - kosten_perfekt
    print("-" * 82)
    print(f"Kosten bei perfekter Voraussicht:      {kosten_perfekt:>10,.0f} EUR")
    print(f"Wert perfekter Information (EVPI):     {evpi:>10,.0f} EUR "
          f"({evpi/optimal*100:.1f} % der Kosten)")
    print("  -> So viel duerfte eine perfekte Bedarfsprognose hoechstens kosten.")

    # --- Automatische Interpretation --------------------------------------
    print("-" * 82)
    if kapazitaet > mittelwert + 1e-6:
        print(f"Analyse: Der Solver waehlt {kapazitaet:.0f} Einheiten und damit MEHR als")
        print(f"den Mittelwert ({mittelwert:.0f}), weil Unterdeckung ({KOSTEN_SPOT:.0f} EUR)")
        print(f"deutlich teurer ist als Leerlauf ({KOSTEN_LEERLAUF:.0f} EUR).")
    elif kapazitaet < mittelwert - 1e-6:
        print(f"Analyse: Der Solver waehlt {kapazitaet:.0f} und damit WENIGER als den")
        print(f"Mittelwert ({mittelwert:.0f}) - Leerlauf ist hier teurer als Zukauf.")
    else:
        print("Analyse: Kapazitaet entspricht dem Mittelwert (symmetrische Kosten).")
    print("=" * 82)
