"""
Padel court financial model — server-side calculation engine.

Ported from planner.js calc()/pmt()/calcIRR() so the full financial
model is no longer exposed in client-side JavaScript.
"""
import math

from ..i18n import get_calc_item_names


# JS-compatible rounding: half-up (0.5 rounds to 1), not Python's
# banker's rounding (round-half-even).
def _round(n):
    return math.floor(n + 0.5)


# -- Default state (mirrors the JS `S` object) --
DEFAULTS = {
    "venue": "indoor",
    "own": "rent",
    "dblCourts": 4,
    "sglCourts": 0,
    "sqmPerDblHall": 250,
    "sqmPerSglHall": 160,
    "sqmPerDblOutdoor": 230,
    "sqmPerSglOutdoor": 150,
    "ratePeak": 50,
    "rateOffPeak": 35,
    "rateSingle": 30,
    "peakPct": 40,
    "hoursPerDay": 16,
    "daysPerMonthIndoor": 29,
    "daysPerMonthOutdoor": 25,
    "bookingFee": 10,
    "utilTarget": 40,
    "membershipRevPerCourt": 500,
    "fbRevPerCourt": 300,
    "coachingRevPerCourt": 200,
    "retailRevPerCourt": 80,
    "racketRentalRate": 15,
    "racketPrice": 5,
    "racketQty": 2,
    "ballRate": 10,
    "ballPrice": 3,
    "ballCost": 1.5,
    "courtCostDbl": 25000,
    "courtCostSgl": 15000,
    "shipping": 3000,
    "hallCostSqm": 500,
    "foundationSqm": 150,
    "landPriceSqm": 60,
    "hvac": 100000,
    "electrical": 60000,
    "sanitary": 80000,
    "parking": 50000,
    "fitout": 40000,
    "planning": 100000,
    "fireProtection": 80000,
    "floorPrep": 12000,
    "hvacUpgrade": 20000,
    "lightingUpgrade": 10000,
    "outdoorFoundation": 35,
    "outdoorSiteWork": 8000,
    "outdoorLighting": 4000,
    "outdoorFencing": 6000,
    "equipment": 2000,
    "workingCapital": 15000,
    "contingencyPct": 10,
    "rentSqm": 4,
    "outdoorRent": 400,
    "insurance": 300,
    "electricity": 600,
    "heating": 400,
    "maintenance": 300,
    "cleaning": 300,
    "marketing": 350,
    "staff": 0,
    "propertyTax": 250,
    "water": 125,
    "loanPct": 85,
    "interestRate": 5,
    "loanTerm": 10,
    "constructionMonths": 0,
    "holdYears": 5,
    "exitMultiple": 6,
    "annualRevGrowth": 2,
    "annualOpexGrowth": 2,
    "hurdleRate": 12,
    "interestOnlyMonths": 0,
    "budgetTarget": 0,
    "country": "DE",
    "permitsCompliance": 12000,
    "glassType": "standard",
    "lightingType": "led_standard",
    "ramp": [0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.82, 0.88, 0.93, 0.96, 0.98, 1],
    "season": [0, 0, 0, 0.7, 0.9, 1, 1, 1, 0.8, 0, 0, 0],
}


CURRENCY_DEFAULT = {"sym": "\u20ac", "eu_style": True}

COUNTRY_CURRENCY: dict[str, dict] = {
    "DE": CURRENCY_DEFAULT,
    "ES": CURRENCY_DEFAULT,
    "IT": CURRENCY_DEFAULT,
    "FR": CURRENCY_DEFAULT,
    "NL": CURRENCY_DEFAULT,
    "SE": CURRENCY_DEFAULT,
    "UK": {"sym": "\u00a3", "eu_style": False},
    "US": {"sym": "$", "eu_style": False},
}


def validate_state(s: dict) -> dict:
    """Apply defaults and coerce types. Returns a clean copy."""
    out = {**DEFAULTS}
    for k, default in DEFAULTS.items():
        if k not in s:
            continue
        v = s[k]
        if isinstance(default, list):
            if isinstance(v, list):
                out[k] = [float(x) for x in v]
        elif isinstance(default, float):
            try:
                out[k] = float(v)
            except (TypeError, ValueError):
                pass
        elif isinstance(default, int):
            try:
                out[k] = int(float(v))
            except (TypeError, ValueError):
                pass
        elif isinstance(default, str):
            out[k] = str(v)
    return out


