"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CdpSession = exports.CdpConnection = void 0;
// lib/v3/understudy/cdp.ts
const ws_1 = __importDefault(require("ws"));
const version_js_1 = require("../../version.js");
const sdkErrors_js_1 = require("../types/public/sdkErrors.js");
class CdpConnection {
    ws;
    nextId = 1;
    inflight = new Map();
    eventHandlers = new Map();
    sessions = new Map();
    /** Maps sessionId -> targetId (1:1 mapping) */
    sessionToTarget = new Map();
    id = null; // root
    transportCloseHandlers = new Set();
    /** Optional CDP logger - set this to receive all outgoing CDP method calls */
    cdpLogger;
    /** Optional CDP event logger - set this to receive all incoming CDP events */
    cdpEventLogger;
    onTransportClosed(handler) {
        this.transportCloseHandlers.add(handler);
    }
    offTransportClosed(handler) {
        this.transportCloseHandlers.delete(handler);
    }
    emitTransportClosed(why) {
        for (const h of this.transportCloseHandlers) {
            try {
                h(why);
            }
            catch {
                //
            }
        }
    }
    constructor(ws) {
        this.ws = ws;
        this.ws.on("close", (code, reason) => {
            // Reason is a Buffer in ws; stringify defensively
            const why = `socket-close code=${code} reason=${String(reason || "")}`;
            this.rejectAllInflight(why);
            this.emitTransportClosed(why);
        });
        this.ws.on("error", (err) => {
            const why = `socket-error ${err?.message ?? String(err)}`;
            this.rejectAllInflight(why);
            this.emitTransportClosed(why);
        });
        this.ws.on("message", (data) => this.onMessage(data.toString()));
    }
    static async connect(wsUrl) {
        // Include User-Agent header for server-side observability and version tracking
        const ws = new ws_1.default(wsUrl, {
            headers: {
                "User-Agent": `Stagehand/${version_js_1.STAGEHAND_VERSION}`,
            },
        });
        await new Promise((resolve, reject) => {
            ws.once("open", () => resolve());
            ws.once("error", (e) => reject(e));
        });
        return new CdpConnection(ws);
    }
    async enableAutoAttach() {
        await this.send("Target.setAutoAttach", {
            autoAttach: true,
            flatten: true,
            waitForDebuggerOnStart: true,
        });
        await this.send("Target.setDiscoverTargets", { discover: true });
    }
    async send(method, params) {
        const id = this.nextId++;
        const payload = { id, method, params };
        const stack = new Error().stack?.split("\n").slice(1, 4).join("\n");
        const p = new Promise((resolve, reject) => {
            this.inflight.set(id, {
                resolve,
                reject,
                sessionId: null,
                method,
                params,
                stack,
                ts: Date.now(),
            });
        });
        // Prevent unhandledRejection if a session detaches before the caller awaits.
        void p.catch(() => { });
        this.cdpLogger?.({ method, params, targetId: null });
        this.ws.send(JSON.stringify(payload));
        return p;
    }
    on(event, handler) {
        const set = this.eventHandlers.get(event) ?? new Set();
        set.add(handler);
        this.eventHandlers.set(event, set);
    }
    off(event, handler) {
        const set = this.eventHandlers.get(event);
        if (set)
            set.delete(handler);
    }
    async close() {
        if (this.ws.readyState === ws_1.default.CLOSED)
            return;
        await new Promise((resolve) => {
            this.ws.once("close", () => resolve());
            this.ws.close();
        });
    }
    rejectAllInflight(why) {
        for (const [id, entry] of this.inflight.entries()) {
            entry.reject(new sdkErrors_js_1.CdpConnectionClosedError(why));
            this.inflight.delete(id);
        }
    }
    getSession(sessionId) {
        return this.sessions.get(sessionId);
    }
    async attachToTarget(targetId) {
        const { sessionId } = (await this.send("Target.attachToTarget", { targetId, flatten: true }));
        let session = this.sessions.get(sessionId);
        if (!session) {
            session = new CdpSession(this, sessionId);
            this.sessions.set(sessionId, session);
        }
        this.sessionToTarget.set(sessionId, targetId);
        return session;
    }
    async getTargets() {
        const res = await this.send("Target.getTargets");
        return res.targetInfos;
    }
    onMessage(json) {
        const msg = JSON.parse(json);
        if ("id" in msg) {
            const rec = this.inflight.get(msg.id);
            if (!rec)
                return;
            this.inflight.delete(msg.id);
            if ("error" in msg && msg.error) {
                rec.reject(new Error(`${msg.error.code} ${msg.error.message}`));
            }
            else {
                rec.resolve(msg.result);
            }
            return;
        }
        if ("method" in msg) {
            if (msg.method === "Target.attachedToTarget") {
                const p = msg
                    .params;
                if (!this.sessions.has(p.sessionId)) {
                    this.sessions.set(p.sessionId, new CdpSession(this, p.sessionId));
                }
                this.sessionToTarget.set(p.sessionId, p.targetInfo.targetId);
            }
            else if (msg.method === "Target.detachedFromTarget") {
                const p = msg
                    .params;
                for (const [id, entry] of this.inflight.entries()) {
                    if (entry.sessionId === p.sessionId) {
                        entry.reject(new Error("CDP session detached"));
                        this.inflight.delete(id);
                    }
                }
                this.sessions.delete(p.sessionId);
                this.sessionToTarget.delete(p.sessionId);
            }
            else if (msg.method === "Target.targetDestroyed") {
                const p = msg.params;
                // Remove any session mapping for this target
                for (const [sessionId, targetId] of this.sessionToTarget.entries()) {
                    if (targetId === p.targetId) {
                        this.sessionToTarget.delete(sessionId);
                        break;
                    }
                }
            }
            const { method, params, sessionId } = msg;
            // Log incoming CDP events
            const targetId = this.sessionToTarget.get(sessionId) || sessionId;
            this.cdpEventLogger?.({ method, params, targetId });
            if (sessionId) {
                const session = this.sessions.get(sessionId);
                session?.dispatch(method, params);
            }
            else {
                const handlers = this.eventHandlers.get(method);
                if (handlers)
                    for (const h of handlers)
                        h(params);
            }
        }
    }
    _sendViaSession(sessionId, method, params) {
        const id = this.nextId++;
        const payload = { id, method, params, sessionId };
        const stack = new Error().stack?.split("\n").slice(1, 4).join("\n");
        const p = new Promise((resolve, reject) => {
            this.inflight.set(id, {
                resolve,
                reject,
                sessionId,
                method,
                params,
                stack,
                ts: Date.now(),
            });
        });
        // Prevent unhandledRejection if a session detaches before the caller awaits.
        void p.catch(() => { });
        const targetId = this.sessionToTarget.get(sessionId) ?? null;
        this.cdpLogger?.({ method, params, targetId });
        this.ws.send(JSON.stringify(payload));
        return p;
    }
    _onSessionEvent(sessionId, event, handler) {
        const key = `${sessionId}:${event}`;
        const set = this.eventHandlers.get(key) ?? new Set();
        set.add(handler);
        this.eventHandlers.set(key, set);
    }
    _offSessionEvent(sessionId, event, handler) {
        const key = `${sessionId}:${event}`;
        const set = this.eventHandlers.get(key);
        if (set)
            set.delete(handler);
    }
    _dispatchToSession(sessionId, event, params) {
        const key = `${sessionId}:${event}`;
        const handlers = this.eventHandlers.get(key);
        if (handlers)
            for (const h of handlers)
                h(params);
    }
}
exports.CdpConnection = CdpConnection;
class CdpSession {
    root;
    id;
    constructor(root, id) {
        this.root = root;
        this.id = id;
    }
    send(method, params) {
        return this.root._sendViaSession(this.id, method, params);
    }
    on(event, handler) {
        this.root._onSessionEvent(this.id, event, handler);
    }
    off(event, handler) {
        this.root._offSessionEvent(this.id, event, handler);
    }
    async close() {
        await this.root.send("Target.detachFromTarget", {
            sessionId: this.id,
        });
    }
    dispatch(event, params) {
        this.root._dispatchToSession(this.id, event, params);
    }
}
exports.CdpSession = CdpSession;
//# sourceMappingURL=cdp.js.map