/**
 * Electron IPC → HTTP compatibility layer.
 *
 * Background: SOUND's renderer (src/ui) talks to the Electron main process
 * through ~63 IPC channels exposed via `window.electronAPI` (defined in
 * src/preload/index.ts). When SOUND runs as a headless daemon (no Electron),
 * there is no IPC bridge — but the browser-loaded UI still calls those
 * functions through the `electron-api-shim.js` polyfill, which translates
 * each call into a POST against this route.
 *
 * URL pattern: POST /api/electron/<kebab-channel>
 *   body: { ...args }
 *   response: JSON value (or { error } on failure)
 *
 * This module currently registers every channel as a **501 Not Implemented**
 * stub. The stubs log each hit, so by loading the UI in a browser and
 * watching the network panel we get a real, demand-ordered priority list of
 * which channels to actually implement first. Channels never hit by the UI
 * can be dropped entirely; channels that ARE hit get their IPC handler
 * logic extracted into a shared service and called from here.
 *
 * NOT mounted in Electron mode? — actually, it IS mounted in both modes.
 * In Electron the renderer never hits these routes (window.electronAPI is
 * real, so the shim is dormant), so they're just dead URLs. In daemon mode
 * they carry the load.
 */ import { Router } from "express";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import * as os from "node:os";
import { serverLogger } from "../../utils/serverLogger.js";
import { CredentialsManager } from "../../utils/credentials-no-keytar.js";
/**
 * Every IPC channel exposed by src/preload/index.ts's `electronAPI`.
 * Keep in sync with that file. Categorized for readability — order doesn't
 * matter functionally.
 */ const ELECTRON_API_CHANNELS = [
    // App / system info
    "get-version",
    "get-build-info",
    "get-app-icon",
    "get-api-key",
    "regenerate-api-key",
    "is-mac",
    // Library scan
    "select-directory",
    "scan-library",
    "stop-scan",
    "reset-database",
    // Settings
    "get-settings",
    "update-settings",
    "get-connection-settings",
    "save-connection-settings",
    "reset-connection-settings",
    "get-connection-status",
    "configure-upnp",
    // Auth + Wave sync
    "get-auth-status",
    "connect-wave",
    "disconnect-wave",
    "get-wave-auth",
    "save-wave-auth",
    "sync-library-to-wave",
    "unsync-library-from-wave",
    "check-wave-sync-available",
    // Remote access (tunnel) toggle
    "enable-remote-access",
    "disable-remote-access",
    "get-remote-access-status",
    // Native shell ops
    "show-in-finder",
    "play-audio",
    "open-external",
    "open-streamline",
    "open-settings-window",
    "open-window",
    "copy-to-clipboard",
    // Cloudflare credentials + tunnel management
    "has-cloudflare-credentials",
    "save-cloudflare-credentials",
    "get-cloudflare-credentials",
    "clear-cloudflare-credentials",
    "test-cloudflare-credentials",
    "get-tunnel-status",
    "get-tunnel-config",
    "start-user-cloudflare-oauth",
    "start-cloudflare-onboarding",
    "start-quick-tunnel",
    "stop-quick-tunnel",
    // Tailscale
    "check-tailscale-installed",
    "start-tailscale-connection",
    "get-tailscale-status",
    // Auto-launch (in daemon mode this is launchd's job — likely no-ops here)
    "enable-auto-launch",
    "disable-auto-launch",
    "get-auto-launch-status",
    // Keyboard shortcuts (Electron globalShortcut — UI surface only in daemon)
    "get-keyboard-shortcuts",
    "update-keyboard-shortcut",
    "toggle-keyboard-shortcuts",
    "get-keyboard-shortcuts-enabled",
    // Updates (daemon mode will likely defer to launchd + a separate updater)
    "check-for-updates",
    "download-update",
    "install-update",
    "get-update-status",
    "set-auto-update-enabled",
    "get-auto-update-enabled",
    // Secure storage (Map-backed daemon-local fallback; mirrors IPC's no-keytar path)
    "get-secure-value",
    "set-secure-value",
    "remove-secure-value",
    // Diagnostic
    "test-sentry-error",
    // Environment detection — not in the preload's named electronAPI surface
    // but called via electronAPI.invoke("is-dev-mode") at least twice in the UI.
    "is-dev-mode"
];
/**
 * Channels that have a real implementation registered (filled in as we
 * extract IPC handler logic into shared services). Anything NOT in this
 * map falls through to the 501 stub.
 */ const implementations = new Map();
