// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import {
    BrowserWindow,
    DevicePermissionHandlerHandlerDetails,
    globalShortcut,
    ipcMain,
    shell,
    WebContents,
    WebContentsView,
} from "electron";
import path from "node:path";
import type { WebSocketMessageV2 } from "websocket-utils";
import { runDemo, breakDemo, isDemoActive } from "./demo.js";
import {
    ShellUserSettings,
    ShellWindowState,
    ShellSettingManager,
    BrowserTabState,
} from "./shellSettings.js";
import { loadLocalWebContents } from "./utils.js";
import { BrowserAgentIpc } from "./browserIpc.js";
import {
    BrowserViewManager,
    BrowserViewContext,
} from "./browserViewManager.js";

import registerDebug from "debug";
import { ChatServer } from "./chatServer.js";

const debugShellWindow = registerDebug("typeagent:shell:window");
const debugShellWindowError = registerDebug("typeagent:shell:window:error");

const isMac = process.platform === "darwin";
const isLinux = process.platform === "linux";
const TITLE_BAR_HEIGHT = isMac ? 36 : 40; // Custom title bar height
function setupResizeHandler(mainWindow: BrowserWindow, handler: () => void) {
    let scheduleHandler: (() => void) | undefined;
    let timeout: NodeJS.Timeout | undefined;
    mainWindow.on("resize", handler);
    if (isLinux) {
        // Workaround for electron bug where getContentSize isn't updated when "resize" event is fired
        // https://github.com/electron/electron/issues/42586
        scheduleHandler = () => {
            debugShellWindow("Scheduled Maximize/Unmaximize update");
            timeout = setTimeout(() => {
                timeout = undefined;
                debugShellWindow(
                    "Running scheduled Maximize/Unmaximize update",
                );
                handler();
            }, 100);
        };
        mainWindow.on("maximize", scheduleHandler);
        mainWindow.on("unmaximize", scheduleHandler);
    }

    // Clean up function for close
    return () => {
        clearTimeout(timeout);
        mainWindow.removeListener("resize", handler);
        if (scheduleHandler !== undefined) {
            mainWindow.removeListener("maximize", scheduleHandler);
            mainWindow.removeListener("unmaximize", scheduleHandler);
        }
    };
}

type BottomAlignedPosition = { left: number; bottom: number };
type OverlayData = BottomAlignedPosition & {
    width: number;
    height: number;
};

const dividerSize = 4; // 4px divider
export class ShellWindow {
    public readonly mainWindow: BrowserWindow;
    public readonly chatView: WebContentsView;
    private readonly overlayWebContentsViews: Map<
        WebContentsView,
        OverlayData
    > = new Map();

    private verticalLayout: boolean;
    private contentVisible: boolean = false;
    // For use in horizontal layout
    private chatWidth: number;
    private contentWidth: number; // does not include dividerSize
    private windowHeight: number; // includes TITLE_BAR_HEIGHT

    // For use in vertical layout
    private chatHeight: number;
    private contentHeight: number; // does not include dividerSize
    private windowWidth: number;

    private readonly contentLoadP: Promise<void>[];
    private readonly handlers = new Map<string, (event: any) => void>();
    private readyP = Promise.withResolvers<void>();
    private browserTabRestored = false;
    private closing: boolean = false;

    // Multi-tab browser support
    private readonly browserViewManager: BrowserViewManager;

    // Chat as a tab support
    public chatViewServer: ChatServer | undefined;

    public get inlineBrowser(): WebContentsView {
        // Always use multi-tab browser
        const activeBrowserView =
            this.browserViewManager.getActiveBrowserView();
        if (!activeBrowserView) {
            throw new Error("No browser tab is open");
        }
        return activeBrowserView.webContentsView;
    }

