"""
AI Gigafactory bidder economics, EuroHPC call EUROHPC-2026-CEI-AIGF-01.
Transparent cash-flow model. Every input is named, sourced in the accompanying note,
and can be overridden. Pre-tax, unlevered project cash flows in EUR millions, real terms
(no general inflation), plus a simple levered view.

Run: python3 aigf_model.py  -> prints tables and writes results.csv / sensitivity.csv
"""
import itertools, csv, math, sys, os

# ---------------------------------------------------------------- inputs (base case)
I = dict(
    # Tender mechanics (Tender Specifications V1, sections 3.d, 4, Annex 4; FWC I.6, SC Art. 11-14)
    h100eq_min   = {"Lot1": 25_000, "Lot2": 40_000},     # phase 1 minimum H100-equivalent units
    eu_cap_p1    = {"Lot1": 100.0,  "Lot2": 200.0},      # EUR m, EU phase 1 ceiling per site
    eu_cap_p2    = {"Lot1": 400.0,  "Lot2": 800.0},      # EUR m, indicative, not committed
    it_capex_share_cap = 0.17,                            # EU money <= 17% of IT CAPEX, per phase
    match_ratio  = 1.0,                                   # participating states at least match the EU
    offtake_years = 5,                                    # payments from T0 for a fixed 5 years
    build_months = 18,                                    # set-up period max 18 months to T0
    # Hardware (GB300 NVL72 as reference; spec performance index GB300 NVL72 = 4.55 H100-eq per GPU)
    rack_gpus    = 72,
    h100eq_per_gpu = 4.55,
    rack_cost_usd_m = 4.0,     # all-in per NVL72 rack incl. servers, fabric, cabling, software (IREN/Microsoft: $5.8bn / 200 MW; Loop, Morgan Stanley ~$4m)
    rack_kw      = 135.0,      # IT power per NVL72 rack, kW (vendor spec class; assumption)
    eurusd       = 1.12,       # ECB reference 22 Oct 2026: 1.1205
    it_overhead  = 1.00,       # multiplier on rack cost for anything the $4m all-in misses (sensitivity 1.2-1.35)
    # Facility
    facility_eur_m_per_mw = 11.0,   # shell, power, liquid cooling per MW IT (T&T 2025 Frankfurt $11.6m; JLL 2026 $11.3m + $1-2m liquid; Pantera $12m)
    facility_headroom = 1.25,       # build power envelope 25% above phase 1 IT need
    # Operations
    pue          = 1.15,       # direct-to-chip liquid cooling 1.10-1.20
    avg_draw     = 0.80,       # average IT draw as share of nameplate
    elec_eur_mwh = 165.0,      # Czechia, Eurostat nrg_pc_205 band IF, 2026-S1, excl. VAT (DE 127, FR 72, ES 78, FI 79, SE 88, IT 133, PL 184)
    other_opex_eur_m_per_mw = 0.30,   # staff, facility O&M, network, insurance (Epoch: ~$0.3m/MW/yr non-energy)
    hw_support_pct = 0.05,     # hardware maintenance/support, share of IT capex per year (vendor contracts 8-12%; self-supported operators lower)
    # Market revenue
    util         = 0.70,       # fleet utilisation of non-public capacity (no operator discloses; Pantera 60%, simulators 80%)
    price_h100eq_hr_eur_y1 = 2.00,  # EUR per H100-equivalent-hour, first operating year (B200 neocloud median $7.88/3.2 = $2.46; H100 1-yr contract $2.10-2.70 in 2026)
    price_decline = 0.20,      # per year, same hardware (H100 composite fell from $6.62 2H23 to $2.80 Mar 2026, about -25%/yr)
    # Public offtake price: the bidder sets it; competition scores price at 25%. Assume bid at market year-1 price, fixed nominal for 5 years.
    public_price_discount = 0.10,   # bid 10% under market year-1 price
    # Life and residual
    hw_life_years = 5,
    hw_residual   = 0.15,      # share of IT capex recovered after 5 years of operation (used H100 at 50-85% after 2 years, falling)
    facility_life_years = 15,
    # Capital
    wacc         = 0.10,       # hurdle for a merchant compute sponsor; IG utility-like ~6%, neocloud debt Euribor+4.5-5.5 = 8-9%, equity higher
    ltv          = 0.70, debt_rate = 0.085, equity_hurdle = 0.15,
)

