const {
    app,
    BaseWindow,
    BrowserWindow,
    WebContentsView,
    session,
    ipcMain,
    net,
} = require("electron");
const fs = require("fs");
const path = require("path");
const { parse_arg, openUrlInBrowser, wildCardMatchList } = require("../utils/swai_utils");
const Ecosystem = require("../utils/Ecosystem");
const { GsettingsMonitor } = require("../utils/GsettingsMonitor");
const { getManifestAllowedUrls } = require("../utils/WebAppManifest");
const { configureAppDataPaths } = require("../utils/AppDataPaths");
const {
    findInstalledIcon,
    getLinuxAppIdentity,
} = require("../utils/LinuxAppIdentity");

// gsettings keys whose changes should re-style every titlebar at runtime.
const GSETTINGS_TITLEBAR_WATCHES = [
    { schema: "org.gnome.desktop.wm.preferences", key: "button-layout" },
    { schema: "org.gnome.desktop.interface", key: "font-name" },
];

class WindowManager {
    constructor(debug_mode = false, ecosystemId = null, swaiRegistry = null) {
        this.windowId = 0;
        this.debug_mode = debug_mode;
        this.ecosystemId = ecosystemId;
        this.swaiRegistry = swaiRegistry;
        this.ecosystemManager = null; // Will be set by EcosystemManager
        this.dataPathsInitialized = false; // Track if app data paths have been set
        this.loginModeByWindow = new Map(); // Track login mode state per window (windowId -> boolean)

        // Track every live titlebar webContents so gsettings changes can be broadcast.
        // Map<windowId, Electron.WebContents>
        this.titlebarsByWindow = new Map();
        this.gsettingsMonitor = null;

        // webRequest only allows one listener per event on a session. Keep a
        // small dispatcher so every view in an ecosystem can report HTTP 404s.
        this.statusPageSessions = new Map();
        this.offlinePwaSessions = new Map();
    }

    _ensureGsettingsMonitor() {
        if (this.gsettingsMonitor || process.platform !== "linux") return;

        this.gsettingsMonitor = new GsettingsMonitor(GSETTINGS_TITLEBAR_WATCHES);
        this.gsettingsMonitor.on("change", ({ schema, key, value }) => {
            for (const [winId, wc] of this.titlebarsByWindow) {
                if (!wc || wc.isDestroyed()) {
                    this.titlebarsByWindow.delete(winId);
                    continue;
                }
                try {
                    wc.send(`gsettings-changed-${winId}`, { schema, key, value });
                } catch (err) {
                    console.warn(
                        `Failed to forward gsettings change to titlebar ${winId}:`,
                        err.message,
                    );
                }
            }
        });
        this.gsettingsMonitor.start();
    }

    /**
     * Stop background watchers. Safe to call multiple times.
     */
    dispose() {
        if (this.gsettingsMonitor) {
            this.gsettingsMonitor.stop();
            this.gsettingsMonitor = null;
        }
        this.titlebarsByWindow.clear();
        for (const { session: statusSession } of this.statusPageSessions.values()) {
            statusSession.webRequest.onCompleted(null);
        }
        this.statusPageSessions.clear();
        for (const entry of this.offlinePwaSessions.values()) {
            entry.session.serviceWorkers.off("registration-completed", entry.listener);
        }
        this.offlinePwaSessions.clear();
    }
    
    setEcosystemManager(ecosystemManager) {
        this.ecosystemManager = ecosystemManager;
    }

    initializeDataPaths(dataId) {
        if (this.dataPathsInitialized) return;

        configureAppDataPaths(app, dataId);
        this.dataPathsInitialized = true;
        console.log(`Initialized app data paths for: ${dataId}`);
    }

