"""Genera las advertencias que recuerdan que un EDA es EXPLORATORIO, no confirmatorio.

Funcion pura y determinista: dict (TableProfile del grupo ``eda``) entra, dict con
una lista de caveats sale. No hace I/O, no muta el input, no lanza excepciones.

Doctrina (Tukey, *EDA* 1977; Aronson; López de Prado 2018): el análisis exploratorio
sirve para GENERAR hipótesis, no para confirmarlas. Lo que se ve mirando todo el
dataset a la vez —correlaciones, clusters, "significancias", outliers— es un punto de
partida, no una conclusión: hay que validarlo fuera de muestra con un análisis dirigido.
Esta función inspecciona qué contiene el perfil y devuelve solo las advertencias que
aplican a lo que realmente se ha calculado (si hay correlaciones → caveat de
causalidad; si hay modelos → caveat de overfitting; etc.), además de una advertencia
general que siempre acompaña a un EDA.
"""

from __future__ import annotations

# Umbrales para disparar caveats dependientes de magnitud.
_SMALL_SAMPLE_ROWS = 30          # n_rows por debajo de esto -> baja potencia.
_HIGH_MISSING_FRACTION = 0.2     # null_cell_pct (fracción) por encima -> sesgo MNAR.


def _to_float(v):
    """Parsea a float; None si es None/bool/no parseable (NaN incluido)."""
    if v is None or isinstance(v, bool):
        return None
    try:
        f = float(v)
    except (TypeError, ValueError):
        return None
    if f != f:  # NaN
        return None
    return f


def _correlation_pairs(profile: dict) -> list:
    """Extrae la lista de pares de correlación del perfil, tolerando varios shapes.

    ``correlations`` puede ser una lista de pares o un dict con ``pairs`` /
    ``strongest``. Devuelve siempre una lista (vacía si no hay nada usable).
    """
    correlations = profile.get("correlations")
    if not correlations:
        return []
    if isinstance(correlations, dict):
        pairs = correlations.get("pairs") or correlations.get("strongest") or []
    else:
        pairs = correlations
    return list(pairs) if isinstance(pairs, (list, tuple)) else []


def _has_models(profile: dict) -> bool:
    """True si el perfil contiene un bloque de modelos multivariantes ajustados."""
    models = profile.get("models")
    if not isinstance(models, dict):
        return False
    return any(models.get(k) for k in ("pca", "kmeans", "outliers"))


def _has_pvalues(profile: dict) -> bool:
    """True si el perfil contiene p-values (tests de normalidad o de tendencia)."""
    models = profile.get("models")
    if isinstance(models, dict) and models.get("normality"):
        return True
    # Tests de tendencia adjuntados por columna (trend_slope) también traen p-value.
    for col in profile.get("columns") or []:
        if isinstance(col, dict) and isinstance(col.get("trend"), dict):
            if col["trend"].get("p_value") is not None:
                return True
    return False


def _has_outliers(profile: dict) -> bool:
    """True si se han detectado outliers (multivariantes o por columna numérica)."""
    models = profile.get("models")
    if isinstance(models, dict) and models.get("outliers"):
        return True
    for col in profile.get("columns") or []:
        if not isinstance(col, dict):
            continue
        num = col.get("numeric")
        if isinstance(num, dict):
            n_out = _to_float(num.get("n_outliers"))
            opct = _to_float(num.get("outlier_pct"))
            if (n_out is not None and n_out > 0) or (opct is not None and opct > 0):
                return True
    return False


