"""
analysis.py — Risk metrics, scenario ranking, control ROI, sensitivity
analysis, and bootstrap confidence intervals for Open FAIR simulation results.
"""

from __future__ import annotations

import warnings
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

import numpy as np

from .simulation import SimulationResult


# ---------------------------------------------------------------------------
# RiskMetrics dataclass
# ---------------------------------------------------------------------------

@dataclass
class RiskMetrics:
    """Summary risk metrics derived from a :class:`~pycrq.simulation.SimulationResult`.

    Attributes
    ----------
    scenario_name: Name of the scenario.
    mean_ale:      Mean Annual Loss Exposure.
    std_ale:       Standard deviation of ALE.
    min_ale:       Minimum ALE observed in simulations.
    p10_ale:       10th percentile ALE.
    median_ale:    50th percentile (median) ALE.
    p90_ale:       90th percentile ALE.
    p95_ale:       95th percentile ALE.
    p99_ale:       99th percentile ALE.
    max_ale:       Maximum ALE observed.
    var_95:        Value-at-Risk at 95% confidence.
    cvar_95:       Conditional Value-at-Risk at 95% confidence.
    cv:            Coefficient of Variation (std / mean); measures relative uncertainty.
    mean_lef:      Mean Loss Event Frequency.
    p50_lef:       Median Loss Event Frequency.
    mean_lm:       Mean Loss Magnitude.
    p50_lm:        Median Loss Magnitude.
    risk_level:    Qualitative classification: LOW, MEDIUM, HIGH, CRITICAL.
    """

    scenario_name: str
    mean_ale: float
    std_ale: float
    min_ale: float
    p10_ale: float
    median_ale: float
    p90_ale: float
    p95_ale: float
    p99_ale: float
    max_ale: float
    var_95: float
    cvar_95: float
    cv: float
    mean_lef: float
    p50_lef: float
    mean_lm: float
    p50_lm: float
    risk_level: str


# ---------------------------------------------------------------------------
# compute_metrics
# ---------------------------------------------------------------------------

def compute_metrics(result: SimulationResult) -> RiskMetrics:
    """Compute all risk metrics from a :class:`~pycrq.simulation.SimulationResult`.

    Parameters
    ----------
    result: Simulation output for a single scenario.

    Returns
    -------
    :class:`RiskMetrics` populated with statistical summaries.
    """
    ale = result.annual_loss_exposure
    lef = result.loss_event_frequency
    lm = result.loss_magnitude

    mean_ale = float(np.mean(ale))
    std_ale = float(np.std(ale))
    cv = (std_ale / mean_ale) if mean_ale > 0 else 0.0

    return RiskMetrics(
        scenario_name=result.scenario_name,
        mean_ale=mean_ale,
        std_ale=std_ale,
        min_ale=float(np.min(ale)),
        p10_ale=float(np.percentile(ale, 10)),
        median_ale=float(np.percentile(ale, 50)),
        p90_ale=float(np.percentile(ale, 90)),
        p95_ale=float(np.percentile(ale, 95)),
        p99_ale=float(np.percentile(ale, 99)),
        max_ale=float(np.max(ale)),
        var_95=result.var(0.95),
        cvar_95=result.cvar(0.95),
        cv=cv,
        mean_lef=float(np.mean(lef)) if lef is not None else 0.0,
        p50_lef=float(np.percentile(lef, 50)) if lef is not None else 0.0,
        mean_lm=float(np.mean(lm)) if lm is not None else 0.0,
        p50_lm=float(np.percentile(lm, 50)) if lm is not None else 0.0,
        risk_level=classify_risk_level(mean_ale),
    )


# ---------------------------------------------------------------------------
# classify_risk_level
# ---------------------------------------------------------------------------

def classify_risk_level(mean_ale: float) -> str:
    """Return a qualitative risk level based on Mean ALE.

    Thresholds
    ----------
    LOW      : mean_ale <  $10,000
    MEDIUM   : $10,000 <= mean_ale < $100,000
    HIGH     : $100,000 <= mean_ale < $1,000,000
    CRITICAL : mean_ale >= $1,000,000

    Parameters
    ----------
    mean_ale: Mean Annual Loss Exposure in dollars.

    Returns
    -------
    One of ``"LOW"``, ``"MEDIUM"``, ``"HIGH"``, ``"CRITICAL"``.
    """
    if mean_ale < 10_000:
        return "LOW"
    if mean_ale < 100_000:
        return "MEDIUM"
    if mean_ale < 1_000_000:
        return "HIGH"
    return "CRITICAL"