    createWindow(webapp) {
        this.windowId += 1;
        let boundsTimeout = null;
        let webapp_ecosystem = null;

        // Use ecosystem ID for data path so apps in same ecosystem share data/cookies
        const dataId = this.ecosystemId || webapp.app_id;
        
        // Only set app data paths once per process (subsequent calls can cause issues)
        this.initializeDataPaths(dataId);

        // Use ecosystem ID for session partition so apps in same ecosystem share sessions/cookies
        let webAppSession = session.fromPartition(`persist:${dataId}`);

        let thisWindow = null;

        // For ecosystem apps, use internal title for DBus registration
        let windowTitle = webapp.app_name;
        let internalTitle = null;
        let currentDisplayTitle = webapp.app_name; // Track current display title to avoid redundant DBus calls
        
        if (webapp.ecosystem && this.swaiRegistry) {
            // Always generate internal title for ecosystem apps, even if DBus not available yet
            internalTitle = this.swaiRegistry.generateInternalTitle(webapp.app_id, this.windowId);
            windowTitle = internalTitle; // Set window title to internal title for DBus registration
        }

        // Electron applies these values to the next native window it creates.
        // Re-apply them for every ecosystem app so each window gets its own
        // Wayland app ID / X11 WM_CLASS instead of inheriting the first app.
        if (process.platform === "linux") {
            const linuxIdentity = getLinuxAppIdentity(webapp.app_id);
            app.setName(linuxIdentity.id);
            if (typeof app.setDesktopName === "function") {
                app.setDesktopName(linuxIdentity.desktopName);
            }
        }

        const statePath = path.join(app.getPath("userData"), `window-state-${webapp.app_id}.json`);
        let windowState = { width: 1200, height: 900 };
        try {
            if (fs.existsSync(statePath)) {
                windowState = JSON.parse(fs.readFileSync(statePath, 'utf8'));
            }
        } catch (e) {
            console.error("Failed to load window state:", e);
        }

        const windowOptions = {
            width: windowState.width || 1200,
            height: windowState.height || 900,
            title: windowTitle,
            frame: false,
            show: true, // Explicitly show the window
        };

        if (windowState.x !== undefined && windowState.y !== undefined) {
            windowOptions.x = windowState.x;
            windowOptions.y = windowState.y;
        }

        thisWindow = new BaseWindow(windowOptions);
        thisWindow.swaiWebApp = webapp;

        
        console.log(`Created window ${this.windowId} for ${webapp.app_name}`);

        const nativeWindowId = this.windowId;
        const set_title = (title) => {
            if (!thisWindow || thisWindow.isDestroyed()) return;
            if (internalTitle && this.swaiRegistry && this.swaiRegistry.isAvailable()) {
                // For ecosystem apps, keep window title as internal title but update DBus display title
                thisWindow.title = internalTitle;
                
                // Only update DBus if the display title has actually changed
                if (title !== currentDisplayTitle) {
                    currentDisplayTitle = title;
                    this.swaiRegistry.updateTitle(internalTitle, title, thisWindow).catch(error => {
                        console.warn('Failed to update DBus title registration:', error.message);
                    });
                }
            } else {
                // For non-ecosystem apps, set title normally
                thisWindow.title = title;
            }
        };

        thisWindow.swaiUpdateWindowIdentity = async (nextWebapp) => {
            currentDisplayTitle = nextWebapp.app_name;
            if (nextWebapp.ecosystem && this.swaiRegistry) {
                internalTitle = this.swaiRegistry.generateInternalTitle(
                    nextWebapp.app_id,
                    nativeWindowId,
                );
                await this.swaiRegistry.reclassifyWindow(
                    internalTitle,
                    currentDisplayTitle,
                    thisWindow,
                );
            } else {
                internalTitle = null;
                thisWindow.title = currentDisplayTitle;
            }
        };

        let titlebar = new WebContentsView({
            webPreferences: {
                contextIsolation: false,
                nodeIntegration: true,
                additionalArguments: [
                    `--windowId=${this.windowId}`,
                    `--webappName=${webapp.app_name}`,
                ],
            }
        });

        // Register this titlebar with the (lazily-started) gsettings monitor
        // so it gets live updates when the user changes button-layout / font.
        this._ensureGsettingsMonitor();
        const titlebarWindowId = this.windowId;
        this.titlebarsByWindow.set(titlebarWindowId, titlebar.webContents);
        titlebar.webContents.on("destroyed", () => {
            this.titlebarsByWindow.delete(titlebarWindowId);
        });

        let mainWebContents = new WebContentsView({
            nodeIntegration: false,
            contextIsolation: true,
            webPreferences: {
                session: webAppSession
            }
        });

        titlebar.webContents.loadURL(
            `file://${path.join(__dirname, "../components/titlebar.html")}`,
        );
        thisWindow.removeMenu();
        thisWindow.contentView.addChildView(titlebar);
        thisWindow.contentView.addChildView(mainWebContents);
        thisWindow.swaiMainWebContents = mainWebContents;
        thisWindow.swaiTitlebar = titlebar;

        let custom_main_url = parse_arg("--custom_main_url");
        if (custom_main_url) {
            webapp.main_url = custom_main_url;
            if (!wildCardMatchList(custom_main_url, webapp.allowed_urls)) {
                webapp.allowed_urls.push(custom_main_url);
            }
        }

        let cleanUserAgent;
        if (webapp.custom_user_agent) {
            cleanUserAgent = webapp.custom_user_agent;
        } else {
            const nativeUserAgent = session.defaultSession.getUserAgent();
            cleanUserAgent = nativeUserAgent.replace(/Electron\/\S*\s/, "");
        }

        mainWebContents.webContents.setUserAgent(cleanUserAgent); 
        console.log(cleanUserAgent);
        if (this.debug_mode) {
            titlebar.webContents.openDevTools();
            mainWebContents.webContents.openDevTools();
        }

        const updateBounds = () => {
            const { width, height } = thisWindow.getBounds();
            
            if (thisWindow.isFullScreen()) {
                titlebar.setBounds({ x: 0, y: 0, width: 0, height: 0 });
                mainWebContents.setBounds({ x: 0, y: 0, width, height });
                mainWebContents.webContents.focus();
            } else {
                let titlebarHeight = this.debug_mode ? 300 : 36;
                titlebar.setBounds({ x: 0, y: 0, width, height: titlebarHeight });
                mainWebContents.setBounds({
                    x: 0,
                    y: titlebarHeight,
                    width,
                    height: height - titlebarHeight,
                });
            }
        };

        updateBounds();

        thisWindow.on("resize", () => {
            updateBounds();
            if (boundsTimeout) {
                clearTimeout(boundsTimeout);
            }
            boundsTimeout = setTimeout(updateBounds, 1000);
        });

        thisWindow.on("enter-full-screen", () => {
            updateBounds(); 
        });

        thisWindow.on("leave-full-screen", () => {
            updateBounds();
        });

        this._setupMainWebContents(mainWebContents, thisWindow, webapp, set_title, webAppSession, titlebar);
        this._setupStatusPages(mainWebContents, webAppSession);
        this._setupIpcHandlers(thisWindow, mainWebContents);
        this._setupContextMenu(mainWebContents);

        // Start a previously registered PWA worker before the first navigation.
        // This lets it answer from Cache Storage when the app launches offline.
        this._loadURLWithOfflinePwa(
            mainWebContents.webContents,
            webAppSession,
            webapp.main_url,
        );

        // Register the window with DBus for custom WM_CLASS handling (ecosystem apps only)
        if (webapp.ecosystem && this.swaiRegistry) {
            // Ensure SwaiRegistry is initialized before attempting registration
            // Don't await this - let it happen asynchronously so window creation isn't blocked
            this._registerWindowWithDelay(webapp, internalTitle, currentDisplayTitle, thisWindow).catch(err => {
                console.error('Window registration failed:', err);
            });
        }

        // Clean up login mode state when window closes
        const windowIdForCleanup = this.windowId;
        
        thisWindow.on('close', () => {
            try {
                if (!thisWindow.isDestroyed()) {
                    const bounds = thisWindow.getBounds();
                    const state = {
                        x: bounds.x,
                        y: bounds.y,
                        width: bounds.width,
                        height: bounds.height
                    };
                    fs.writeFileSync(statePath, JSON.stringify(state));
                }
            } catch (e) {
                console.error("Failed to save window state:", e);
            }
        });

        thisWindow.on('closed', () => {
            this.loginModeByWindow.delete(windowIdForCleanup);
            console.log(`Cleaned up login mode state for window ${windowIdForCleanup}`);
        });

        // Ensure window is visible
        thisWindow.show();
        console.log(`✓ Window ${this.windowId} for ${webapp.app_name} created and shown`);

        return thisWindow;
    }