    constructor(
        private readonly settings: ShellSettingManager,
        private readonly inputOnly: boolean,
    ) {
        const state = this.settings.window;
        this.chatWidth = state.chatWidth;
        this.chatHeight = state.chatHeight;
        this.contentWidth = state.contentWidth;
        this.contentHeight = state.contentHeight;
        this.windowWidth = state.windowWidth;
        this.windowHeight = state.windowHeight;

        const uiSettings = this.settings.user.ui;
        // Calculate the initial window bound.
        this.verticalLayout = uiSettings.verticalLayout;
        const mainWindow = createMainWindow(inputOnly);

        setupDevicePermissions(mainWindow);

        // Initialize browser view manager
        this.browserViewManager = new BrowserViewManager(this);

        // Set up event callbacks for browser view manager
        this.browserViewManager.setTabUpdateCallback(() => {
            this.sendTabsUpdate();
        });

        this.browserViewManager.setNavigationUpdateCallback(() => {
            this.sendNavigationUpdate();
        });

        this.browserViewManager.setPageLoadCompleteCallback((tabId: string) => {
            // Only restore focus if this is the active tab
            const activeTab = this.browserViewManager.getActiveBrowserView();
            if (activeTab && activeTab.id === tabId) {
                this.focusChatInput();
            }
        });

        const resizeHandlerCleanup = setupResizeHandler(mainWindow, () =>
            this.updateContentSize(),
        );
        mainWindow.on("close", () => {
            this.cleanup();
            this.settings.save(this.getWindowState());

            mainWindow.hide();
            // Remove the handler to avoid update the content size on close
            resizeHandlerCleanup();
            // Close all browser tabs on window close
            this.browserViewManager.cleanup();
        });

        const chatView = createChatView(state);
        chatView.setBackgroundColor("#00000000");
        this.setupZoomHandlers(chatView.webContents, () => {
            this.updateOverlayBounds();
        });

        chatView.webContents.on("focus", () => {
            this.setOverlayVisibility(true);
        });

        mainWindow.contentView.addChildView(chatView);

        this.mainWindow = mainWindow;
        this.chatView = chatView;

        this.installHandler("chat-view-ready", () => {
            this.ready({ x: state.x, y: state.y });
        });

        // Browser tab management IPC handlers
        ipcMain.on("browser-new-tab", () => {
            this.createBrowserTab();
        });

        ipcMain.on("browser-close-tab", (_, tabId: string) => {
            const success = this.closeBrowserTab(tabId);
            if (success) {
                this.sendTabsUpdate();

                // Update layout if no tabs left
                if (!this.hasBrowserTabs()) {
                    this.updateWindowBounds(this.getWindowPositionState());
                }

                // Restore focus to chat after closing tab
                this.focusChatInput();
            }
        });

        ipcMain.on("browser-switch-tab", (_, tabId: string) => {
            const success = this.switchBrowserTab(tabId);
            if (success) {
                this.sendTabsUpdate();
            }
        });

        ipcMain.on("browser-go-back", () => {
            if (this.browserGoBack()) {
                this.sendNavigationUpdate();
            }
        });

        ipcMain.on("browser-go-forward", () => {
            if (this.browserGoForward()) {
                this.sendNavigationUpdate();
            }
        });

        ipcMain.on("browser-reload", () => {
            this.browserReload();
        });

        if (!isLinux) {
            // REVIEW: on linux, setting windows bound before showing has not affect?
            this.updateWindowBounds({ x: state.x, y: state.y });
        }

        const contentLoadP: Promise<void>[] = [];
        if (inputOnly) {
            contentLoadP.push(
                loadLocalWebContents(chatView.webContents, "chatView.html", {
                    inputOnly: "true",
                }),
            );
        } else {
            contentLoadP.push(
                loadLocalWebContents(chatView.webContents, "chatView.html"),
            );

            contentLoadP.push(
                loadLocalWebContents(mainWindow.webContents, "viewHost.html"),
            );
        }
        this.contentLoadP = contentLoadP;
    }

    public waitForReady(): Promise<void> {
        return this.readyP.promise;
    }

    public showAndFocus() {
        if (this.closing) {
            return;
        }
        this.mainWindow.show();
        this.focusChatInput();
    }

    public focusChatInput() {
        this.chatView.webContents.focus();
        this.chatView.webContents.send("focus-chat-input");
    }

    private async restoreBrowserTabs() {
        // Restore browser tabs if they were previously open
        const states = this.settings.window;
        if (!this.inputOnly && states.browserTabsJson) {
            try {
                const browserTabsState: BrowserTabState[] = JSON.parse(
                    states.browserTabsJson,
                );

                // Notify the chat view that tabs are being restored
                this.chatView.webContents.send(
                    "tab-restore-status",
                    browserTabsState.length,
                );

                debugShellWindow(
                    `Restoring ${browserTabsState.length} browser tabs`,
                );

                // Restore all tabs in parallel
                await Promise.all(
                    browserTabsState.map(async (tabState) => {
                        try {
                            const tabId = await this.createBrowserTab(
                                new URL(tabState.url),
                                {
                                    background: !tabState.isActive,
                                },
                            );
                            debugShellWindow(
                                `Restored tab: ${tabId} - ${tabState.title} (${tabState.url})`,
                            );
                        } catch (e) {
                            debugShellWindowError(
                                `Failed to restore tab ${tabState.title} (${tabState.url}): ${e}`,
                            );
                        }
                    }),
                );
            } catch (e) {
                debugShellWindowError(
                    `Failed to parse browser tabs JSON: ${e}`,
                );
            }
        }

        // Notify chat view that tab restoration is complete
        this.chatView.webContents.send("tab-restore-status", 0);

        this.browserTabRestored = true;
        await this.ensureAutoEmptyTab();
    }

    private async ensureAutoEmptyTab() {
        if (
            this.contentVisible &&
            !this.hasBrowserTabs() &&
            this.settings.user.ui.autoEmptyTab &&
            this.browserTabRestored &&
            !this.closing
        ) {
            await this.createBrowserTab();
        }
    }

