/**
 * Browser Controller
 *
 * Provides browser automation via Chrome DevTools Protocol.
 * Works with both Puppeteer and Playwright-style APIs.
 */
import { EventEmitter } from 'events';
import { spawn } from 'child_process';
import { logger } from '../utils/logger.js';
import { DEFAULT_BROWSER_OPTIONS, DEFAULT_NAVIGATION_OPTIONS } from './types.js';
// ============================================================================
// CDP Connection
// ============================================================================
/**
 * CDP WebSocket connection
 */
export class CDPConnection {
    ws = null;
    messageId = 0;
    pendingMessages = new Map();
    eventListeners = new Map();
    async connect(wsUrl) {
        return new Promise((resolve, reject) => {
            try {
                // In Node.js, we'd use the 'ws' package
                // This is a simplified version for the interface
                this.ws = new WebSocket(wsUrl);
                this.ws.onopen = () => resolve();
                this.ws.onerror = (err) => reject(new Error(`WebSocket error: ${err}`));
                this.ws.onmessage = (event) => this.handleMessage(JSON.parse(event.data));
                this.ws.onclose = () => this.handleClose();
            }
            catch (error) {
                reject(error);
            }
        });
    }
    handleMessage(message) {
        if (message.id !== undefined) {
            // Response to a command
            const pending = this.pendingMessages.get(message.id);
            if (pending) {
                this.pendingMessages.delete(message.id);
                if (message.error) {
                    pending.reject(new Error(message.error.message));
                }
                else {
                    pending.resolve(message.result);
                }
            }
        }
        else if (message.method) {
            // Event
            const listeners = this.eventListeners.get(message.method);
            if (listeners) {
                for (const listener of listeners) {
                    listener(message.params);
                }
            }
        }
    }
    handleClose() {
        for (const pending of this.pendingMessages.values()) {
            pending.reject(new Error('Connection closed'));
        }
        this.pendingMessages.clear();
    }
    async send(method, params = {}) {
        if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
            throw new Error('Not connected');
        }
        const id = ++this.messageId;
        return new Promise((resolve, reject) => {
            this.pendingMessages.set(id, { resolve, reject });
            this.ws.send(JSON.stringify({ id, method, params }));
        });
    }
    on(event, callback) {
        if (!this.eventListeners.has(event)) {
            this.eventListeners.set(event, new Set());
        }
        this.eventListeners.get(event).add(callback);
    }
    off(event, callback) {
        this.eventListeners.get(event)?.delete(callback);
    }
    disconnect() {
        if (this.ws) {
            this.ws.close();
            this.ws = null;
        }
    }
}
// ============================================================================
// Page Controller
// ============================================================================
/**
 * Controls a browser page/tab
 */