# ---------------------------------------------------------------------------
# rank_scenarios
# ---------------------------------------------------------------------------

_SORT_KEY_MAP = {
    "mean_ale": lambda r: r.mean_ale,
    "p95_ale": lambda r: r.percentile(95),
    "median_ale": lambda r: r.percentile(50),
    "var_95": lambda r: r.var(0.95),
    "cvar_95": lambda r: r.cvar(0.95),
    "max_ale": lambda r: float(np.max(r.annual_loss_exposure)),
}


def rank_scenarios(
    results: List[SimulationResult],
    by: str = "mean_ale",
) -> List[Tuple[str, float]]:
    """Rank scenarios by the chosen metric (descending — highest risk first).

    Parameters
    ----------
    results: List of :class:`~pycrq.simulation.SimulationResult` objects.
    by:      Metric to sort by.  One of:
             ``'mean_ale'``, ``'p95_ale'``, ``'median_ale'``,
             ``'var_95'``, ``'cvar_95'``, ``'max_ale'``.

    Returns
    -------
    List of ``(scenario_name, metric_value)`` tuples, sorted descending.
    """
    if by not in _SORT_KEY_MAP:
        raise ValueError(
            f"Unknown ranking metric '{by}'. Choose from: {list(_SORT_KEY_MAP)}"
        )
    key_fn = _SORT_KEY_MAP[by]
    ranked = sorted(results, key=key_fn, reverse=True)
    return [(r.scenario_name, key_fn(r)) for r in ranked]


# ---------------------------------------------------------------------------
# compute_risk_reduction
# ---------------------------------------------------------------------------

def compute_risk_reduction(
    before: SimulationResult,
    after: SimulationResult,
) -> Dict[str, float]:
    """Compare risk metrics before and after a control or change.

    Parameters
    ----------
    before: Simulation result representing the baseline (pre-control) state.
    after:  Simulation result representing the improved (post-control) state.

    Returns
    -------
    Dictionary with absolute and percentage reductions for mean ALE, P95,
    VaR(95%), and CVaR(95%).  Positive values indicate risk reduction.
    """
    metrics = {}

    def _reduction(before_val: float, after_val: float, label: str) -> None:
        abs_red = before_val - after_val
        pct_red = (abs_red / before_val * 100.0) if before_val != 0 else 0.0
        metrics[f"{label}_before"] = before_val
        metrics[f"{label}_after"] = after_val
        metrics[f"{label}_reduction_abs"] = abs_red
        metrics[f"{label}_reduction_pct"] = pct_red

    _reduction(before.mean_ale, after.mean_ale, "mean_ale")
    _reduction(before.percentile(95), after.percentile(95), "p95_ale")
    _reduction(before.var(0.95), after.var(0.95), "var_95")
    _reduction(before.cvar(0.95), after.cvar(0.95), "cvar_95")

    return metrics


# ---------------------------------------------------------------------------
# control_roi
# ---------------------------------------------------------------------------

def control_roi(
    before: SimulationResult,
    after: SimulationResult,
    control_cost: float,
) -> Dict[str, Any]:
    """Compute Return on Security Investment (ROSI) for a control.

    The classic ROSI formula:
        ROSI% = (Risk Reduction - Control Cost) / Control Cost × 100

    Parameters
    ----------
    before:       Simulation result before the control is applied.
    after:        Simulation result after the control is applied.
    control_cost: Annual cost of the control in dollars.

    Returns
    -------
    Dictionary containing:

    - ``risk_reduction``:    Absolute ALE reduction ($ mean).
    - ``control_cost``:      Annual cost of the control.
    - ``net_benefit``:       risk_reduction − control_cost.
    - ``rosi_pct``:          ROSI as a percentage.
    - ``break_even_years``:  Years until cumulative savings cover the cost.
    - ``recommended``:       True if ROSI% > 0.
    - ``risk_reduction_pct``: Percentage reduction in mean ALE.
    """
    if control_cost < 0:
        raise ValueError(f"control_cost must be non-negative; got {control_cost}")

    reduction = before.mean_ale - after.mean_ale
    net_benefit = reduction - control_cost

    if control_cost > 0:
        rosi_pct = (net_benefit / control_cost) * 100.0
    else:
        rosi_pct = float("inf") if reduction > 0 else 0.0

    # Break-even: annual savings vs. one-time cost (treat as annual for simplicity)
    if reduction > 0 and control_cost > 0:
        break_even_years = control_cost / reduction
    elif reduction <= 0:
        break_even_years = float("inf")
    else:
        break_even_years = 0.0

    risk_reduction_pct = (
        (reduction / before.mean_ale * 100.0) if before.mean_ale > 0 else 0.0
    )

    return {
        "risk_reduction": reduction,
        "control_cost": control_cost,
        "net_benefit": net_benefit,
        "rosi_pct": rosi_pct,
        "break_even_years": break_even_years,
        "recommended": rosi_pct > 0,
        "risk_reduction_pct": risk_reduction_pct,
        "mean_ale_before": before.mean_ale,
        "mean_ale_after": after.mean_ale,
    }


