"""Evalua una expresion JavaScript en una pestana de Chrome via Chrome DevTools Protocol.

Primitiva de transport CDP: localiza un target (pestana) por substring de su URL,
abre el WebSocket de depuracion, ejecuta `Runtime.evaluate` y devuelve el valor
serializado. Base reutilizable para automatizar el navegador diario sin abrir
ventana nueva ni darle foco.
"""

import json
import urllib.request

import websocket


def cdp_eval(
    expression: str,
    *,
    port: int = 9222,
    target_url_substr: str = "",
    await_promise: bool = False,
    timeout_s: float = 10.0,
) -> dict:
    """Evalua una expresion JS en una pestana de Chrome elegida por substring de URL.

    Args:
        expression: Expresion JavaScript a evaluar en el contexto de la pagina.
        port: Puerto de remote debugging de Chrome. Default 9222.
        target_url_substr: Substring que debe contener la URL del target. Si "",
            usa el primer target de tipo "page".
        await_promise: Si True, espera a que se resuelva una Promise antes de
            devolver el valor (`awaitPromise` de CDP).
        timeout_s: Timeout (segundos) para la conexion WebSocket.

    Returns:
        dict con claves:
            ok: bool — True si la evaluacion produjo un valor sin excepcion.
            value: valor Python serializable devuelto por la expresion, o None.
            error: str — mensaje de error (vacio si ok).
            target_url: str — URL del target usado (vacio si ninguno).
    """
    # 1. Listar targets via HTTP.
    try:
        with urllib.request.urlopen(
            f"http://127.0.0.1:{port}/json", timeout=5
        ) as resp:
            targets = json.loads(resp.read().decode())
    except Exception as e:  # noqa: BLE001 — red/HTTP/JSON, no relanzar
        return {"ok": False, "value": None, "error": str(e), "target_url": ""}

    # 2. Elegir el primer target type=="page" cuya url contenga el substring.
    chosen = None
    for t in targets:
        if t.get("type") != "page":
            continue
        url = t.get("url", "")
        if target_url_substr == "" or target_url_substr in url:
            chosen = t
            break

    if chosen is None:
        return {
            "ok": False,
            "value": None,
            "error": f"no target matching {target_url_substr}",
            "target_url": "",
        }

    target_url = chosen.get("url", "")
    ws_url = chosen.get("webSocketDebuggerUrl", "")

    # 3-4. Abrir WS, enviar Runtime.evaluate, drenar eventos hasta id==1.
    try:
        ws = websocket.create_connection(ws_url, timeout=timeout_s)
    except Exception as e:  # noqa: BLE001 — conexion WS
        return {"ok": False, "value": None, "error": str(e), "target_url": ""}

    try:
        ws.send(json.dumps({
            "id": 1,
            "method": "Runtime.evaluate",
            "params": {
                "expression": expression,
                "returnByValue": True,
                "awaitPromise": await_promise,
            },
        }))

        msg = None
        # Drenar eventos intermedios hasta encontrar la respuesta con id==1.
        while True:
            raw = ws.recv()
            if not raw:
                break
            try:
                parsed = json.loads(raw)
            except Exception:  # noqa: BLE001 — frame no-JSON, ignorar
                continue
            if parsed.get("id") == 1:
                msg = parsed
                break
    except Exception as e:  # noqa: BLE001 — fallo de transport durante recv/send
        return {"ok": False, "value": None, "error": str(e), "target_url": target_url}
    finally:
        try:
            ws.close()
        except Exception:  # noqa: BLE001 — cierre best-effort
            pass

    if msg is None:
        return {
            "ok": False,
            "value": None,
            "error": "no response for evaluate (id=1)",
            "target_url": target_url,
        }

    result = msg.get("result", {})

    # 5. Si hubo excepcion en la evaluacion, devolverla como error.
    exc = result.get("exceptionDetails")
    if exc:
        text = exc.get("text", "evaluation exception")
        exception = exc.get("exception", {})
        detail = exception.get("description") or exception.get("value")
        error = f"{text}: {detail}" if detail else text
        return {"ok": False, "value": None, "error": error, "target_url": target_url}

    # 6. Extraer el valor serializado por returnByValue.
    value = result.get("result", {}).get("value")
    return {"ok": True, "value": value, "error": "", "target_url": target_url}


if __name__ == "__main__":
    import sys

    expr = sys.argv[1] if len(sys.argv) > 1 else "document.title"
    substr = sys.argv[2] if len(sys.argv) > 2 else ""
    out = cdp_eval(expr, port=9222, target_url_substr=substr)
    print(json.dumps(out, ensure_ascii=False, indent=2))