    async reuseWindowForApp(window, nextWebapp) {
        if (!window || window.isDestroyed()) {
            throw new Error("Cannot reuse a closed ecosystem window");
        }

        const mainWebContents = window.swaiMainWebContents;
        const titlebar = window.swaiTitlebar;
        const currentWebapp = window.swaiWebApp;
        if (!mainWebContents?.webContents || !currentWebapp) {
            throw new Error("Window is missing its SWAI navigation context");
        }

        // Event handlers close over this object, so update it in place. This
        // immediately switches URL policy, custom JS, manifests, and login rules
        // to those of the destination app without replacing the WebContents.
        Object.assign(currentWebapp, nextWebapp);

        const icon = nextWebapp.icon_path || findInstalledIcon(nextWebapp.app_id);
        if (icon && typeof window.setIcon === "function") {
            window.setIcon(icon);
        }

        if (titlebar?.webContents && !titlebar.webContents.isDestroyed()) {
            titlebar.webContents.send("app-name-changed", nextWebapp.app_name);
        }

        const identity = getLinuxAppIdentity(nextWebapp.app_id);
        if (process.platform === "linux") {
            app.setName(identity.id);
            if (typeof app.setDesktopName === "function") {
                app.setDesktopName(identity.desktopName);
            }
        }

        if (typeof window.swaiUpdateWindowIdentity === "function") {
            await window.swaiUpdateWindowIdentity(nextWebapp);
        } else {
            window.title = nextWebapp.app_name;
        }
        await this._loadURLWithOfflinePwa(
            mainWebContents.webContents,
            mainWebContents.webContents.session,
            nextWebapp.main_url,
        );
    }

    async _registerWindowWithDelay(webapp, internalTitle, currentDisplayTitle, thisWindow) {
        try {
            console.log(`Starting registration for window ${this.windowId} - ${webapp.app_name}`);
            
            // Ensure SwaiRegistry is initialized
            if (!this.swaiRegistry.initializationComplete) {
                console.log(`Waiting for SwaiRegistry initialization for ${webapp.app_name}...`);
                await this.swaiRegistry.initialize();
            }

            // Generate internal title if not already done
            if (!internalTitle) {
                internalTitle = this.swaiRegistry.generateInternalTitle(webapp.app_id, this.windowId);
                thisWindow.title = internalTitle;
                console.log(`Generated internal title: ${internalTitle}`);
            }

            if (internalTitle && this.swaiRegistry.isAvailable()) {
                const success = await this.swaiRegistry.registerTitle(internalTitle, currentDisplayTitle, thisWindow);
                if (success) {
                    console.log(`✓ Registered ${webapp.app_name} (window ${this.windowId}) with custom WM_CLASS: ${webapp.app_id}`);
                } else {
                    console.warn(`Failed to register ${webapp.app_name} with SwaiRegistry`);
                }
            } else {
                console.log(`DBus not available, skipping registration for ${webapp.app_name}`);
            }
        } catch (error) {
            console.error('Error during window registration:', error);
            // Don't let registration errors prevent the window from working
        }
    }