    private async ready(position: { x: number; y: number }) {
        try {
            // Send settings asap
            this.sendUserSettingChanged();
            let lastDemoAdvance = 0;
            globalShortcut.register("Control+Right", () => {
                const now = Date.now();
                if (now - lastDemoAdvance < 500) {
                    return;
                }
                lastDemoAdvance = now;
                this.chatView.webContents.send("send-demo-event", "Alt+Right");
            });

            // Register global shortcuts to focus chat input from any app
            globalShortcut.register("CommandOrControl+L", () => {
                this.showAndFocus();
            });

            globalShortcut.register("Alt+E", () => {
                this.showAndFocus();
            });

            // Make sure content is loaded so we can adjust the size including the divider.
            await Promise.all(this.contentLoadP);

            if (!isLinux) {
                // REVIEW: on linux, setting windows bound before showing has not affect?
                this.updateContentSize();
            }

            const mainWindow = this.mainWindow;
            this.setZoomFactor(1, mainWindow.webContents);
            this.showAndFocus();

            // Restore browser tabs after window is visible so startup isn't blocked
            // Note: Site agent registration messages will be queued if WebSocket
            // isn't ready yet, then flushed when connection is established
            await this.restoreBrowserTabs();

            if (isLinux) {
                // REVIEW: on linux, setting windows bound before showing has not affect?
                this.updateWindowBounds(position);
            }

            const states = this.settings.window;
            if (states.devTools) {
                this.chatView.webContents.openDevTools();
            }
            this.readyP.resolve();
        } catch (e) {
            if (!this.closing) {
                this.readyP.reject(e);
            }
        }
    }

    /**
     * Starts the chat server
     * @param port - The port upon which to start the chat server
     */
    public startChatServer(port: number) {
        if (this.chatViewServer !== undefined) {
            debugShellWindow("Chat server already started");
            return;
        }

        if (port > 0) {
            this.chatViewServer = new ChatServer(port);
            this.chatViewServer.onConnection(async (ws: WebSocket) => {
                // when a new connection is established send the chat log to the client
                try {
                    const html: string =
                        await this.chatView.webContents.executeJavaScript(
                            `document.getElementById('wrapper').innerHTML`,
                        );
                    ws.send(html);
                } catch (error) {
                    console.error("Error sending chat log:", error);
                    ws.send(JSON.stringify(error));
                }
            });
            this.chatViewServer.start();
        }
    }

    private installHandler(name: string, handler: (event: any) => void) {
        this.handlers.set(name, handler);
        ipcMain.on(name, handler);
    }

    private cleanup() {
        this.closing = true;
        for (const [key, handler] of this.handlers) {
            ipcMain.removeListener(key, handler);
        }
        this.handlers.clear();

        // Clean up browser IPC handlers
        ipcMain.removeAllListeners("browser-new-tab");
        ipcMain.removeAllListeners("browser-close-tab");
        ipcMain.removeAllListeners("browser-switch-tab");
        ipcMain.removeAllListeners("browser-go-back");
        ipcMain.removeAllListeners("browser-go-forward");
        ipcMain.removeAllListeners("browser-reload");

        globalShortcut.unregister("Control+Right");
        globalShortcut.unregister("CommandOrControl+L");
        globalShortcut.unregister("Alt+E");

        // shutdown chat server
        if (this.chatViewServer) {
            this.chatViewServer.stop();
        }
    }

    public minimize() {
        this.mainWindow.minimize();
    }

    public toggleMaximize() {
        if (this.mainWindow.isMaximized()) {
            this.mainWindow.unmaximize();
        } else {
            this.mainWindow.maximize();
        }
    }

    public async close() {
        if (this.mainWindow.isDestroyed()) {
            return;
        }
        if (!this.closing) {
            this.mainWindow.close();
        }
        return new Promise<void>((resolve) => {
            this.mainWindow.once("closed", () => {
                resolve();
            });
        });
    }

    public sendMessageToInlineWebContent(message: WebSocketMessageV2) {
        const activeBrowserView =
            this.browserViewManager.getActiveBrowserView();
        activeBrowserView?.webContentsView.webContents.send(
            "received-from-browser-ipc",
            message,
        );
    }

    public sendSystemNotification(message: string, id?: string): void {
        this.chatView.webContents.send("system-notification", {
            message,
            id: id || `system-${Date.now()}`,
            timestamp: Date.now(),
        });
    }

    /**
     * Notify the renderer that the active conversation has changed.
     */
    public sendConversationChanged(conversationId: string, name: string): void {
        this.chatView.webContents.send(
            "conversation-changed",
            conversationId,
            name,
        );
    }

    public sendMarkHistory(): void {
        this.chatView.webContents.send("mark-history");
    }

    /**
     * Start a demo.  Returns true if a demo was started, false if one
     * was already in progress (caller should surface this to the user).
     * The returned promise resolves immediately after the start guard;
     * the demo itself runs in the background.
     */
    public runDemo(interactive: boolean = false): boolean {
        if (isDemoActive() || this.demoStarting) {
            return false;
        }
        this.demoStarting = true;
        let started = false;
        runDemo(this.mainWindow, this.chatView, interactive, () => {
            started = true;
            this.setDemoMode(true);
        })
            .catch((error) => {
                console.error("runDemo failed:", error);
            })
            .finally(() => {
                this.demoStarting = false;
                if (started) this.setDemoMode(false);
            });
        return true;
    }
    private demoStarting: boolean = false;

    public breakDemo(): boolean {
        return breakDemo();
    }

