"""Built-in metric calculators for pyfortis.

Stub implementations using stdlib math. For production use, install
the ``metrics`` extra (numpy/scipy) and register enhanced calculators.
"""

from __future__ import annotations

import math
from typing import Any

from pyfortis._types import MetricResult, MetricSpec, Position


def compute_var(
    metric: MetricSpec,
    positions: dict[str, Position],
    returns_data: list[float],
    context: dict[str, Any],
) -> MetricResult:
    """Value at Risk — historical percentile method (stdlib)."""
    confidence = metric.params.get("confidence", 0.95)
    if not returns_data:
        return MetricResult(metric_name=metric.name, value=0.0, message="No returns data")

    sorted_returns = sorted(returns_data)
    idx = int((1 - confidence) * len(sorted_returns))
    idx = max(0, min(idx, len(sorted_returns) - 1))
    var_value = -sorted_returns[idx]

    total_notional = sum(p.notional for p in positions.values()) if positions else 1.0
    var_dollar = var_value * total_notional

    breach = False
    threshold = metric.params.get("threshold")
    if threshold is not None and var_dollar > threshold:
        breach = True

    return MetricResult(
        metric_name=metric.name,
        value=var_dollar,
        breach=breach,
        severity=metric.severity,
        message=f"VaR({confidence:.0%}) = {var_dollar:,.2f}",
        details={"confidence": confidence, "var_pct": var_value},
    )


def compute_cvar(
    metric: MetricSpec,
    positions: dict[str, Position],
    returns_data: list[float],
    context: dict[str, Any],
) -> MetricResult:
    """Conditional VaR (Expected Shortfall)."""
    confidence = metric.params.get("confidence", 0.95)
    if not returns_data:
        return MetricResult(metric_name=metric.name, value=0.0, message="No returns data")

    sorted_returns = sorted(returns_data)
    cutoff = int((1 - confidence) * len(sorted_returns))
    cutoff = max(1, cutoff)
    tail = sorted_returns[:cutoff]
    cvar_pct = -sum(tail) / len(tail)

    total_notional = sum(p.notional for p in positions.values()) if positions else 1.0
    cvar_dollar = cvar_pct * total_notional

    return MetricResult(
        metric_name=metric.name,
        value=cvar_dollar,
        severity=metric.severity,
        message=f"CVaR({confidence:.0%}) = {cvar_dollar:,.2f}",
        details={"confidence": confidence, "cvar_pct": cvar_pct},
    )


def compute_drawdown(
    metric: MetricSpec,
    positions: dict[str, Position],
    returns_data: list[float],
    context: dict[str, Any],
) -> MetricResult:
    """Maximum drawdown from peak."""
    peak_value = context.get("peak_value", 0.0)
    current_value = context.get("current_value", 0.0)
    threshold = metric.params.get("threshold", 1.0)

    if peak_value <= 0:
        return MetricResult(
            metric_name=metric.name,
            value=0.0,
            message="No peak value, drawdown = 0",
        )

    drawdown = (peak_value - current_value) / peak_value
    breach = drawdown > threshold

    return MetricResult(
        metric_name=metric.name,
        value=drawdown,
        breach=breach,
        severity=metric.severity,
        message=f"Drawdown = {drawdown:.2%} (threshold {threshold:.2%})",
        details={"peak_value": peak_value, "current_value": current_value},
    )


def compute_volatility(
    metric: MetricSpec,
    positions: dict[str, Position],
    returns_data: list[float],
    context: dict[str, Any],
) -> MetricResult:
    """Annualized volatility from returns (stdlib)."""
    if len(returns_data) < 2:
        return MetricResult(
            metric_name=metric.name,
            value=0.0,
            message="Insufficient returns data for volatility",
        )

    n = len(returns_data)
    mean = sum(returns_data) / n
    variance = sum((r - mean) ** 2 for r in returns_data) / (n - 1)
    daily_vol = math.sqrt(variance)
    annual_factor = metric.params.get("annualization_factor", 252)
    annual_vol = daily_vol * math.sqrt(annual_factor)

    threshold = metric.params.get("threshold")
    breach = threshold is not None and annual_vol > threshold

    return MetricResult(
        metric_name=metric.name,
        value=annual_vol,
        breach=breach,
        severity=metric.severity,
        message=f"Annualized volatility = {annual_vol:.4f}",
        details={"daily_vol": daily_vol, "annual_factor": annual_factor},
    )


