"""Autocorrelacion (ACF) y autocorrelacion parcial (PACF) de una serie (grupo eda).

Funcion pura y determinista que calcula la funcion de autocorrelacion y la
parcial con sus bandas de confianza, mas el test de Ljung-Box de autocorrelacion
global. Motivada por Hyndman ("Forecasting") para identificar el orden de un
modelo ARIMA, y por Lopez de Prado ("Advances in Financial ML"): una serie
autocorrelacionada viola el supuesto IID, de modo que los p-valores de una
regresion OLS estandar sobre ella estan inflados (falsos positivos).
"""

from __future__ import annotations

import math

import numpy as np
from statsmodels.stats.diagnostic import acorr_ljungbox
from statsmodels.tsa.stattools import acf, pacf


def _clean(values: list) -> list[float]:
    """Conserva solo valores numericos finitos, descartando None/NaN/no-numericos.

    Los booleanos se excluyen explicitamente (en Python ``bool`` es subclase de
    ``int``, pero no es un valor de serie temporal valido).
    """
    out: list[float] = []
    for v in values:
        if v is None or isinstance(v, bool):
            continue
        if not isinstance(v, (int, float)):
            continue
        x = float(v)
        if math.isnan(x) or math.isinf(x):
            continue
        out.append(x)
    return out


def acf_pacf(values: list, nlags: int = 40, alpha: float = 0.05) -> dict:
    """Calcula ACF, PACF y el test Ljung-Box de una serie temporal.

    Computa la funcion de autocorrelacion (ACF) y la autocorrelacion parcial
    (PACF) hasta ``nlags`` retardos, con sus bandas de confianza al nivel
    ``1 - alpha``, e identifica que retardos son significativos (cuyo intervalo
    de confianza no contiene 0). Ademas corre el test de **Ljung-Box** sobre el
    conjunto de retardos: H0 = "los datos son independientes" (sin
    autocorrelacion); si ``p < alpha`` se rechaza -> la serie esta
    autocorrelacionada.

    Funcion pura y determinista: no hace I/O, no muta los inputs.

    Args:
        values: serie temporal de valores numericos en orden cronologico.
            None/NaN/infinitos/no-numericos se descartan antes del calculo.
        nlags: numero maximo de retardos a calcular (default 40). Se recorta
            automaticamente a ``n // 2`` para PACF (statsmodels exige
            ``nlags < n/2``) y a ``n - 1`` para ACF.
        alpha: nivel de significancia para las bandas de confianza y para el
            test de Ljung-Box (default 0.05).

    Returns:
        Con menos de 8 puntos validos devuelve
        ``{"n": n, "note": "datos insuficientes", "is_autocorrelated": None}``.

        En otro caso un dict con::

            {
              "n": int,
              "nlags": int,            # retardos efectivamente calculados
              "acf": [float, ...],     # incluye lag 0 (=1.0) en el indice 0
              "pacf": [float, ...],
              "acf_confint": [[low, high], ...],   # banda por lag
              "pacf_confint": [[low, high], ...],
              "significant_acf_lags": [int, ...],  # lags (>=1) significativos
              "significant_pacf_lags": [int, ...],
              "ljung_box": {"stat": float, "p_value": float, "lags": int},
              "is_autocorrelated": bool,  # Ljung-Box rechaza independencia
            }

        ``is_autocorrelated = True`` significa que la serie NO es ruido blanco:
        cuidado al aplicarle inferencia OLS clasica (p-valores inflados).
    """
    clean = _clean(values)
    n = len(clean)

    if n < 8:
        return {"n": n, "note": "datos insuficientes", "is_autocorrelated": None}

    arr = np.asarray(clean, dtype=float)

    # Recorta nlags a los limites de statsmodels: ACF admite hasta n-1, PACF < n/2.
    eff_lags = min(nlags, n - 1, (n // 2) - 1)
    eff_lags = max(eff_lags, 1)

    acf_vals, acf_confint = acf(arr, nlags=eff_lags, alpha=alpha, fft=False)
    pacf_vals, pacf_confint = pacf(arr, nlags=eff_lags, alpha=alpha)

    # Un lag es significativo si su banda de confianza (centrada en el valor) no
    # contiene 0. statsmodels devuelve confint como intervalos centrados en el
    # estimador, asi que comparamos el intervalo desplazado al origen.
    def _significant(vals, confint) -> list[int]:
        out: list[int] = []
        for lag in range(1, len(vals)):
            low = confint[lag][0] - vals[lag]
            high = confint[lag][1] - vals[lag]
            if vals[lag] < low or vals[lag] > high:
                out.append(lag)
        return out

    significant_acf = _significant(acf_vals, acf_confint)
    significant_pacf = _significant(pacf_vals, pacf_confint)

    # Ljung-Box sobre el maximo retardo calculado.
    lb = acorr_ljungbox(arr, lags=[eff_lags], return_df=True)
    lb_stat = float(lb["lb_stat"].iloc[0])
    lb_p = float(lb["lb_pvalue"].iloc[0])
    is_autocorrelated = bool(lb_p < alpha)

    return {
        "n": n,
        "nlags": int(eff_lags),
        "acf": [float(v) for v in acf_vals],
        "pacf": [float(v) for v in pacf_vals],
        "acf_confint": [[float(lo), float(hi)] for lo, hi in acf_confint],
        "pacf_confint": [[float(lo), float(hi)] for lo, hi in pacf_confint],
        "significant_acf_lags": significant_acf,
        "significant_pacf_lags": significant_pacf,
        "ljung_box": {
            "stat": lb_stat,
            "p_value": lb_p,
            "lags": int(eff_lags),
        },
        "is_autocorrelated": is_autocorrelated,
    }