def pmt(rate: float, nper: int, pv: float) -> float:
    """Loan payment calculation (matches JS pmt exactly)."""
    if rate == 0:
        return pv / nper
    return pv * rate * math.pow(1 + rate, nper) / (math.pow(1 + rate, nper) - 1)


def calc_irr(cfs: list[float], guess: float = 0.1) -> float:
    """Newton-Raphson IRR solver (matches JS calcIRR exactly)."""
    r = guess
    for _ in range(300):
        npv = 0.0
        d = 0.0
        for t, cf in enumerate(cfs):
            npv += cf / math.pow(1 + r, t)
            d -= t * cf / math.pow(1 + r, t + 1)
        if abs(d) < 1e-12:
            break
        nr = r - npv / d
        if abs(nr - r) < 1e-9:
            return nr
        r = nr
        if r < -0.99:
            r = -0.99
        if r > 10:
            r = 10
    return r


def calc(s: dict, lang: str = "en") -> dict:
    """
    Main financial model. Takes validated state dict, returns full
    derived-data dict (the `d` object from the JS version).
    """
    names = get_calc_item_names(lang)
    sym = COUNTRY_CURRENCY.get(s.get("country", "DE"), CURRENCY_DEFAULT)["sym"]
    d: dict = {}
    is_in = s["venue"] == "indoor"
    is_buy = s["own"] == "buy"

    d["totalCourts"] = s["dblCourts"] + s["sglCourts"]
    total_courts = d["totalCourts"]

    d["hallSqm"] = (
        (s["dblCourts"] * s["sqmPerDblHall"] + s["sglCourts"] * s["sqmPerSglHall"] + 200 + total_courts * 20)
        if total_courts
        else 0
    )
    d["outdoorLandSqm"] = (
        (s["dblCourts"] * s["sqmPerDblOutdoor"] + s["sglCourts"] * s["sqmPerSglOutdoor"] + 100)
        if total_courts
        else 0
    )
    d["sqm"] = d["hallSqm"] if is_in else d["outdoorLandSqm"]

    # -- Multipliers for glass and lighting --
    glass_mult = 1.4 if s["glassType"] == "panoramic" else 1.0
    light_mult = 1.5 if s["lightingType"] == "led_competition" else 1.0
    # Natural light zeroes out lighting costs (outdoor only)
    if s["lightingType"] == "natural" and not is_in:
        light_mult = 0

    # -- CAPEX --
    capex_items: list[dict] = []

    def ci(name: str, amount: float, info: str = ""):
        capex_items.append({"name": name, "amount": _round(amount), "info": info})

    ci(
        names["padel_courts"],
        (s["dblCourts"] * s["courtCostDbl"] + s["sglCourts"] * s["courtCostSgl"]) * glass_mult,
        f"{s['dblCourts']}\u00d7dbl + {s['sglCourts']}\u00d7sgl"
        + (" (panoramic)" if s["glassType"] == "panoramic" else ""),
    )
    ci(names["shipping"], math.ceil(total_courts / 2) * s["shipping"] if total_courts else 0)

    if is_in:
        if is_buy:
            ci(names["hall_construction"], d["hallSqm"] * s["hallCostSqm"],
               f"{d['hallSqm']}m\u00b2 \u00d7 {sym}{s['hallCostSqm']}/m\u00b2")
            ci(names["foundation"], d["hallSqm"] * s["foundationSqm"],
               f"{d['hallSqm']}m\u00b2 \u00d7 {sym}{s['foundationSqm']}/m\u00b2")
            land_sqm = _round(d["hallSqm"] * 1.25)
            ci(names["land_purchase"], land_sqm * s["landPriceSqm"],
               f"{land_sqm}m\u00b2 \u00d7 {sym}{s['landPriceSqm']}/m\u00b2")
            ci(names["transaction_costs"], _round(land_sqm * s["landPriceSqm"] * 0.1), "~10% of land")
            ci(names["hvac_system"], s["hvac"])
            ci(names["electrical_lighting"], s["electrical"] * light_mult)
            ci(names["sanitary_changing"], s["sanitary"])
            ci(names["parking_exterior"], s["parking"])
            ci(names["planning_permits"], s["planning"])
            ci(names["fire_protection"], s["fireProtection"])
        else:
            ci(names["floor_preparation"], s["floorPrep"])
            ci(names["hvac_upgrade"], s["hvacUpgrade"])
            ci(names["lighting_upgrade"], s["lightingUpgrade"] * light_mult)
            ci(names["fitout_reception"], s["fitout"])
            ci(names["permits_compliance"], s["permitsCompliance"])
    else:
        ci(names["concrete_foundation"], (s["dblCourts"] * 250 + s["sglCourts"] * 150) * s["outdoorFoundation"])
        ci(names["site_work"], s["outdoorSiteWork"])
        ci(names["outdoor_lighting"], total_courts * s["outdoorLighting"] * light_mult)
        ci(names["fencing"], s["outdoorFencing"])
        ci(names["permits_compliance"], s["permitsCompliance"])
        if is_buy:
            ci(names["land_purchase"], d["outdoorLandSqm"] * s["landPriceSqm"],
               f"{d['outdoorLandSqm']}m\u00b2 \u00d7 {sym}{s['landPriceSqm']}/m\u00b2")
            ci(names["transaction_costs"], _round(d["outdoorLandSqm"] * s["landPriceSqm"] * 0.1))

    ci(names["equipment"], s["equipment"] + total_courts * 300)
    ci(names["working_capital"], s["workingCapital"])
    ci(names["miscellaneous"], 8000 if is_buy else 6000)

    sub = sum(i["amount"] for i in capex_items)
    cont = _round(sub * s["contingencyPct"] / 100)
    if s["contingencyPct"] > 0:
        ci(f"{names['contingency']} ({s['contingencyPct']}%)", cont)

    d["capexItems"] = capex_items
    d["capex"] = sub + cont
    d["capexPerCourt"] = d["capex"] / total_courts if total_courts > 0 else 0
    d["capexPerSqm"] = d["capex"] / d["sqm"] if d["sqm"] > 0 else 0

    # -- OPEX --
    opex_items: list[dict] = []

    def oi(name: str, amount: float, info: str = ""):
        opex_items.append({"name": name, "amount": _round(amount), "info": info})

    rent_amount = 0
    if not is_buy:
        if is_in:
            rent_amount = _round(d["hallSqm"] * s["rentSqm"])
            oi(names["rent"], d["hallSqm"] * s["rentSqm"],
               f"{d['hallSqm']}m\u00b2 \u00d7 {sym}{s['rentSqm']}/m\u00b2")
        else:
            rent_amount = s["outdoorRent"]
            oi(names["rent"], s["outdoorRent"])
    else:
        oi(names["property_tax"], s["propertyTax"])

    oi(names["insurance"], s["insurance"])
    oi(names["electricity"], s["electricity"])
    if is_in:
        oi(names["heating"], s["heating"])
        oi(names["water"], s["water"])
    oi(names["maintenance"], s["maintenance"])
    if is_in:
        oi(names["cleaning"], s["cleaning"])
    oi(names["marketing_misc"], s["marketing"])
    if s["staff"] > 0:
        oi(names["staff"], s["staff"])

    d["opexItems"] = opex_items
    d["opex"] = sum(i["amount"] for i in opex_items)
    d["annualOpex"] = d["opex"] * 12

    # -- Financing --
    d["equity"] = _round(d["capex"] * (1 - s["loanPct"] / 100))
    d["loanAmount"] = d["capex"] - d["equity"]
    d["monthlyPayment"] = (
        pmt(s["interestRate"] / 100 / 12, max(s["loanTerm"], 1) * 12, d["loanAmount"])
        if d["loanAmount"] > 0
        else 0
    )
    d["annualDebtService"] = d["monthlyPayment"] * 12
    d["ltv"] = d["loanAmount"] / d["capex"] if d["capex"] > 0 else 0

    # -- Revenue model --
    dpm = s["daysPerMonthIndoor"] if is_in else s["daysPerMonthOutdoor"]
    d["daysPerMonth"] = dpm

    if total_courts > 0:
        w_rate = (
            s["dblCourts"] * (s["ratePeak"] * s["peakPct"] / 100 + s["rateOffPeak"] * (1 - s["peakPct"] / 100))
            + s["sglCourts"] * s["rateSingle"]
        ) / total_courts
    else:
        w_rate = s["ratePeak"]
    d["weightedRate"] = w_rate

    d["availHoursMonth"] = s["hoursPerDay"] * dpm * total_courts
    d["bookedHoursMonth"] = d["availHoursMonth"] * (s["utilTarget"] / 100)

    d["courtRevMonth"] = d["bookedHoursMonth"] * w_rate
    d["feeDeduction"] = d["courtRevMonth"] * (s["bookingFee"] / 100)
    d["racketRev"] = d["bookedHoursMonth"] * (s["racketRentalRate"] / 100) * s["racketQty"] * s["racketPrice"]
    d["ballMargin"] = d["bookedHoursMonth"] * (s["ballRate"] / 100) * (s["ballPrice"] - s["ballCost"])
    d["membershipRev"] = total_courts * s["membershipRevPerCourt"]
    d["fbRev"] = total_courts * s["fbRevPerCourt"]
    d["coachingRev"] = total_courts * s["coachingRevPerCourt"]
    d["retailRev"] = total_courts * s["retailRevPerCourt"]

    d["grossRevMonth"] = (
        d["courtRevMonth"] + d["racketRev"] + d["ballMargin"]
        + d["membershipRev"] + d["fbRev"] + d["coachingRev"] + d["retailRev"]
    )
    d["netRevMonth"] = d["grossRevMonth"] - d["feeDeduction"]
    d["ebitdaMonth"] = d["netRevMonth"] - d["opex"]
    d["netCFMonth"] = d["ebitdaMonth"] - d["monthlyPayment"]

    # -- 60-month cash flow projection --
    # Fix 1: annualRevGrowth applied to all revenue streams.
    # Fix 8: annualOpexGrowth applied to all operating costs (utilities, staff, insurance inflate).
    # Fix 10: interest-only period — first N months pay only interest, not P+I.
    months: list[dict] = []
    for m in range(1, 61):
        cm = (m - 1) % 12
        yr = math.ceil(m / 12)
        ramp = s["ramp"][m - 1] if m <= 12 else 1
        seas = 1 if is_in else s["season"][cm]
        eff_util = (s["utilTarget"] / 100) * ramp * seas
        avail = s["hoursPerDay"] * dpm * total_courts if seas > 0 else 0
        booked = avail * eff_util

        # Revenue growth compounds from Year 2 onwards (Year 1 = base)
        rev_growth = math.pow(1 + s["annualRevGrowth"] / 100, max(0, yr - 1))
        court_rev = booked * w_rate * rev_growth
        fees = -court_rev * (s["bookingFee"] / 100)
        ancillary = booked * (
            (s["racketRentalRate"] / 100) * s["racketQty"] * s["racketPrice"]
            + (s["ballRate"] / 100) * (s["ballPrice"] - s["ballCost"])
        ) * rev_growth
        membership = total_courts * s["membershipRevPerCourt"] * (ramp if seas > 0 else 0) * rev_growth
        fb = total_courts * s["fbRevPerCourt"] * (ramp if seas > 0 else 0) * rev_growth
        coaching = total_courts * s["coachingRevPerCourt"] * (ramp if seas > 0 else 0) * rev_growth
        retail = total_courts * s["retailRevPerCourt"] * (ramp if seas > 0 else 0) * rev_growth
        total_rev = court_rev + fees + ancillary + membership + fb + coaching + retail

        # OPEX inflates from Year 2 onwards (utilities, staff, insurance)
        opex_growth = math.pow(1 + s["annualOpexGrowth"] / 100, max(0, yr - 1))
        opex_val = -(d["opex"] * opex_growth)

        # Fix 10: interest-only period — lower debt service during construction/ramp
        if m <= s["interestOnlyMonths"] and d["loanAmount"] > 0:
            # Interest-only payment: loan balance × monthly rate
            loan = -(d["loanAmount"] * s["interestRate"] / 100 / 12)
        else:
            loan = -d["monthlyPayment"]

        ebitda = total_rev + opex_val
        ncf = ebitda + loan
        prev = months[-1] if months else None
        cum = (prev["cum"] if prev else -d["capex"]) + ncf
        months.append({
            "m": m, "cm": cm + 1, "yr": yr, "ramp": ramp, "seas": seas,
            "effUtil": eff_util, "avail": avail, "booked": booked,
            "courtRev": court_rev, "fees": fees, "ancillary": ancillary,
            "membership": membership, "totalRev": total_rev, "opex": opex_val,
            "loan": loan, "ebitda": ebitda, "ncf": ncf, "cum": cum,
        })
    d["months"] = months

    # -- Annual summaries --
    annuals: list[dict] = []
    for y in range(1, 6):
        ym = [m for m in months if m["yr"] == y]
        annuals.append({
            "year": y,
            "revenue": sum(m["totalRev"] for m in ym),
            "ebitda": sum(m["ebitda"] for m in ym),
            "ncf": sum(m["ncf"] for m in ym),
            "ds": sum(abs(m["loan"]) for m in ym),
            "booked": sum(m["booked"] for m in ym),
            "avail": sum(m["avail"] for m in ym),
        })
    d["annuals"] = annuals

    # -- Returns & exit --
    # Fix 5: use terminal year EBITDA (exit year), not hardcoded Year 3
    exit_yr_idx = min(s["holdYears"] - 1, len(annuals) - 1)
    d["stabEbitda"] = annuals[exit_yr_idx]["ebitda"]
    d["exitValue"] = d["stabEbitda"] * s["exitMultiple"]

    # Fix 4: remaining loan via amortization math (PV of remaining payments),
    # replacing the heuristic loanAmount * max(0, 1 - holdYears / (loanTerm * 1.5))
    k = s["holdYears"] * 12  # number of P+I payments made (after interest-only period)
    n = max(s["loanTerm"], 1) * 12
    r_monthly_loan = s["interestRate"] / 100 / 12
    if r_monthly_loan > 0 and d["loanAmount"] > 0 and n > k:
        d["remainingLoan"] = _round(
            d["monthlyPayment"] * (1 - math.pow(1 + r_monthly_loan, -(n - k))) / r_monthly_loan
        )
    elif d["loanAmount"] > 0 and n > k:
        # Zero-interest loan: straight-line amortization
        d["remainingLoan"] = _round(d["loanAmount"] * (n - k) / n)
    else:
        d["remainingLoan"] = 0

    d["netExit"] = d["exitValue"] - d["remainingLoan"]

    # Fix 2: equity IRR — use equity invested as initial outflow (not full capex).
    # NCFs are already post-debt-service (levered), so the denominator must match.
    # Using capex here would produce a hybrid metric that's neither equity IRR
    # nor project IRR — it systematically understates returns for leveraged deals.
    irr_cfs = [-d["equity"]]
    for y in range(s["holdYears"]):
        ycf = annuals[y]["ncf"] if y < len(annuals) else (annuals[-1]["ncf"] if annuals else 0)
        if y == s["holdYears"] - 1:
            irr_cfs.append(ycf + d["netExit"])
        else:
            irr_cfs.append(ycf)
    d["irr"] = calc_irr(irr_cfs)

    # Project IRR (unlevered): uses full capex as outflow and EBITDA as cash flows.
    # Useful for lender analysis and comparing across capital structures.
    unlevered_cfs = [-d["capex"]]
    for y in range(s["holdYears"]):
        ya = annuals[y] if y < len(annuals) else annuals[-1]
        if y == s["holdYears"] - 1:
            unlevered_cfs.append(ya["ebitda"] + d["netExit"])
        else:
            unlevered_cfs.append(ya["ebitda"])
    d["projectIrr"] = calc_irr(unlevered_cfs)

    # Fix 3: NPV at hurdle rate (discounts equity NCFs + exit at hurdleRate)
    r_hurdle_monthly = math.pow(1 + s["hurdleRate"] / 100, 1 / 12) - 1
    pv_ncf = sum(m["ncf"] / math.pow(1 + r_hurdle_monthly, m["m"]) for m in months)
    pv_exit = d["netExit"] / math.pow(1 + s["hurdleRate"] / 100, s["holdYears"])
    d["npv"] = _round(-d["equity"] + pv_ncf + pv_exit)
    d["npvPositive"] = d["npv"] >= 0

    d["totalReturned"] = sum(irr_cfs[1:])

    # Fix 6: leveraged MOIC (equity cash flows / equity invested — what the investor earns).
    # Also keep project MOIC (total returns / capex) for reference.
    equity_cfs = irr_cfs[1:]
    d["moic"] = sum(equity_cfs) / d["equity"] if d["equity"] > 0 else 0
    d["projectMoic"] = d["totalReturned"] / d["capex"] if d["capex"] > 0 else 0

    # Fix 7: return decomposition / value bridge (PE-style attribution).
    # Shows what drove equity returns: operational improvement vs. financial leverage.
    entry_ebitda = annuals[0]["ebitda"] if annuals else 0
    ebitda_growth_value = (d["stabEbitda"] - entry_ebitda) * s["exitMultiple"]
    deleverage_value = d["loanAmount"] - d["remainingLoan"]
    d["valueDrivers"] = {
        "ebitda_growth": _round(ebitda_growth_value),
        "deleverage": _round(deleverage_value),
        "entry_equity": d["equity"],
        "exit_equity": _round(d["netExit"]),
    }

    d["dscr"] = [
        {"year": a["year"], "dscr": a["ebitda"] / a["ds"] if a["ds"] > 0 else 999}
        for a in annuals
    ]

    # Fix 9: LTV and DSCR warnings for lender compliance thresholds
    d["ltvWarning"] = d["ltv"] > 0.75  # above typical commercial RE lending limit
    d["dscrWarning"] = any(row["dscr"] < 1.25 for row in d["dscr"] if row["dscr"] < 999)
    d["dscrMinYear"] = None
    if d["dscrWarning"]:
        d["dscrMinYear"] = min(
            (row["year"] for row in d["dscr"] if row["dscr"] < 999),
            key=lambda yr: next(r["dscr"] for r in d["dscr"] if r["year"] == yr),
            default=None,
        )

    payback_idx = -1
    for i, m in enumerate(months):
        if m["cum"] >= 0:
            payback_idx = i
            break
    d["paybackIdx"] = payback_idx

    # -- Efficiency metrics --
    rev_per_hr = (
        w_rate * (1 - s["bookingFee"] / 100)
        + (s["racketRentalRate"] / 100) * s["racketQty"] * s["racketPrice"]
        + (s["ballRate"] / 100) * (s["ballPrice"] - s["ballCost"])
    )
    fixed_month = d["opex"] + d["monthlyPayment"]
    d["breakEvenHrs"] = fixed_month / max(rev_per_hr, 0.01)
    d["breakEvenUtil"] = d["breakEvenHrs"] / d["availHoursMonth"] if d["availHoursMonth"] > 0 else 1
    d["breakEvenHrsPerCourt"] = d["breakEvenHrs"] / total_courts / dpm if total_courts > 0 else 0
    d["revPAH"] = d["netRevMonth"] / d["availHoursMonth"] if d["availHoursMonth"] > 0 else 0
    d["revPerSqm"] = (d["netRevMonth"] * 12) / d["sqm"] if d["sqm"] > 0 else 0
    d["ebitdaMargin"] = d["ebitdaMonth"] / d["netRevMonth"] if d["netRevMonth"] > 0 else 0
    d["opexRatio"] = d["opex"] / d["netRevMonth"] if d["netRevMonth"] > 0 else 0

    # Rent ratio — use the tracked rent_amount (set when oi() for rent was called)
    d["rentRatio"] = rent_amount / d["netRevMonth"] if d["netRevMonth"] > 0 else 0

    d["cashOnCash"] = (annuals[2]["ncf"] if len(annuals) >= 3 else 0) / d["equity"] if d["equity"] > 0 else 0
    d["yieldOnCost"] = d["stabEbitda"] / d["capex"] if d["capex"] > 0 else 0
    d["debtYield"] = d["stabEbitda"] / d["loanAmount"] if d["loanAmount"] > 0 else 0
    d["costPerBookedHr"] = (
        (d["opex"] + d["monthlyPayment"]) / d["bookedHoursMonth"]
        if d["bookedHoursMonth"] > 0
        else 0
    )
    d["avgUtil"] = (
        annuals[2]["booked"] / annuals[2]["avail"]
        if len(annuals) >= 3 and annuals[2]["avail"] > 0
        else 0
    )

    # -- Budget comparison --
    d["budgetTarget"] = s["budgetTarget"]
    d["budgetVariance"] = d["capex"] - s["budgetTarget"] if s["budgetTarget"] > 0 else 0
    d["budgetPct"] = d["capex"] / s["budgetTarget"] * 100 if s["budgetTarget"] > 0 else 0

    return d
