"""
Scenario modelling and risk adjustment.

Forrester TEI applies a *downward* risk adjustment to benefits (subtract
``risk_factor × benefit``) and an *upward* adjustment to costs (add
``risk_factor × cost``). Scenarios scale both adoption (cashflow magnitude)
and uncertainty (risk factor).

Default scenario multipliers are sensible starting points. Override per
study by editing the study's ``config.py`` and passing a custom dict to
:func:`apply_scenario`.
"""

from __future__ import annotations

from collections.abc import Iterable
from copy import deepcopy
from typing import Any

#: Default scenario multipliers used by Palladium notebooks.
#:
#: * ``adoption`` scales nominal annual values up or down.
#: * ``risk_delta`` is *added* to the benefit's risk_adjustment factor and
#:   *subtracted* from the cost's risk_adjustment factor (conservative
#:   = more uncertainty on benefits, less padding on costs).
SCENARIOS: dict[str, dict[str, float]] = {
    "conservative": {"adoption": 0.80, "risk_delta": 0.10},
    "moderate": {"adoption": 1.00, "risk_delta": 0.00},
    "aggressive": {"adoption": 1.15, "risk_delta": -0.05},
}


def risk_adjust_benefit(amount: float, risk_factor: float) -> float:
    """
    Apply a downward risk adjustment to a benefit.

    ``adjusted = amount × (1 − risk_factor)``.

    ``risk_factor`` is clamped to ``[0, 1]``.
    """
    rf = max(0.0, min(1.0, float(risk_factor)))
    return amount * (1.0 - rf)


def risk_adjust_cost(amount: float, risk_factor: float) -> float:
    """
    Apply an upward risk adjustment to a cost.

    ``adjusted = amount × (1 + risk_factor)``.

    ``risk_factor`` is clamped to ``[0, 1]``.
    """
    rf = max(0.0, min(1.0, float(risk_factor)))
    return amount * (1.0 + rf)


def _scale_yearly(values: Iterable[float], factor: float) -> list[float]:
    return [float(v) * factor for v in values]


def apply_scenario(
    items: list[dict],
    scenario: str = "moderate",
    *,
    multipliers: dict[str, dict[str, float]] | None = None,
    table: str | None = None,
) -> list[dict]:
    """
    Return a deep-copied list of value-rows with scenario adjustments applied.

    Each item is expected to have:
        - ``table`` (``'benefits'`` or ``'costs'``) — required for sign of
          risk_delta. If absent, defaults to ``benefits`` unless ``table=``
          is passed explicitly.
        - ``year_values`` (dict of year-string → float) **or** a scalar
          ``value``.
        - ``risk_adjustment`` (optional float).
        - ``initial`` (optional, costs only) — scaled by adoption.

    Args:
        items: friendly value rows (``year_values`` / ``value`` / ``initial``)
            as returned by :meth:`core.tei_client.TEIClient.get_values`.
        scenario: key into ``multipliers`` (default ``SCENARIOS``).
        multipliers: override map. Same shape as ``SCENARIOS``.
        table: force a table when items lack one.

    Returns:
        A new list — original items are not mutated.
    """
    cfg = (multipliers or SCENARIOS).get(scenario)
    if cfg is None:
        raise KeyError(f"Unknown scenario: {scenario!r}")
    adoption = float(cfg.get("adoption", 1.0))
    risk_delta = float(cfg.get("risk_delta", 0.0))

    out: list[dict] = []
    for raw in items:
        item: dict[str, Any] = deepcopy(raw)
        item_table = item.get("table") or table or "benefits"
        item["table"] = item_table

        # Adoption scaling
        if "year_values" in item and isinstance(item["year_values"], dict):
            item["year_values"] = {
                k: float(v) * adoption for k, v in item["year_values"].items()
            }
        if "value" in item and item["value"] is not None:
            try:
                item["value"] = float(item["value"]) * adoption
            except (TypeError, ValueError):
                pass
        if "initial" in item and item["initial"] is not None:
            try:
                item["initial"] = float(item["initial"]) * adoption
            except (TypeError, ValueError):
                pass

        # Risk adjustment delta
        ra = item.get("risk_adjustment")
        if ra is None:
            ra = 0.0
        if item_table == "benefits":
            new_ra = float(ra) + risk_delta
        else:  # costs: adverse scenario should *raise* costs less, so subtract
            new_ra = float(ra) - risk_delta
        item["risk_adjustment"] = max(0.0, min(1.0, new_ra))
        out.append(item)
    return out
