"""Clasifica el TIPO de relacion entre dos variables numericas pareadas.

Funcion pura del grupo eda. Dadas dos listas numericas pareadas por indice,
limpia los pares de forma defensiva, calcula correlaciones lineal (Pearson) y de
rangos (Spearman) y ajustes polinomicos de grado 2 y 3, y a partir de esas
senales etiqueta la forma de la relacion para el EDA automatico:

    "lineal" | "polinómica (grado 2)" | "polinómica (grado 3)" |
    "monótona no-lineal" | "débil/sin forma"

Ademas devuelve los coeficientes del mejor modelo (en orden de numpy.polyval)
para que el capitulo pinte la curva de ajuste sobre el scatter. Reusa las
funciones del registry `pearson` y `spearman_corr` en vez de reimplementarlas.

NUNCA lanza: ante cualquier fallo o dato insuficiente devuelve el dict canonico
con tipo="débil/sin forma" y el resto de claves a None.
"""

import math
import warnings

import numpy as np

from datascience.datascience import pearson
from datascience.spearman_corr import spearman_corr

# Forma canonica de la respuesta cuando no se puede clasificar (datos
# insuficientes, varianza nula o error interno). Siempre las mismas claves.
_WEAK = {
    "tipo": "débil/sin forma",
    "pearson": None,
    "r2_linear": None,
    "spearman": None,
    "r2_poly2": None,
    "r2_poly3": None,
    "best_degree": None,
    "coeffs": None,
}


def _is_num(v) -> bool:
    """True si v es un numero real finito (int/float, no bool, no NaN, no inf)."""
    return (
        isinstance(v, (int, float))
        and not isinstance(v, bool)
        and not (isinstance(v, float) and (math.isnan(v) or math.isinf(v)))
    )


def _poly_r2(coeffs, x_arr, y_arr, ss_tot: float) -> float:
    """R^2 de un ajuste polinomico: 1 - SS_res/SS_tot. 0 si SS_tot==0."""
    if ss_tot == 0.0:
        return 0.0
    pred = np.polyval(coeffs, x_arr)
    ss_res = float(np.sum((y_arr - pred) ** 2))
    return 1.0 - ss_res / ss_tot