    _setupMainWebContents(mainWebContents, thisWindow, webapp, set_title, webAppSession, titlebar) {
        const processedManifestUrls = new Set();

        // Set up theme color listener once (outside dom-ready to avoid duplicate listeners)
        if (webapp.use_dynamic_titlebar_colors !== false) {
            this._setupThemeColorListener(mainWebContents, titlebar);
        }
        
        mainWebContents.webContents.on("dom-ready", () => {
            console.log("dom ready");
            const isStatusPage = mainWebContents.webContents
                .getURL()
                .includes("status-page.html");

            if (thisWindow) {
                const cssPath = path.join(__dirname, "../../system_theme.css");
                fs.readFile(cssPath, "utf-8", (err, data) => {
                    if (err) {
                        console.error("Failed to read CSS file:", err);
                        return;
                    }
                    if (thisWindow) {
                        mainWebContents.webContents.insertCSS(data).catch((error) => {
                            console.error("Failed to inject CSS:", error);
                        });
                    }
                });
                
                // Inject theme color detector script (needs to be done on each navigation)
                if (webapp.use_dynamic_titlebar_colors !== false) {
                    this._injectThemeColorDetectorScript(mainWebContents);
                }

                if (webapp.use_manifest && !isStatusPage) {
                    this._loadWebAppManifest(
                        mainWebContents,
                        webapp,
                        webAppSession,
                        processedManifestUrls,
                    );
                }

                // Inject custom JS
                if (webapp.custom_js && !isStatusPage) {
                    try {
                        mainWebContents.webContents.executeJavaScript(webapp.custom_js)
                            .then(() => {
                                if (this.debug_mode) console.log(`[SWAI] Injected custom JS for ${webapp.app_name}`);
                            })
                            .catch((error) => {
                                console.error(`[SWAI] Failed to execute custom JS:`, error);
                            });
                    } catch (error) {
                        console.error(`[SWAI] Error processing custom JS:`, error);
                    }
                }
            }
        });

        const currentWindowId = this.windowId;

        mainWebContents.webContents.setWindowOpenHandler((details) => {
            console.log("Attempted new window:", details.url);
            console.log("Current URL:", mainWebContents.webContents.getURL());
            console.log("Multi-window allowed:", webapp.allow_multi_window);

            const navResult = this._get_navigation_allowed(
                mainWebContents.webContents.getURL(),
                details.url,
                webapp,
                currentWindowId
            );

            if (!navResult.allowed) {
                console.log(`[SWAI] New window blocked - reason: ${navResult.reason}`);
                console.log(`[SWAI]   URL: ${details.url}`);
                console.log(`[SWAI]   Ecosystem app available: ${navResult.ecosystemApp?.app_name || 'none'}`);

                // Check if an ecosystem app can handle this URL
                if (navResult.ecosystemApp && this.ecosystemManager) {
                    console.log(`[SWAI] Opening in ecosystem app: ${navResult.ecosystemApp.app_name}`);
                    this.ecosystemManager.handleNewAppRequest({
                        ...navResult.ecosystemApp,
                        main_url: details.url
                    }, thisWindow);
                    return { action: "deny" };
                }

                console.log("[SWAI] No ecosystem app found, opening in system browser");
                openUrlInBrowser(details.url);
                return { action: "deny" };
            }

            // Navigation is allowed - check how to handle the new window.
            const isAllowedUrl = wildCardMatchList(details.url, webapp.allowed_urls);
            const isLoginUrl = wildCardMatchList(details.url, webapp.login_urls);
            const isProgrammaticPopup = details.features && details.features.length > 0;

            if (isProgrammaticPopup) {
                console.log("[SWAI] Allowing native popup (Login or Programmatic UI):", details.url);
                
                return {
                    action: "allow",
                    overrideBrowserWindowOptions: {
                        autoHideMenuBar: true,
                        webPreferences: {
                            session: webAppSession,
                            contextIsolation: true,
                            nodeIntegration: false
                        }
                    }
                };
            }

            if (webapp.allow_multi_window && (isAllowedUrl || isLoginUrl)) {
                console.log("URL matches allowed list and multi-window enabled, creating new window");
                try {
                    let new_window_app = webapp.clone();
                    new_window_app.main_url = details.url;
                    const newWindow = this.createWindow(new_window_app);

                    if (this.ecosystemManager) {
                        this.ecosystemManager.trackWindow(new_window_app.app_id, newWindow);
                        console.log("New window tracked in ecosystem manager");
                    }

                    console.log("New window created successfully");
                    return { action: "deny" };
                } catch (error) {
                    console.error("Error creating new window:", error);
                    return { action: "deny" };
                }
            }

            console.log("Creating transient window (multi-window disabled or URL not in allowed list)");
            this.createTransientWindow(details.url, webAppSession, thisWindow);
            return { action: "deny" };
        });

        this._setupNavigationHandlers(mainWebContents, webapp, set_title, titlebar, currentWindowId, thisWindow);
        this._setupKeyboardShortcuts(mainWebContents, thisWindow, titlebar);
    }

    async _loadWebAppManifest(mainWebContents, webapp, webAppSession, processedUrls) {
        const webContents = mainWebContents.webContents;
        if (!webContents || webContents.isDestroyed()) return;

        const pageURL = webContents.getURL();
        if (!pageURL.startsWith("http://") && !pageURL.startsWith("https://")) return;

        let manifestURL;
        try {
            manifestURL = await webContents.executeJavaScript(`
                (() => {
                    const link = document.querySelector('link[rel~="manifest"]');
                    return link ? link.href : null;
                })()
            `);
        } catch (error) {
            console.warn("Failed to discover web app manifest:", error.message);
            return;
        }

        if (!manifestURL || processedUrls.has(manifestURL)) return;
        processedUrls.add(manifestURL);

        try {
            const parsedManifestURL = new URL(manifestURL);
            if (parsedManifestURL.protocol !== "http:"
                && parsedManifestURL.protocol !== "https:") {
                throw new Error("manifest URL must use HTTP or HTTPS");
            }

            const response = await webAppSession.fetch(parsedManifestURL.href, {
                headers: { Accept: "application/manifest+json, application/json" },
            });
            if (!response.ok) {
                throw new Error(`manifest request returned HTTP ${response.status}`);
            }

            const manifestText = await response.text();
            if (manifestText.length > 1024 * 1024) {
                throw new Error("manifest exceeds the 1 MiB size limit");
            }

            const manifest = JSON.parse(manifestText);
            const manifestUrls = getManifestAllowedUrls(
                manifest,
                parsedManifestURL.href,
                pageURL,
            );
            const newUrls = manifestUrls.filter(
                (url) => !webapp.allowed_urls.includes(url),
            );
            webapp.allowed_urls.push(...newUrls);

            if (newUrls.length > 0) {
                console.log(
                    `[SWAI] Added ${newUrls.length} URL pattern(s) from ${parsedManifestURL.href}`,
                );
            }
        } catch (error) {
            // Permit a later navigation to retry a temporarily unavailable manifest.
            processedUrls.delete(manifestURL);
            console.warn("Failed to load web app manifest:", error.message);
        }
    }

