"""Portfolio performance metrics for empirical asset pricing.

Provides the PortfolioMetrics dataclass and evaluate_portfolio() function
for computing standard portfolio performance statistics including Sharpe ratio,
maximum drawdown, factor alpha, and higher moments.
"""

from __future__ import annotations

from dataclasses import dataclass

import numpy as np
import pandas as pd


@dataclass
class PortfolioMetrics:
    """Container for standard portfolio performance metrics.

    Attributes
    ----------
    annualized_return : float
        Mean return scaled to annual frequency.
    annualized_vol : float
        Standard deviation scaled to annual frequency.
    sharpe_ratio : float
        Annualized Sharpe ratio (excess return / vol).
    max_drawdown : float
        Maximum peak-to-trough drawdown (positive number).
    turnover : float
        Average portfolio turnover per period.
    ff_alpha : float
        Fama-French alpha from OLS on factor returns.
    ff_alpha_t : float
        t-statistic for the Fama-French alpha.
    information_ratio : float
        Alpha divided by residual standard deviation.
    skewness : float
        Third moment of return distribution.
    kurtosis : float
        Fourth moment (excess kurtosis) of return distribution.
    """

    annualized_return: float
    annualized_vol: float
    sharpe_ratio: float
    max_drawdown: float
    turnover: float
    ff_alpha: float
    ff_alpha_t: float
    information_ratio: float
    skewness: float
    kurtosis: float


def evaluate_portfolio(
    portfolio_returns: pd.Series[float],
    factor_returns: pd.DataFrame | None = None,
    rf_returns: pd.Series[float] | None = None,
    date_col: str = "date",
    annualize: bool = True,
    periods_per_year: int = 12,
) -> PortfolioMetrics:
    """Compute full performance metrics for a portfolio return series.

    Parameters
    ----------
    portfolio_returns : pd.Series
        Time series of portfolio returns (simple, not log).
    factor_returns : pd.DataFrame | None
        Factor return matrix for alpha regression (e.g. FF3).
    rf_returns : pd.Series | None
        Risk-free rate series aligned with portfolio_returns.
    date_col : str
        Unused; kept for API consistency with panel functions.
    annualize : bool
        Whether to annualize return and volatility.
    periods_per_year : int
        Number of return periods per year (12 for monthly, 252 for daily).

    Returns
    -------
    PortfolioMetrics
        Dataclass with all computed metrics.

    References
    ----------
    Fama, E. F., & French, K. R. (1993). Common risk factors in the
        returns on stocks and bonds. Journal of Financial Economics, 33(1), 3-56.
    """
    # Clean the return series by dropping NaN values
    r = portfolio_returns.dropna().astype(float)

    # Subtract risk-free rate if provided to get excess returns
    if rf_returns is not None:
        rf_aligned = rf_returns.reindex(r.index).fillna(0.0)
        excess = r - rf_aligned
    else:
        excess = r

    # Annualization multiplier
    ann_mult = periods_per_year if annualize else 1
    sqrt_mult = np.sqrt(ann_mult)

    # Core moments
    mean_r = float(r.mean())
    std_r = float(r.std(ddof=1)) if len(r) > 1 else 0.0
    mean_ex = float(excess.mean())
    std_ex = float(excess.std(ddof=1)) if len(excess) > 1 else 0.0

    annualized_return = mean_r * ann_mult
    annualized_vol = std_r * sqrt_mult

    # Sharpe ratio: annualized excess return / annualized vol of excess returns
    sharpe_ratio = (mean_ex / std_ex * sqrt_mult) if std_ex > 0 else 0.0

    # Maximum drawdown: largest peak-to-trough decline in cumulative wealth
    cum_wealth = (1.0 + r).cumprod()
    running_max = cum_wealth.cummax()
    drawdowns = (running_max - cum_wealth) / running_max
    max_drawdown = float(drawdowns.max()) if len(drawdowns) > 0 else 0.0

    # Higher moments
    skewness = float(r.skew()) if len(r) > 2 else 0.0  # type: ignore[arg-type]
    kurtosis = float(r.kurtosis()) if len(r) > 3 else 0.0  # type: ignore[arg-type]

    # Factor model alpha via OLS (if factor_returns provided)
    ff_alpha = 0.0
    ff_alpha_t = 0.0
    information_ratio = 0.0

    if factor_returns is not None and len(factor_returns) > 0:
        # Align factor returns with portfolio returns
        common_idx = r.index.intersection(factor_returns.index)
        if len(common_idx) > factor_returns.shape[1] + 1:
            y = np.asarray(excess.loc[common_idx].values, dtype=float)
            x_raw = np.asarray(factor_returns.loc[common_idx].values, dtype=float)
            # Add intercept column
            ones = np.ones((len(common_idx), 1))
            x_mat = np.hstack([ones, x_raw])

            # OLS: beta = (X'X)^{-1} X'y
            try:
                beta = np.linalg.lstsq(x_mat, y, rcond=None)[0]
                residuals = y - x_mat @ beta
                ff_alpha = float(beta[0]) * ann_mult

                # Standard error of alpha via OLS formula
                n_obs = len(y)
                k = x_mat.shape[1]
                resid_var = float(np.sum(residuals**2)) / max(n_obs - k, 1)
                # Variance of beta_hat = sigma^2 * (X'X)^{-1}
                xtx_inv = np.linalg.inv(x_mat.T @ x_mat)
                se_alpha = np.sqrt(resid_var * xtx_inv[0, 0])
                ff_alpha_t = float(beta[0] / se_alpha) if se_alpha > 0 else 0.0

                # Information ratio = alpha / residual vol (annualized)
                resid_std = float(np.std(residuals, ddof=1))
                if resid_std > 0:
                    information_ratio = float(beta[0]) / resid_std * sqrt_mult
            except np.linalg.LinAlgError:
                pass

    # Turnover: not computable from returns alone, default to 0
    turnover = 0.0

    return PortfolioMetrics(
        annualized_return=annualized_return,
        annualized_vol=annualized_vol,
        sharpe_ratio=sharpe_ratio,
        max_drawdown=max_drawdown,
        turnover=turnover,
        ff_alpha=ff_alpha,
        ff_alpha_t=ff_alpha_t,
        information_ratio=information_ratio,
        skewness=skewness,
        kurtosis=kurtosis,
    )