    private demoMode = false;
    private demoEscHandler:
        | ((event: Electron.Event, input: Electron.Input) => void)
        | undefined;
    private setDemoMode(active: boolean) {
        if (this.demoMode === active) return;
        this.demoMode = active;
        this.mainWindow.webContents.send("demo-mode-changed", active);
        if (active) {
            this.demoEscHandler = (event, input) => {
                if (input.type === "keyDown" && input.key === "Escape") {
                    event.preventDefault();
                    breakDemo();
                }
            };
            this.chatView.webContents.on(
                "before-input-event",
                this.demoEscHandler,
            );
            this.mainWindow.webContents.on(
                "before-input-event",
                this.demoEscHandler,
            );
        } else if (this.demoEscHandler) {
            this.chatView.webContents.off(
                "before-input-event",
                this.demoEscHandler,
            );
            this.mainWindow.webContents.off(
                "before-input-event",
                this.demoEscHandler,
            );
            this.demoEscHandler = undefined;
        }
    }

    // ================================================================
    // Settings
    // ================================================================
    public getUserSettings() {
        return this.settings.user;
    }

    public setUserSettings(userSettings: ShellUserSettings) {
        // This comes from the renderer process, so we don't need to call sendUserSettingChanged
        this.settings.setUserSettings(userSettings);
        this.settings.save(this.getWindowState());
    }
    public setUserSettingValue(name: string, value: unknown) {
        const changed = this.settings.setUserSettingValue(name, value);
        if (changed) {
            // Overly general test for need to update window size,
            // as some of the UI settings doesn't need to update the window size
            if (name.startsWith("ui")) {
                const position = this.getWindowPositionState();
                this.verticalLayout = this.settings.user.ui.verticalLayout;
                this.updateWindowBounds(position);
            }
            this.sendUserSettingChanged();
            this.settings.save(this.getWindowState());
        }
        return changed;
    }

    private getWindowPositionState(): { x: number; y: number } {
        // get the window position
        const bounds = this.mainWindow.getNormalBounds();
        const addContent = this.verticalLayout && !this.contentVisible;

        return {
            x: bounds.x,
            y: addContent
                ? bounds.y - this.contentHeight - dividerSize
                : bounds.y,
        };
    }
    public getWindowState(): ShellWindowState {
        // Get browser tabs state for saving
        const browserTabs = this.browserViewManager.getAllBrowserTabs();
        const activeBrowserView =
            this.browserViewManager.getActiveBrowserView();

        const browserTabsState: BrowserTabState[] | undefined =
            browserTabs.length > 0
                ? browserTabs.map((tab) => ({
                      id: tab.id,
                      url: tab.url,
                      title: tab.title,
                      isActive: tab.isActive,
                  }))
                : undefined;

        const browserTabsJson = browserTabsState
            ? JSON.stringify(browserTabsState)
            : undefined;

        return {
            ...this.getWindowPositionState(),
            chatWidth: this.chatWidth,
            chatHeight: this.chatHeight,
            contentWidth: this.contentWidth,
            contentHeight: this.contentHeight,
            windowWidth: this.windowWidth,
            windowHeight: this.windowHeight,
            zoomLevel: this.chatView.webContents.zoomFactor,
            devTools: this.chatView.webContents.isDevToolsOpened(),
            canvas: this.browserViewManager.getActiveBrowserView()?.url, // Save active tab URL
            browserTabsJson: browserTabsJson,
            activeBrowserTabId: activeBrowserView?.id,
        };
    }

    public setWindowState(settings: ShellWindowState) {
        this.mainWindow.setBounds({
            x: settings.x,
            y: settings.y,
            width: settings.windowWidth,
            height: settings.windowHeight,
        });

        this.setZoomFactor(settings.zoomLevel, this.chatView.webContents);

        this.settings.save(this.getWindowState());
    }

    private sendUserSettingChanged() {
        this.chatView.webContents.send("settings-changed", this.settings.user);
    }