    _setupStatusPages(mainWebContents, webAppSession) {
        const webContents = mainWebContents.webContents;
        const webContentsId = webContents.id;

        let sessionEntry = this.statusPageSessions.get(webAppSession);
        if (!sessionEntry) {
            sessionEntry = { session: webAppSession, handlers: new Map() };
            this.statusPageSessions.set(webAppSession, sessionEntry);
            webAppSession.webRequest.onCompleted(
                { urls: ["http://*/*", "https://*/*"] },
                (details) => {
                    if (details.resourceType !== "mainFrame") return;
                    const handler = sessionEntry.handlers.get(details.webContentsId);
                    if (handler) handler(details);
                },
            );
        }

        sessionEntry.handlers.set(webContentsId, (details) => {
            if (details.statusCode === 401 || details.statusCode === 403) {
                this._showStatusPage(mainWebContents, "forbidden", details.url);
            } else if (details.statusCode === 404) {
                this._showStatusPage(mainWebContents, "not-found", details.url);
            }
        });

        webContents.on(
            "did-fail-load",
            (event, errorCode, errorDescription, validatedURL, isMainFrame) => {
                // ERR_ABORTED is emitted for normal navigation cancellation.
                if (!isMainFrame || errorCode === -3 || validatedURL.includes("status-page.html")) return;

                const offlineErrors = new Set([
                    -7,   // TIMED_OUT
                    -21,  // NETWORK_CHANGED
                    -102, // CONNECTION_REFUSED
                    -106, // INTERNET_DISCONNECTED
                    -109, // ADDRESS_UNREACHABLE
                    -118, // CONNECTION_TIMED_OUT
                    -137, // NAME_RESOLUTION_FAILED
                    -352, // NETWORK_IO_SUSPENDED
                ]);
                const isOffline = errorCode === -106
                    || ((offlineErrors.has(errorCode) || errorCode === -105) && !net.isOnline());
                const type = isOffline
                    ? "offline"
                    : (errorCode === -6 || errorCode === -105 ? "not-found" : "load-error");

                console.warn(
                    `[SWAI] Showing ${type} status page for ${validatedURL}: ${errorDescription}`,
                );
                this._showStatusPage(mainWebContents, type, validatedURL);
            },
        );

        webContents.on("render-process-gone", (event, details) => {
            if (details.reason === "clean-exit" || webContents.isDestroyed()) return;

            const failedURL = webContents.swaiFailedURL || webContents.getURL();
            const recoveryType = webContents.swaiRecoveryType || "page-crashed";
            webContents.swaiRecoveryType = null;
            console.error(
                `[SWAI] Renderer process exited (${details.reason}, code ${details.exitCode})`,
            );
            this._showStatusPage(mainWebContents, recoveryType, failedURL);
        });

        webContents.on("unresponsive", () => {
            if (webContents.isDestroyed() || webContents.getURL().includes("status-page.html")) return;

            const failedURL = webContents.getURL();
            console.warn(`[SWAI] Page became unresponsive: ${failedURL}`);
            // A hung renderer cannot reliably navigate itself to a recovery page.
            // Terminate it and let render-process-gone create a fresh renderer.
            webContents.swaiFailedURL = failedURL;
            webContents.swaiRecoveryType = "page-unresponsive";
            webContents.forcefullyCrashRenderer();
        });

        webContents.on("destroyed", () => {
            sessionEntry.handlers.delete(webContentsId);
        });
    }

    _loadURLWithOfflinePwa(webContents, webAppSession, targetURL) {
        this._prepareOfflinePwaSession(webAppSession, targetURL)
            .catch((error) => {
                console.warn("Failed to prepare offline PWA service worker:", error.message);
            })
            .finally(() => {
                if (!webContents.isDestroyed()) {
                    webContents.loadURL(targetURL).catch(() => {
                        // did-fail-load displays the shared status page when neither
                        // the network nor a service worker can provide the page.
                    });
                }
            });
    }

    async _prepareOfflinePwaSession(webAppSession, targetURL) {
        let entry = this.offlinePwaSessions.get(webAppSession);
        if (!entry) {
            const scopesPath = path.join(
                app.getPath("userData"),
                "pwa-service-worker-scopes.json",
            );
            let scopes = new Set();

            try {
                if (fs.existsSync(scopesPath)) {
                    const savedScopes = JSON.parse(fs.readFileSync(scopesPath, "utf8"));
                    if (Array.isArray(savedScopes)) scopes = new Set(savedScopes);
                }
            } catch (error) {
                console.warn("Failed to read saved PWA service-worker scopes:", error.message);
            }

            const listener = (event, details) => {
                if (!details.scope || scopes.has(details.scope)) return;

                scopes.add(details.scope);
                try {
                    fs.mkdirSync(path.dirname(scopesPath), { recursive: true });
                    fs.writeFileSync(scopesPath, JSON.stringify([...scopes], null, 2));
                } catch (error) {
                    console.warn("Failed to save PWA service-worker scope:", error.message);
                }
            };

            webAppSession.serviceWorkers.on("registration-completed", listener);
            entry = { session: webAppSession, scopes, listener };
            this.offlinePwaSessions.set(webAppSession, entry);
        }

        const matchingScopes = [...entry.scopes].filter((scope) => {
            try {
                const scopeURL = new URL(scope);
                const pageURL = new URL(targetURL);
                return scopeURL.origin === pageURL.origin
                    && pageURL.href.startsWith(scopeURL.href);
            } catch (error) {
                return false;
            }
        });

        await Promise.allSettled(
            matchingScopes.map((scope) =>
                webAppSession.serviceWorkers.startWorkerForScope(scope),
            ),
        );
    }