HOURS = 8760.0

def site(lot, h100eq, phase2=False, I=I):
    """Phase 1 site economics. h100eq = phase 1 deployed H100-equivalent units."""
    gpus  = h100eq / I["h100eq_per_gpu"]
    racks = gpus / I["rack_gpus"]
    it_capex = racks * I["rack_cost_usd_m"] * I["it_overhead"] / I["eurusd"]          # EUR m
    it_mw = racks * I["rack_kw"] / 1000.0
    fac_mw = it_mw * I["facility_headroom"]
    fac_capex = fac_mw * I["facility_eur_m_per_mw"]
    capex = it_capex + fac_capex
    # public money
    eu = min(I["eu_cap_p1"][lot], I["it_capex_share_cap"] * it_capex)
    public_total = eu * (1 + I["match_ratio"])
    public_per_year = public_total / I["offtake_years"]
    # public price and the capacity it buys
    p1 = I["price_h100eq_hr_eur_y1"] * (1 - I["public_price_discount"])
    public_h100eq_hours_per_year = public_per_year * 1e6 / p1
    public_share_of_capacity = public_h100eq_hours_per_year / (h100eq * HOURS)
    # operating years
    years = I["offtake_years"]
    rows = []
    elec = it_mw * I["avg_draw"] * I["pue"] * HOURS * I["elec_eur_mwh"] / 1e6
    other = it_mw * I["other_opex_eur_m_per_mw"] + I["hw_support_pct"] * it_capex
    for y in range(1, years + 1):
        price = I["price_h100eq_hr_eur_y1"] * (1 - I["price_decline"]) ** (y - 1)
        u = I["util"] if not I.get("ramp") else min(I["util"], I["ramp"][min(y-1, len(I["ramp"])-1)])
        market_hours = (h100eq * HOURS - public_h100eq_hours_per_year) * u
        market_rev = market_hours * price / 1e6
        rev = market_rev + public_per_year
        ebitda = rev - elec - other
        rows.append(dict(year=y, price=price, market_rev=market_rev, public_rev=public_per_year, rev=rev, elec=elec, other=other, ebitda=ebitda))
    # cash flows: build over 18 months (two years of spend), then 5 operating years, residuals at end
    cf = [-capex * 0.4, -capex * 0.6]
    for r in rows:
        cf.append(r["ebitda"])
    fac_residual = fac_capex * (1 - I["offtake_years"] / I["facility_life_years"])
    cf[-1] += it_capex * I["hw_residual"] + fac_residual
    irr = _irr(cf)
    npv = sum(c / (1 + I["wacc"]) ** t for t, c in enumerate(cf))
    tot_rev = sum(r["rev"] for r in rows); tot_pub = public_per_year * years
    return dict(lot=lot, h100eq=h100eq, gpus=gpus, racks=racks, it_mw=it_mw, fac_mw=fac_mw,
                it_capex=it_capex, fac_capex=fac_capex, capex=capex, eu=eu, public_total=public_total,
                public_per_year=public_per_year, public_share_capacity=public_share_of_capacity,
                public_share_revenue=tot_pub / tot_rev, public_share_capex=public_total / capex,
                rows=rows, cf=cf, irr=irr, npv=npv, elec=elec, other=other,
                y1_rev=rows[0]["rev"], y1_ebitda=rows[0]["ebitda"], y5_ebitda=rows[-1]["ebitda"])