    // ================================================================
    // UI
    // ================================================================
    public updateContentSize(newDividerPos?: number) {
        const bounds = this.mainWindow.getContentBounds();
        debugShellWindow(
            `Updating content size with window content bound: ${JSON.stringify(bounds)}`,
        );

        const verticalLayout = this.verticalLayout;
        const { width, height } = bounds;
        const availableHeight = height - TITLE_BAR_HEIGHT; // Account for title bar

        let dividerPos = -1;
        let chatViewBounds: Electron.Rectangle;
        if (verticalLayout) {
            this.windowWidth = width;

            let chatHeight = height;
            if (this.contentVisible) {
                // Keep existing chat height unless the divider position changed.
                chatHeight =
                    newDividerPos !== undefined
                        ? availableHeight - newDividerPos - dividerSize
                        : this.chatHeight;

                // Clamp for window resize.
                if (chatHeight < 0) {
                    chatHeight = 0;
                } else {
                    const maxHeight = availableHeight - dividerSize;
                    if (chatHeight > maxHeight) {
                        chatHeight = maxHeight;
                    }
                }

                const contentHeight =
                    availableHeight - chatHeight - dividerSize;
                this.contentHeight = contentHeight;
                dividerPos = contentHeight;

                const browserViewBounds = {
                    x: 0,
                    y: TITLE_BAR_HEIGHT,
                    width: width,
                    height: contentHeight,
                };

                // Update browser view manager for multi-tab layout
                this.browserViewManager.setBounds(browserViewBounds);
            }
            this.chatHeight = chatHeight;

            chatViewBounds = {
                x: 0,
                y: TITLE_BAR_HEIGHT + availableHeight - chatHeight,
                width,
                height: chatHeight,
            };
        } else {
            this.windowHeight = height;
            let chatWidth = width;

            if (this.contentVisible) {
                // Keep existing chat width unless the divider position changed.
                chatWidth = newDividerPos ?? this.chatWidth;

                // Clamp for window resize.
                if (chatWidth < 0) {
                    chatWidth = 0;
                } else {
                    const maxWidth = width - dividerSize;
                    if (chatWidth > maxWidth) {
                        chatWidth = maxWidth;
                    }
                }
                const contentWidth = width - chatWidth - dividerSize;
                this.contentWidth = contentWidth;
                dividerPos = chatWidth;

                const browserViewBounds = {
                    x: dividerPos + dividerSize,
                    y: TITLE_BAR_HEIGHT,
                    width: contentWidth,
                    height: availableHeight,
                };

                // Update browser view manager for multi-tab layout
                this.browserViewManager.setBounds(browserViewBounds);
            }

            this.chatWidth = chatWidth;

            chatViewBounds = {
                x: 0,
                y: TITLE_BAR_HEIGHT,
                width: chatWidth,
                height: availableHeight,
            };
        }
        debugShellWindow(`Chat view bounds: ${JSON.stringify(chatViewBounds)}`);
        this.chatView.setBounds(chatViewBounds);

        this.updateOverlayBounds();

        const dividerLayout = {
            verticalLayout,
            pos: dividerPos,
        };

        // Set the divider position
        debugShellWindow("Divider Pos", dividerLayout);
        this.mainWindow.webContents.send("set-layout", dividerLayout);
    }

    public toggleTopMost() {
        this.mainWindow.setAlwaysOnTop(!this.mainWindow.isAlwaysOnTop());
    }

    public showDialog(dialogName: string) {
        this.chatView.webContents.send("show-dialog", dialogName);
    }

    // ================================================================
    // Multi-Tab Browser Support
    // ================================================================

    /**
     * Resolve custom typeagent-browser protocol URLs
     */
    public resolveCustomProtocolUrl(targetUrl: URL): URL {
        const browserExtensionUrls = (global as any).browserExtensionUrls;
        if (browserExtensionUrls) {
            // Map custom protocol to actual extension URL
            const libraryName = targetUrl.pathname;

            if (libraryName && browserExtensionUrls[libraryName]) {
                const resolvedUrl = new URL(browserExtensionUrls[libraryName]);
                debugShellWindow(
                    `Resolved custom protocol URL: ${targetUrl.toString()} -> ${resolvedUrl.toString()}`,
                );
                return resolvedUrl;
            } else {
                throw new Error(`Unknown library page: ${libraryName}`);
            }
        } else {
            throw new Error(
                "Browser extension not loaded - library pages unavailable",
            );
        }
    }

    private computeWindowBounds(
        position: { x: number; y: number },
        hasBrowserTabs: boolean,
    ) {
        const uiSettings = this.settings.user.ui;
        let bounds: Electron.Rectangle;
        if (this.verticalLayout) {
            bounds = {
                ...position,
                width: this.windowWidth,
                height: this.chatHeight + TITLE_BAR_HEIGHT,
            };
            const contentVisible =
                uiSettings.verticalContentAlwaysVisible || hasBrowserTabs;
            if (contentVisible) {
                bounds.height += this.contentHeight + dividerSize;
            } else {
                bounds.y += this.contentHeight + dividerSize;
            }
            this.contentVisible = contentVisible;
        } else {
            bounds = {
                ...position,
                width: this.chatWidth,
                height: this.windowHeight,
            };
            const contentVisible =
                uiSettings.horizontalContentAlwaysVisible || hasBrowserTabs;
            if (contentVisible) {
                bounds.width += this.contentWidth + dividerSize;
            }

            this.contentVisible = contentVisible;
        }

        // contentVisible may have changed
        this.ensureAutoEmptyTab();

        return bounds;
    }

    private updateWindowBounds(position: { x: number; y: number }) {
        const bounds = this.computeWindowBounds(
            position,
            this.hasBrowserTabs(),
        );
        debugShellWindow("Set window bound: ", bounds, this.contentVisible);
        this.mainWindow.setPosition(bounds.x, bounds.y);
        this.mainWindow.setContentSize(bounds.width, bounds.height);

        if (isLinux) {
            // Workaround for electron bug where getContentSize isn't updated when "resize" event is fired
            // https://github.com/electron/electron/issues/42586
            setTimeout(() => {
                if (!this.closing) {
                    this.updateContentSize();
                }
            }, 100);
        } else {
            this.updateContentSize();
        }
    }

