/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { PuppeteerDevToolsConnection } from './DevToolsConnectionAdapter.js';
import { ISSUE_UTILS } from './issue-descriptions.js';
import { logger } from './logger.js';
import { Mutex } from './Mutex.js';
import { DevTools } from './third_party/index.js';
export function extractUrlLikeFromDevToolsTitle(title) {
    const match = title.match(new RegExp(`DevTools - (.*)`));
    return match?.[1] ?? undefined;
}
export function urlsEqual(url1, url2) {
    const normalizedUrl1 = normalizeUrl(url1);
    const normalizedUrl2 = normalizeUrl(url2);
    return normalizedUrl1 === normalizedUrl2;
}
/**
 * For the sake of the MCP server, when we determine if two URLs are equal we
 * remove some parts:
 *
 * 1. We do not care about the protocol.
 * 2. We do not care about trailing slashes.
 * 3. We do not care about "www".
 * 4. We ignore the hash parts.
 *
 * For example, if the user types "record a trace on foo.com", we would want to
 * match a tab in the connected Chrome instance that is showing "www.foo.com/"
 */
function normalizeUrl(url) {
    let result = url.trim();
    // Remove protocols
    if (result.startsWith('https://')) {
        result = result.slice(8);
    }
    else if (result.startsWith('http://')) {
        result = result.slice(7);
    }
    // Remove 'www.'. This ensures that we find the right URL regardless of if the user adds `www` or not.
    if (result.startsWith('www.')) {
        result = result.slice(4);
    }
    // We use target URLs to locate DevTools but those often do
    // no include hash.
    const hashIdx = result.lastIndexOf('#');
    if (hashIdx !== -1) {
        result = result.slice(0, hashIdx);
    }
    // Remove trailing slash
    if (result.endsWith('/')) {
        result = result.slice(0, -1);
    }
    return result;
}
/**
 * A mock implementation of an issues manager that only implements the methods
 * that are actually used by the IssuesAggregator
 */
export class FakeIssuesManager extends DevTools.Common.ObjectWrapper
    .ObjectWrapper {
    issues() {
        return [];
    }
}
export function mapIssueToMessageObject(issue) {
    const count = issue.getAggregatedIssuesCount();
    const markdownDescription = issue.getDescription();
    const filename = markdownDescription?.file;
    if (!markdownDescription) {
        logger(`no description found for issue:` + issue.code);
        return null;
    }
    const rawMarkdown = filename
        ? ISSUE_UTILS.getIssueDescription(filename)
        : null;
    if (!rawMarkdown) {
        logger(`no markdown ${filename} found for issue:` + issue.code);
        return null;
    }
    let processedMarkdown;
    let title;
    try {
        processedMarkdown =
            DevTools.MarkdownIssueDescription.substitutePlaceholders(rawMarkdown, markdownDescription.substitutions);
        const markdownAst = DevTools.Marked.Marked.lexer(processedMarkdown);
        title =
            DevTools.MarkdownIssueDescription.findTitleFromMarkdownAst(markdownAst);
    }
    catch {
        logger('error parsing markdown for issue ' + issue.code());
        return null;
    }
    if (!title) {
        logger('cannot read issue title from ' + filename);
        return null;
    }
    return {
        type: 'issue',
        item: issue,
        message: title,
        count,
        description: processedMarkdown,
    };
}
// DevTools CDP errors can get noisy.
DevTools.ProtocolClient.InspectorBackend.test.suppressRequestErrors = true;
DevTools.I18n.DevToolsLocale.DevToolsLocale.instance({
    create: true,
    data: {
        navigatorLanguage: 'en-US',
        settingLanguage: 'en-US',
        lookupClosestDevToolsLocale: l => l,
    },
});
DevTools.I18n.i18n.registerLocaleDataForTest('en-US', {});
export class UniverseManager {
    #browser;
    #createUniverseFor;
    #universes = new WeakMap();
    /** Guard access to #universes so we don't create unnecessary universes */
    #mutex = new Mutex();
    constructor(browser, factory = DEFAULT_FACTORY) {
        this.#browser = browser;
        this.#createUniverseFor = factory;
    }
    async init(pages) {
        try {
            await this.#mutex.acquire();
            const promises = [];
            for (const page of pages) {
                promises.push(this.#createUniverseFor(page).then(targetUniverse => this.#universes.set(page, targetUniverse)));
            }
            this.#browser.on('targetcreated', this.#onTargetCreated);
            this.#browser.on('targetdestroyed', this.#onTargetDestroyed);
            await Promise.all(promises);
        }
        finally {
            this.#mutex.release();
        }
    }
    get(page) {
        return this.#universes.get(page) ?? null;
    }
    dispose() {
        this.#browser.off('targetcreated', this.#onTargetCreated);
        this.#browser.off('targetdestroyed', this.#onTargetDestroyed);
    }
    #onTargetCreated = async (target) => {
        const page = await target.page();
        try {
            await this.#mutex.acquire();
            if (!page || this.#universes.has(page)) {
                return;
            }
            this.#universes.set(page, await this.#createUniverseFor(page));
        }
        finally {
            this.#mutex.release();
        }
    };
    #onTargetDestroyed = async (target) => {
        const page = await target.page();
        try {
            await this.#mutex.acquire();
            if (!page || !this.#universes.has(page)) {
                return;
            }
            this.#universes.delete(page);
        }
        finally {
            this.#mutex.release();
        }
    };
}
const DEFAULT_FACTORY = async (page) => {
    const settingStorage = new DevTools.Common.Settings.SettingsStorage({});
    const universe = new DevTools.Foundation.Universe.Universe({
        settingsCreationOptions: {
            syncedStorage: settingStorage,
            globalStorage: settingStorage,
            localStorage: settingStorage,
            settingRegistrations: DevTools.Common.SettingRegistration.getRegisteredSettings(),
        },
        overrideAutoStartModels: new Set([DevTools.DebuggerModel]),
    });
    const session = await page.createCDPSession();
    const connection = new PuppeteerDevToolsConnection(session);
    const targetManager = universe.context.get(DevTools.TargetManager);
    targetManager.observeModels(DevTools.DebuggerModel, SKIP_ALL_PAUSES);
    const target = targetManager.createTarget('main', '', 'frame', // eslint-disable-line @typescript-eslint/no-explicit-any
    /* parentTarget */ null, session.id(), undefined, connection);
    return { target, universe };
};
// We don't want to pause any DevTools universe session ever on the MCP side.
//
// Note that calling `setSkipAllPauses` only affects the session on which it was
// sent. This means DevTools can still pause, step and do whatever. We just won't
// see the `Debugger.paused`/`Debugger.resumed` events on the MCP side.
const SKIP_ALL_PAUSES = {
    modelAdded(model) {
        void model.agent.invoke_setSkipAllPauses({ skip: true });
    },
    modelRemoved() {
        // Do nothing.
    },
};
