"""extract_timeseries_raw — extrae la serie temporal CRUDA de una tabla.

Lector read-only inyectado: recibe `query_fn(sql) -> dict` con el mismo contrato
que duckdb_query_readonly / pg_query (y que el `_q` de profile_table):
`{"status": "ok", "rows": [{col: val, ...}, ...]}`. Esta funcion NO abre ninguna
conexion por su cuenta — solo usa `query_fn`. Construye UNA sola query ordenada
por la columna temporal y devuelve las fechas (`t`) mas cada columna numerica en
listas paralelas (`series`), listas para alimentar el render del capitulo
TIMESERIES de AutomaticEDA (linea valor-vs-tiempo + conteo por periodo) sin que
el capitulo toque la base de datos: recibe esto en `ctx['timeseries_raw']`.

Estilo dict-no-throw del grupo `eda`: nunca lanza; captura cualquier excepcion y
degrada a `{"status": "error", "error": str, ...}`.
"""

from datetime import date, datetime


def _to_float(value):
    """Convierte un valor a float de forma defensiva. None si no es convertible."""
    if value is None:
        return None
    if isinstance(value, bool):
        # Un bool es subclase de int en Python; no es un valor de serie valido.
        return None
    if isinstance(value, (int, float)):
        return float(value)
    s = str(value).strip()
    if not s:
        return None
    try:
        return float(s)
    except (TypeError, ValueError):
        return None


def _to_iso(value):
    """Convierte un valor temporal a string ISO conservando el orden de la query.

    date/datetime -> isoformat(); cualquier otro valor (string, etc.) -> str().
    None se preserva como None.
    """
    if value is None:
        return None
    if isinstance(value, (datetime, date)):
        return value.isoformat()
    return str(value)


def extract_timeseries_raw(query_fn, table, time_col, value_cols, max_rows=5000):
    """Extrae la serie temporal cruda (fechas + columnas numericas) de una tabla.

    Args:
        query_fn: callable lector read-only del backend activo. Recibe un string
            SQL y devuelve un dict {"status": "ok", "rows": [{col: val, ...}]}
            (mismo contrato que duckdb_query_readonly / el `_q` de profile_table).
            No se abre ninguna conexion aqui: toda la lectura pasa por query_fn.
        table: nombre de la tabla.
        time_col: nombre de la columna de orden temporal.
        value_cols: lista de nombres de columnas numericas a extraer.
        max_rows: limite de filas (LIMIT). Default 5000.

    Returns:
        dict (nunca lanza):
            {
              "status": "ok" | "error",
              "error": str,                 # solo si status == "error"
              "time_col": str,
              "t": [str, ...],              # time_col como ISO string, en orden
              "series": {col: [float|None, ...], ...},  # paralela a t por columna
              "n": int                      # nº de filas devueltas
            }
    """
    base = {"status": "ok", "time_col": time_col, "t": [], "series": {}, "n": 0}
    try:
        if query_fn is None:
            return {**base, "status": "error", "error": "query_fn es None"}
        if not value_cols:
            return {**base, "status": "error", "error": "value_cols vacío"}
        if not table or not time_col:
            return {
                **base,
                "status": "error",
                "error": "table y time_col son obligatorios",
            }

        # Identificadores escapados con comillas dobles (como hace profile_table)
        # para tolerar nombres con mayusculas/espacios/palabras reservadas.
        cols_sql = ", ".join(f'"{c}"' for c in value_cols)
        sql = (
            f'SELECT "{time_col}", {cols_sql} FROM "{table}" '
            f'WHERE "{time_col}" IS NOT NULL '
            f'ORDER BY "{time_col}" '
            f"LIMIT {int(max_rows)}"
        )

        q = query_fn(sql)
        if not isinstance(q, dict) or q.get("status") != "ok":
            err = (
                q.get("error", "query_fn fallo")
                if isinstance(q, dict)
                else "query_fn no devolvio un dict"
            )
            return {**base, "status": "error", "error": err}

        rows = q.get("rows", []) or []
        t = []
        series = {c: [] for c in value_cols}
        for row in rows:
            t.append(_to_iso(row.get(time_col)))
            for c in value_cols:
                series[c].append(_to_float(row.get(c)))

        return {
            "status": "ok",
            "time_col": time_col,
            "t": t,
            "series": series,
            "n": len(t),
        }
    except Exception as e:  # noqa: BLE001 - dict-no-throw: degradar, nunca lanzar
        return {**base, "status": "error", "error": str(e)}