    /**
     * Create a new browser tab and navigate to the specified page
     */
    public async createBrowserTab(
        url: URL = new URL("about:blank"),
        options?: { background?: boolean; waitForPageLoad?: boolean },
    ): Promise<string> {
        if (this.inputOnly) {
            throw new Error(
                "Browser tabs are not supported in input-only mode",
            );
        }
        // Handle custom typeagent-browser protocol
        let resolvedUrl = url;
        if (url.protocol === "typeagent-browser:") {
            resolvedUrl = this.resolveCustomProtocolUrl(url);
        }

        const tabId = await this.browserViewManager.createBrowserTab({
            url: resolvedUrl.toString(),
            background: options?.background,
            waitForPageLoad: options?.waitForPageLoad,
        });

        // Re-add overlay views so they are on top
        for (const views of this.overlayWebContentsViews.keys()) {
            this.mainWindow.contentView.addChildView(views);
        }

        // setup zoom handlers for the new browser tab
        this.setupZoomHandlers(
            this.browserViewManager.getBrowserTab(tabId)?.webContentsView
                ?.webContents,
        );

        // Update windows bound when content becomes visible
        if (!this.contentVisible && this.hasBrowserTabs()) {
            this.updateWindowBounds(this.getWindowPositionState());
        }

        // Send update to renderer
        this.sendTabsUpdate();

        // Restore focus to chat after tab operations
        if (!options?.background) {
            // Small delay to let browser view settle
            setTimeout(() => {
                if (!this.chatView.webContents.isDestroyed()) {
                    this.focusChatInput();
                }
            }, 50);
        }

        return tabId;
    }

    /**
     * Switch to a specific browser tab
     */
    public switchBrowserTab(tabId: string): boolean {
        const success = this.browserViewManager.setActiveBrowserView(tabId);
        if (success) {
            this.updateContentSize();
        }
        return success;
    }

    /**
     * Close a browser tab
     */
    public closeBrowserTab(tabId: string): boolean {
        const success = this.browserViewManager.closeBrowserTab(tabId);
        if (success) {
            this.mainWindow.webContents.send("browser-tab-closed", tabId);

            // Update layout if no tabs left
            if (!this.hasBrowserTabs()) {
                this.updateWindowBounds(this.getWindowPositionState());
            }

            // Restore focus to chat after closing tab
            this.focusChatInput();
        }
        return success;
    }

    /**
     * Get the active browser view
     */
    public getActiveBrowserView(): BrowserViewContext | null {
        return this.browserViewManager.getActiveBrowserView();
    }

    /**
     * Get all browser tabs
     */
    public getAllBrowserTabs(): BrowserViewContext[] {
        return this.browserViewManager.getAllBrowserTabs();
    }

    /**
     * Check if browser tabs are open
     */
    public hasBrowserTabs(): boolean {
        return this.browserViewManager.hasBrowserTabs();
    }

    /**
     * Get browser navigation state
     */
    public getBrowserNavigationState(): {
        canGoBack: boolean;
        canGoForward: boolean;
    } {
        // Always use multi-tab browser
        return this.browserViewManager.getNavigationState();
    }

    /**
     * Navigate browser back
     */
    public browserGoBack(): boolean {
        // Always use multi-tab browser
        return this.browserViewManager.goBack();
    }

    /**
     * Navigate browser forward
     */
    public browserGoForward(): boolean {
        // Always use multi-tab browser
        return this.browserViewManager.goForward();
    }

    /**
     * Reload active browser tab
     */
    public browserReload(): boolean {
        // Always use multi-tab browser
        return this.browserViewManager.reload();
    }

    /**
     * Send tab updates to renderer
     */
    private sendTabsUpdate(): void {
        const tabs = this.getAllBrowserTabs().map((tab) => ({
            id: tab.id,
            title: tab.title,
            url: tab.url,
            favicon: tab.favicon,
            isActive: tab.isActive,
        }));

        const activeTab = this.getActiveBrowserView();
        const tabsData = {
            tabs,
            activeTabId: activeTab?.id || null,
            navigationState: this.getBrowserNavigationState(),
        };

        this.mainWindow.webContents.send("browser-tabs-updated", tabsData);

        // Also send to all browser tab WebContents for tab validation
        this.browserViewManager.getAllBrowserTabs().forEach((tab) => {
            tab.webContentsView.webContents.send(
                "browser-tabs-updated",
                tabsData,
            );
        });
    }

    /**
     * Send navigation state update to renderer
     */
    private sendNavigationUpdate(): void {
        const navigationState = this.getBrowserNavigationState();
        this.mainWindow.webContents.send(
            "browser-navigation-updated",
            navigationState,
        );
    }

    // ================================================================
    // PDF Viewer Support
    // ================================================================
    public async checkTypeAgentConnection(): Promise<boolean> {
        try {
            const browserIpc = BrowserAgentIpc.getinstance();
            const webSocket = await browserIpc.ensureWebsocketConnected();
            return webSocket !== undefined && webSocket.readyState === 1; // WebSocket.OPEN
        } catch (error) {
            debugShellWindowError(
                "Error checking TypeAgent connection:",
                error,
            );
            return false;
        }
    }

    public async openPDFViewer(pdfUrl: string): Promise<void> {
        try {
            debugShellWindow(`Opening PDF viewer for: ${pdfUrl}`);

            const viewerUrl = await this.constructPDFViewerUrl(pdfUrl);
            this.createBrowserTab(new URL(viewerUrl), { background: false });
            return Promise.resolve();
        } catch (error) {
            debugShellWindowError("Error opening PDF viewer:", error);
            throw error;
        }
    }