    _showStatusPage(mainWebContents, type, failedURL) {
        const webContents = mainWebContents.webContents;
        if (!webContents || webContents.isDestroyed()) return;

        webContents.swaiFailedURL = failedURL;
        webContents.loadFile(
            path.join(__dirname, "../components/status-page.html"),
            { query: { type, retry: failedURL || "" } },
        ).catch((error) => {
            console.error("Failed to load status page:", error);
        });
    }

    _setupNavigationHandlers(mainWebContents, webapp, set_title, titlebar, windowId, thisWindow) {
        mainWebContents.webContents.on("will-navigate", (event, navigationUrl) => {
            const navResult = this._get_navigation_allowed(
                mainWebContents.webContents.getURL(),
                navigationUrl,
                webapp,
                windowId
            );
            
            if (!navResult.allowed) {
                event.preventDefault();
                console.log(`[SWAI] Navigation blocked - reason: ${navResult.reason}`);
                console.log(`[SWAI]   URL: ${navigationUrl}`);
                console.log(`[SWAI]   Ecosystem app available: ${navResult.ecosystemApp?.app_name || 'none'}`);
                
                // Check if an ecosystem app can handle this URL
                if (navResult.ecosystemApp && this.ecosystemManager) {
                    console.log(`[SWAI] Opening in ecosystem app: ${navResult.ecosystemApp.app_name}`);
                    this.ecosystemManager.handleNewAppRequest({
                        ...navResult.ecosystemApp,
                        main_url: navigationUrl
                    }, thisWindow);
                } else {
                    console.log("[SWAI] No ecosystem app found, opening in system browser");
                    openUrlInBrowser(navigationUrl);
                }
            }
        });

        // Handle redirects - catch URLs that redirect to not_allowed_urls
        mainWebContents.webContents.on("will-redirect", (event, redirectUrl, isMainFrame) => {
            if (!isMainFrame) return; // Only check main frame redirects
            
            const currentUrl = mainWebContents.webContents.getURL();
            const navResult = this._get_navigation_allowed(
                currentUrl,
                redirectUrl,
                webapp,
                windowId
            );
            
            if (!navResult.allowed) {
                event.preventDefault();
                console.log(`[SWAI] Redirect blocked - reason: ${navResult.reason}`);
                console.log(`[SWAI]   Redirect URL: ${redirectUrl}`);
                console.log(`[SWAI]   From: ${currentUrl}`);
                
                // Check if an ecosystem app can handle this URL
                if (navResult.ecosystemApp && this.ecosystemManager) {
                    console.log(`[SWAI] Opening redirect in ecosystem app: ${navResult.ecosystemApp.app_name}`);
                    this.ecosystemManager.handleNewAppRequest({
                        ...navResult.ecosystemApp,
                        main_url: redirectUrl
                    }, thisWindow);
                } else {
                    console.log("[SWAI] Opening redirect in system browser");
                    openUrlInBrowser(redirectUrl);
                }
            }
        });

        mainWebContents.webContents.on("did-navigate", (event, navigationUrl) => {
            if (!navigationUrl.startsWith("file:")) {
                mainWebContents.webContents.swaiFailedURL = null;
            }
            set_title(mainWebContents.webContents.getTitle());
            this._sendUrlChanged(mainWebContents, titlebar);
            
            // Update login mode based on the URL we landed on
            this._updateLoginModeAfterNavigation(windowId, navigationUrl, webapp);
        });

        mainWebContents.webContents.on("did-navigate-in-page", (event, navigationUrl) => {
            set_title(mainWebContents.webContents.getTitle());
            this._sendUrlChanged(mainWebContents, titlebar);
            
            // Update login mode based on the URL we landed on
            this._updateLoginModeAfterNavigation(windowId, navigationUrl, webapp);
        });

        mainWebContents.webContents.on("page-title-updated", (event, title) => {
            set_title(mainWebContents.webContents.getTitle());
            this._sendUrlChanged(mainWebContents, titlebar);
        });
    }

    _sendUrlChanged(mainWebContents, titlebar) {
        if (titlebar) {
            titlebar.webContents.send(
                `url-changed-${this.windowId}`,
                mainWebContents.webContents.getTitle(),
                mainWebContents.webContents.navigationHistory.canGoBack(),
                mainWebContents.webContents.navigationHistory.canGoForward(),
            );
        }
    }

    /**
     * Set up the one-time listener for theme color updates from the detector
     * This should be called once per window, not on every dom-ready
     */
    _setupThemeColorListener(mainWebContents, titlebar) {
        const windowId = this.windowId;
        
        // Listen for theme color updates via console messages
        mainWebContents.webContents.on('console-message', (event, level, message) => {
            if (message.startsWith('__SWAI_THEME_COLOR__')) {
                try {
                    const themeData = JSON.parse(message.slice('__SWAI_THEME_COLOR__'.length));
                    if (titlebar && !titlebar.webContents.isDestroyed()) {
                        titlebar.webContents.send(`theme-color-changed-${windowId}`, themeData);
                    }
                } catch (parseError) {
                    console.error("Failed to parse theme color data:", parseError);
                }
            }
        });
    }

