/**
 * CDP Capture Adapter
 *
 * Captures video frames from any application that exposes a Chrome DevTools Protocol
 * debug port (Electron, Tauri, Chrome, React Native via Metro, etc.).
 *
 * Uses Page.startScreencast to receive JPEG frames over WebSocket, saves them
 * as a timestamped JPEG sequence in the output directory.
 */
import { mkdirSync, statSync, writeFileSync } from 'node:fs';
import * as http from 'node:http';
import { join } from 'node:path';
/**
 * CDPCaptureAdapter — records JPEG frames from a CDP-enabled application.
 *
 * Lifecycle: new CDPCaptureAdapter() → isAvailable() → start(config) → stop()
 */
export class CDPCaptureAdapter {
    name = 'cdp';
    /** Optional frame callback for live preview */
    onFrame;
    ws = null;
    frames = [];
    frameIndex = 0;
    startTime = 0;
    config = null;
    outputDir = '';
    framePaths = [];
    stopTimer = null;
    cmdId = 1;
    lastFrameTime = 0;
    // ---------------------------------------------------------------------------
    // isAvailable
    // ---------------------------------------------------------------------------
    /**
     * Check if a CDP endpoint is reachable at the configured host/port.
     * Accepts an optional port/host override for availability checks before start().
     */
    async isAvailable(port = 9222, host = 'localhost') {
        try {
            await this.fetchTargets(host, port);
            return true;
        }
        catch {
            return false;
        }
    }
    // ---------------------------------------------------------------------------
    // start
    // ---------------------------------------------------------------------------
    async start(config) {
        this.config = config;
        this.frames = [];
        this.frameIndex = 0;
        this.framePaths = [];
        this.startTime = Date.now();
        this.lastFrameTime = 0;
        this.cmdId = 1;
        // Extract CDP-specific options from config (cast to access optional fields)
        const cdpConfig = config;
        const port = cdpConfig.port ?? 9222;
        const host = cdpConfig.host ?? 'localhost';
        const targetId = cdpConfig.targetId;
        // Create output directory
        this.outputDir = config.outputPath;
        mkdirSync(this.outputDir, { recursive: true });
        // Fetch available targets
        const targets = await this.fetchTargets(host, port);
        if (targets.length === 0) {
            throw new Error(`No debuggable pages found at ${host}:${port}. Make sure the app is running with --remote-debugging-port=${port}`);
        }
        // Select target
        let target;
        if (targetId) {
            const found = targets.find((t) => t.id === targetId);
            if (!found)
                throw new Error(`Target ${targetId} not found. Available: ${targets.map((t) => t.id).join(', ')}`);
            target = found;
        }
        else {
            // Prefer 'page' type targets over extensions/workers
            target = targets.find((t) => t.type === 'page') ?? targets[0];
        }
        if (!target.webSocketDebuggerUrl) {
            throw new Error(`Target ${target.id} has no WebSocket debugger URL`);
        }
        // Connect WebSocket
        this.ws = await this.connectWebSocket(target.webSocketDebuggerUrl);
        // Set up message handler
        this.ws.onmessage = (event) => {
            this.handleMessage(event.data);
        };
        this.ws.onerror = (err) => {
            process.stderr.write(`CDP WebSocket error: ${String(err)}\n`);
        };
        this.ws.onclose = () => {
            process.stderr.write('CDP WebSocket closed\n');
        };
        // Start screencast
        const duration = config.duration ?? 60;
        const fps = config.fps ?? 2;
        const everyNthFrame = Math.max(1, Math.round(15 / fps)); // CDP max ~15fps
        this.sendCommand('Page.startScreencast', {
            format: config.format ?? 'jpeg',
            quality: config.quality ?? 80,
            maxWidth: config.maxWidth ?? 1280,
            maxHeight: config.maxHeight ?? 720,
            everyNthFrame,
        });
        // Auto-stop after duration
        this.stopTimer = setTimeout(() => {
            this.stop().catch((err) => {
                process.stderr.write(`Auto-stop error: ${String(err)}\n`);
            });
        }, duration * 1000);
        process.stderr.write(`CDP capture started on ${target.url} (${duration}s, ${fps} fps target)\n`);
    }
    // ---------------------------------------------------------------------------
    // stop
    // ---------------------------------------------------------------------------
    async stop() {
        if (this.stopTimer) {
            clearTimeout(this.stopTimer);
            this.stopTimer = null;
        }
        const elapsed = (Date.now() - this.startTime) / 1000;
        if (this.ws) {
            // Stop screencast gracefully
            try {
                this.sendCommand('Page.stopScreencast', {});
            }
            catch {
                // Ignore send errors on close
            }
            this.ws.close();
            this.ws = null;
        }
        // Calculate total size of saved frames
        let totalBytes = 0;
        for (const fp of this.framePaths) {
            try {
                totalBytes += statSync(fp).size;
            }
            catch {
                // Frame may not exist if capture failed early
            }
        }
        const result = {
            success: true,
            outputPath: this.outputDir,
            frameCount: this.frameIndex,
            duration: elapsed,
            fileSizeBytes: totalBytes,
            framePaths: [...this.framePaths],
        };
        process.stderr.write(`CDP capture stopped: ${this.frameIndex} frames in ${elapsed.toFixed(1)}s (${(totalBytes / 1024).toFixed(0)} KB)\n`);
        return result;
    }
    // ---------------------------------------------------------------------------
    // Internal helpers
    // ---------------------------------------------------------------------------
    handleMessage(raw) {
        let msg;
        try {
            msg = JSON.parse(raw);
        }
        catch {
            return;
        }
        if (msg.method === 'Page.screencastFrame') {
            const params = msg.params;
            // Rate-limit frames client-side based on fps config
            const fps = this.config?.fps ?? 2;
            const minIntervalMs = 1000 / fps;
            const now = Date.now();
            const elapsed = now - this.startTime;
            if (this.lastFrameTime > 0 && now - this.lastFrameTime < minIntervalMs * 0.8) {
                // Skip this frame — too soon
                // Still must ACK
                this.sendCommand('Page.screencastFrameAck', { sessionId: params.sessionId });
                return;
            }
            this.lastFrameTime = now;
            const format = (this.config?.format ?? 'jpeg');
            const frame = {
                index: this.frameIndex,
                timestampMs: elapsed,
                data: params.data,
                format,
                width: params.metadata.deviceWidth,
                height: params.metadata.deviceHeight,
            };
            // Save frame to disk
            const paddedIndex = String(this.frameIndex).padStart(4, '0');
            const ext = format === 'png' ? 'png' : 'jpg';
            const filename = `frame-${paddedIndex}-${elapsed}ms.${ext}`;
            const framePath = join(this.outputDir, filename);
            try {
                const buf = Buffer.from(params.data, 'base64');
                writeFileSync(framePath, buf);
                this.framePaths.push(framePath);
            }
            catch (err) {
                process.stderr.write(`Failed to save frame ${this.frameIndex}: ${String(err)}\n`);
            }
            this.frameIndex++;
            // Notify live preview callback
            if (this.onFrame) {
                this.onFrame(frame);
            }
            // Log progress every 10 frames
            if (this.frameIndex % 10 === 0) {
                process.stderr.write(`  Frame ${this.frameIndex} at ${(elapsed / 1000).toFixed(1)}s\n`);
            }
            // ACK the frame so CDP continues sending
            this.sendCommand('Page.screencastFrameAck', { sessionId: params.sessionId });
        }
    }
    sendCommand(method, params) {
        if (!this.ws)
            return;
        const msg = JSON.stringify({ id: this.cmdId++, method, params });
        this.ws.send(msg);
    }
    /** HTTP GET to /json and parse CDP targets */
    fetchTargets(host, port) {
        return new Promise((resolve, reject) => {
            const req = http.get(`http://${host}:${port}/json`, (res) => {
                let body = '';
                res.on('data', (chunk) => {
                    body += chunk.toString();
                });
                res.on('end', () => {
                    try {
                        resolve(JSON.parse(body));
                    }
                    catch (err) {
                        reject(new Error(`Failed to parse /json response: ${String(err)}`));
                    }
                });
            });
            req.on('error', reject);
            req.setTimeout(5000, () => {
                req.destroy();
                reject(new Error(`CDP endpoint timeout at ${host}:${port}/json`));
            });
        });
    }
    /** Connect a WebSocket to the CDP debugger URL */
    async connectWebSocket(url) {
        let WSCtor;
        if (typeof globalThis.WebSocket !== 'undefined') {
            WSCtor = globalThis.WebSocket;
        }
        else {
            // Dynamic import via Function() avoids TypeScript resolving the module path
            // at compile time (no `@types/ws` required in the framework package).
            let wsModule;
            try {
                // biome-ignore lint/security/noGlobalEval: intentional dynamic import escape hatch
                wsModule = (await new Function('u', 'return import(u)')('ws'));
            }
            catch {
                throw new Error('No WebSocket implementation available. Node 22+ has built-in WebSocket, ' +
                    'or install the `ws` package: npm install ws');
            }
            WSCtor = (wsModule.default ?? wsModule);
        }
        return new Promise((resolve, reject) => {
            const ws = new WSCtor(url);
            // Both `ws` and native WebSocket support onopen/onerror property assignment.
            const wsAny = ws;
            wsAny.onopen = () => {
                resolve(ws);
            };
            wsAny.onerror = (err) => reject(err);
        });
    }
}
//# sourceMappingURL=cdp-adapter.js.map