    private async constructPDFViewerUrl(pdfUrl: string): Promise<string> {
        try {
            const browserIpc = BrowserAgentIpc.getinstance();

            const response = await new Promise<any>((resolve, reject) => {
                const timeoutId = setTimeout(() => {
                    reject(new Error("Timeout getting view host URL"));
                }, 10000);

                const messageId = Math.random().toString(36).substring(7);
                const message = {
                    id: messageId,
                    method: "getViewHostUrl",
                    params: {},
                };

                const originalHandler = browserIpc.onMessageReceived;
                browserIpc.onMessageReceived = (response: any) => {
                    if (response.id === messageId) {
                        clearTimeout(timeoutId);
                        browserIpc.onMessageReceived = originalHandler;
                        resolve(response.result);
                    } else if (originalHandler) {
                        originalHandler(response);
                    }
                };

                browserIpc.send(message).catch(reject);
            });

            if (!response || !response.url) {
                throw new Error(
                    "Unable to get view host URL from TypeAgent service",
                );
            }

            const viewerUrl = `${response.url}/pdf/?url=${encodeURIComponent(pdfUrl)}`;
            debugShellWindow(`Constructed PDF viewer URL: ${viewerUrl}`);
            return viewerUrl;
        } catch (error) {
            debugShellWindowError("Error constructing PDF viewer URL:", error);
            throw new Error("Failed to get TypeAgent PDF service URL");
        }
    }

    // ================================================================
    // Zoom Handler
    // ================================================================
    private setupZoomHandlers(
        webContents: WebContents | undefined,
        onZoomChanged?: (zoomFactor: number) => void,
    ) {
        if (!webContents) return;

        webContents.on("before-input-event", (_event, input) => {
            if (
                (isMac ? input.meta : input.control) &&
                input.type === "keyDown"
            ) {
                if (
                    input.key === "NumpadAdd" ||
                    input.key === "+" ||
                    input.key === "="
                ) {
                    this.zoomIn(webContents, onZoomChanged);
                } else if (input.key === "-" || input.key === "NumpadMinus") {
                    this.zoomOut(webContents, onZoomChanged);
                } else if (input.key === "0") {
                    this.setZoomFactor(1, webContents, onZoomChanged);
                }
            }
        });

        // Register mouse wheel as well.
        webContents.on("zoom-changed", (_event, zoomDirection) => {
            if (zoomDirection === "in") {
                this.zoomIn(webContents, onZoomChanged);
            } else {
                this.zoomOut(webContents, onZoomChanged);
            }
        });
    }

    private zoomIn(
        webContents: WebContents,
        onZoomChanged?: (zoomFactor: number) => void,
    ) {
        this.setZoomFactor(
            webContents.zoomFactor + 0.1,
            webContents,
            onZoomChanged,
        );
    }

    private zoomOut(
        webContents: WebContents,
        onZoomChanged?: (zoomFactor: number) => void,
    ) {
        this.setZoomFactor(
            webContents.zoomFactor - 0.1,
            webContents,
            onZoomChanged,
        );
    }

    /**
     * Sets the zoom factor for the active window/tab.
     * @param zoomFactor - The zoom factor to set for the active window/tab
     */
    public setZoomFactor(
        zoomFactor: number,
        webContents: WebContents,
        onZoomChanged?: (zoomFactor: number) => void,
    ) {
        // limit zoom factor to reasonable numbers
        if (zoomFactor < 0.25) {
            zoomFactor = 0.25;
        } else if (zoomFactor > 5) {
            zoomFactor = 5;
        }

        webContents.zoomFactor = zoomFactor;
        onZoomChanged?.(zoomFactor);
    }

    public updateSummary(title: string, status: [string, string][]) {
        if (this.closing) {
            return;
        }
        this.mainWindow.setTitle(title);
        this.chatView.webContents.send("updated-title", title);
        this.chatView.webContents.send("setting-summary-changed", status);
    }

    // ================================================================
    // Overlays
    // ================================================================
    private updateOverlayBounds() {
        for (const [view, data] of this.overlayWebContentsViews.entries()) {
            this.setOverlayBounds(view, data);
        }
    }
    public setOverlayVisibility(visible: boolean) {
        for (const view of this.overlayWebContentsViews.keys()) {
            view.setVisible(visible);
        }
    }

    public updateOverlay(view: WebContentsView, update?: Partial<OverlayData>) {
        if (update) {
            let data = this.overlayWebContentsViews.get(view);
            if (data === undefined) {
                this.mainWindow.contentView.addChildView(view);
                const bounds = view.getBounds();
                data = {
                    left: 0,
                    bottom: 0,
                    width: bounds.width,
                    height: bounds.height,
                };

                this.overlayWebContentsViews.set(view, data);
            }

            if (update.left) {
                data.left = update.left;
            }
            if (update.bottom) {
                data.bottom = update.bottom;
            }

            if (update.width) {
                data.width = update.width;
            }
            if (update.height) {
                data.height = update.height;
            }

            this.setOverlayBounds(view, data);
        } else {
            this.overlayWebContentsViews.delete(view);
            this.mainWindow.contentView.removeChildView(view);
        }
    }