    /**
     * Inject the theme color detector script into the web contents
     * This needs to be called on each dom-ready as the page context changes
     */
    _injectThemeColorDetectorScript(mainWebContents) {
        const detectorPath = path.join(__dirname, "../components/theme-color-detector.js");
        
        fs.readFile(detectorPath, "utf-8", (err, detectorCode) => {
            if (err) {
                console.error("Failed to read theme color detector:", err);
                return;
            }
            
            // Set up callback that uses console.log to communicate back
            // (works with contextIsolation: true)
            const callbackSetup = `
                window.__swai_theme_color_callback = (themeData) => {
                    console.log('__SWAI_THEME_COLOR__' + JSON.stringify(themeData));
                };
            `;
            
            mainWebContents.webContents.executeJavaScript(callbackSetup + detectorCode).catch(error => {
                console.error("Failed to inject theme color detector:", error);
            });
        });
    }

    _setupKeyboardShortcuts(mainWebContents, thisWindow, titlebar) {
        const shortcutHandler = (event, input) => {
            if (input.type !== "keyDown") return;

            if (input.key === "F11" && thisWindow) {
                thisWindow.setFullScreen(!thisWindow.isFullScreen());
                event.preventDefault();
                return;
            }

            if (input.control) {
                if (input.key === "I" && input.control && input.shift) {
                    mainWebContents.webContents.toggleDevTools();
                    event.preventDefault();
                } else if (input.key === "+") {
                    mainWebContents.webContents.setZoomFactor(
                        mainWebContents.webContents.getZoomFactor() + 0.1,
                    );
                    event.preventDefault();
                } else if (input.key === "-") {
                    mainWebContents.webContents.setZoomFactor(
                        mainWebContents.webContents.getZoomFactor() - 0.1,
                    );
                    event.preventDefault();
                } else if (input.key === "0") {
                    mainWebContents.webContents.setZoomFactor(1.0);
                    event.preventDefault();
                } else if (input.key == "r") {
                    const failedURL = mainWebContents.webContents.swaiFailedURL;
                    if (failedURL) {
                        mainWebContents.webContents.loadURL(failedURL).catch(() => {});
                    } else {
                        mainWebContents.webContents.reload();
                    }
                    event.preventDefault();
                }
            }
        };
        mainWebContents.webContents.on("before-input-event", shortcutHandler);
        titlebar.webContents.on("before-input-event", shortcutHandler);
    }

    _setupIpcHandlers(thisWindow, mainWebContents) {
        ipcMain.on(`back-${this.windowId}`, () => {
            if (thisWindow) {
                mainWebContents.webContents.navigationHistory.goBack();
            }
        });

        ipcMain.on(`forward-${this.windowId}`, () => {
            if (thisWindow) {
                mainWebContents.webContents.navigationHistory.goForward();
            }
        });

        ipcMain.on(`close-${this.windowId}`, () => {
            if (thisWindow) {
                thisWindow.close();
            }
        });

        ipcMain.on(`minimize-${this.windowId}`, () => {
            if (thisWindow) {
                thisWindow.minimize();
            }
        });

        ipcMain.on(`maximize-${this.windowId}`, () => {
            if (thisWindow) {
                if (thisWindow.isMaximized()) {
                    thisWindow.unmaximize();
                } else {
                    thisWindow.maximize();
                }
            }
        });
    }

    async _setupContextMenu(mainWebContents) {
        try {
            const contextMenu = await import("electron-context-menu");
            const dispose = contextMenu.default({
                window: mainWebContents.webContents,
                showInspectElement: false,
            });
        } catch (error) {
            console.error("Failed to setup context menu:", error);
        }
    }

    /**
     * Check if login mode is active for a window
     * Login mode allows navigation to any URL (for external auth like Okta)
     */
    _isLoginModeActive(windowId) {
        return this.loginModeByWindow.get(windowId) === true;
    }

    /**
     * Enable login mode for a window
     */
    _enableLoginMode(windowId) {
        if (!this.loginModeByWindow.get(windowId)) {
            console.log(`Login mode ENABLED for window ${windowId}`);
            this.loginModeByWindow.set(windowId, true);
        }
    }

    /**
     * Disable login mode for a window
     */
    _disableLoginMode(windowId) {
        if (this.loginModeByWindow.get(windowId)) {
            console.log(`Login mode DISABLED for window ${windowId}`);
            this.loginModeByWindow.set(windowId, false);
        }
    }

    /**
     * Update login mode based on current URL after navigation completes
     * Login mode is disabled when we land on an allowed URL that is NOT a login URL
     */
    _updateLoginModeAfterNavigation(windowId, currentUrl, webapp) {
        const isOnLoginUrl = wildCardMatchList(currentUrl, webapp.login_urls);
        const isOnAllowedUrl = wildCardMatchList(currentUrl, webapp.allowed_urls);
        
        if (isOnLoginUrl) {
            // We're on a login URL - enable login mode
            this._enableLoginMode(windowId);
        } else if (isOnAllowedUrl) {
            // We're on an allowed URL that is NOT a login URL - disable login mode
            this._disableLoginMode(windowId);
        }
        // If neither login nor allowed URL, maintain current state (could be mid-OAuth flow)
    }

    /**
     * Check if a URL path suggests it will redirect to a not_allowed domain
     * For example: https://www.notion.so/calendarAuth -> https://calendar.notion.so/*
     * Returns true if the URL path contains keywords that match blocked subdomains
     */
    _isRedirectToBlockedDomain(url, notAllowedUrls) {
        try {
            const urlObj = new URL(url);
            const pathname = urlObj.pathname.toLowerCase();
            
            // Extract subdomain keywords from not_allowed_urls
            // e.g., "calendar.notion.so" -> "calendar", "mail.notion.so" -> "mail"
            for (const pattern of notAllowedUrls) {
                try {
                    // Extract domain from pattern (handle wildcards)
                    const patternUrl = pattern.replace(/\*/g, '');
                    const patternObj = new URL(patternUrl);
                    const subdomain = patternObj.hostname.split('.')[0];
                    
                    // Check if pathname contains this subdomain keyword
                    if (subdomain && pathname.includes(subdomain)) {
                        console.log(`[SWAI] URL path "${pathname}" suggests redirect to blocked domain "${subdomain}"`);
                        return true;
                    }
                } catch (e) {
                    // Skip invalid patterns
                }
            }
        } catch (e) {
            // URL parsing failed, skip check
        }
        return false;
    }

