"""Obtiene el AX tree completo de un tab Chrome via CDP WebSocket."""

import json
import threading
import urllib.request
import urllib.error


def cdp_get_ax_tree(
    debug_port: int,
    tab_id: str,
    depth: int = -1,
) -> list[dict]:
    """Conecta al Chrome remoto via WebSocket (CDP) y devuelve el AX tree completo.

    Pasos:
      1. HTTP GET /json/list para obtener webSocketDebuggerUrl del tab.
      2. WebSocket connect (usa websocket-client si disponible, sino implementa
         minimal RFC6455 con socket stdlib).
      3. Envía Accessibility.enable y espera ack.
      4. Envía Accessibility.getFullAXTree con depth=-1.
      5. Lee response y devuelve la lista de AXNode.

    Args:
        debug_port: Puerto de debug remoto de Chrome (ej. 9222).
        tab_id: ID del tab obtenido via /json/list (campo "id").
        depth: Profundidad del árbol. -1 = completo.

    Returns:
        Lista de AXNode en formato CDP.

    Raises:
        RuntimeError: Si no se encuentra el tab, falla la conexión WS,
                      o la respuesta CDP contiene error.
        TimeoutError: Si el servidor no responde en 10 segundos.
    """
    # 1. Obtener webSocketDebuggerUrl del tab
    ws_url = _get_ws_url(debug_port, tab_id)

    # 2. Conectar y obtener nodos
    return _cdp_get_ax_nodes(ws_url, depth)


def _get_ws_url(debug_port: int, tab_id: str) -> str:
    """Obtiene el webSocketDebuggerUrl del tab via HTTP /json/list."""
    url = f"http://127.0.0.1:{debug_port}/json/list"
    try:
        with urllib.request.urlopen(url, timeout=10) as resp:
            tabs = json.loads(resp.read().decode())
    except urllib.error.URLError as e:
        raise RuntimeError(
            f"No se pudo conectar a Chrome en puerto {debug_port}: {e}"
        ) from e

    for tab in tabs:
        if tab.get("id") == tab_id:
            ws_url = tab.get("webSocketDebuggerUrl")
            if not ws_url:
                raise RuntimeError(
                    f"Tab {tab_id} no tiene webSocketDebuggerUrl "
                    "(puede estar adjunto a otro debugger)"
                )
            return ws_url

    raise RuntimeError(
        f"Tab {tab_id} no encontrado. Tabs disponibles: "
        f"{[t.get('id') for t in tabs]}"
    )


def _cdp_get_ax_nodes(ws_url: str, depth: int) -> list[dict]:
    """Conecta via WebSocket y ejecuta la secuencia CDP para obtener AX tree."""
    try:
        import websocket  # websocket-client
        return _cdp_via_websocket_client(ws_url, depth)
    except ImportError:
        pass

    # Fallback: websockets (async) via threading
    try:
        import websockets  # noqa: F401
        return _cdp_via_websockets(ws_url, depth)
    except ImportError:
        pass

    raise RuntimeError(
        "Ninguna librería WebSocket disponible. "
        "Instala websocket-client: pip install websocket-client"
    )


def _cdp_via_websocket_client(ws_url: str, depth: int) -> list[dict]:
    """Implementación usando websocket-client (síncrono)."""
    import websocket

    results: dict = {}
    error_container: list = []

    def on_message(ws, message):
        try:
            msg = json.loads(message)
            msg_id = msg.get("id")
            if msg_id in (1, 2):
                results[msg_id] = msg
            if msg_id == 2 or "error" in msg:
                ws.close()
        except Exception as e:
            error_container.append(e)
            ws.close()

    def on_error(ws, error):
        error_container.append(RuntimeError(f"WebSocket error: {error}"))

    def on_open(ws):
        # Paso 3: habilitar Accessibility
        ws.send(json.dumps({"id": 1, "method": "Accessibility.enable"}))
        # Paso 4: obtener AX tree completo
        params: dict = {}
        if depth != -1:
            params["depth"] = depth
        ws.send(json.dumps({
            "id": 2,
            "method": "Accessibility.getFullAXTree",
            "params": params,
        }))

    ws_app = websocket.WebSocketApp(
        ws_url,
        on_open=on_open,
        on_message=on_message,
        on_error=on_error,
    )

    t = threading.Thread(
        target=lambda: ws_app.run_forever(ping_timeout=10),
        daemon=True,
    )
    t.start()
    t.join(timeout=15)

    if error_container:
        raise error_container[0]

    if 2 not in results:
        raise TimeoutError(
            "No se recibió respuesta de Accessibility.getFullAXTree en 15s"
        )

    resp = results[2]
    if "error" in resp:
        raise RuntimeError(f"CDP error: {resp['error']}")

    result_data = resp.get("result", {})
    nodes = result_data.get("nodes", [])
    return nodes


def _cdp_via_websockets(ws_url: str, depth: int) -> list[dict]:
    """Fallback usando websockets (async), ejecutado en thread con asyncio."""
    import asyncio

    async def _run():
        import websockets

        async with websockets.connect(ws_url, open_timeout=10) as ws:
            # Habilitar Accessibility
            await ws.send(json.dumps({"id": 1, "method": "Accessibility.enable"}))
            await ws.recv()  # ack

            # Obtener AX tree
            params: dict = {}
            if depth != -1:
                params["depth"] = depth
            await ws.send(json.dumps({
                "id": 2,
                "method": "Accessibility.getFullAXTree",
                "params": params,
            }))

            # Leer hasta recibir respuesta con id=2
            import asyncio as _asyncio
            async with _asyncio.timeout(10):
                while True:
                    raw = await ws.recv()
                    msg = json.loads(raw)
                    if msg.get("id") == 2:
                        if "error" in msg:
                            raise RuntimeError(f"CDP error: {msg['error']}")
                        return msg.get("result", {}).get("nodes", [])

    result_holder: list = []
    error_holder: list = []

    def _thread_run():
        try:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            nodes = loop.run_until_complete(_run())
            result_holder.append(nodes)
        except Exception as e:
            error_holder.append(e)

    t = threading.Thread(target=_thread_run, daemon=True)
    t.start()
    t.join(timeout=15)

    if error_holder:
        raise error_holder[0]
    if not result_holder:
        raise TimeoutError("No se recibió respuesta en 15s")
    return result_holder[0]
