"""Orquesta los modelos baratos del grupo `eda` en un solo bloque.

Compone las funciones puras de modelado del registry (PCA, KMeans, Isolation
Forest, tests de normalidad) sobre el subconjunto de columnas numericas de un
perfil de tabla y devuelve el bloque "models" canonico que consume el flag
``--models`` de ``profile_table``. No reescribe logica: delega en cada funcion
del registry. Es pura y determinista (todas las dependencias lo son).
"""

from datascience import (
    isolation_forest_outliers,
    kmeans_segments,
    normality_tests,
    pca_explained,
)


def _pf(v):
    """Parsea un valor a float; devuelve None si es None/bool/no parseable."""
    if v is None or isinstance(v, bool):
        return None
    try:
        return float(v)
    except (TypeError, ValueError):
        return None


def _to_numeric_subset(columns: dict) -> dict:
    """Extrae las columnas numericas alineadas por fila (listwise deletion).

    Solo se quedan las columnas con ``type == "numeric"``. CLAVE: la alineacion
    por fila se preserva. Pasos:
      1. Descarta columnas numericas con menos del 50% de valores parseables
         (evita que una columna casi-toda-nula tire todas las filas en el paso 3).
      2. Sobre las columnas buenas, conserva SOLO las filas en las que TODAS
         tienen un valor numerico (listwise deletion).
    El resultado es un mapa {nombre: [float, ...]} donde todas las listas tienen
    la MISMA longitud (filas completas) — requisito de PCA/KMeans/IsolationForest
    (matriz rectangular sin NaN). El bug previo descartaba None por columna,
    dejando longitudes desiguales y reventando sklearn con ValueError.

    Args:
        columns: mapa {nombre_columna: {"values": list, "type": str}}; las listas
            llegan alineadas por fila (misma longitud, None donde no hay dato).

    Returns:
        dict {nombre_columna: [float, ...]} con columnas numericas de igual
        longitud. Vacio si no hay columnas numericas validas.
    """
    if not isinstance(columns, dict):
        return {}
    raw: dict[str, list] = {}
    for name, meta in columns.items():
        if not isinstance(meta, dict):
            continue
        if meta.get("type") != "numeric":
            continue
        values = meta.get("values")
        if isinstance(values, (list, tuple)):
            raw[name] = list(values)
    if not raw:
        return {}

    # Longitud comun (min, defensivo si llegaran desalineadas).
    n = min(len(v) for v in raw.values())
    if n == 0:
        return {}

    # 1) Parsea por celda y descarta columnas con <50% de valores parseables.
    good: dict[str, list] = {}
    for name, values in raw.items():
        parsed = [_pf(values[i]) for i in range(n)]
        if sum(1 for x in parsed if x is not None) >= 0.5 * n:
            good[name] = parsed
    if not good:
        return {}

    # 2) Listwise: conserva solo filas donde TODAS las columnas tienen valor.
    names = list(good.keys())
    numeric: dict[str, list] = {name: [] for name in names}
    for i in range(n):
        if all(good[name][i] is not None for name in names):
            for name in names:
                numeric[name].append(good[name][i])
    return numeric


def run_eda_models(
    columns: dict,
    run_pca: bool = True,
    run_kmeans: bool = True,
    run_isolation: bool = True,
    run_normality: bool = True,
) -> dict:
    """Ejecuta los modelos baratos del grupo `eda` sobre las columnas numericas.

    Composicion canonica para el flag ``--models`` de ``profile_table``. Toma el
    mapa de columnas con el mismo shape que recibe ``association_matrix`` (cada
    columna con ``values`` y ``type``), extrae el subconjunto numerico, y corre
    los modelos pedidos sobre el. No reescribe ninguno: compone las funciones
    puras ``pca_explained``, ``kmeans_segments``, ``isolation_forest_outliers``
    y ``normality_tests`` del registry.

    Los tests de normalidad se corren por columna numerica individual (basta 1
    columna). PCA, KMeans e Isolation Forest son multivariantes y necesitan al
    menos 2 columnas numericas; con menos, sus claves quedan en None y se
    devuelve una ``note`` explicativa. No lanza excepciones.

    ``trend_slope`` NO se ejecuta aqui: requiere un orden temporal explicito y
    queda disponible suelto en el registry.

    Args:
        columns: mapa {nombre_columna: {"values": list, "type": str}}, mismo
            shape que recibe ``association_matrix``; listas alineadas por fila.
        run_pca: si True, ejecuta PCA sobre el subconjunto numerico.
        run_kmeans: si True, ejecuta KMeans con seleccion automatica de k.
        run_isolation: si True, ejecuta Isolation Forest multivariante.
        run_normality: si True, ejecuta tests de normalidad por columna.

    Returns:
        dict con:
            n_numeric_cols: numero de columnas numericas detectadas.
            pca: salida de pca_explained o None (si run_pca False / <2 cols).
            kmeans: salida de kmeans_segments o None (si run_kmeans False / <2).
            outliers: salida de isolation_forest_outliers o None.
            normality: {col: salida de normality_tests} o None (si run_normality
                False o no hay columnas numericas).
            note: descripcion de por que faltan los multivariantes, si aplica.

        Con menos de 2 columnas numericas devuelve los multivariantes en None y
        una ``note``; ``normality`` sigue poblandose si run_normality True y hay
        al menos 1 columna numerica.
    """
    numeric = _to_numeric_subset(columns)
    n_numeric_cols = len(numeric)

    # normality es univariante: basta una columna numerica.
    normality = None
    if run_normality and n_numeric_cols >= 1:
        normality = {name: normality_tests(values) for name, values in numeric.items()}

    if n_numeric_cols < 2:
        return {
            "n_numeric_cols": n_numeric_cols,
            "pca": None,
            "kmeans": None,
            "outliers": None,
            "normality": normality,
            "note": "insuficientes columnas numericas para modelos multivariantes",
        }

    pca = pca_explained(numeric) if run_pca else None
    kmeans = kmeans_segments(numeric) if run_kmeans else None
    outliers = isolation_forest_outliers(numeric) if run_isolation else None

    return {
        "n_numeric_cols": n_numeric_cols,
        "pca": pca,
        "kmeans": kmeans,
        "outliers": outliers,
        "normality": normality,
        "note": "",
    }