def exploratory_caveats(profile: dict) -> dict:
    """Devuelve las advertencias de que el EDA es exploratorio según lo que contiene.

    Inspecciona un TableProfile (dict del grupo ``eda``) y arma la lista de caveats
    relevantes. Una advertencia general (la naturaleza exploratoria del EDA) se
    incluye SIEMPRE; el resto solo se añaden cuando el perfil contiene aquello a lo
    que aplican:

        - correlaciones presentes   -> correlación ≠ causalidad.
        - modelos / correlaciones   -> riesgo de overfitting in-sample (validar OOS).
        - p-values (normalidad/tendencia) -> no son confirmación sin corregir / IID.
        - ≥2 pares de correlación   -> comparaciones múltiples (falsos positivos).
        - outliers detectados       -> no implican errores.
        - n_rows pequeño            -> baja potencia, estimaciones inestables.
        - muchos faltantes          -> posible sesgo si no son aleatorios (MNAR).

    Es pura, determinista y no lanza excepciones. Un perfil vacío o ``None`` devuelve
    solo el caveat general con una nota.

    Args:
        profile: TableProfile dict del grupo ``eda``. Se lee todo defensivamente con
            ``.get(...)`` porque casi cualquier fase puede faltar.

    Returns:
        dict con:
            - ``n``: número de caveats devueltos (int).
            - ``caveats``: lista de dicts ``{"id", "topic", "message", "reference"}``,
              empezando por el general ``exploratory_nature``.
            - ``note``: cadena vacía en el caso normal; mensaje cuando el perfil está
              vacío y solo se devuelve la advertencia general.
    """
    if not isinstance(profile, dict):
        profile = {}

    caveats: list = []

    # Caveat general: SIEMPRE presente. El EDA genera hipótesis, no conclusiones.
    caveats.append({
        "id": "exploratory_nature",
        "topic": "naturaleza exploratoria",
        "message": (
            "El EDA genera HIPÓTESIS, no conclusiones. Cada patrón que veas aquí es un "
            "punto de partida para confirmarlo con un análisis dirigido sobre datos "
            "nuevos, no una verdad ya establecida."
        ),
        "reference": "Tukey (1977), Exploratory Data Analysis; Aronson",
    })

    if not profile:
        return {
            "n": len(caveats),
            "caveats": caveats,
            "note": "perfil vacío: solo se devuelve la advertencia general",
        }

    corr_pairs = _correlation_pairs(profile)
    has_corr = len(corr_pairs) > 0
    has_models = _has_models(profile)

    # Correlación ≠ causalidad.
    if has_corr:
        caveats.append({
            "id": "correlation_not_causation",
            "topic": "correlación vs causalidad",
            "message": (
                "Las correlaciones son asociaciones, no relaciones causales. Una "
                "correlación fuerte puede venir de una variable de confusión o del "
                "azar; valídala out-of-sample o con un diseño experimental antes de "
                "actuar sobre ella."
            ),
            "reference": "Tukey (1977), EDA",
        })

    # Overfitting in-sample: cualquier patrón ajustado sobre todo el dataset.
    if has_models or has_corr:
        caveats.append({
            "id": "in_sample_overfitting",
            "topic": "overfitting in-sample",
            "message": (
                "Los patrones (modelos, clusters, correlaciones) se han extraído sobre "
                "TODO el dataset. Lo aprendido in-sample puede no replicar fuera de "
                "muestra (overfitting / selección por backtest). Valida con holdout o "
                "walk-forward antes de confiar en ellos."
            ),
            "reference": "López de Prado (2018), Advances in Financial Machine Learning",
        })

    # p-values: no son confirmación sin corregir multiplicidad / sobre datos no-IID.
    if _has_pvalues(profile):
        caveats.append({
            "id": "p_values_not_confirmation",
            "topic": "p-values",
            "message": (
                "Los p-values sin corregir por comparaciones múltiples, o calculados "
                "sobre datos no-IID (series temporales, datos agrupados), no son "
                "confirmación. Trata cualquier 'significancia' vista en exploración "
                "como provisional."
            ),
            "reference": "Tukey (1977), EDA",
        })

    # Comparaciones múltiples: cuantos más pares/columnas miras, más falsos positivos.
    if len(corr_pairs) >= 2:
        caveats.append({
            "id": "multiple_comparisons",
            "topic": "comparaciones múltiples",
            "message": (
                "Al examinar muchos pares/columnas a la vez, algunos parecerán "
                "'significativos' solo por azar (problema de comparaciones múltiples). "
                "Cuantas más combinaciones miras, más falsos positivos esperas."
            ),
            "reference": "López de Prado (2018), AFML",
        })

    # Outliers detectados no implican errores.
    if _has_outliers(profile):
        caveats.append({
            "id": "outliers_not_errors",
            "topic": "outliers",
            "message": (
                "Los outliers detectados son puntos estadísticamente atípicos, NO "
                "necesariamente errores. Pueden ser el dato más interesante (fraude, "
                "evento raro). Investígalos antes de eliminarlos."
            ),
            "reference": "Tukey (1977), EDA",
        })

    # Muestra pequeña: baja potencia, estimaciones inestables.
    n_rows = _to_float(profile.get("n_rows"))
    if n_rows is not None and n_rows < _SMALL_SAMPLE_ROWS:
        caveats.append({
            "id": "small_sample",
            "topic": "muestra pequeña",
            "message": (
                f"Pocas filas (n={int(n_rows)}): la potencia estadística es baja y las "
                "estimaciones (media, correlación, forma de la distribución) son "
                "inestables. Los patrones pueden cambiar con más datos."
            ),
            "reference": "Tukey (1977), EDA",
        })

    # Datos faltantes: posible sesgo si no son aleatorios (MNAR).
    null_frac = _to_float(profile.get("null_cell_pct"))
    all_null_cols = profile.get("all_null_cols") or []
    if (null_frac is not None and null_frac > _HIGH_MISSING_FRACTION) or all_null_cols:
        caveats.append({
            "id": "missing_data_bias",
            "topic": "datos faltantes",
            "message": (
                "Hay un volumen notable de datos faltantes. Si los ausentes no son "
                "aleatorios (MNAR), los estadísticos calculados sobre lo presente "
                "están sesgados; no extrapoles sin entender por qué faltan."
            ),
            "reference": "Tukey (1977), EDA",
        })

    return {"n": len(caveats), "caveats": caveats, "note": ""}
