#!/usr/bin/env python3
"""
DevTools driver: attach to the page target via the remote-debugging WebSocket,
then disconnect. This exercises RenderFrameDevToolsAgentHost::AttachSession
followed by DetachSession on the same frame.
"""
import base64
import hashlib
import json
import os
import socket
import sys
import time
import urllib.request
import urllib.parse

WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"


def http_get_json(url):
    with urllib.request.urlopen(url, timeout=5) as r:
        return json.loads(r.read().decode())


def ws_connect(ws_url):
    u = urllib.parse.urlparse(ws_url)
    host = u.hostname
    port = u.port or 80
    path = u.path
    if u.query:
        path += "?" + u.query
    s = socket.create_connection((host, port), timeout=5)
    key = base64.b64encode(os.urandom(16)).decode()
    req = (
        f"GET {path} HTTP/1.1\r\n"
        f"Host: {host}:{port}\r\n"
        "Upgrade: websocket\r\n"
        "Connection: Upgrade\r\n"
        f"Sec-WebSocket-Key: {key}\r\n"
        "Sec-WebSocket-Version: 13\r\n"
        "\r\n"
    )
    s.sendall(req.encode())
    # Read 101 response.
    buf = b""
    while b"\r\n\r\n" not in buf:
        chunk = s.recv(4096)
        if not chunk:
            raise RuntimeError("ws handshake failed (eof)")
        buf += chunk
    head = buf.split(b"\r\n\r\n", 1)[0].decode("latin1")
    if "101" not in head.split("\r\n", 1)[0]:
        raise RuntimeError(f"ws handshake failed: {head}")
    return s


def ws_send_text(s, text):
    payload = text.encode()
    mask = os.urandom(4)
    masked = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))
    n = len(payload)
    if n < 126:
        header = bytes([0x81, 0x80 | n])
    elif n < 65536:
        header = bytes([0x81, 0x80 | 126]) + n.to_bytes(2, "big")
    else:
        header = bytes([0x81, 0x80 | 127]) + n.to_bytes(8, "big")
    s.sendall(header + mask + masked)


def ws_recv_text(s):
    s.settimeout(5)
    h = s.recv(2)
    if len(h) < 2:
        return None
    opcode = h[0] & 0x0F
    masked = h[1] & 0x80
    n = h[1] & 0x7F
    if n == 126:
        n = int.from_bytes(s.recv(2), "big")
    elif n == 127:
        n = int.from_bytes(s.recv(8), "big")
    mask = s.recv(4) if masked else b""
    data = b""
    while len(data) < n:
        chunk = s.recv(n - len(data))
        if not chunk:
            break
        data += chunk
    if masked:
        data = bytes(b ^ mask[i % 4] for i, b in enumerate(data))
    if opcode == 1:
        return data.decode()
    return None


def main():
    debug_port = int(sys.argv[1]) if len(sys.argv) > 1 else 9222

    # Wait for the page target.
    target = None
    for _ in range(40):
        try:
            targets = http_get_json(f"http://127.0.0.1:{debug_port}/json/list")
            for t in targets:
                if t.get("type") == "page" and "localhost:8765" in t.get("url", ""):
                    target = t
                    break
            if target:
                break
        except Exception:
            pass
        time.sleep(0.5)
    if not target:
        print("[driver] ERROR: page target not found", file=sys.stderr)
        sys.exit(1)

    ws_url = target["webSocketDebuggerUrl"]
    print(f"[driver] attaching to {ws_url}")
    s = ws_connect(ws_url)
    # Send a trivial CDP message so the session is fully live.
    ws_send_text(s, json.dumps({"id": 1, "method": "Runtime.enable"}))
    try:
        reply = ws_recv_text(s)
        print(f"[driver] cdp reply: {reply}")
    except Exception as e:
        print(f"[driver] recv: {e}")
    print("[driver] attached; sleeping 1s")
    time.sleep(1.0)
    print("[driver] detaching (closing devtools websocket)")
    s.close()
    print("[driver] detached")


if __name__ == "__main__":
    main()
