"""extract_text_sample — muestrea columnas de texto de una tabla sin cargarla en RAM.

Funcion impura (lee de la base de datos) del grupo de capacidad `eda`. Dado un
``db_path`` + ``table`` (DuckDB o PostgreSQL) y una lista de ``columns`` de texto,
trae una MUESTRA de esas columnas con push-down SQL (``LIMIT sample``), nunca la
tabla entera. La usan los capitulos de texto/NLP del AutomaticEDA que necesitan
valores crudos de texto (longitudes, tokens, ejemplos) sin materializar millones
de filas en memoria.

El lector read-only ``query_fn(sql) -> dict`` se construye igual que en
``build_eda_render_ctx`` / ``profile_table``: un closure sobre el wrapper del
registry (``duckdb_query_readonly`` / ``pg_query``), importado perezosamente
dentro de la funcion para no crear ciclos al cargar el ``__init__`` del paquete
``datascience``. Nunca abre conexiones fuera de esos wrappers.

Estilo dict-no-throw del grupo `eda`: la funcion NUNCA lanza. Captura cualquier
excepcion (query, conversion) y devuelve ``{"status":"error", "error":str(e),
"columns":{}, "n":0}``. Si la query subyacente devuelve ``status != "ok"``, se
propaga como error con el mensaje del wrapper.

Por columna, la lista de strings solo contiene valores NO nulos y NO vacios:
cada celda no-None se convierte con ``str(...)`` y se descarta si queda ``""``.
La clave ``n`` reporta el numero de FILAS leidas por la query (antes de filtrar
los None/vacios), util para saber cuanto se muestreo realmente.
"""


def extract_text_sample(db_path, table, columns, backend="duckdb", sample=2000):
    """Muestrea columnas de texto de una tabla DuckDB/Postgres con push-down SQL.

    Args:
        db_path: ruta al archivo DuckDB, o DSN PostgreSQL si backend="postgres".
            Se inyecta en el closure query_fn. No se valida aqui: si la base no
            existe o el DSN es invalido, la query devuelve status error y el
            resultado es {status:'error', ...} (no lanza).
        table: nombre de la tabla. Se escapa con comillas dobles en la query.
        columns: lista de nombres de columna de texto a muestrear. Se filtra a las
            entradas que sean str no vacio; cada nombre se escapa con comillas
            dobles. Si tras filtrar queda vacia -> {status:'ok', columns:{}, n:0}.
        backend: "duckdb" (default) o "postgres". Selecciona el lector read-only
            del registry (duckdb_query_readonly / pg_query). Cualquier otro valor
            -> {status:'error', error:'backend desconocido: ...', columns:{}, n:0}.
        sample: maximo de filas a muestrear (clausula LIMIT). Default 2000. Acota
            memoria y tiempo: con tablas grandes obtienes el primer tramo por
            orden fisico, no un muestreo uniforme.

    Returns:
        dict (dict-no-throw, NUNCA lanza):
        {"status": "ok"|"error",
         "columns": {col_name: [str, str, ...], ...},  # solo no-None, no-""
         "n": int,        # nº de filas leidas por la query (antes de filtrar)
         "error": str}    # solo presente si status == "error"
    """
    try:
        # 1) Lector read-only del backend activo, construido como en
        # build_eda_render_ctx (closure sobre el wrapper del registry). Imports
        # perezosos: este modulo vive en el paquete `datascience`, importar a
        # `infra` a nivel de modulo crearia un ciclo al cargar el __init__.
        if backend == "duckdb":
            from infra import duckdb_query_readonly

            def query_fn(sql):
                return duckdb_query_readonly(db_path, sql)

        elif backend == "postgres":
            from infra import pg_query

            def query_fn(sql):
                return pg_query(db_path, sql)

        else:
            return {
                "status": "error",
                "error": f"backend desconocido: {backend}",
                "columns": {},
                "n": 0,
            }

        # 2) Columnas validas (str no vacio). Si no queda ninguna, nada que
        # muestrear: ok con columns vacio.
        cols = []
        if isinstance(columns, (list, tuple)):
            cols = [c for c in columns if isinstance(c, str) and c != ""]
        if not cols:
            return {"status": "ok", "columns": {}, "n": 0}

        # 3) Push-down: una sola query con LIMIT. Identificadores escapados con
        # comillas dobles, igual que build_eda_render_ctx.
        cols_sql = ", ".join(f'"{c}"' for c in cols)
        sql = f'SELECT {cols_sql} FROM "{table}" LIMIT {int(sample)}'
        q = query_fn(sql)
        if not isinstance(q, dict) or q.get("status") != "ok":
            err = q.get("error") if isinstance(q, dict) else "query sin resultado"
            return {"status": "error", "error": str(err), "columns": {}, "n": 0}

        rows = q.get("rows") or []
        out = {c: [] for c in cols}
        for row in rows:
            if not isinstance(row, dict):
                continue
            for c in cols:
                value = row.get(c)
                if value is None:
                    continue
                s = str(value)
                if s == "":
                    continue
                out[c].append(s)

        return {"status": "ok", "columns": out, "n": len(rows)}
    except Exception as exc:  # noqa: BLE001 - dict-no-throw del grupo eda
        return {"status": "error", "error": str(exc), "columns": {}, "n": 0}