# ---------------------------------------------------------------------------
# sensitivity_analysis
# ---------------------------------------------------------------------------

def sensitivity_analysis(
    scenario_factory: Callable[[Any], "FAIRScenario"],  # noqa: F821
    param_name: str,
    param_values: List[Any],
    n_simulations: int = 10_000,
    seed: Optional[int] = None,
) -> List[Dict[str, Any]]:
    """Run a one-at-a-time sensitivity analysis by varying a single parameter.

    Parameters
    ----------
    scenario_factory: A callable that accepts a single parameter value and
                      returns a :class:`~pycrq.ontology.FAIRScenario`.
    param_name:       Label for the parameter being varied (used in output).
    param_values:     List of values to pass to ``scenario_factory``.
    n_simulations:    Number of Monte Carlo iterations per run.
    seed:             Optional random seed for reproducibility.

    Returns
    -------
    List of dictionaries, one per ``param_value``, containing:
    ``param_value``, ``mean_ale``, ``median_ale``, ``p90_ale``,
    ``p95_ale``, ``std_ale``, ``var_95``, ``cvar_95``.
    """
    from .simulation import FAIRSimulator

    simulator = FAIRSimulator(n_simulations=n_simulations, seed=seed)
    results = []

    for val in param_values:
        scenario = scenario_factory(val)
        sim_result = simulator.simulate(scenario)
        metrics = compute_metrics(sim_result)
        results.append(
            {
                param_name: val,
                "mean_ale": metrics.mean_ale,
                "median_ale": metrics.median_ale,
                "p90_ale": metrics.p90_ale,
                "p95_ale": metrics.p95_ale,
                "std_ale": metrics.std_ale,
                "var_95": metrics.var_95,
                "cvar_95": metrics.cvar_95,
                "risk_level": metrics.risk_level,
            }
        )

    return results


# ---------------------------------------------------------------------------
# bootstrap_confidence_interval
# ---------------------------------------------------------------------------

def bootstrap_confidence_interval(
    result: SimulationResult,
    statistic: str = "mean",
    confidence: float = 0.95,
    n_bootstrap: int = 1_000,
) -> Tuple[float, float]:
    """Estimate a confidence interval for an ALE statistic via bootstrapping.

    Parameters
    ----------
    result:     Simulation result whose ALE array will be resampled.
    statistic:  The ALE statistic to estimate.  One of:
                ``'mean'``, ``'median'``, ``'p90'``, ``'p95'``, ``'std'``.
    confidence: Desired confidence level (default 0.95).
    n_bootstrap: Number of bootstrap resamples (default 1 000).

    Returns
    -------
    ``(lower, upper)`` confidence interval bounds.
    """
    if not 0 < confidence < 1:
        raise ValueError(f"confidence must be in (0, 1); got {confidence}")

    stat_fns: Dict[str, Callable] = {
        "mean": np.mean,
        "median": np.median,
        "p90": lambda x: np.percentile(x, 90),
        "p95": lambda x: np.percentile(x, 95),
        "std": np.std,
    }
    if statistic not in stat_fns:
        raise ValueError(
            f"Unknown statistic '{statistic}'. Choose from: {list(stat_fns)}"
        )

    stat_fn = stat_fns[statistic]
    ale = result.annual_loss_exposure
    n = len(ale)

    rng = np.random.default_rng()
    bootstrap_stats = np.empty(n_bootstrap)
    for i in range(n_bootstrap):
        resample = rng.choice(ale, size=n, replace=True)
        bootstrap_stats[i] = stat_fn(resample)

    alpha = (1.0 - confidence) / 2.0
    lower = float(np.percentile(bootstrap_stats, alpha * 100))
    upper = float(np.percentile(bootstrap_stats, (1 - alpha) * 100))
    return lower, upper