def _irr(cf, lo=-0.99, hi=5.0):
    f = lambda r: sum(c / (1 + r) ** t for t, c in enumerate(cf))
    if f(lo) * f(hi) > 0: return float("nan")
    for _ in range(200):
        mid = (lo + hi) / 2
        if f(lo) * f(mid) <= 0: hi = mid
        else: lo = mid
    return (lo + hi) / 2

def breakeven(lot, h100eq, var, lo, hi, target=None, I=I):
    """Find the value of input `var` at which project IRR equals wacc (or target)."""
    target = I["wacc"] if target is None else target
    J = dict(I)
    def g(v):
        J[var] = v
        return site(lot, h100eq, I=J)["irr"] - target
    a, b = lo, hi
    if math.isnan(g(a)) or math.isnan(g(b)) or g(a) * g(b) > 0: return float("nan")
    for _ in range(100):
        m = (a + b) / 2
        if g(a) * g(m) <= 0: b = m
        else: a = m
    return (a + b) / 2

def fmt(x, d=0):
    return "nan" if (isinstance(x, float) and math.isnan(x)) else f"{x:,.{d}f}"

def main():
    out = []
    print("=== Base inputs ===")
    for k in ("rack_cost_usd_m", "facility_eur_m_per_mw", "elec_eur_mwh", "pue", "util", "price_h100eq_hr_eur_y1", "price_decline", "hw_support_pct", "hw_residual", "wacc"):
        print(f"  {k} = {I[k]}")
    per_h100eq = I["rack_cost_usd_m"] * I["it_overhead"] / I["eurusd"] / (I["rack_gpus"] * I["h100eq_per_gpu"])  # EUR m per H100-eq
    full1 = int(round(I["eu_cap_p1"]["Lot1"] / I["it_capex_share_cap"] / per_h100eq, -3))
    full2 = int(round(I["eu_cap_p1"]["Lot2"] / I["it_capex_share_cap"] / per_h100eq, -3))
    cases = [("Lot1", 25_000, "Lot 1 minimum (25k)"), ("Lot1", 50_000, "Lot 1 score cap (50k)"), ("Lot1", full1, f"Lot 1 full EU draw ({full1//1000}k)"),
             ("Lot2", 40_000, "Lot 2 minimum (40k)"), ("Lot2", 70_000, "Lot 2 score cap (70k)"), ("Lot2", full2, f"Lot 2 full EU draw ({full2//1000}k)")]
    print("\n=== Phase 1 sites, base case ===")
    hdr = ["case", "GPUs", "racks", "IT MW", "IT capex", "facility", "total capex", "EU EURm", "public total", "public/yr", "public % capacity", "public % revenue", "public % capex", "Y1 revenue", "Y1 EBITDA", "Y5 EBITDA", "IRR", "NPV@wacc"]
    print(" | ".join(hdr))
    for lot, n, name in cases:
        s = site(lot, n)
        row = [name, fmt(s["gpus"]), fmt(s["racks"]), fmt(s["it_mw"], 1), fmt(s["it_capex"]), fmt(s["fac_capex"]), fmt(s["capex"]), fmt(s["eu"]), fmt(s["public_total"]), fmt(s["public_per_year"], 1),
               fmt(100 * s["public_share_capacity"], 1) + "%", fmt(100 * s["public_share_revenue"], 1) + "%", fmt(100 * s["public_share_capex"], 1) + "%",
               fmt(s["y1_rev"]), fmt(s["y1_ebitda"]), fmt(s["y5_ebitda"]), fmt(100 * s["irr"], 1) + "%", fmt(s["npv"])]
        print(" | ".join(row)); out.append(dict(zip(hdr, row)))
    with open(os.path.join(os.path.dirname(__file__), "results.csv"), "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=hdr); w.writeheader(); w.writerows(out)

    print("\n=== Break-evens, Lot 1 full EU draw (54k), IRR = wacc ===")
    for var, lo, hi, label in (("util", 0.05, 1.0, "utilisation"), ("price_h100eq_hr_eur_y1", 0.3, 6.0, "year-1 price EUR/H100eq-h"), ("price_decline", 0.0, 0.95, "annual price decline"), ("elec_eur_mwh", 10, 600, "electricity EUR/MWh"), ("rack_cost_usd_m", 1.0, 12.0, "rack cost USD m")):
        print(f"  {label}: {fmt(breakeven('Lot1', 41_000, var, lo, hi), 3)}")

    print("\n=== Sensitivity, Lot 1 full EU draw (54k): IRR by utilisation x annual price decline ===")
    utils = [0.5, 0.6, 0.7, 0.8, 0.9]; decs = [0.0, 0.1, 0.2, 0.3, 0.4]
    print("util \\ decline | " + " | ".join(f"{d:.0%}" for d in decs))
    sens = []
    for u in utils:
        J = dict(I); J["util"] = u
        cells = []
        for d in decs:
            J["price_decline"] = d
            irr = site("Lot1", 54_000, I=J)["irr"]
            cells.append(fmt(100 * irr, 0) + "%"); sens.append(dict(util=u, decline=d, irr=irr))
        print(f"{u:.0%} | " + " | ".join(cells))
    with open(os.path.join(os.path.dirname(__file__), "sensitivity.csv"), "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=["util", "decline", "irr"]); w.writeheader(); w.writerows(sens)

    print("\n=== Electricity: Lot 1 full draw IRR by host country price (Eurostat 2026-S1, excl. VAT, large consumers) ===")
    for c, p in (("France", 72), ("Spain", 78), ("Finland", 79), ("Sweden", 88), ("Germany", 127), ("Italy", 133), ("Czechia", 165), ("Poland", 184), ("US Texas", 63)):
        J = dict(I); J["elec_eur_mwh"] = p
        s = site("Lot1", 54_000, I=J)
        print(f"  {c:9s} {p:4d} EUR/MWh: electricity {fmt(s['elec'],1)} m/yr, IRR {fmt(100*s['irr'],1)}%")

    print("\n=== What the public money buys (Lot 1 full draw, 54k H100-eq) ===")
    s = site("Lot1", 54_000)
    print(f"  Public EUR {fmt(s['public_total'])} m over 5 years = {fmt(s['public_per_year'],1)} m/yr")
    print(f"  At the bid price it buys {fmt(100*s['public_share_capacity'],1)}% of the site's capacity; {fmt(100*s['public_share_revenue'],1)}% of 5-year revenue; {fmt(100*s['public_share_capex'],1)}% of capex")
    print(f"  Klaba (Reuters 23 Sep 2026): break-even 300-400 m/yr per site; public order about 40 m/yr = 10-15%")

    print("\n=== Phase 2: IT capex needed to draw the indicative EU money (17% rule) ===")
    for lot in ("Lot1", "Lot2"):
        need = I["eu_cap_p2"][lot] / I["it_capex_share_cap"]
        per_h100eq = I["rack_cost_usd_m"] * I["it_overhead"] / I["eurusd"] / (I["rack_gpus"] * I["h100eq_per_gpu"]) * 1e6  # EUR per H100-eq today
        print(f"  {lot}: EU {I['eu_cap_p2'][lot]} m indicative -> IT capex >= {fmt(need)} m; at today's EUR {fmt(per_h100eq)} per H100-eq that is {fmt(need*1e6/per_h100eq)} H100-eq, against a phase 2 minimum of {75_000 if lot=='Lot1' else 100_000}")

    print("\n=== Scenarios, Lot 1 full EU draw (54k): IRR and 5-year cumulative EBITDA vs capex ===")
    SC = {
      "Base (today's prices, 70% full from day one)": {},
      "Ramp (40/60/70% utilisation)": {"ramp": [0.4, 0.6, 0.7]},
      "Conservative (1.35x IT overhead, 8% support, 60% util, -30%/yr price, no residual)": {"it_overhead": 1.35, "hw_support_pct": 0.08, "util": 0.6, "price_decline": 0.3, "hw_residual": 0.0},
      "Bear (EUR 1.50 y1, -35%/yr, 50% util, ramp 30/50, 1.35x, 8%, no residual)": {"price_h100eq_hr_eur_y1": 1.5, "price_decline": 0.35, "util": 0.5, "ramp": [0.3, 0.5], "it_overhead": 1.35, "hw_support_pct": 0.08, "hw_residual": 0.0},
      "Empty (20% utilisation, otherwise base)": {"util": 0.2},
    }
    # revenue-per-MW calibrations: override price so that market revenue per IT MW matches a disclosed benchmark, flat for 5 years
    base_site = site("Lot1", 54_000)
    for label, usd_per_mw in (("Nebius mid-term contracts, $20-25m/MW/yr (Q2 2026 call), flat", 22.0), ("CoreWeave FY2025 realised, $5.1bn on ~850 MW = $6m/MW/yr, flat", 6.0)):
        eur_per_mw = usd_per_mw / I["eurusd"]
        # market revenue target per year = eur_per_mw * it_mw ; solve price with util=1 and no decline
        target = eur_per_mw * base_site["it_mw"]
        hours = base_site["h100eq"] * HOURS - base_site["public_per_year"] * 1e6 / (I["price_h100eq_hr_eur_y1"] * (1 - I["public_price_discount"]))
        price = target * 1e6 / hours
        SC[label] = {"price_h100eq_hr_eur_y1": price, "price_decline": 0.0, "util": 1.0}
    for name, over in SC.items():
        J = dict(I); J.update(over)
        s = site("Lot1", 54_000, I=J)
        cum = sum(r["ebitda"] for r in s["rows"])
        print(f"  {name}: capex {fmt(s['capex'])} m, Y1 rev {fmt(s['y1_rev'])} m, 5y EBITDA {fmt(cum)} m, IRR {fmt(100*s['irr'],1)}%, public share of revenue {fmt(100*s['public_share_revenue'],1)}%")

    print("\n=== Full scale: 120 MW (Lot 1) and 150 MW (Lot 2) filled with NVL72-class racks ===")
    for lot, mw, eu_total in (("Lot1", 120.0, 500.0), ("Lot2", 150.0, 1000.0)):
        racks = mw * 1000 / I["rack_kw"]; h = racks * I["rack_gpus"] * I["h100eq_per_gpu"]
        it = racks * I["rack_cost_usd_m"] * I["it_overhead"] / I["eurusd"]; fac = mw * I["facility_eur_m_per_mw"]
        rev_full = h * HOURS * I["util"] * I["price_h100eq_hr_eur_y1"] / 1e6
        pub = eu_total * (1 + I["match_ratio"])
        print(f"  {lot}: {fmt(racks)} racks, {fmt(h)} H100-eq, IT capex {fmt(it)} m + facility {fmt(fac)} m = {fmt(it+fac)} m; public ceiling EU+match {fmt(pub)} m = {fmt(100*pub/(it+fac),1)}% of capex, paid over two 5-year windows; market revenue needed to fill it at base price and utilisation {fmt(rev_full)} m/yr")

    print("\n=== Levered view, Lot 1 full draw, base case ===")
    s = site("Lot1", 54_000)
    debt = I["ltv"] * s["capex"]; interest = debt * I["debt_rate"]
    print(f"  Debt {fmt(debt)} m at {I['debt_rate']:.1%} = {fmt(interest)} m/yr interest; Y1 EBITDA {fmt(s['y1_ebitda'])} m; Y5 EBITDA {fmt(s['y5_ebitda'])} m; 5-year straight-line amortisation of debt = {fmt(debt/5)} m/yr")

if __name__ == "__main__":
    main()
