/**
 * BrowserService — singleton managing Playwright browser lifecycle.
 *
 * One browser instance, one persistent context, multiple pages (tabs).
 * Collects console messages and network requests automatically.
 * Follows the same singleton pattern as fetcher-mcp's BrowserService.
 *
 * Playwright is loaded lazily on first use (not at import time). This allows
 * the module to load in standalone Bun binaries where playwright is unavailable.
 * Tools will report a clear "Browser unavailable" error instead of crashing.
 */
import { createRequire } from "node:module";
import { getBrowserConfig } from "./config.js";
const _require = createRequire(import.meta.url);
let _pw = null;
let _pwError = null;
function loadPlaywright() {
    if (_pw)
        return _pw;
    if (_pwError)
        throw new Error(_pwError);
    try {
        _pw = _require("playwright");
        return _pw;
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        _pwError = `Browser automation unavailable: playwright could not be loaded (${msg}).`;
        throw new Error(_pwError);
    }
}
let instance = null;
export class BrowserService {
    browser = null;
    context = null;
    pages = [];
    activePageIndex = 0;
    config;
    consoleBuffer = [];
    networkBuffer = new Map();
    nextNetworkIndex = 0;
    pendingDialogs = [];
    constructor(config) {
        this.config = { ...getBrowserConfig(), ...config };
    }
    /** Get or create the singleton instance. */
    static getInstance(config) {
        if (!instance) {
            instance = new BrowserService(config);
        }
        return instance;
    }
    /** Reset singleton (for testing). */
    static resetInstance() {
        instance = null;
    }
    /** Check whether Playwright is installed and loadable (static, does NOT start a browser). */
    static isAvailable() {
        try {
            loadPlaywright();
            return true;
        }
        catch {
            return false;
        }
    }
    /**
     * Human-readable message explaining why the browser is unavailable.
     * Returns null when the browser is available.
     */
    static unavailabilityReason() {
        if (_pwError)
            return _pwError;
        if (_pw)
            return null;
        try {
            loadPlaywright();
            return null;
        }
        catch (err) {
            return err instanceof Error ? err.message : String(err);
        }
    }
    /** Ensure browser, context, and at least one page are ready. */
    async ensureBrowser() {
        if (this.browser?.isConnected() && this.context && this.pages.length > 0) {
            const page = this.pages[this.activePageIndex];
            return { browser: this.browser, context: this.context, page };
        }
        await this.close();
        const pw = loadPlaywright();
        const launchOptions = {
            headless: this.config.headless,
            args: [
                "--no-sandbox",
                "--disable-setuid-sandbox",
                "--disable-dev-shm-usage",
            ],
        };
        switch (this.config.browserName) {
            case "firefox":
                this.browser = await pw.firefox.launch(launchOptions);
                break;
            case "webkit":
                this.browser = await pw.webkit.launch(launchOptions);
                break;
            default:
                this.browser = await pw.chromium.launch(launchOptions);
        }
        this.context = await this.browser.newContext({
            viewport: this.config.viewport,
        });
        // Auto-collect console messages and network requests
        this.context.on("page", (page) => {
            page.on("console", (msg) => {
                this.consoleBuffer.push({
                    type: msg.type(),
                    text: msg.text(),
                    pageUrl: page.url(),
                });
            });
            this.trackNetworkRequest(page);
            this.installDialogInterceptor(page);
        });
        const page = await this.context.newPage();
        this.trackNetworkRequest(page);
        this.installDialogInterceptor(page);
        this.pages = [page];
        this.activePageIndex = 0;
        return { browser: this.browser, context: this.context, page };
    }
    /** Get the currently active page. */
    getActivePage() {
        if (this.pages.length === 0) {
            throw new Error("No browser pages available. Call ensureBrowser() first.");
        }
        return this.pages[this.activePageIndex];
    }
    /** Navigate to a URL on the active page. */
    async navigate(url) {
        const { page } = await this.ensureBrowser();
        // Auto-prepend https if missing
        const normalizedUrl = url.startsWith("http") ? url : `https://${url}`;
        await page.goto(normalizedUrl, {
            timeout: this.config.navigationTimeout,
            waitUntil: "domcontentloaded",
        });
        return { title: await page.title(), url: page.url() };
    }
    /** Navigate back. */
    async navigateBack() {
        const page = this.getActivePage();
        await page.goBack({ timeout: this.config.navigationTimeout });
        return { title: await page.title(), url: page.url() };
    }
    /** Close the active page. If last page, create a blank one. */
    async closePage() {
        if (this.pages.length === 0)
            return;
        const page = this.pages[this.activePageIndex];
        await page.close();
        this.pages.splice(this.activePageIndex, 1);
        if (this.pages.length === 0 && this.context) {
            const newPage = await this.context.newPage();
            this.pages = [newPage];
            this.activePageIndex = 0;
        }
        else {
            this.activePageIndex = Math.min(this.activePageIndex, this.pages.length - 1);
        }
    }
    /** Resize the viewport. */
    async resize(width, height) {
        const page = this.getActivePage();
        await page.setViewportSize({ width, height });
        this.config.viewport = { width, height };
    }
    // --- Tab management ---
    listTabs() {
        return this.pages.map((p, i) => ({
            index: i,
            title: p.url() ? `Tab ${i}` : p.url(),
            url: p.url(),
        }));
    }
    async createTab() {
        if (!this.context)
            await this.ensureBrowser();
        const page = await this.context.newPage();
        this.trackNetworkRequest(page);
        this.installDialogInterceptor(page);
        this.pages.push(page);
        this.activePageIndex = this.pages.length - 1;
        return { index: this.activePageIndex };
    }
    async closeTab(index) {
        if (this.pages.length <= 1)
            return; // Keep at least one page
        if (index < 0 || index >= this.pages.length)
            return;
        const page = this.pages[index];
        await page.close();
        this.pages.splice(index, 1);
        if (this.activePageIndex >= this.pages.length) {
            this.activePageIndex = this.pages.length - 1;
        }
    }
    async selectTab(index) {
        if (index < 0 || index >= this.pages.length) {
            throw new Error(`Tab index ${index} out of range (0-${this.pages.length - 1})`);
        }
        this.activePageIndex = index;
        const page = this.getActivePage();
        await page.bringToFront();
        return { index };
    }
    // --- Dialog interception ---
    installDialogInterceptor(page) {
        page.on("dialog", (dialog) => {
            // Hold the dialog until agent handles it via browser_handle_dialog
            this.pendingDialogs.push({
                message: dialog.message(),
                type: dialog.type(),
                resolve: (text) => {
                    dialog.accept(text).catch(() => { });
                },
                reject: () => {
                    dialog.dismiss().catch(() => { });
                },
            });
        });
    }
    getPendingDialogs() {
        return this.pendingDialogs.map((d) => ({ message: d.message, type: d.type }));
    }
    async handleDialog(accept, promptText) {
        if (this.pendingDialogs.length === 0) {
            throw new Error("No pending dialogs.");
        }
        const dialog = this.pendingDialogs.shift();
        if (accept) {
            dialog.resolve(promptText);
        }
        else {
            dialog.reject(new Error("Dialog dismissed"));
        }
        return dialog.message;
    }
    // --- Console messages ---
    getConsoleMessages(level, all = false) {
        const msgs = all ? [...this.consoleBuffer] : [...this.consoleBuffer];
        if (level) {
            const severity = { error: 0, warning: 1, info: 2, debug: 3 };
            const threshold = severity[level];
            return msgs.filter((m) => (severity[m.type] ?? 3) <= threshold);
        }
        return msgs;
    }
    clearConsoleMessages() {
        this.consoleBuffer = [];
    }
    // --- Network requests ---
    trackNetworkRequest(page) {
        page.on("request", (request) => {
            const index = this.nextNetworkIndex++;
            const entry = {
                index,
                url: request.url(),
                method: request.method(),
                status: 0,
                statusText: "",
                headers: request.headers(),
                responseHeaders: {},
                postData: request.postData() ?? undefined,
                timestamp: Date.now(),
            };
            this.networkBuffer.set(index, entry);
            request.response().then((response) => {
                if (!response)
                    return;
                entry.status = response.status();
                entry.statusText = response.statusText();
                entry.responseHeaders = response.headers();
            }).catch(() => {
                // Request may fail or be aborted
            });
        });
    }
    getNetworkRequests() {
        return Array.from(this.networkBuffer.values()).sort((a, b) => a.index - b.index);
    }
    getNetworkRequest(index) {
        return this.networkBuffer.get(index);
    }
    clearNetworkRequests() {
        this.networkBuffer.clear();
        this.nextNetworkIndex = 0;
    }
    // --- Lifecycle ---
    /** Close browser, context, and all pages. */
    async close() {
        for (const page of this.pages) {
            try {
                await page.close();
            }
            catch { /* ignore */ }
        }
        this.pages = [];
        this.activePageIndex = 0;
        if (this.context) {
            try {
                await this.context.close();
            }
            catch { /* ignore */ }
            this.context = null;
        }
        if (this.browser) {
            try {
                await this.browser.close();
            }
            catch { /* ignore */ }
            this.browser = null;
        }
    }
    /** Reset console/network buffers. */
    resetBuffers() {
        this.clearConsoleMessages();
        this.clearNetworkRequests();
    }
    getConfig() {
        return { ...this.config };
    }
}
