"""Fama-French factor regressions to isolate the strategy's alpha.

The long-short portfolio is dollar-neutral and self-financing, so its return is
already an excess return and can be regressed directly on the factor returns.
We use Newey-West (HAC) standard errors to be robust to autocorrelation and
heteroskedasticity in the monthly residuals.
"""
from __future__ import annotations

import numpy as np
import pandas as pd
import statsmodels.api as sm

from .signals import _month_key


def daily_factors_to_monthly(daily: pd.DataFrame) -> pd.DataFrame:
    """Compound daily factor returns into monthly returns (month-end index)."""
    grouped = daily.groupby(_month_key(daily.index))
    out = grouped.apply(lambda block: (1.0 + block).prod() - 1.0)
    out.index = out.index.to_timestamp(how="end").normalize()
    out.index.name = "month"
    return out


def factor_regression(
    strategy_returns: pd.Series,
    monthly_factors: pd.DataFrame,
    factor_cols: list[str] | None = None,
    hac_lags: int = 6,
) -> dict:
    """Regress monthly strategy returns on factors with HAC standard errors.

    Returns a dict with the fitted ``statsmodels`` results plus a tidy table of
    coefficients, t-stats and the annualised alpha.
    """
    factor_cols = factor_cols or ["MKT_RF", "SMB", "HML", "MOM"]
    factor_cols = [c for c in factor_cols if c in monthly_factors.columns]

    df = pd.concat(
        [strategy_returns.rename("y"), monthly_factors[factor_cols]], axis=1
    ).dropna()
    if df.empty or len(df) <= len(factor_cols) + 1:
        raise ValueError("Not enough overlapping observations for a regression.")

    X = sm.add_constant(df[factor_cols])
    y = df["y"]
    model = sm.OLS(y, X).fit(cov_type="HAC", cov_kwds={"maxlags": hac_lags})

    params = model.params
    tvals = model.tvalues
    pvals = model.pvalues
    table = pd.DataFrame(
        {
            "coef": params,
            "t_stat": tvals,
            "p_value": pvals,
        }
    )
    table = table.rename(index={"const": "alpha"})

    alpha_monthly = float(params["const"])
    summary = {
        "model": model,
        "table": table,
        "alpha_monthly": alpha_monthly,
        "alpha_annualized": float(alpha_monthly * 12.0),
        "alpha_tstat": float(tvals["const"]),
        "alpha_pvalue": float(pvals["const"]),
        "r_squared": float(model.rsquared),
        "n_obs": int(model.nobs),
        "factors": factor_cols,
    }
    return summary


def max_ivol_correlation(
    max_panel: pd.DataFrame, ivol_panel: pd.DataFrame
) -> dict:
    """Cross-sectional correlation between MAX and idiosyncratic volatility.

    Reports the pooled (stacked) Pearson and Spearman correlations plus the
    time-series average of the per-month cross-sectional correlations, which is
    the cleaner statistic for a cross-sectional relationship.
    """
    common_idx = max_panel.index.intersection(ivol_panel.index)
    common_cols = max_panel.columns.intersection(ivol_panel.columns)
    m = max_panel.loc[common_idx, common_cols]
    v = ivol_panel.loc[common_idx, common_cols]

    pooled = pd.DataFrame({"max": m.to_numpy().ravel(), "ivol": v.to_numpy().ravel()}).dropna()
    pearson = float(pooled["max"].corr(pooled["ivol"], method="pearson"))
    spearman = float(pooled["max"].corr(pooled["ivol"], method="spearman"))

    per_month = []
    for ts in common_idx:
        row = pd.DataFrame({"max": m.loc[ts], "ivol": v.loc[ts]}).dropna()
        if len(row) >= 5:
            per_month.append(row["max"].corr(row["ivol"]))
    avg_cs = float(np.nanmean(per_month)) if per_month else float("nan")

    return {
        "pooled_pearson": pearson,
        "pooled_spearman": spearman,
        "avg_cross_sectional": avg_cs,
        "n_obs": int(len(pooled)),
        "n_months": int(len(per_month)),
    }