/**
 * Register a real implementation for a channel. Call this from app.ts
 * once the shared-service refactor is done for that channel. The 501
 * stub for the same channel becomes unreachable.
 */ export function registerElectronCompatHandler(channel, handler) {
    implementations.set(channel, handler);
    serverLogger.info("[electron-compat] handler registered", {
        channel
    });
}
export function createElectronCompatRoutes(appState, tunnelManager) {
    const router = Router();
    // ---- Built-in stub implementations -------------------------------------
    // The UI's app-main.js initialize() chain calls a handful of "info" methods
    // synchronously at boot. A 501 on ANY of them throws and bails initialize(),
    // leaving the page bricked. These minimal stubs return safe defaults so the
    // UI proceeds far enough that we can observe its real downstream IPC calls
    // in the network panel. Each will be replaced by a real implementation that
    // calls the same appState services the Electron IPC handler uses.
    //
    // Shapes match src/main/ipcHandlers.ts so the UI sees the same data shape
    // it does under Electron.
    implementations.set("get-version", (_req, res)=>{
        res.json(process.env.SOUND_VERSION || process.env.npm_package_version || "1.1.1-daemon");
    });
    implementations.set("get-build-info", (_req, res)=>{
        res.json({
            version: process.env.SOUND_VERSION || "1.1.1-daemon",
            build: process.env.BUILD_NUMBER || "0",
            electron: null,
            node: process.versions.node,
            platform: process.platform,
            arch: process.arch
        });
    });
    implementations.set("is-mac", (_req, res)=>{
        res.json(process.platform === "darwin");
    });
    // App icon. UI gracefully degrades on null (uses the HTML <img> fallback).
    // In Electron this returns a data URL via app.dock.getIcon(). Real daemon
    // implementation should serve the icon from dist/assets/ as a static asset
    // and return its URL, but null is safe for spike phase.
    implementations.set("get-app-icon", (_req, res)=>{
        res.json(null);
    });
    // get-api-key / get-auth-status / get-settings return SAFE EMPTY defaults
    // here. They will be wired to appState once we extract that out of the
    // IPC handlers. For now the UI will see "no key / not authenticated /
    // default settings" and proceed with init.
    implementations.set("get-api-key", (_req, res)=>{
        res.json(null);
    });
    implementations.set("get-auth-status", (_req, res)=>{
        res.json({
            authenticated: false,
            hasAuth: false
        });
    });
    implementations.set("get-settings", (_req, res)=>{
        res.json({});
    });
    // ---- Tier-1 real implementations (settings page initial load) ----------
    // These five channels are called by settings-unified.js during page load.
    // Each mirrors the IPC handler body in src/main/ipcHandlers.ts, calling
    // the same module-level services (CredentialsManager, tunnelManager,
    // appState, playerWebSocket) — the only differences are around things
    // Electron has that the daemon doesn't (UPnP port-forwarding lookups,
    // electron-auto-launch). Where a daemon-equivalent exists we use it;
    // otherwise we return a safe minimal shape matching what the UI expects.
    // has-cloudflare-credentials — IPC body just delegates to CredentialsManager
    // (the no-keytar version, which works in headless mode out of the box).
    implementations.set("has-cloudflare-credentials", async (_req, res)=>{
        try {
            const has = await CredentialsManager.hasCredentials();
            res.json(has);
        } catch (err) {
            serverLogger.warn("[electron-compat] has-cloudflare-credentials failed", {
                error: err instanceof Error ? err.message : String(err)
            });
            res.json(false);
        }
    });
    // get-tunnel-status — mirrors the IPC body: return tunnelManager.getStatus()
    // when a manager is attached, otherwise a "not initialized" status object.
    implementations.set("get-tunnel-status", (_req, res)=>{
        if (tunnelManager) {
            res.json(tunnelManager.getStatus());
        } else {
            res.json({
                active: false,
                url: null,
                error: "Tunnel not initialized"
            });
        }
    });
    // get-remote-access-status — the IPC body merges appState.isRemoteAccessEnabled()
    // with portForwarding.getConnectionStatus(). The daemon has no equivalent of
    // portForwarding (that's an Electron UPnP wrapper), so we return the enabled
    // flag plus zero'd UPnP fields so the UI's render code doesn't break on
    // undefined property access.
    implementations.set("get-remote-access-status", (_req, res)=>{
        const enabled = appState?.isRemoteAccessEnabled?.() ?? false;
        res.json({
            enabled,
            isPortMapped: false,
            externalIp: null,
            externalPort: null,
            method: enabled ? "tunnel" : "none"
        });
    });
    // get-connection-status — IPC body returns { remoteUrl, activeClients }.
    // remoteUrl falls back from tunnel-current-url to appState.externalUrl.
    // activeClients comes from the playerWebSocket singleton (lazy-imported
    // because the route module is loaded before the WS singleton has any
    // connections, and we want to avoid an import cycle at module-eval time).
    implementations.set("get-connection-status", async (_req, res)=>{
        const tunnelStatus = tunnelManager ? tunnelManager.getStatus() : null;
        let activeClients = 0;
        try {
            const { playerWebSocket } = await import("../playerWebSocket.js");
            activeClients = playerWebSocket.getClientCount();
        } catch (err) {
            serverLogger.warn("[electron-compat] could not read playerWebSocket count", {
                error: err instanceof Error ? err.message : String(err)
            });
        }
        res.json({
            remoteUrl: tunnelStatus?.url || appState?.getExternalUrl?.() || null,
            activeClients
        });
    });
    // get-auto-launch-status — in Electron mode this wraps the auto-launch
    // package, which writes a LoginItem entry. In daemon mode the equivalent
    // is the launchd LaunchAgent plist at ~/Library/LaunchAgents/com.streamline.
    // sound.plist (see scripts/launchd/). Presence of that file means the
    // daemon will be (re)launched on login. Return a plain boolean to match
    // the IPC body's return shape.
    implementations.set("get-auto-launch-status", async (_req, res)=>{
        const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", "com.streamline.sound.plist");
        try {
            await fs.access(plistPath);
            res.json(true);
        } catch  {
            res.json(false);
        }
    });
    // ---- Tier-2 real implementations (reads + one contained delete) --------
    // All non-cascading: pure reads of appState / CredentialsManager, plus
    // clear-cloudflare-credentials which deletes a stored cred entry. The
    // write that DOES cascade (save-cloudflare-credentials → clear tunnel
    // config → restart tunnel) is deferred to a later batch.
    // get-tunnel-config — pure read of appState. IPC handler swallows errors
    // and returns null; we do the same so the UI's "no config yet" branch
    // works.
    implementations.set("get-tunnel-config", async (_req, res)=>{
        try {
            const cfg = await appState?.getTunnelConfig?.();
            res.json(cfg || null);
        } catch (err) {
            serverLogger.warn("[electron-compat] get-tunnel-config failed", {
                error: err instanceof Error ? err.message : String(err)
            });
            res.json(null);
        }
    });
    // get-wave-auth — synchronous appState read.
    implementations.set("get-wave-auth", (_req, res)=>{
        res.json(appState?.getWaveAuth?.() ?? null);
    });
    // get-connection-settings — composes the full settings object from
    // appState.getSettings(). Shape mirrors src/main/ipcHandlers.ts:1193
    // exactly so the UI's settings form binds correctly.
    implementations.set("get-connection-settings", (_req, res)=>{
        const settings = appState?.getSettings?.() || {};
        res.json({
            mode: settings.connectionMode || "auto",
            network: {
                port: settings.serverPort || parseInt(process.env.PORT || "51823", 10),
                bindAddress: settings.bindAddress || "0.0.0.0",
                enableIPv6: settings.enableIPv6 !== false
            },
            remoteAccess: {
                enabled: appState?.isRemoteAccessEnabled?.() ?? false,
                method: settings.remoteMethod || "cloudflare",
                externalPort: settings.externalPort || parseInt(process.env.PORT || "51823", 10),
                externalUrl: appState?.getExternalUrl?.() || ""
            },
            security: {
                requireAuth: settings.requireAuth !== false,
                enableCORS: settings.enableCORS !== false,
                allowedOrigins: settings.allowedOrigins || [
                    "*"
                ]
            },
            performance: {
                enableCompression: settings.enableCompression !== false,
                enableCache: settings.enableCache !== false,
                maxStreams: settings.maxStreams || 10,
                bufferSize: settings.bufferSize || 1048576
            },
            discovery: {
                enableMDNS: settings.enableMDNS !== false,
                enableUPnP: settings.enableUPnP !== false,
                broadcastName: settings.broadcastName || "SOUND"
            }
        });
    });
    // get-keyboard-shortcuts / get-keyboard-shortcuts-enabled
    //
    // In Electron mode these read from a keyboard-shortcut manager that wraps
    // Electron's globalShortcut API — that subsystem doesn't exist in daemon
    // mode (a Node daemon has no concept of global hotkeys). Return an empty
    // list and "disabled" so the settings UI renders a clean empty state.
    // Future: surface in-browser keyboard shortcuts (Mousetrap etc.) as a
    // separate concept.
    implementations.set("get-keyboard-shortcuts", (_req, res)=>{
        res.json([]);
    });
    implementations.set("get-keyboard-shortcuts-enabled", (_req, res)=>{
        res.json(false);
    });
    // get-cloudflare-credentials — pure read of stored credentials. Returns
    // the full credentials object (account_id, api_token, etc.) or null.
    implementations.set("get-cloudflare-credentials", async (_req, res)=>{
        try {
            const creds = await CredentialsManager.getCloudflareCredentials();
            res.json(creds);
        } catch (err) {
            serverLogger.warn("[electron-compat] get-cloudflare-credentials failed", {
                error: err instanceof Error ? err.message : String(err)
            });
            res.json(null);
        }
    });
    // clear-cloudflare-credentials — deletes stored credentials.
    //
    // Bounded mutation: only touches the cred entry (no tunnel restart, no
    // appState changes). The IPC handler throws on failure; we surface
    // { success: false, error } since the catch wrapper in the channel loop
    // would otherwise produce a 500 — the UI's success-branching code prefers
    // a shaped response.
    implementations.set("clear-cloudflare-credentials", async (_req, res)=>{
        try {
            await CredentialsManager.deleteCloudflareCredentials();
            res.json({
                success: true
            });
        } catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            serverLogger.warn("[electron-compat] clear-cloudflare-credentials failed", {
                error: msg
            });
            res.status(500).json({
                success: false,
                error: msg
            });
        }
    });
    // is-dev-mode — called via electronAPI.invoke("is-dev-mode") for UI feature
    // gates. In Electron the IPC handler also OR's `!app.isPackaged`; we drop
    // that since a daemon has no packaged/unpackaged distinction.
    implementations.set("is-dev-mode", (_req, res)=>{
        res.json(process.env.NODE_ENV === "development");
    });
    // ---- Tier-3 writes + secure storage + diagnostics ----------------------
    // Group D mutations on appState, secure-storage CRUD, the contained
    // cascading writes (save-cloudflare-credentials with tunnel restart,
    // enable/disable-remote-access), and the test-creds diagnostic.
    //
    // What's still deferred:
    //   scan-library / stop-scan      — heavy, fires SSE progress; needs design
    //   reset-database                — destructive multi-step file ops
    //   select-directory              — needs server-side dir browser UI work
    // Secure-storage backing: module-scope Map mirroring the IPC handler's
    // own non-keytar fallback (src/main/ipcHandlers.ts:1530). Daemon has no
    // keytar so we always use this. In-memory only — values are lost on
    // daemon restart. If real persistence is needed later, swap this Map
    // for an appState-backed encrypted store; the API surface stays the same.
    const secureStore = new Map();
    // update-settings — IPC body also toggles macOS dock icon visibility; we
    // drop that branch in daemon mode (no Electron dock). All other settings
    // changes flow through appState.updateSettings as in IPC.
    implementations.set("update-settings", async (req, res)=>{
        try {
            const settings = req.body?.settings ?? req.body ?? {};
            appState?.updateSettings?.(settings);
            res.json({
                success: true
            });
        } catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            serverLogger.warn("[electron-compat] update-settings failed", {
                error: msg
            });
            res.status(500).json({
                success: false,
                error: msg
            });
        }
    });
    // save-connection-settings — composition mirrors IPC:1227 exactly.
    implementations.set("save-connection-settings", async (req, res)=>{
        try {
            const cs = req.body?.settings ?? req.body ?? {};
            appState?.updateSettings?.({
                connectionMode: cs.mode,
                serverPort: cs.network?.port,
                bindAddress: cs.network?.bindAddress,
                enableIPv6: cs.network?.enableIPv6,
                remoteMethod: cs.remoteAccess?.method,
                externalPort: cs.remoteAccess?.externalPort,
                requireAuth: cs.security?.requireAuth,
                enableCORS: cs.security?.enableCORS,
                allowedOrigins: cs.security?.allowedOrigins,
                enableCompression: cs.performance?.enableCompression,
                enableCache: cs.performance?.enableCache,
                maxStreams: cs.performance?.maxStreams,
                bufferSize: cs.performance?.bufferSize,
                enableMDNS: cs.discovery?.enableMDNS,
                enableUPnP: cs.discovery?.enableUPnP,
                broadcastName: cs.discovery?.broadcastName
            });
            res.json({
                success: true
            });
        } catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            serverLogger.warn("[electron-compat] save-connection-settings failed", {
                error: msg
            });
            res.status(500).json({
                success: false,
                error: msg
            });
        }
    });
    // save-wave-auth — direct delegation. IPC returns {success, error?}.
    implementations.set("save-wave-auth", (req, res)=>{
        try {
            const auth = req.body?.auth ?? req.body ?? {};
            appState?.saveWaveAuth?.(auth);
            res.json({
                success: true
            });
        } catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            res.json({
                success: false,
                error: msg
            });
        }
    });
    // regenerate-api-key — returns the new key, same as IPC.
    implementations.set("regenerate-api-key", (_req, res)=>{
        try {
            const key = appState?.regenerateApiKey?.();
            res.json(key ?? null);
        } catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            serverLogger.warn("[electron-compat] regenerate-api-key failed", {
                error: msg
            });
            res.status(500).json({
                success: false,
                error: msg
            });
        }
    });
    // ---- Secure storage --------------------------------------------------
    // Map-backed, mirroring IPC's "no keytar" fallback. Args come through as
    // { key, value }; UI calls electronAPI.getSecureValue(key) etc. and the
    // shim wraps that as the POST body.
    implementations.set("get-secure-value", (req, res)=>{
        const key = req.body?.key;
        if (typeof key !== "string") {
            res.json(null);
            return;
        }
        res.json(secureStore.get(key) ?? null);
    });
    implementations.set("set-secure-value", (req, res)=>{
        const key = req.body?.key;
        const value = req.body?.value;
        if (typeof key !== "string" || typeof value !== "string") {
            res.status(400).json({
                success: false,
                error: "key and value must be strings"
            });
            return;
        }
        secureStore.set(key, value);
        res.json({
            success: true
        });
    });
    implementations.set("remove-secure-value", (req, res)=>{
        const key = req.body?.key;
        if (typeof key === "string") secureStore.delete(key);
        res.json({
            success: true
        });
    });
    // ---- Tunnel/credentials writes (contained cascades) ------------------
    // test-cloudflare-credentials — diagnostic call to Cloudflare's
    // /user/tokens/verify endpoint. Pure read, no state mutation.
    implementations.set("test-cloudflare-credentials", async (_req, res)=>{
        try {
            const credentials = await CredentialsManager.getCloudflareCredentials();
            if (!credentials || !credentials.apiToken) {
                res.json({
                    success: false,
                    error: "No credentials found"
                });
                return;
            }
            const cfRes = await fetch("https://api.cloudflare.com/client/v4/user/tokens/verify", {
                headers: {
                    Authorization: `Bearer ${credentials.apiToken}`
                }
            });
            if (cfRes.ok) {
                const data = await cfRes.json();
                res.json({
                    success: true,
                    status: data.result?.status
                });
            } else {
                res.json({
                    success: false,
                    error: `HTTP ${cfRes.status}`,
                    status: cfRes.statusText
                });
            }
        } catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            serverLogger.warn("[electron-compat] test-cloudflare-credentials failed", {
                error: msg
            });
            res.json({
                success: false,
                error: msg
            });
        }
    });
    // save-cloudflare-credentials — IPC body cascades into:
    //   1. CredentialsManager.setCloudflareCredentials(creds)
    //   2. appState.clearTunnelConfig() — discards the old tunnel mapping
    //   3. tunnelManager.stop() + start() — picks up new creds
    //
    // With CREDENTIALS_FILE env override in place, step 1 can be isolated
    // from production during spike testing. Steps 2+3 are still real state
    // mutations — wrap in try/catch and return shaped result so the UI's
    // success/error branching works either way.
    implementations.set("save-cloudflare-credentials", async (req, res)=>{
        const credentials = req.body?.credentials ?? req.body ?? {};
        try {
            await CredentialsManager.setCloudflareCredentials(credentials);
            await appState?.clearTunnelConfig?.();
            // Restart the tunnel non-blockingly. Bounded timeouts: stop ≤ 5s,
            // start kicks off and returns immediately so the UI gets a fast
            // response.
            if (tunnelManager) {
                try {
                    await Promise.race([
                        tunnelManager.stop(),
                        new Promise((r)=>setTimeout(r, 5000))
                    ]);
                    // Fire-and-forget restart
                    setTimeout(()=>{
                        tunnelManager.start().catch((err)=>{
                            serverLogger.warn("[electron-compat] tunnel restart after save failed", {
                                error: err instanceof Error ? err.message : String(err)
                            });
                        });
                    }, 100);
                } catch (err) {
                    serverLogger.warn("[electron-compat] tunnel stop during save failed (non-fatal)", {
                        error: err instanceof Error ? err.message : String(err)
                    });
                }
            }
            res.json({
                success: true
            });
        } catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            serverLogger.warn("[electron-compat] save-cloudflare-credentials failed", {
                error: msg
            });
            res.status(500).json({
                success: false,
                error: msg
            });
        }
    });
    // enable-remote-access — IPC checks creds, starts tunnel, flips appState.
    // Daemon mirrors that; tunnelManager is already attached at boot.
    implementations.set("enable-remote-access", async (_req, res)=>{
        try {
            const credentials = await CredentialsManager.getCloudflareCredentials();
            if (!credentials || !credentials.apiToken) {
                res.json({
                    success: false,
                    error: "No Cloudflare credentials — save them first via Settings"
                });
                return;
            }
            if (!tunnelManager) {
                res.json({
                    success: false,
                    error: "Tunnel manager not initialized"
                });
                return;
            }
            // Start non-blocking; UI gets fast ack
            setTimeout(()=>{
                tunnelManager.start().catch((err)=>{
                    serverLogger.warn("[electron-compat] tunnel start in enable-remote-access failed", {
                        error: err instanceof Error ? err.message : String(err)
                    });
                });
            }, 50);
            appState?.setRemoteAccessEnabled?.(true);
            res.json({
                success: true
            });
        } catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            serverLogger.warn("[electron-compat] enable-remote-access failed", {
                error: msg
            });
            res.json({
                success: false,
                error: msg
            });
        }
    });
    // disable-remote-access — IPC body calls portForwarding.disableRemoteAccess()
    // (Electron-only UPnP wrapper) + flips appState. Daemon skips the
    // portForwarding hop and just flips the flags + stops the tunnel if up.
    implementations.set("disable-remote-access", async (_req, res)=>{
        try {
            if (tunnelManager) {
                try {
                    await Promise.race([
                        tunnelManager.stop(),
                        new Promise((r)=>setTimeout(r, 5000))
                    ]);
                } catch (err) {
                    serverLogger.warn("[electron-compat] tunnel stop in disable-remote-access failed", {
                        error: err instanceof Error ? err.message : String(err)
                    });
                }
            }
            appState?.setRemoteAccessEnabled?.(false);
            appState?.setExternalUrl?.(null);
            res.json({
                success: true
            });
        } catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            res.json({
                success: false,
                error: msg
            });
        }
    });
    // enable-auto-launch / disable-auto-launch
    //
    // IPC calls into autoLaunchManager (electron-auto-launch wrapper) AND
    // flips appState.startAtLogin. In daemon mode, launchd manages
    // auto-launch via the .plist; toggling it programmatically would mean
    // writing/removing ~/Library/LaunchAgents/com.streamline.sound.plist.
    // That's a separate ops concern — for now we just flip the setting so
    // the UI checkbox state persists, and surface a clear hint that the
    // actual launchd registration is managed outside the daemon.
    implementations.set("enable-auto-launch", (_req, res)=>{
        try {
            appState?.updateSettings?.({
                startAtLogin: true
            });
            res.json(true);
        } catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            serverLogger.warn("[electron-compat] enable-auto-launch failed", {
                error: msg
            });
            res.json(false);
        }
    });
    implementations.set("disable-auto-launch", (_req, res)=>{
        try {
            appState?.updateSettings?.({
                startAtLogin: false
            });
            res.json(true);
        } catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            serverLogger.warn("[electron-compat] disable-auto-launch failed", {
                error: msg
            });
            res.json(false);
        }
    });
    // ---- Tier-4: scan-library + stop-scan + select-directory --------------
    //
    // The IPC handlers for scan-library and stop-scan don't call scanner.ts
    // directly — they POST to the server's own /scan and /scan/stop HTTP
    // endpoints (see src/main/ipcHandlers.ts:266 and :416). The compat
    // handlers do the same thing: build the same POST body + auth header
    // and forward to 127.0.0.1:$PORT/scan{,/stop}. This preserves the auth
    // middleware (checkAuth + checkOwner) that already lives on /scan, and
    // keeps the scan-fire-and-forget semantics the UI expects.
    function getOwnPort() {
        return parseInt(process.env.PORT || "51823", 10);
    }
    function getOwnApiKey() {
        const k = appState?.getApiKey?.();
        return typeof k === "string" && k.length > 0 ? k : null;
    }
    implementations.set("scan-library", async (req, res)=>{
        const reqPath = req.body?.path;
        const isIncrementalSync = req.body?.isIncrementalSync !== false; // default true
        if (typeof reqPath !== "string" || !reqPath) {
            res.status(400).json({
                success: false,
                error: "path is required"
            });
            return;
        }
        // Expand leading ~
        const normalizedPath = reqPath.replace(/^~/, os.homedir());
        let sinceTimestamp;
        if (isIncrementalSync) {
            sinceTimestamp = appState?.getLastSyncTimestamp?.(normalizedPath);
        }
        const apiKey = getOwnApiKey();
        if (!apiKey) {
            res.status(500).json({
                success: false,
                error: "no API key available — appState missing or uninitialized"
            });
            return;
        }
        try {
            const r = await fetch(`http://127.0.0.1:${getOwnPort()}/scan`, {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    "x-api-key": apiKey
                },
                body: JSON.stringify({
                    path: normalizedPath,
                    since_timestamp: sinceTimestamp
                }),
                // 30s timeout in case the scan endpoint itself blocks (it shouldn't —
                // it kicks off async scan + returns) but be safe
                signal: AbortSignal.timeout(30000)
            });
            const text = await r.text();
            const json = text ? (()=>{
                try {
                    return JSON.parse(text);
                } catch  {
                    return {
                        raw: text
                    };
                }
            })() : {};
            if (!r.ok) {
                serverLogger.warn("[electron-compat] scan-library upstream non-ok", {
                    status: r.status,
                    path: normalizedPath.substring(0, 80)
                });
            }
            res.status(r.status).json(json);
        } catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            serverLogger.warn("[electron-compat] scan-library forward failed", {
                error: msg
            });
            res.status(500).json({
                success: false,
                error: msg
            });
        }
    });
    implementations.set("stop-scan", async (_req, res)=>{
        const apiKey = getOwnApiKey();
        if (!apiKey) {
            res.json(false);
            return;
        }
        try {
            const r = await fetch(`http://127.0.0.1:${getOwnPort()}/scan/stop`, {
                method: "POST",
                headers: {
                    "x-api-key": apiKey
                },
                signal: AbortSignal.timeout(5000)
            });
            res.json(r.ok);
        } catch (err) {
            serverLogger.warn("[electron-compat] stop-scan forward failed", {
                error: err instanceof Error ? err.message : String(err)
            });
            res.json(false);
        }
    });
    // select-directory — IPC uses Electron's native dialog.showOpenDialog,
    // which doesn't exist in a daemon. Daemon-mode replacement is a
    // server-side directory-browser modal in the UI (future work). For
    // now: return null. Callers should handle null by showing a path
    // text-input as the fallback affordance.
    implementations.set("select-directory", (_req, res)=>{
        serverLogger.info("[electron-compat] select-directory called in daemon mode — returning null (no native dialog)");
        res.json(null);
    });
    // ====================================================================
    // Tier 5: Native-shell ops, tunnel toggles, no-op auto-launch / updates,
    // Tailscale probes, settings reset, diagnostics.
    //
    // For ops whose Electron-side behavior was "do something on the user's
    // local machine" (open URL, copy to clipboard, reveal in Finder), the
    // daemon path is best handled by the browser-side shim using window.open
    // / navigator.clipboard / etc. — the daemon literally cannot know which
    // machine the browser is on, and "open URL on daemon host" is rarely the
    // right behavior. These handlers return { success: true, handled: false }
    // (= "I acknowledge but did nothing — caller, you handle it client-side")
    // so the UI doesn't see a 501 stub while still being able to defer to
    // the shim for the actual side effect.
    // ====================================================================
    // --- native shell ops (client-side actually handles them; daemon ack only) ---
    implementations.set("open-external", (req, res)=>{
        const url = typeof req.body?.url === "string" ? req.body.url : undefined;
        serverLogger.debug("[electron-compat] open-external (deferred to browser shim)", {
            url
        });
        res.json({
            success: true,
            handled: false,
            url
        });
    });
    implementations.set("open-streamline", (_req, res)=>{
        serverLogger.debug("[electron-compat] open-streamline (deferred to browser shim)");
        res.json({
            success: true,
            handled: false,
            url: "https://streamline.club"
        });
    });
    implementations.set("copy-to-clipboard", (req, res)=>{
        const text = typeof req.body?.text === "string" ? req.body.text : "";
        serverLogger.debug("[electron-compat] copy-to-clipboard (deferred to browser shim)", {
            length: text.length
        });
        res.json({
            success: true,
            handled: false,
            text
        });
    });
    implementations.set("show-in-finder", (req, res)=>{
        const filePath = typeof req.body?.path === "string" ? req.body.path : undefined;
        // For the local-host case where the daemon's machine IS the user's machine,
        // we can shell out — but only if the path is safe. For now, ack and let
        // the shim decide based on whether window.location.host is localhost.
        serverLogger.debug("[electron-compat] show-in-finder (deferred to browser shim)", {
            filePath
        });
        res.json({
            success: true,
            handled: false,
            path: filePath
        });
    });
    implementations.set("play-audio", (req, res)=>{
        // Audio playback is always client-side in daemon mode — the WAVE web/native
        // clients consume /api/stream/* themselves. This IPC was the Electron renderer
        // asking main to spawn a local audio process for preview purposes; no
        // analogue in browser-rendered UI.
        serverLogger.debug("[electron-compat] play-audio (deferred to browser shim)", {
            url: req.body?.url
        });
        res.json({
            success: true,
            handled: false
        });
    });
    // --- "open a new Electron window" → "browser routes itself" ---
    implementations.set("open-settings-window", (_req, res)=>{
        res.json({
            success: true,
            handled: false,
            hint: "navigate to /#/settings in the browser UI"
        });
    });
    implementations.set("open-window", (req, res)=>{
        res.json({
            success: true,
            handled: false,
            hint: "navigate via browser router",
            route: req.body?.route
        });
    });
    // --- Quick Tunnel start/stop — bridge to HybridConnectionManager ---
    // These were Electron-only IPCs that drove the manager directly; daemon
    // exposes them via the same compat surface. Both are best-effort — if the
    // manager isn't wired up (no remote-access enabled), report failure
    // gracefully without throwing.
    implementations.set("start-quick-tunnel", async (_req, res)=>{
        try {
            const hcm = (await import("../hybridConnectionManager.js")).getHybridConnectionManager(appState);
            // setRelayEnabled(true) goes through the same startRelayServices path,
            // which handles the custom-creds-vs-no-creds gating correctly.
            await hcm.setRelayEnabled(true);
            const status = hcm.getStatus();
            res.json({
                success: true,
                url: status?.quickTunnelUrl || status?.primaryUrl || null
            });
        } catch (err) {
            serverLogger.warn("[electron-compat] start-quick-tunnel failed", {
                error: err instanceof Error ? err.message : String(err)
            });
            res.status(500).json({
                success: false,
                error: err instanceof Error ? err.message : String(err)
            });
        }
    });
    implementations.set("stop-quick-tunnel", async (_req, res)=>{
        try {
            const hcm = (await import("../hybridConnectionManager.js")).getHybridConnectionManager(appState);
            await hcm.setRelayEnabled(false);
            res.json({
                success: true
            });
        } catch (err) {
            serverLogger.warn("[electron-compat] stop-quick-tunnel failed", {
                error: err instanceof Error ? err.message : String(err)
            });
            res.status(500).json({
                success: false,
                error: err instanceof Error ? err.message : String(err)
            });
        }
    });
    // --- reset operations (destructive, guarded) ---
    implementations.set("reset-connection-settings", async (_req, res)=>{
        try {
            await appState.setConfig({
                connectionSettings: undefined
            });
            serverLogger.info("[electron-compat] reset-connection-settings cleared");
            res.json({
                success: true
            });
        } catch (err) {
            res.status(500).json({
                success: false,
                error: err instanceof Error ? err.message : String(err)
            });
        }
    });
    implementations.set("reset-database", async (_req, res)=>{
        // Real wipe-and-recreate is risky from a browser-triggered IPC; require
        // an explicit confirm token to reduce risk of accidental hits. Until the
        // UI wires up that confirmation flow, surface a 409 with guidance.
        const confirm = _req.body?.confirm;
        if (confirm !== "RESET-DATABASE-CONFIRMED") {
            res.status(409).json({
                success: false,
                error: "Destructive op requires confirmation",
                hint: "POST { confirm: 'RESET-DATABASE-CONFIRMED' } from the UI after explicit user opt-in"
            });
            return;
        }
        res.status(501).json({
            success: false,
            error: "reset-database wipe not yet implemented in daemon mode",
            hint: "Stop the daemon, delete the DATABASE_PATH file, restart — or wait for full impl."
        });
    });
    // --- Tailscale probes ---
    // Tailscale CLI presence ≈ "is tailscale installed". Status is via the
    // existing TailscaleService used elsewhere in HybridConnectionManager.
    implementations.set("check-tailscale-installed", async (_req, res)=>{
        try {
            const { execSync } = await import("node:child_process");
            execSync("which tailscale", {
                stdio: "ignore"
            });
            res.json(true);
        } catch  {
            res.json(false);
        }
    });
    implementations.set("get-tailscale-status", async (_req, res)=>{
        try {
            const { getTailscaleService } = await import("../tailscaleService.js");
            const status = await getTailscaleService().getStatus();
            res.json(status);
        } catch (err) {
            serverLogger.debug("[electron-compat] get-tailscale-status failed", {
                error: err instanceof Error ? err.message : String(err)
            });
            res.json({
                isRunning: false,
                installed: false,
                error: err instanceof Error ? err.message : String(err)
            });
        }
    });
    implementations.set("start-tailscale-connection", (_req, res)=>{
        // Tailscale needs user-driven `tailscale up` from a terminal (interactive
        // browser auth flow). Daemon can't drive that. Return guidance.
        res.json({
            success: false,
            handled: false,
            hint: "Run `tailscale up` in a terminal on this machine; the daemon will pick up the connection automatically."
        });
    });
    // --- Auto-launch — launchd plist's RunAtLoad=true already handles this
    //     for the daemon. enable/disable here are no-ops because the install
    //     script writes the plist with RunAtLoad=true; users wanting to
    //     disable should uninstall the LaunchAgent. get-auto-launch-status
    //     is already implemented (reads the plist file).
    implementations.set("enable-auto-launch", (_req, res)=>{
        res.json({
            success: true,
            handled: false,
            hint: "Daemon mode: auto-launch is controlled by the launchd plist (RunAtLoad=true after install-daemon.sh). Nothing to enable."
        });
    });
    implementations.set("disable-auto-launch", (_req, res)=>{
        res.json({
            success: true,
            handled: false,
            hint: "Daemon mode: run scripts/uninstall-daemon.sh to remove the LaunchAgent."
        });
    });
    // --- Keyboard shortcuts — Electron registers globalShortcut at the OS
    //     level. Browser UI has no analogue (DOM-level keydown is page-scoped).
    //     No-ops with empty list.
    implementations.set("update-keyboard-shortcut", (_req, res)=>{
        res.json({
            success: true,
            handled: false,
            hint: "Keyboard shortcuts are not registered globally in daemon mode."
        });
    });
    implementations.set("toggle-keyboard-shortcuts", (_req, res)=>{
        res.json({
            success: true,
            handled: false,
            enabled: false
        });
    });
    // --- Updates — wired to the UpdateChecker service. Polls a manifest URL
    //     (default: downloads.streamline.club) on a 6h interval. Install is
    //     gated behind explicit user trigger via install-update; never
    //     auto-applies (the install briefly drops the tunnel during the
    //     launchctl unload/load swap).
    implementations.set("get-update-status", async (_req, res)=>{
        try {
            const { getUpdateChecker } = await import("../services/updateChecker.js");
            const version = process.env.SOUND_VERSION || // Best-effort version read — the daemon's package.json sits next to dist/
            await import("node:fs/promises").then((fs)=>fs.readFile(new URL("../../../package.json", import.meta.url), "utf8")).then((s)=>JSON.parse(s).version).catch(()=>"0.0.0");
            const checker = getUpdateChecker(version);
            checker.start(); // idempotent
            res.json(checker.getStatus());
        } catch (err) {
            res.status(500).json({
                error: err instanceof Error ? err.message : String(err)
            });
        }
    });
    implementations.set("check-for-updates", async (_req, res)=>{
        try {
            const { getUpdateChecker } = await import("../services/updateChecker.js");
            const version = process.env.SOUND_VERSION || "1.1.1";
            const checker = getUpdateChecker(version);
            checker.start();
            await checker.check();
            res.json(checker.getStatus());
        } catch (err) {
            res.status(500).json({
                success: false,
                error: err instanceof Error ? err.message : String(err)
            });
        }
    });
    implementations.set("download-update", async (_req, res)=>{
        // download-update was an Electron concept (separate from install). For the
        // daemon, "download" and "install" are one step because we run
        // update-daemon.sh which downloads AND runs cp + launchctl. Route this to
        // install-update with a clear hint, so UIs that wire both buttons behave
        // sensibly.
        res.json({
            success: true,
            handled: false,
            hint: "Daemon mode combines download + install. Call install-update to do both."
        });
    });
    implementations.set("install-update", async (_req, res)=>{
        try {
            const { getUpdateChecker } = await import("../services/updateChecker.js");
            const version = process.env.SOUND_VERSION || "1.1.1";
            const checker = getUpdateChecker(version);
            // Reply success BEFORE installing — installAvailable() may kill this
            // process via launchctl unload once update-daemon.sh runs. The UI
            // should show "Installing..." UI based on this response, then poll
            // get-update-status (which goes silent for ~30s during the swap).
            res.json({
                success: true,
                message: "Update initiated — daemon will restart in ~10-30s"
            });
            void checker.installAvailable().catch((err)=>{
                serverLogger.error("[electron-compat] install-update failed", {
                    error: err instanceof Error ? err.message : String(err)
                });
            });
        } catch (err) {
            if (!res.headersSent) {
                res.status(500).json({
                    success: false,
                    error: err instanceof Error ? err.message : String(err)
                });
            }
        }
    });
    implementations.set("get-auto-update-enabled", (_req, res)=>{
        // Polling is enabled by default unless SOUND_DISABLE_AUTO_UPDATE=1.
        res.json(process.env.SOUND_DISABLE_AUTO_UPDATE !== "1");
    });
    implementations.set("set-auto-update-enabled", (_req, res)=>{
        // Daemon mode: this is controlled by env vars at launchd-plist level,
        // not via runtime IPC. Surface that fact rather than silently accepting.
        res.json({
            success: false,
            handled: false,
            hint: "Auto-update polling is controlled by SOUND_DISABLE_AUTO_UPDATE in the launchd plist. Edit ~/Library/LaunchAgents/com.streamline.sound.plist and `launchctl unload && launchctl load` to apply."
        });
    });
    // --- Cloudflare OAuth — Electron version opened a BrowserWindow to drive
    //     the auth dance. Daemon mode would need a different architecture:
    //     UI calls this → daemon generates state + returns Cloudflare authorize
    //     URL → UI window.opens it → Cloudflare redirects to a callback route on
    //     the daemon → daemon exchanges code for token → notifies UI via WS.
    //     Until that's wired up (Phase 1 follow-up), point users at the manual
    //     API-token entry flow which is fully implemented and works today.
    implementations.set("start-user-cloudflare-oauth", (_req, res)=>{
        serverLogger.info("[electron-compat] start-user-cloudflare-oauth — directing user to manual token path");
        res.status(503).json({
            success: false,
            reason: "oauth-not-yet-implemented",
            hint: "Create a Cloudflare API token manually: Cloudflare Dashboard → My Profile → API Tokens → " + "Create Token → Use template 'Edit Cloudflare Workers' (or custom with Zone:DNS:Edit + " + "Account:Cloudflare Tunnel:Edit). Paste the token into Settings → Cloudflare → " + "API Token. The daemon will create the tunnel automatically.",
            manualFlowUrl: "/#settings/cloudflare"
        });
    });
    implementations.set("start-cloudflare-onboarding", (_req, res)=>{
        serverLogger.info("[electron-compat] start-cloudflare-onboarding — directing user to manual token path");
        res.status(503).json({
            success: false,
            reason: "oauth-not-yet-implemented",
            hint: "Onboarding via embedded OAuth window isn't supported in daemon mode yet. " + "Use the manual flow: Settings → Cloudflare → paste API token.",
            manualFlowUrl: "/#settings/cloudflare"
        });
    });
    // --- Wave Cloud sync — these IPCs talked to Wave Cloud (Streamline's
    //     hosted relay layer). The daemon registers with the streamline.club
    //     registry through HybridConnectionManager already; the sync-library
    //     IPCs were a separate, less-used path. Stub as not-supported in
    //     daemon mode pending a decision on whether to keep that surface.
    implementations.set("check-wave-sync-available", (_req, res)=>{
        res.json({
            available: false,
            reason: "Wave Cloud sync not yet ported to daemon mode"
        });
    });
    implementations.set("connect-wave", (_req, res)=>{
        res.status(503).json({
            success: false,
            hint: "Wave Cloud sync not yet ported to daemon mode"
        });
    });
    implementations.set("disconnect-wave", (_req, res)=>{
        res.status(503).json({
            success: false,
            hint: "Wave Cloud sync not yet ported to daemon mode"
        });
    });
    implementations.set("sync-library-to-wave", (_req, res)=>{
        res.status(503).json({
            success: false,
            hint: "Wave Cloud sync not yet ported to daemon mode"
        });
    });
    implementations.set("unsync-library-from-wave", (_req, res)=>{
        res.status(503).json({
            success: false,
            hint: "Wave Cloud sync not yet ported to daemon mode"
        });
    });
    // --- Misc ---
    implementations.set("configure-upnp", (_req, res)=>{
        // UPnP port forwarding was an Electron-only convenience using nat-upnp.
        // Daemon users typically use Cloudflare tunnels or direct port forward.
        res.json({
            success: false,
            handled: false,
            hint: "UPnP not supported in daemon mode; use Cloudflare tunnel (Settings → Remote Access)."
        });
    });
    implementations.set("test-sentry-error", async (_req, res)=>{
        try {
            const Sentry = await import("@sentry/node").catch(()=>null);
            if (Sentry) {
                Sentry.captureException(new Error("test-sentry-error triggered from daemon compat layer"));
                res.json({
                    success: true
                });
            } else {
                res.json({
                    success: false,
                    error: "@sentry/node not installed in daemon"
                });
            }
        } catch (err) {
            res.json({
                success: false,
                error: err instanceof Error ? err.message : String(err)
            });
        }
    });
    for (const channel of ELECTRON_API_CHANNELS){
        router.post(`/${channel}`, async (req, res, next)=>{
            const impl = implementations.get(channel);
            if (impl) {
                try {
                    await impl(req, res, next);
                } catch (err) {
                    serverLogger.error("[electron-compat] handler threw", {
                        channel,
                        error: err instanceof Error ? err.message : String(err)
                    });
                    if (!res.headersSent) {
                        res.status(500).json({
                            error: "Internal Server Error",
                            channel,
                            message: err instanceof Error ? err.message : String(err)
                        });
                    }
                }
                return;
            }
            // Stub: log so the browser network panel + server log give us a real
            // demand-ordered list of which channels are actually called by the UI.
            serverLogger.info("[electron-compat] 501 stub hit", {
                channel,
                body: req.body && Object.keys(req.body).length > 0 ? req.body : undefined,
                ua: req.get("user-agent")
            });
            res.status(501).json({
                error: "Not Implemented",
                channel,
                message: `Electron IPC channel "${channel}" has no daemon implementation yet. ` + `Hit by the UI via the electron-api-shim; the shim mapped it to this URL.`
            });
        });
    }
    // Catch-all for unknown channels — 404 with helpful message
    router.post("/*", (req, res)=>{
        const channel = req.path.replace(/^\//, "");
        serverLogger.warn("[electron-compat] 404 unknown channel", {
            channel
        });
        res.status(404).json({
            error: "Unknown channel",
            channel,
            message: `No Electron IPC channel "${channel}" is registered. ` + `If the UI relies on it, add it to ELECTRON_API_CHANNELS in electronCompatRoutes.ts.`
        });
    });
    return router;
}

//# sourceMappingURL=electronCompatRoutes.js.map