export class PageController extends EventEmitter {
    cdp;
    targetId;
    frameId = '';
    constructor(cdp, targetId) {
        super();
        this.cdp = cdp;
        this.targetId = targetId;
        this.setupEventListeners();
    }
    setupEventListeners() {
        this.cdp.on('Console.messageAdded', (params) => {
            const { message } = params;
            this.emit('console', {
                type: message.level,
                text: message.text,
                args: [],
            });
        });
        this.cdp.on('Page.javascriptDialogOpening', (params) => {
            const { type, message } = params;
            this.emit('dialog', { type, message });
        });
    }
    /**
     * Navigate to URL
     */
    async goto(url, options = {}) {
        const opts = { ...DEFAULT_NAVIGATION_OPTIONS, ...options };
        await this.cdp.send('Page.enable');
        await this.cdp.send('Page.navigate', { url, referrer: opts.referer });
        // Wait for load
        await this.waitForNavigation(opts);
    }
    /**
     * Wait for navigation to complete
     */
    async waitForNavigation(options = {}) {
        const opts = { ...DEFAULT_NAVIGATION_OPTIONS, ...options };
        return new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                reject(new Error('Navigation timeout'));
            }, opts.timeout);
            const eventName = opts.waitUntil === 'domcontentloaded'
                ? 'Page.domContentEventFired'
                : 'Page.loadEventFired';
            const handler = () => {
                clearTimeout(timeout);
                this.cdp.off(eventName, handler);
                resolve();
            };
            this.cdp.on(eventName, handler);
        });
    }
    /**
     * Get current URL
     */
    async url() {
        const result = await this.cdp.send('Runtime.evaluate', {
            expression: 'window.location.href',
            returnByValue: true,
        });
        return result.result.value;
    }
    /**
     * Get page title
     */
    async title() {
        const result = await this.cdp.send('Runtime.evaluate', {
            expression: 'document.title',
            returnByValue: true,
        });
        return result.result.value;
    }
    /**
     * Get page content
     */
    async content() {
        const result = await this.cdp.send('Runtime.evaluate', {
            expression: 'document.documentElement.outerHTML',
            returnByValue: true,
        });
        return result.result.value;
    }
    /**
     * Set page content
     */
    async setContent(html) {
        await this.cdp.send('Page.setDocumentContent', {
            frameId: this.frameId,
            html,
        });
    }
    /**
     * Wait for selector
     */
    async waitForSelector(selector, options = {}) {
        const timeout = options.timeout ?? 30000;
        const startTime = Date.now();
        while (Date.now() - startTime < timeout) {
            const result = await this.cdp.send('Runtime.evaluate', {
                expression: `document.querySelector('${selector.replace(/'/g, "\\'")}')`,
                returnByValue: false,
            });
            if (result.result.objectId) {
                return;
            }
            await new Promise(r => setTimeout(r, 100));
        }
        throw new Error(`Timeout waiting for selector: ${selector}`);
    }
    /**
     * Click element
     */
    async click(selector, options = {}) {
        // Get element position
        const box = await this.cdp.send('Runtime.evaluate', {
            expression: `(() => {
        const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
        if (!el) return null;
        const rect = el.getBoundingClientRect();
        return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
      })()`,
            returnByValue: true,
        });
        if (!box.result.value) {
            throw new Error(`Element not found: ${selector}`);
        }
        const { x, y } = box.result.value;
        const offsetX = options.offsetX ?? 0;
        const offsetY = options.offsetY ?? 0;
        await this.cdp.send('Input.dispatchMouseEvent', {
            type: 'mousePressed',
            x: x + offsetX,
            y: y + offsetY,
            button: options.button ?? 'left',
            clickCount: options.clickCount ?? 1,
        });
        if (options.delay) {
            await new Promise(r => setTimeout(r, options.delay));
        }
        await this.cdp.send('Input.dispatchMouseEvent', {
            type: 'mouseReleased',
            x: x + offsetX,
            y: y + offsetY,
            button: options.button ?? 'left',
            clickCount: options.clickCount ?? 1,
        });
    }
    /**
     * Type text into element
     */
    async type(selector, text, options = {}) {
        await this.click(selector);
        for (const char of text) {
            await this.cdp.send('Input.dispatchKeyEvent', {
                type: 'keyDown',
                text: char,
            });
            await this.cdp.send('Input.dispatchKeyEvent', {
                type: 'keyUp',
                text: char,
            });
            if (options.delay) {
                await new Promise(r => setTimeout(r, options.delay));
            }
        }
    }
    /**
     * Evaluate JavaScript
     */
    async evaluate(fn, ...args) {
        const expression = typeof fn === 'string'
            ? fn
            : `(${fn.toString()})(${args.map(a => JSON.stringify(a)).join(',')})`;
        const result = await this.cdp.send('Runtime.evaluate', {
            expression,
            returnByValue: true,
            awaitPromise: true,
        });
        return result.result.value;
    }
    /**
     * Take screenshot
     */
    async screenshot(options = {}) {
        const format = options.type ?? 'png';
        const quality = format === 'png' ? undefined : options.quality ?? 80;
        const params = {
            format,
            quality,
            captureBeyondViewport: options.fullPage ?? false,
        };
        if (options.clip) {
            params.clip = {
                ...options.clip,
                scale: 1,
            };
        }
        const result = await this.cdp.send('Page.captureScreenshot', params);
        if (options.encoding === 'base64') {
            return result.data;
        }
        return Buffer.from(result.data, 'base64');
    }
    /**
     * Generate PDF
     */
    async pdf(options = {}) {
        const result = await this.cdp.send('Page.printToPDF', {
            landscape: options.landscape ?? false,
            displayHeaderFooter: options.displayHeaderFooter ?? false,
            headerTemplate: options.headerTemplate ?? '',
            footerTemplate: options.footerTemplate ?? '',
            printBackground: options.printBackground ?? false,
            scale: options.scale ?? 1,
            paperWidth: typeof options.width === 'number' ? options.width / 96 : undefined,
            paperHeight: typeof options.height === 'number' ? options.height / 96 : undefined,
            marginTop: typeof options.margin?.top === 'number' ? options.margin.top / 96 : undefined,
            marginBottom: typeof options.margin?.bottom === 'number' ? options.margin.bottom / 96 : undefined,
            marginLeft: typeof options.margin?.left === 'number' ? options.margin.left / 96 : undefined,
            marginRight: typeof options.margin?.right === 'number' ? options.margin.right / 96 : undefined,
            pageRanges: options.pageRanges ?? '',
            preferCSSPageSize: options.preferCSSPageSize ?? false,
        });
        return Buffer.from(result.data, 'base64');
    }
    /**
     * Set viewport
     */
    async setViewport(viewport) {
        await this.cdp.send('Emulation.setDeviceMetricsOverride', {
            width: viewport.width,
            height: viewport.height,
            deviceScaleFactor: viewport.deviceScaleFactor ?? 1,
            mobile: viewport.isMobile ?? false,
            screenOrientation: viewport.isLandscape
                ? { angle: 90, type: 'landscapePrimary' }
                : { angle: 0, type: 'portraitPrimary' },
        });
    }
    /**
     * Get cookies
     */
    async cookies() {
        const result = await this.cdp.send('Network.getCookies');
        return result.cookies;
    }
    /**
     * Set cookies
     */
    async setCookie(...cookies) {
        for (const cookie of cookies) {
            await this.cdp.send('Network.setCookie', cookie);
        }
    }
    /**
     * Delete cookies
     */
    async deleteCookie(...cookies) {
        for (const cookie of cookies) {
            await this.cdp.send('Network.deleteCookies', cookie);
        }
    }
    /**
     * Get page metrics
     */
    async metrics() {
        const result = await this.cdp.send('Performance.getMetrics');
        const metrics = {};
        for (const { name, value } of result.metrics) {
            metrics[name] = value;
        }
        return metrics;
    }
    /**
     * Reload page
     */
    async reload(options) {
        await this.cdp.send('Page.reload');
        await this.waitForNavigation(options);
    }
    /**
     * Go back
     */
    async goBack(options) {
        await this.cdp.send('Page.goBack');
        await this.waitForNavigation(options);
    }
    /**
     * Go forward
     */
    async goForward(options) {
        await this.cdp.send('Page.goForward');
        await this.waitForNavigation(options);
    }
    /**
     * Close page
     */
    async close() {
        await this.cdp.send('Target.closeTarget', { targetId: this.targetId });
    }
}
// ============================================================================
// Browser Controller
// ============================================================================
/**
 * Main browser controller
 */
