"""Normaliza entries HAR en call specs reproducibles.

Segundo paso del patron "grabar -> destilar -> reproducir" un flujo web como
funcion del registry. Toma la salida de `har_filter_flows` (lista de entries
HAR) y produce call specs limpias, con auth (cookies/headers) expuesta para
que el humano/Claude marque luego los valores dinamicos con `{{param}}`.

Funcion PURA: sin I/O, transforma listas/dicts de forma determinista.
"""

# Headers hop-by-hop / ruidosos que se eliminan por defecto (case-insensitive).
# `cookie` se trata aparte: se extrae a `cookies` y se quita de `headers`.
_HOP_BY_HOP = frozenset(
    {
        "host",
        "content-length",
        "connection",
        "keep-alive",
        "proxy-connection",
        "accept-encoding",
        "te",
        "trailer",
        "transfer-encoding",
        "upgrade",
    }
)


def _parse_cookie_header(value: str) -> dict:
    """Parsea el valor de un header `Cookie` en un dict {name: value}.

    Formato: `a=1; b=2; c=3`. El ultimo gana si hay nombres repetidos.
    """
    cookies: dict = {}
    for pair in value.split(";"):
        pair = pair.strip()
        if not pair:
            continue
        name, sep, val = pair.partition("=")
        name = name.strip()
        if not name:
            continue
        cookies[name] = val.strip() if sep else ""
    return cookies


def _infer_body_type(mime_type: str | None) -> str | None:
    """Infiere el tipo de body a partir del mimeType de postData.

    application/json            -> "json"
    application/x-www-form-...  -> "form"
    multipart/*                 -> "raw"
    otro / None                 -> "raw" si hay body, None lo decide el caller.
    """
    if not mime_type:
        return None
    mt = mime_type.split(";", 1)[0].strip().lower()
    if mt == "application/json":
        return "json"
    if mt == "application/x-www-form-urlencoded":
        return "form"
    if mt.startswith("multipart/"):
        return "raw"
    return "raw"


def _set_cookie_names_from_headers(headers: list[dict]) -> list[str]:
    """Extrae nombres de cookies de los headers `Set-Cookie` de la respuesta."""
    names: list[str] = []
    for h in headers:
        if str(h.get("name", "")).lower() != "set-cookie":
            continue
        raw = str(h.get("value", ""))
        # Set-Cookie: name=value; Path=/; HttpOnly  -> nos quedamos con `name`.
        first = raw.split(";", 1)[0].strip()
        name = first.split("=", 1)[0].strip()
        if name:
            names.append(name)
    return names


def har_extract_calls(
    entries: list[dict],
    *,
    drop_headers: list[str] | None = None,
) -> list[dict]:
    """Convierte entries HAR en call specs normalizadas y reproducibles.

    Por cada entry HAR produce un dict call spec con method, url, headers
    (sin hop-by-hop, sin Cookie), cookies (parseadas del header Cookie),
    body, body_type inferido, status de respuesta y nombres de cookies que
    la respuesta setea. NO auto-parametriza: deja los valores tal cual para
    que el humano marque despues los dinamicos con `{{param}}`.

    Args:
        entries: lista de entries HAR (cada uno con `request` y, opcional,
            `response`). Tipicamente la salida de `har_filter_flows`.
        drop_headers: nombres extra de headers a eliminar (case-insensitive),
            aparte de los hop-by-hop por defecto. None = no quitar extras.

    Returns:
        lista de call specs, una por entry, con las claves: method, url,
        headers, cookies, body, body_type, status, sets_cookies.
    """
    extra_drop = {h.lower() for h in (drop_headers or [])}

    specs: list[dict] = []
    for entry in entries:
        request = entry.get("request") or {}
        response = entry.get("response") or {}

        method = str(request.get("method", "")).upper()
        url = request.get("url", "")

        # Headers: lista [{name, value}] -> dict, con drop de hop-by-hop +
        # extras, y extraccion del header Cookie a `cookies`.
        headers: dict = {}
        cookies: dict = {}
        for h in request.get("headers") or []:
            name = str(h.get("name", ""))
            value = str(h.get("value", ""))
            lname = name.lower()
            if lname == "cookie":
                cookies.update(_parse_cookie_header(value))
                continue
            if lname in _HOP_BY_HOP or lname in extra_drop:
                continue
            headers[name] = value  # ultimo gana si repetidos

        # Body desde postData.
        post_data = request.get("postData") or {}
        body = post_data.get("text")
        mime_type = post_data.get("mimeType")
        body_type = _infer_body_type(mime_type) if body is not None else None

        # Status de respuesta.
        raw_status = response.get("status")
        status = int(raw_status) if isinstance(raw_status, (int, float)) and raw_status else None
        if isinstance(raw_status, str) and raw_status.isdigit():
            status = int(raw_status)

        # Cookies que setea la respuesta: preferir response.cookies (HAR);
        # si no, parsear los headers Set-Cookie.
        sets_cookies: list[str] = []
        resp_cookies = response.get("cookies")
        if resp_cookies:
            sets_cookies = [
                str(c.get("name", "")) for c in resp_cookies if c.get("name")
            ]
        else:
            sets_cookies = _set_cookie_names_from_headers(
                response.get("headers") or []
            )

        specs.append(
            {
                "method": method,
                "url": url,
                "headers": headers,
                "cookies": cookies,
                "body": body,
                "body_type": body_type,
                "status": status,
                "sets_cookies": sets_cookies,
            }
        )

    return specs
