"""
Monte Carlo — Financial risk simulation with Gaussian perturbations.
"""

from __future__ import annotations

import math
import statistics
from dataclasses import dataclass, field
from typing import Literal

import numpy as np

from bizanalyticsai.core.dataframe import BizFrame


@dataclass
class MonteCarloResult:
    """Results from a Monte Carlo simulation run."""

    iterations:             int
    months:                 int
    results:                list[float]   # sorted ascending
    p10:                    float
    p25:                    float
    p50:                    float
    p75:                    float
    p90:                    float
    mean:                   float
    std:                    float
    min:                    float
    max:                    float
    bankruptcy_probability: float         # %
    positive_probability:   float         # % of scenarios with positive outcome

    def summary(self) -> str:
        return (
            f"Monte Carlo ({self.iterations:,} iterations, {self.months}mo)\n"
            f"  P10 (pessimist):  {self.p10:>15,.2f}\n"
            f"  P50 (median):     {self.p50:>15,.2f}\n"
            f"  P90 (optimist):   {self.p90:>15,.2f}\n"
            f"  Mean:             {self.mean:>15,.2f}\n"
            f"  Std Dev:          {self.std:>15,.2f}\n"
            f"  Positive outcome: {self.positive_probability:.1f}%\n"
            f"  Bankruptcy risk:  {self.bankruptcy_probability:.1f}%"
        )

    def to_dataframe(self) -> BizFrame:
        """Return percentile summary as a BizFrame."""
        data = {
            "percentile": ["P10", "P25", "P50 (Median)", "P75", "P90"],
            "value":      [self.p10, self.p25, self.p50, self.p75, self.p90],
            "interpretation": [
                "Pessimistic (worst 10%)", "Conservative", "Most likely", "Optimistic", "Best case (top 10%)",
            ],
        }
        return BizFrame(data)


def _gaussian(mean: float = 0.0, std: float = 1.0) -> float:
    """Box-Muller Gaussian random number generator (no external dependency)."""
    u, v = 0.0, 0.0
    while u == 0.0:
        u = np.random.random()
    while v == 0.0:
        v = np.random.random()
    return mean + std * math.sqrt(-2.0 * math.log(u)) * math.cos(2.0 * math.pi * v)


def montecarlo(
    initial_revenue:    float,
    growth_pct:         float,
    fixed_costs:        float,
    variable_cost_pct:  float = 20.0,
    initial_investment: float = 0.0,
    months:             int   = 24,
    iterations:         int   = 10_000,
    revenue_sigma_pct:  float = 20.0,
    growth_sigma:       float = 0.02,
    seed:               int | None = None,
) -> MonteCarloResult:
    """
    Run a Monte Carlo financial simulation.

    At each month, revenue is perturbed by a Gaussian noise term:
        actual_revenue = revenue × N(1, revenue_sigma_pct/100)

    Revenue grows by:
        revenue *= (1 + N(growth_pct/100, growth_sigma))

    Bankruptcy is triggered when cumulative cash < -3× initial_investment.

    Args:
        initial_revenue:    Starting monthly revenue.
        growth_pct:         Expected monthly revenue growth (%).
        fixed_costs:        Fixed monthly costs.
        variable_cost_pct:  Variable costs as % of revenue.
        initial_investment: Initial capital outlay.
        months:             Simulation horizon in months.
        iterations:         Number of Monte Carlo iterations.
        revenue_sigma_pct:  Revenue uncertainty (standard deviation in %).
        growth_sigma:       Growth rate uncertainty (standard deviation).
        seed:               Random seed for reproducibility.

    Returns:
        MonteCarloResult dataclass.
    """
    if seed is not None:
        np.random.seed(seed)

    results       = np.empty(iterations)
    bankrupt_count = 0
    floor          = -max(initial_investment * 3, 1)

    for i in range(iterations):
        cash    = -initial_investment
        revenue = initial_revenue
        bankrupt = False

        for _ in range(months):
            mult   = 1 + np.random.normal(0, revenue_sigma_pct / 100)
            actual = max(0.0, revenue * mult)
            cost   = fixed_costs + actual * (variable_cost_pct / 100)
            cash  += actual - cost
            revenue *= (1 + np.random.normal(growth_pct / 100, growth_sigma))
            revenue  = max(0.0, revenue)
            if cash < floor:
                bankrupt = True
                break

        if bankrupt:
            bankrupt_count += 1
        results[i] = cash

    results.sort()

    def _p(q: float) -> float:
        return float(results[int(iterations * q)])

    return MonteCarloResult(
        iterations=iterations,
        months=months,
        results=results.tolist(),
        p10=_p(0.10),
        p25=_p(0.25),
        p50=_p(0.50),
        p75=_p(0.75),
        p90=_p(0.90),
        mean=round(float(results.mean()), 2),
        std=round(float(results.std()), 2),
        min=float(results[0]),
        max=float(results[-1]),
        bankruptcy_probability=round(bankrupt_count / iterations * 100, 2),
        positive_probability=round(float((results > 0).sum()) / iterations * 100, 2),
    )