    private setOverlayBounds(view: WebContentsView, data: OverlayData) {
        const chatBounds = this.chatView.getBounds();
        const zoomFactor = this.chatView.webContents.zoomFactor;
        const left = chatBounds.x + data.left * zoomFactor;
        const bottom =
            chatBounds.y + chatBounds.height - data.bottom * zoomFactor;
        const width = data.width * zoomFactor;
        const height = data.height * zoomFactor;
        const newBounds = {
            x: left,
            y: bottom - height,
            width,
            height,
        };
        view.setBounds(newBounds);
        view.webContents.setZoomFactor(zoomFactor);
    }
}

function createMainWindow(inputOnly: boolean): BrowserWindow {
    const isMac = process.platform === "darwin";
    const isWindows = process.platform === "win32";

    const inputOnlyOptions = inputOnly
        ? {
              transparent: true,
              backgroundColor: "#00000000", // Fully transparent background
              hasShadow: false,
              titleBarOverlay: false,
          }
        : {};

    const mainWindow = new BrowserWindow({
        show: false,
        frame: false, // Remove default frame

        titleBarStyle: isMac ? "hiddenInset" : "hidden", // Hide title bar
        titleBarOverlay: isWindows
            ? {
                  color: "#e5e5e5", // Matches --title-bar-bg
                  symbolColor: "#333333", // Matches --text-color
                  height: 40,
              }
            : undefined,
        trafficLightPosition: isMac ? { x: 12, y: 10 } : undefined,
        autoHideMenuBar: true,
        webPreferences: {
            preload: path.join(__dirname, "../preload/expose.mjs"),
            sandbox: false,
            zoomFactor: 1,
        },
        ...inputOnlyOptions,
    });

    mainWindow.removeMenu();

    // Track whether a modifier key is held so the window-open handler can
    // decide whether to open in the system browser instead.
    let modifierHeld = false;
    mainWindow.webContents.on("before-input-event", (_event, input) => {
        modifierHeld =
            (isMac ? input.meta : input.control) || input.shift || input.alt;
    });

    // make sure links are opened in the the shell; open in system browser when
    // the user holds a modifier key (Ctrl/Meta, Shift, or Alt) while clicking
    mainWindow.webContents.setWindowOpenHandler((details) => {
        if (modifierHeld) {
            // Only allow http/https URLs to prevent malicious schemes (e.g., file:, javascript:).
            try {
                const { protocol } = new URL(details.url);
                if (protocol === "http:" || protocol === "https:") {
                    shell.openExternal(details.url);
                }
            } catch {
                // Invalid URL — deny silently.
            }
            return { action: "deny" };
        }
        return { action: "allow" };
    });

    return mainWindow;
}

function createChatView(state: ShellWindowState) {
    const chatView = new WebContentsView({
        webPreferences: {
            preload: path.join(__dirname, "../preload/chatView.mjs"),
            sandbox: false,
            zoomFactor: state.zoomLevel,
        },
    });

    return chatView;
}

/**
 * Allows the application to gain access to camera devices
 * @param mainWindow the main browser window
 */
function setupDevicePermissions(mainWindow: BrowserWindow) {
    let grantedDeviceThroughPermHandler;

    mainWindow.webContents.session.on(
        "select-usb-device",
        (event, details, callback) => {
            // Add events to handle devices being added or removed before the callback on
            // `select-usb-device` is called.
            mainWindow.webContents.session.on(
                "usb-device-added",
                (_event, device) => {
                    console.log("usb-device-added FIRED WITH", device);
                    // Optionally update details.deviceList
                },
            );

            mainWindow.webContents.session.on(
                "usb-device-removed",
                (_event, device) => {
                    console.log("usb-device-removed FIRED WITH", device);
                    // Optionally update details.deviceList
                },
            );

            event.preventDefault();
            if (details.deviceList && details.deviceList.length > 0) {
                const deviceToReturn = details.deviceList.find((device) => {
                    return (
                        !grantedDeviceThroughPermHandler ||
                        device.deviceId !==
                            grantedDeviceThroughPermHandler.deviceId
                    );
                });
                if (deviceToReturn) {
                    callback(deviceToReturn.deviceId);
                } else {
                    callback();
                }
            }
        },
    );

    mainWindow.webContents.session.setPermissionCheckHandler(
        (
            _webContents: WebContents | null,
            permission,
            _requestingOrigin,
            details,
        ): boolean => {
            if (
                (permission === "usb" &&
                    details.securityOrigin === "file:///") ||
                (permission === "media" &&
                    (details.securityOrigin?.startsWith("http://localhost") ||
                        details.securityOrigin?.startsWith(
                            "https://localhost",
                        ))) ||
                permission.endsWith("fullscreen")
            ) {
                return true;
            }

            return false;
        },
    );

    mainWindow.webContents.session.setDevicePermissionHandler(
        (details: DevicePermissionHandlerHandlerDetails): boolean => {
            if (details.deviceType === "usb" && details.origin === "file://") {
                if (!grantedDeviceThroughPermHandler) {
                    grantedDeviceThroughPermHandler = details.device;
                    return true;
                } else {
                    return false;
                }
            }
            return false;
        },
    );
}