def classify_relationship_type(xs: list, ys: list) -> dict:
    """Clasifica el tipo de relacion entre dos variables numericas pareadas.

    Empareja xs[i],ys[i] por indice y descarta el par si cualquiera de los dos
    es None, bool, NaN o inf. Sobre los pares limpios calcula Pearson r
    (r2_linear = r**2), Spearman rho y los R^2 de ajustes polinomicos de grado 2
    y 3 (con numpy.polyfit + R^2 manual). Con esas senales decide la etiqueta.

    Orden de evaluacion de la etiqueta (la primera que matchee gana). Los
    umbrales estan calibrados para datos reales, a menudo discretos y ruidosos
    (conteos, escalas ordinales): una relacion con |r| >= 0.3, |rho| >= 0.3 o un
    polinomio con R^2 >= 0.3 ya tiene FORMA y no debe etiquetarse como "debil".
        1. "débil/sin forma" — todas las senales bajas a la vez:
           abs(pearson) < 0.3 y abs(spearman) < 0.3 y mejor_poly < 0.3.
        2. "monótona no-lineal" — el rango (Spearman) capta una monotonia que el
           Pearson lineal no: abs(spearman) - abs(pearson) >= 0.1 y
           abs(spearman) >= 0.4. No se fuerza un polinomio (coeffs/best_degree =
           None); el capitulo dibuja la tendencia ordenada sobre el scatter.
        3. "polinómica (grado N)" — el mejor polinomico mejora claramente sobre
           el lineal (mejor_poly - r2_linear >= 0.1) y mejor_poly >= 0.3. N es el
           grado (2 o 3) con mejor R^2, prefiriendo el 2 si empatan dentro de 0.02
           (parsimonia).
        4. "lineal" — el resto: hay senal (no es debil) y la forma que existe es
           esencialmente lineal. best_degree=1, coeffs del ajuste de grado 1.

    Si hay menos de 5 pares validos, o la varianza de xs o de ys es ~0
    (constante), devuelve directamente "débil/sin forma".

    Args:
        xs: lista (o tupla) de valores numericos de la primera variable,
            pareada por indice con ys. Pares con None/bool/NaN/inf se descartan.
        ys: lista (o tupla) de valores numericos de la segunda variable,
            pareada por indice con xs.

    Returns:
        dict con SIEMPRE las mismas claves:
            tipo (str), pearson (float|None), r2_linear (float|None),
            spearman (float|None), r2_poly2 (float|None), r2_poly3 (float|None),
            best_degree (int|None: 1, 2, 3 o None),
            coeffs (list|None: coeficientes en orden de numpy.polyval, o None).
        Nunca lanza: ante fallo o datos insuficientes devuelve el dict debil.
    """
    try:
        if xs is None or ys is None:
            return dict(_WEAK)

        pairs = [
            (float(x), float(y))
            for x, y in zip(xs, ys)
            if _is_num(x) and _is_num(y)
        ]

        # Datos insuficientes para hablar de forma de la relacion.
        if len(pairs) < 5:
            return dict(_WEAK)

        clean_x = [p[0] for p in pairs]
        clean_y = [p[1] for p in pairs]

        # Varianza ~0 en cualquiera de las series => relacion indefinida.
        if len(set(clean_x)) < 2 or len(set(clean_y)) < 2:
            return dict(_WEAK)
        x_arr = np.asarray(clean_x, dtype=float)
        y_arr = np.asarray(clean_y, dtype=float)
        if float(np.var(x_arr)) < 1e-15 or float(np.var(y_arr)) < 1e-15:
            return dict(_WEAK)

        # Correlaciones reutilizando las funciones del registry.
        r = pearson(clean_x, clean_y)
        spearman = spearman_corr(clean_x, clean_y)
        r2_linear = r ** 2

        # Ajustes polinomicos grado 2 y 3 con R^2 manual.
        ss_tot = float(np.sum((y_arr - float(np.mean(y_arr))) ** 2))
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            c1 = np.polyfit(x_arr, y_arr, 1)
            c2 = np.polyfit(x_arr, y_arr, 2)
            c3 = np.polyfit(x_arr, y_arr, 3)
        r2_poly2 = _poly_r2(c2, x_arr, y_arr, ss_tot)
        r2_poly3 = _poly_r2(c3, x_arr, y_arr, ss_tot)

        mejor_poly = max(r2_poly2, r2_poly3)
        # Grado del mejor polinomico, con preferencia por la parsimonia: solo se
        # elige el grado 3 si supera al grado 2 por mas de 0.02.
        best_poly_degree = 3 if (r2_poly3 - r2_poly2) > 0.02 else 2

        abs_s = abs(spearman)
        abs_p = abs(r)

        # Decision en orden: debil-temprano -> monotona -> polinomica -> lineal.
        if abs_p < 0.3 and abs_s < 0.3 and mejor_poly < 0.3:
            # Ninguna senal supera el suelo de forma: relacion debil/sin forma.
            tipo = "débil/sin forma"
            best_degree = None
            coeffs = None
        elif (abs_s - abs_p) >= 0.1 and abs_s >= 0.4:
            # Spearman (rango) capta una monotonia que el Pearson lineal no:
            # relacion monotona no-lineal. No se fuerza un polinomio que tal vez
            # no ajusta bien; el capitulo dibuja la tendencia ordenada.
            tipo = "monótona no-lineal"
            best_degree = None
            coeffs = None
        elif (mejor_poly - r2_linear) >= 0.1 and mejor_poly >= 0.3:
            tipo = "polinómica (grado {})".format(best_poly_degree)
            best_degree = best_poly_degree
            best_coeffs = c2 if best_poly_degree == 2 else c3
            coeffs = [float(c) for c in best_coeffs]
        else:
            # Hay senal (no es debil) y no es ni monotona-pura ni polinomica:
            # la correlacion que existe es esencialmente lineal.
            tipo = "lineal"
            best_degree = 1
            coeffs = [float(c) for c in c1]

        return {
            "tipo": tipo,
            "pearson": round(float(r), 6),
            "r2_linear": round(float(r2_linear), 6),
            "spearman": round(float(spearman), 6),
            "r2_poly2": round(float(r2_poly2), 6),
            "r2_poly3": round(float(r2_poly3), 6),
            "best_degree": best_degree,
            "coeffs": (
                [round(c, 8) for c in coeffs] if coeffs is not None else None
            ),
        }
    except Exception:
        return dict(_WEAK)