def compute_beta(
    metric: MetricSpec,
    positions: dict[str, Position],
    returns_data: list[float],
    context: dict[str, Any],
) -> MetricResult:
    """Portfolio beta vs benchmark (stdlib)."""
    benchmark_returns: list[float] = context.get("benchmark_returns", [])

    if len(returns_data) < 2 or len(benchmark_returns) < 2:
        return MetricResult(
            metric_name=metric.name,
            value=1.0,
            message="Insufficient data for beta, defaulting to 1.0",
        )

    n = min(len(returns_data), len(benchmark_returns))
    port = returns_data[:n]
    bench = benchmark_returns[:n]

    mean_p = sum(port) / n
    mean_b = sum(bench) / n

    cov = sum((port[i] - mean_p) * (bench[i] - mean_b) for i in range(n)) / (n - 1)
    var_b = sum((bench[i] - mean_b) ** 2 for i in range(n)) / (n - 1)

    beta = cov / var_b if var_b != 0 else 1.0

    return MetricResult(
        metric_name=metric.name,
        value=beta,
        severity=metric.severity,
        message=f"Beta = {beta:.4f}",
        details={"covariance": cov, "benchmark_variance": var_b},
    )


def compute_sharpe(
    metric: MetricSpec,
    positions: dict[str, Position],
    returns_data: list[float],
    context: dict[str, Any],
) -> MetricResult:
    """Sharpe ratio (stdlib)."""
    risk_free_rate = metric.params.get("risk_free_rate", 0.0)
    annual_factor = metric.params.get("annualization_factor", 252)

    if len(returns_data) < 2:
        return MetricResult(
            metric_name=metric.name,
            value=0.0,
            message="Insufficient data for Sharpe ratio",
        )

    n = len(returns_data)
    mean = sum(returns_data) / n
    variance = sum((r - mean) ** 2 for r in returns_data) / (n - 1)
    std = math.sqrt(variance)

    if std == 0:
        return MetricResult(
            metric_name=metric.name,
            value=0.0,
            message="Zero volatility, Sharpe undefined",
        )

    daily_rf = risk_free_rate / annual_factor
    sharpe = (mean - daily_rf) / std * math.sqrt(annual_factor)

    return MetricResult(
        metric_name=metric.name,
        value=sharpe,
        severity=metric.severity,
        message=f"Sharpe = {sharpe:.4f}",
    )


def compute_sortino(
    metric: MetricSpec,
    positions: dict[str, Position],
    returns_data: list[float],
    context: dict[str, Any],
) -> MetricResult:
    """Sortino ratio (stdlib) — downside deviation only."""
    risk_free_rate = metric.params.get("risk_free_rate", 0.0)
    annual_factor = metric.params.get("annualization_factor", 252)

    if len(returns_data) < 2:
        return MetricResult(
            metric_name=metric.name,
            value=0.0,
            message="Insufficient data for Sortino ratio",
        )

    n = len(returns_data)
    mean = sum(returns_data) / n
    daily_rf = risk_free_rate / annual_factor

    downside = [min(0, r - daily_rf) ** 2 for r in returns_data]
    downside_dev = math.sqrt(sum(downside) / (n - 1))

    if downside_dev == 0:
        return MetricResult(
            metric_name=metric.name,
            value=0.0,
            message="Zero downside deviation, Sortino undefined",
        )

    sortino = (mean - daily_rf) / downside_dev * math.sqrt(annual_factor)

    return MetricResult(
        metric_name=metric.name,
        value=sortino,
        severity=metric.severity,
        message=f"Sortino = {sortino:.4f}",
    )


BUILTIN_CALCULATORS: dict[str, Any] = {
    "var": compute_var,
    "cvar": compute_cvar,
    "drawdown": compute_drawdown,
    "volatility": compute_volatility,
    "beta": compute_beta,
    "sharpe": compute_sharpe,
    "sortino": compute_sortino,
}
