"""Cliente HTTP minimo compartido para el service osint_db (FastAPI + DuckDB).

NO es una funcion del registry — es un helper privado (modulo prefijado con `_`)
que comparten las funciones `browser_profile_*`. Por eso no tiene `.md` con
frontmatter ni se indexa. Mantiene KISS: solo dos helpers sobre `urllib.request`
de la stdlib (sin `requests`).

Contrato del service (FIJO): SIEMPRE responde HTTP 200 con un body JSON
`{"status":"ok"|"error", ...}`. El codigo HTTP NO indica exito — se parsea el body.
Estos helpers nunca lanzan por logica de negocio; convierten cualquier fallo de red
o de parseo en un dict `{"status":"error","error":...}` para que las funciones que
los usan respeten el contrato "no lanzar, devolver dict de estado".
"""

import json
import urllib.error
import urllib.request

# Timeout por defecto de cada request HTTP al service (segundos).
_TIMEOUT_S = 10


def _request(base_url: str, path: str, method: str, payload: dict | None = None) -> dict:
    """Hace una request JSON al service osint_db y devuelve el body parseado.

    Args:
        base_url: base del service (ej. http://127.0.0.1:8771). Se le quita el "/" final.
        path: ruta del endpoint (ej. /api/browser-profile). Debe empezar por "/".
        method: verbo HTTP (POST, DELETE, GET).
        payload: dict a serializar como JSON en el body (None para no enviar body).

    Returns:
        El body JSON del service como dict. Si el service esta caido, la respuesta no
        es JSON, o ocurre cualquier error de transporte, devuelve
        {"status":"error","error": <motivo>} para no romper al llamante.
    """
    url = base_url.rstrip("/") + path
    data = None
    headers = {}
    if payload is not None:
        data = json.dumps(payload).encode("utf-8")
        headers["Content-Type"] = "application/json"

    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=_TIMEOUT_S) as resp:
            raw = resp.read().decode("utf-8")
        parsed = json.loads(raw) if raw else {}
        if not isinstance(parsed, dict):
            return {"status": "error", "error": f"respuesta no-dict del service: {raw[:200]}"}
        return parsed
    except urllib.error.HTTPError as e:
        # El contrato dice HTTP 200 siempre; un HTTPError es anomalia del transporte.
        try:
            body = e.read().decode("utf-8")
            parsed = json.loads(body) if body else {}
            if isinstance(parsed, dict):
                return parsed
        except Exception:  # noqa: BLE001 - el cuerpo del error puede no ser JSON
            pass
        return {"status": "error", "error": f"HTTP {e.code} desde {url}: {e.reason}"}
    except urllib.error.URLError as e:
        return {"status": "error", "error": f"service osint_db inaccesible en {url}: {e.reason}"}
    except (ValueError, UnicodeDecodeError) as e:
        return {"status": "error", "error": f"respuesta no parseable de {url}: {e}"}
    except Exception as e:  # noqa: BLE001 - contrato: nunca lanzar
        return {"status": "error", "error": f"{type(e).__name__}: {e}"}


def post_json(base_url: str, path: str, payload: dict) -> dict:
    """POST JSON al service. Devuelve el body parseado (o dict de error)."""
    return _request(base_url, path, "POST", payload)


def delete(base_url: str, path: str) -> dict:
    """DELETE al service. Devuelve el body parseado (o dict de error)."""
    return _request(base_url, path, "DELETE", None)


def query(base_url: str, sql: str, params: list | None = None, max_rows: int | None = None) -> dict:
    """POST /api/query (read-only). Devuelve {status, columns, rows, row_count} del service.

    Args:
        base_url: base del service.
        sql: SELECT a ejecutar (read-only en el service).
        params: lista de parametros posicionales para el SQL (None -> []).
        max_rows: tope opcional de filas devueltas.

    Returns:
        El body JSON del service. En caso ok trae columns/rows/row_count; en error
        trae {"status":"error","error":...}.
    """
    body: dict = {"sql": sql}
    if params is not None:
        body["params"] = params
    if max_rows is not None:
        body["max_rows"] = max_rows
    return _request(base_url, "/api/query", "POST", body)


def rows_to_dicts(resp: dict) -> list:
    """Normaliza las filas de una respuesta de /api/query a lista de dicts.

    El service osint_db devuelve ``rows`` YA como lista de dicts (claves =
    nombres de columna), así que el caso normal es un passthrough. Por robustez,
    si alguna fila viniera como lista/tupla posicional se mapea con ``columns``.
    Si la respuesta no es un read ok (sin ``rows``), devuelve [].
    """
    rows = resp.get("rows")
    if not isinstance(rows, list):
        return []
    columns = resp.get("columns")
    out: list = []
    for row in rows:
        if isinstance(row, dict):
            out.append(row)
        elif isinstance(row, (list, tuple)) and isinstance(columns, list):
            out.append(dict(zip(columns, row)))
    return out