def sensitivity_analysis(
    base_params: dict,
    param_ranges: dict[str, tuple[float, float, int]],
) -> BizFrame:
    """
    Sensitivity analysis — vary each parameter independently and observe P50 impact.

    Args:
        base_params:  Base parameters dict for montecarlo().
        param_ranges: Dict of {param_name: (min_val, max_val, n_steps)}.

    Returns:
        BizFrame with columns: parameter, value, p50, p90, bankruptcy_pct.

    Example::

        result = bai.finance.sensitivity_analysis(
            base_params=dict(
                initial_revenue=50_000, growth_pct=8, fixed_costs=20_000,
                variable_cost_pct=20, initial_investment=200_000, months=24,
            ),
            param_ranges={
                "growth_pct":   (0, 20, 10),
                "fixed_costs":  (10_000, 50_000, 8),
            },
        )
    """
    rows = []
    for param, (lo, hi, steps) in param_ranges.items():
        for val in np.linspace(lo, hi, steps):
            params = {**base_params, param: val}
            r = montecarlo(**params, iterations=2000)
            rows.append({
                "parameter":       param,
                "value":           round(val, 4),
                "p50":             r.p50,
                "p90":             r.p90,
                "bankruptcy_pct":  r.bankruptcy_probability,
                "positive_pct":    r.positive_probability,
            })
    return BizFrame(rows)


def simulate_paths(
    initial_revenue:    float,
    growth_pct:         float,
    fixed_costs:        float,
    variable_cost_pct:  float = 20.0,
    initial_investment: float = 0.0,
    months:             int   = 24,
    n_paths:            int   = 50,
    revenue_sigma_pct:  float = 20.0,
    seed:               int | None = None,
) -> BizFrame:
    """
    Generate N individual cash-flow paths for spaghetti chart visualization.

    Returns:
        BizFrame with columns: path_id, month, cumulative_cash, revenue, is_positive.
    """
    if seed is not None:
        np.random.seed(seed)

    rows = []
    for path_id in range(n_paths):
        cash    = -initial_investment
        revenue = initial_revenue
        rows.append({"path_id": path_id, "month": 0, "cumulative_cash": cash, "revenue": 0, "is_positive": cash >= 0})

        for m in range(1, months + 1):
            mult   = 1 + np.random.normal(0, revenue_sigma_pct / 100)
            actual = max(0.0, revenue * mult)
            cost   = fixed_costs + actual * (variable_cost_pct / 100)
            cash  += actual - cost
            revenue *= (1 + np.random.normal(growth_pct / 100, 0.02))
            revenue  = max(0.0, revenue)
            rows.append({
                "path_id":         path_id,
                "month":           m,
                "cumulative_cash": round(cash, 2),
                "revenue":         round(actual, 2),
                "is_positive":     cash >= 0,
            })

    return BizFrame(rows)


def histogram_bins(results: list[float], n_bins: int = 50) -> BizFrame:
    """
    Build histogram bins from Monte Carlo results.

    Returns:
        BizFrame with bin_min, bin_max, count, pct, is_negative columns.
    """
    arr = np.array(results)
    counts, edges = np.histogram(arr, bins=n_bins)
    total = len(arr)

    rows = []
    for i, count in enumerate(counts):
        rows.append({
            "bin_min":    round(float(edges[i]), 2),
            "bin_max":    round(float(edges[i + 1]), 2),
            "center":     round(float((edges[i] + edges[i + 1]) / 2), 2),
            "count":      int(count),
            "pct":        round(count / max(total, 1) * 100, 2),
            "is_negative": edges[i + 1] <= 0,
        })

    return BizFrame(rows)