export class BrowserController extends EventEmitter {
    options;
    process = null;
    cdp = null;
    pagesMap = new Map();
    wsEndpoint = '';
    constructor(options = {}) {
        super();
        this.options = { ...DEFAULT_BROWSER_OPTIONS, ...options };
    }
    /**
     * Launch browser
     */
    async launch() {
        const execPath = this.options.executablePath ?? this.findChromePath();
        if (!execPath) {
            throw new Error('Chrome executable not found');
        }
        const args = [
            '--remote-debugging-port=' + (this.options.devToolsPort ?? 0),
            ...(this.options.headless ? ['--headless'] : []),
            ...(this.options.args ?? []),
        ];
        if (this.options.userDataDir) {
            args.push('--user-data-dir=' + this.options.userDataDir);
        }
        this.process = spawn(execPath, args, {
            stdio: ['pipe', 'pipe', 'pipe'],
        });
        // Parse WebSocket URL from stderr
        this.wsEndpoint = await this.getWSEndpoint();
        // Connect via CDP
        this.cdp = new CDPConnection();
        await this.cdp.connect(this.wsEndpoint);
    }
    /**
     * Find Chrome executable path
     */
    findChromePath() {
        const paths = [
            // Linux
            '/usr/bin/google-chrome',
            '/usr/bin/chromium-browser',
            '/usr/bin/chromium',
            // Mac
            '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
            // Windows
            'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
            'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
        ];
        // In a real implementation, we'd check if the path exists
        return paths[0];
    }
    /**
     * Get WebSocket endpoint from browser output
     */
    async getWSEndpoint() {
        return new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                reject(new Error('Timeout waiting for browser to start'));
            }, this.options.timeout ?? 30000);
            this.process.stderr.on('data', (data) => {
                const match = data.toString().match(/ws:\/\/[^\s]+/);
                if (match) {
                    clearTimeout(timeout);
                    resolve(match[0]);
                }
            });
            this.process.on('error', (err) => {
                clearTimeout(timeout);
                reject(err);
            });
            this.process.on('exit', (code) => {
                clearTimeout(timeout);
                reject(new Error(`Browser exited with code ${code}`));
            });
        });
    }
    /**
     * Connect to existing browser
     */
    async connect(wsEndpoint) {
        this.wsEndpoint = wsEndpoint;
        this.cdp = new CDPConnection();
        await this.cdp.connect(wsEndpoint);
    }
    /**
     * Create new page
     */
    async newPage() {
        if (!this.cdp) {
            throw new Error('Browser not connected');
        }
        const result = await this.cdp.send('Target.createTarget', {
            url: 'about:blank',
        });
        const page = new PageController(this.cdp, result.targetId);
        this.pagesMap.set(result.targetId, page);
        if (this.options.defaultViewport) {
            await page.setViewport(this.options.defaultViewport);
        }
        return page;
    }
    /**
     * Get all pages
     */
    async pages() {
        return Array.from(this.pagesMap.values());
    }
    /**
     * Get browser version
     */
    async version() {
        if (!this.cdp) {
            throw new Error('Browser not connected');
        }
        const result = await this.cdp.send('Browser.getVersion');
        return result.product;
    }
    /**
     * Get WebSocket endpoint
     */
    wsEndpointUrl() {
        return this.wsEndpoint;
    }
    /**
     * Close browser
     */
    async close() {
        // Close all pages
        for (const page of this.pagesMap.values()) {
            await page.close().catch((err) => {
                logger.debug('Page close error (ignored)', { error: err instanceof Error ? err.message : String(err) });
            });
        }
        this.pagesMap.clear();
        // Disconnect CDP
        if (this.cdp) {
            this.cdp.disconnect();
            this.cdp = null;
        }
        // Kill process
        if (this.process) {
            this.process.kill();
            this.process = null;
        }
        this.emit('close');
    }
    /**
     * Check if browser is connected
     */
    isConnected() {
        return this.cdp !== null;
    }
}
// ============================================================================
// Singleton
// ============================================================================
let browserInstance = null;
/**
 * Get or launch browser
 */
export async function getBrowser(options) {
    if (!browserInstance) {
        browserInstance = new BrowserController(options);
        await browserInstance.launch();
    }
    return browserInstance;
}
/**
 * Close browser
 */
export async function closeBrowser() {
    if (browserInstance) {
        await browserInstance.close();
        browserInstance = null;
    }
}
//# sourceMappingURL=controller.js.map