    /**
     * Check if a URL can be handled by another app in the same ecosystem
     * Returns the webapp that can handle it, or null
     */
    _findEcosystemAppForUrl(url, currentWebapp) {
        if (!this.ecosystemManager || !currentWebapp.ecosystem) {
            return null;
        }
        
        const ecosystemApps = this.ecosystemManager.getApps();
        for (const app of ecosystemApps) {
            // Skip the current app
            if (app.app_id === currentWebapp.app_id) {
                continue;
            }
            // Check if this app can handle the URL
            if (wildCardMatchList(url, app.allowed_urls) && 
                !wildCardMatchList(url, app.not_allowed_urls)) {
                return app;
            }
        }
        return null;
    }

    /**
     * Determines if navigation should be allowed within the app
     * 
     * Rules:
     * 1. not_allowed_urls: Always blocked - open in browser or ecosystem app
     * 2. Login mode active: Allow all navigation (for external auth flows like Okta)
     * 3. Current URL is a login URL: Enable login mode and allow navigation
     * 4. Navigation URL matches allowed_urls OR login_urls: Allow
     * 5. Otherwise: Block and open in browser or ecosystem app
     * 
     * @param {string} currentUrl - The current page URL
     * @param {string} navigationUrl - The URL being navigated to
     * @param {object} webapp - The webapp configuration
     * @param {number} windowId - The window ID for login mode tracking
     * @returns {object} { allowed: boolean, reason: string, ecosystemApp: WebApp|null }
     */
    _get_navigation_allowed(currentUrl, navigationUrl, webapp, windowId = null) {
        console.log(`[SWAI] Checking navigation: ${currentUrl} -> ${navigationUrl}`);
        console.log(`[SWAI]   Window ID: ${windowId}, Login mode: ${windowId !== null ? this._isLoginModeActive(windowId) : 'N/A'}`);
        console.log(`[SWAI]   not_allowed_urls: ${JSON.stringify(webapp.not_allowed_urls)}`);
        
        // Rule 1: Check not_allowed_urls first (highest priority block)
        const matchesNotAllowed = wildCardMatchList(navigationUrl, webapp.not_allowed_urls);
        console.log(`[SWAI]   Matches not_allowed_urls: ${matchesNotAllowed}`);
        
        // Also check if URL path suggests it will redirect to a blocked domain
        // e.g., https://www.notion.so/calendarAuth -> https://calendar.notion.so/*
        const isRedirectToBlocked = this._isRedirectToBlockedDomain(navigationUrl, webapp.not_allowed_urls);
        console.log(`[SWAI]   Suggests redirect to blocked domain: ${isRedirectToBlocked}`);
        
        if (matchesNotAllowed || isRedirectToBlocked) {
            console.log(`[SWAI] BLOCKED by not_allowed_urls${isRedirectToBlocked ? ' (redirect-prone URL)' : ''}`);
            // Check if an ecosystem app can handle this URL
            const ecosystemApp = this._findEcosystemAppForUrl(navigationUrl, webapp);
            return { 
                allowed: false, 
                reason: 'not_allowed_urls', 
                ecosystemApp 
            };
        }

        // Rule 2: If login mode is active, allow all navigation
        if (windowId !== null && this._isLoginModeActive(windowId)) {
            console.log(`[SWAI] ALLOWED by login mode`);
            return { allowed: true, reason: 'login_mode_active', ecosystemApp: null };
        }

        // Rule 3: Check if currently on a login URL - enable login mode and allow
        if (wildCardMatchList(currentUrl, webapp.login_urls)) {
            if (windowId !== null) {
                this._enableLoginMode(windowId);
            }
            console.log(`[SWAI] ALLOWED - on login URL, login mode enabled`);
            return { allowed: true, reason: 'on_login_url', ecosystemApp: null };
        }

        // Rule 4: Check if navigation URL matches allowed_urls OR is a login URL itself
        if (wildCardMatchList(navigationUrl, webapp.allowed_urls) || wildCardMatchList(navigationUrl, webapp.login_urls)) {
            console.log(`[SWAI] ALLOWED by allowed_urls`);
            return { allowed: true, reason: 'allowed_urls', ecosystemApp: null };
        }

        // Rule 5: Navigation not allowed
        console.log(`[SWAI] BLOCKED - not in allowed_urls`);
        // Check if an ecosystem app can handle this URL
        const ecosystemApp = this._findEcosystemAppForUrl(navigationUrl, webapp);
        return { 
            allowed: false, 
            reason: 'not_in_allowed_urls', 
            ecosystemApp 
        };
    }

    createTransientWindow(url, session, parent) {
        let transientWindow = new BrowserWindow({
            width: 800,
            height: 600,
            parent: parent,
            modal: true,
            title: "SWAI Web App",
            webPreferences: {
                session: session,
            },
        });
        this._setupStatusPages({ webContents: transientWindow.webContents }, session);
        this._loadURLWithOfflinePwa(transientWindow.webContents, session, url);
        transientWindow.once("ready-to-show", () => {
            transientWindow.show();
        });
        transientWindow.removeMenu();
    }
}

module.exports = WindowManager;
