/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { logger } from './logger.js';
import { puppeteer } from './third_party/index.js';
let browser;
let browserMode;
function makeTargetFilter(enableExtensions = false) {
    const ignoredPrefixes = new Set(['chrome://', 'chrome-untrusted://']);
    if (!enableExtensions) {
        ignoredPrefixes.add('chrome-extension://');
    }
    return function targetFilter(target) {
        if (target.url() === 'chrome://newtab/') {
            return true;
        }
        // Could be the only page opened in the browser.
        if (target.url().startsWith('chrome://inspect')) {
            return true;
        }
        for (const prefix of ignoredPrefixes) {
            if (target.url().startsWith(prefix)) {
                return false;
            }
        }
        return true;
    };
}
export async function ensureBrowserConnected(options) {
    const { channel, enableExtensions } = options;
    if (browser?.connected) {
        return browser;
    }
    const connectOptions = {
        targetFilter: makeTargetFilter(enableExtensions),
        defaultViewport: null,
        handleDevToolsAsPage: true,
    };
    let autoConnect = false;
    if (options.wsEndpoint) {
        connectOptions.browserWSEndpoint = options.wsEndpoint;
        if (options.wsHeaders) {
            connectOptions.headers = options.wsHeaders;
        }
    }
    else if (options.browserURL) {
        connectOptions.browserURL = options.browserURL;
    }
    else if (channel || options.userDataDir) {
        const userDataDir = options.userDataDir;
        if (userDataDir) {
            autoConnect = true;
            // TODO: re-expose this logic via Puppeteer.
            const portPath = path.join(userDataDir, 'DevToolsActivePort');
            try {
                const fileContent = await fs.promises.readFile(portPath, 'utf8');
                const [rawPort, rawPath] = fileContent
                    .split('\n')
                    .map(line => {
                    return line.trim();
                })
                    .filter(line => {
                    return !!line;
                });
                if (!rawPort || !rawPath) {
                    throw new Error(`Invalid DevToolsActivePort '${fileContent}' found`);
                }
                const port = parseInt(rawPort, 10);
                if (isNaN(port) || port <= 0 || port > 65535) {
                    throw new Error(`Invalid port '${rawPort}' found`);
                }
                const browserWSEndpoint = `ws://127.0.0.1:${port}${rawPath}`;
                connectOptions.browserWSEndpoint = browserWSEndpoint;
            }
            catch (error) {
                throw new Error(`Could not connect to Chrome in ${userDataDir}. Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.`, {
                    cause: error,
                });
            }
        }
        else {
            if (!channel) {
                throw new Error('Channel must be provided if userDataDir is missing');
            }
            connectOptions.channel = (channel === 'stable' ? 'chrome' : `chrome-${channel}`);
        }
    }
    else {
        throw new Error('Either browserURL, wsEndpoint, channel or userDataDir must be provided');
    }
    logger('Connecting Puppeteer to ', JSON.stringify(connectOptions));
    try {
        // Assign mode before browser so a concurrent closeBrowser() never sees
        // `browser` set with `browserMode` still undefined (would fall through
        // to the disconnect() path and orphan a launched Chrome).
        const connected = await puppeteer.connect(connectOptions);
        browserMode = 'connected';
        browser = connected;
    }
    catch (err) {
        throw new Error(`Could not connect to Chrome. ${autoConnect ? `Check if Chrome is running and remote debugging is enabled by going to chrome://inspect/#remote-debugging.` : `Check if Chrome is running.`}`, {
            cause: err,
        });
    }
    logger('Connected Puppeteer');
    return browser;
}
export function detectDisplay() {
    // Only detect display on Linux/UNIX.
    if (os.platform() === 'win32' || os.platform() === 'darwin') {
        return;
    }
    if (!process.env['DISPLAY']) {
        try {
            const result = execSync(`ps -u $(id -u) -o pid= | xargs -I{} cat /proc/{}/environ 2>/dev/null | tr '\\0' '\\n' | grep -m1 '^DISPLAY=' | cut -d= -f2`);
            const display = result.toString('utf8').trim();
            process.env['DISPLAY'] = display;
        }
        catch {
            // no-op
        }
    }
}
export async function launch(options) {
    const { channel, executablePath, headless, isolated } = options;
    const profileDirName = channel && channel !== 'stable'
        ? `chrome-profile-${channel}`
        : 'chrome-profile';
    let userDataDir = options.userDataDir;
    if (!isolated && !userDataDir) {
        userDataDir = path.join(os.homedir(), '.cache', options.viaCli ? 'chrome-devtools-mcp-cli' : 'chrome-devtools-mcp', profileDirName);
        await fs.promises.mkdir(userDataDir, {
            recursive: true,
        });
    }
    const args = [
        ...(options.chromeArgs ?? []),
        '--hide-crash-restore-bubble',
    ];
    const ignoreDefaultArgs = options.ignoreDefaultChromeArgs ?? false;
    if (headless) {
        args.push('--screen-info={3840x2160}');
    }
    let puppeteerChannel;
    if (options.devtools) {
        args.push('--auto-open-devtools-for-tabs');
    }
    if (!executablePath) {
        puppeteerChannel =
            channel && channel !== 'stable'
                ? `chrome-${channel}`
                : 'chrome';
    }
    if (!headless) {
        detectDisplay();
    }
    try {
        const browser = await puppeteer.launch({
            channel: puppeteerChannel,
            targetFilter: makeTargetFilter(options.enableExtensions),
            executablePath,
            defaultViewport: null,
            userDataDir,
            pipe: true,
            headless,
            args,
            ignoreDefaultArgs: ignoreDefaultArgs,
            acceptInsecureCerts: options.acceptInsecureCerts,
            handleDevToolsAsPage: true,
            enableExtensions: options.enableExtensions,
        });
        if (options.logFile) {
            // FIXME: we are probably subscribing too late to catch startup logs. We
            // should expose the process earlier or expose the getRecentLogs() getter.
            browser.process()?.stderr?.pipe(options.logFile);
            browser.process()?.stdout?.pipe(options.logFile);
        }
        if (options.viewport) {
            const [page] = await browser.pages();
            await page?.resize({
                contentWidth: options.viewport.width,
                contentHeight: options.viewport.height,
            });
        }
        return browser;
    }
    catch (error) {
        if (userDataDir &&
            error.message.includes('The browser is already running')) {
            throw new Error(`The browser is already running for ${userDataDir}. Use --isolated to run multiple browser instances.`, {
                cause: error,
            });
        }
        throw error;
    }
}
export async function ensureBrowserLaunched(options) {
    if (browser?.connected) {
        return browser;
    }
    // Assign mode before browser; see the connect path above for rationale.
    const launched = await launch(options);
    browserMode = 'launched';
    browser = launched;
    return browser;
}
/**
 * Shutdown hook for the active browser. Closes a launched browser (so the
 * Chrome subprocess is reaped) or disconnects from an attached browser (so
 * the user's Chrome instance stays alive). No-op if no browser is active or
 * the connection has already been dropped. Called from the server entrypoint
 * on stdin EOF / SIGTERM / SIGINT.
 */
export async function closeBrowser() {
    const b = browser;
    const mode = browserMode;
    browser = undefined;
    browserMode = undefined;
    if (!b || !b.connected) {
        return;
    }
    if (mode === 'launched') {
        await b.close().catch(err => {
            logger('Failed to close browser', err);
        });
        return;
    }
    await b.disconnect().catch(err => {
        logger('Failed to disconnect from browser', err);
    });
}
//# sourceMappingURL=browser.js.map