/**
 * Main daemon entry point.
 *
 * Orchestrates the full startup sequence per PRD § 3:
 *   Phase 1: Local Init (config dirs, SQLite, control plane, PID file)
 *   Phase 2: Authentication (check/refresh tokens, login if needed)
 *   Phase 3: Connect to Gateway (WebSocket, daemon_register, file watchers)
 *
 * And the shutdown sequence:
 *   1. Stop accepting new requests
 *   2. Kill running Claude/command processes
 *   3. Close file watchers
 *   4. Send disconnect to gateway
 *   5. Close WebSocket
 *   6. Close SQLite connection
 *   7. Remove PID file
 *   8. Exit
 */
import { readFileSync, existsSync } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { ensureConfigDirs } from "./utils/config.js";
import { openDatabase } from "./db/database.js";
import { cleanupOldRuns } from "./db/repositories/command-runs.js";
import { listProjects } from "./db/repositories/projects.js";
import { createControlPlane } from "./control-plane/server.js";
import { writePidFile } from "./utils/pid.js";
import { logger } from "./utils/logger.js";
import { loadTokens } from "./auth/token-storage.js";
import { refreshTokenIfNeeded, startTokenMonitor, stopTokenMonitor } from "./auth/token-refresh.js";
import { login, setGetOAuthUrlFn } from "./auth/login.js";
import { GatewayClient } from "./gateway/websocket-client.js";
import { sendDaemonRegister } from "./gateway/messages/daemon-register.js";
import { MessageRouter } from "./gateway/message-router.js";
import { FileWatcher } from "./services/file-watcher.js";
import { spawnOpenCode } from "./opencode/spawn.js";
import { setOpenCodeAuth, clearOpenCodeAuth } from "./opencode/proxy.js";
import { supervise, stopSupervisor } from "./opencode/supervisor.js";
import { setSseSendFn, startSseSubscription, stopSseSubscription } from "./opencode/sse.js";
import { startHealthCheck, stopHealthCheck } from "./opencode/health-check.js";
import { killAllCommands } from "./services/command-runner.js";
import { setTerminalSendFn, killAllTerminals } from "./services/terminal-manager.js";
import { clearAllQueues } from "./services/message-queue.js";
import { registerShutdownHandler, gracefulShutdown } from "./services/shutdown.js";
import { setMachineLabelProviders } from "./control-plane/routes/machine-label.js";
import { setProjectSyncProviders } from "./control-plane/routes/project-sync.js";
import { setReRegisterProviders } from "./control-plane/routes/re-register.js";
import { setStatusProviders } from "./control-plane/routes/status.js";
import { getConfigDir } from "./utils/paths.js";
// Handler imports
import { handleFsList } from "./handlers/fs-list.js";
import { handleFsRead } from "./handlers/fs-read.js";
import { handleFsWrite } from "./handlers/fs-write.js";
import { handleFsCreate } from "./handlers/fs-create.js";
import { handleFsDelete } from "./handlers/fs-delete.js";
import { handleFsSearch } from "./handlers/fs-search.js";
import { handleFsReplace } from "./handlers/fs-replace.js";
import { handleCommandExec } from "./handlers/command-exec.js";
import { handleCommandCancel } from "./handlers/command-cancel.js";
import { handleGitStatus } from "./handlers/git-status.js";
import { handleGitBranches } from "./handlers/git-branches.js";
import { handleGitBranchCreate } from "./handlers/git-branch-create.js";
import { handleGitCheckout } from "./handlers/git-checkout.js";
import { handleGitStage } from "./handlers/git-stage.js";
import { handleGitUnstage } from "./handlers/git-unstage.js";
import { handleGitCommit } from "./handlers/git-commit.js";
import { handleGitPush } from "./handlers/git-push.js";
import { handleGitPull } from "./handlers/git-pull.js";
import { handleGitFileBase } from "./handlers/git-file-base.js";
import { handleGitDiscard } from "./handlers/git-discard.js";
import { handleGitDiff } from "./handlers/git-diff.js";
import { handleAgentSessionsList } from "./handlers/agent-sessions-list.js";
import { handleAgentSessionNew } from "./handlers/agent-session-new.js";
import { handleAgentMessage } from "./handlers/agent-message.js";
import { handleAgentCancel } from "./handlers/agent-cancel.js";
import { handleAgentPromptResponse } from "./handlers/agent-prompt-response.js";
import { handleTerminalCreate } from "./handlers/terminal-create.js";
import { handleTerminalStdin } from "./handlers/terminal-stdin.js";
import { handleTerminalResize } from "./handlers/terminal-resize.js";
import { handleTerminalKill } from "./handlers/terminal-kill.js";
import { handleMachinesList, setMachinesListGatewayClient } from "./handlers/machines-list.js";
import { handleProjectsList } from "./handlers/projects-list.js";
import { handleProjectCreate, setProjectCreateGatewayClient, setProjectCreateStartWatcher, } from "./handlers/project-create.js";
import { handleProjectDelete, setProjectDeleteGatewayClient, setProjectDeleteStopWatcher, } from "./handlers/project-delete.js";
import { handlePermissionReply } from "./opencode/permission-reply.js";
import { checkOpenCodeInstalled } from "./opencode/check-installed.js";
import { setOpenCodeCheckResult } from "./services/capabilities.js";
import { getEnv } from "./env.js";
/**
 * Start the daemon with the full startup sequence.
 */
export async function startDaemon() {
    logger.info("Starting PocketCoder daemon...");
    // ── Phase 1: Local Initialization ──────────────────────────────────
    // 1. Create config directories
    logger.info("Phase 1: Local initialization");
    await ensureConfigDirs();
    logger.info("Config directories ready");
    // 2. Initialize SQLite
    openDatabase();
    logger.info("Database initialized");
    // Run cleanup job
    const cleaned = cleanupOldRuns();
    if (cleaned > 0) {
        logger.info(`Cleaned up ${cleaned} old command runs`);
    }
    // Check OpenCode installation
    const openCodeCheck = await checkOpenCodeInstalled();
    setOpenCodeCheckResult(openCodeCheck);
    // Spawn OpenCode server if available
    let openCodeProcess = null;
    if (openCodeCheck.available) {
        try {
            openCodeProcess = await spawnOpenCode();
            setOpenCodeAuth(openCodeProcess.auth);
            logger.info("OpenCode server started");
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            logger.warn(`Failed to start OpenCode server: ${message}. Agent features will be unavailable.`);
        }
    }
    else {
        logger.warn("OpenCode not installed. Agent features will be unavailable.");
    }
    // 3. Start Control Plane HTTP
    const controlPlane = createControlPlane();
    try {
        await controlPlane.start();
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        if (message.includes("EADDRINUSE")) {
            logger.error("Daemon already running (port 17333 is busy)");
            throw new Error("Daemon already running");
        }
        throw err;
    }
    // 4. Write PID file
    await writePidFile();
    logger.info("PID file written");
    // Register control plane shutdown handler
    registerShutdownHandler("control plane", async () => {
        await controlPlane.stop();
    });
    // 5. Configure OAuth URL provider
    const { supabaseUrl } = getEnv();
    setGetOAuthUrlFn(async (redirectTo) => {
        // Construct Supabase OAuth URL for GitHub provider using code flow
        const url = new URL(`${supabaseUrl}/auth/v1/authorize`);
        url.searchParams.set("provider", "github");
        url.searchParams.set("redirect_to", redirectTo);
        url.searchParams.set("response_type", "code");
        return url.toString();
    });
    // ── Phase 2: Authentication ────────────────────────────────────────
    logger.info("Phase 2: Authentication");
    // Skip auth in test mode (for E2E testing)
    const skipAuth = process.env.POCKETCODER_SKIP_AUTH === "1";
    if (skipAuth) {
        logger.info("Test mode: skipping authentication (POCKETCODER_SKIP_AUTH=1)");
    }
    else {
        let tokens = await loadTokens();
        if (!tokens) {
            // No tokens — trigger login flow
            logger.info("No auth tokens found, starting login flow...");
            await login();
            tokens = await loadTokens();
            if (!tokens) {
                throw new Error("Login completed but tokens not found");
            }
        }
        else {
            // Validate/refresh tokens
            try {
                await refreshTokenIfNeeded();
                logger.info("Auth tokens valid");
            }
            catch {
                // Refresh failed — trigger login flow
                logger.info("Token refresh failed, starting login flow...");
                await login();
                tokens = await loadTokens();
                if (!tokens) {
                    throw new Error("Login completed but tokens not found");
                }
            }
        }
        logger.info("Authentication complete");
    }
    // Start token monitor to auto-refresh before expiry (skip in test mode)
    if (!skipAuth) {
        startTokenMonitor();
        registerShutdownHandler("token monitor", () => {
            stopTokenMonitor();
        });
    }
    // ── Phase 3: Connect to Gateway ─────────────────────────────────────
    logger.info("Phase 3: Connecting to gateway");
    // Load saved machine label
    let currentLabel = null;
    const labelPath = join(getConfigDir(), "machine-label.txt");
    try {
        currentLabel = (await readFile(labelPath, "utf-8")).trim() || null;
    }
    catch {
        // No saved label
    }
    const { gatewayUrl } = getEnv();
    // Set up message router
    const router = new MessageRouter();
    registerHandlers(router);
    // Create gateway client with auto-reconnect
    const gatewayClient = new GatewayClient({
        url: gatewayUrl,
        autoReconnect: true,
    });
    // Attach message router
    router.attach(gatewayClient);
    // Provide gateway client to handlers that need direct gateway communication
    setMachinesListGatewayClient(gatewayClient);
    setProjectCreateGatewayClient(gatewayClient);
    setProjectDeleteGatewayClient(gatewayClient);
    // On connect/reconnect, send daemon_register
    const onConnected = async () => {
        try {
            await sendDaemonRegister(gatewayClient, currentLabel);
            logger.info("Sent daemon_register to gateway");
        }
        catch (err) {
            logger.error(`Failed to send daemon_register: ${err instanceof Error ? err.message : String(err)}`);
        }
    };
    gatewayClient.on("connected", () => {
        logger.info("Connected to gateway");
        void onConnected();
    });
    gatewayClient.on("reconnected", () => {
        logger.info("Reconnected to gateway");
        void onConnected();
    });
    gatewayClient.on("disconnected", (code, reason) => {
        logger.warn(`Disconnected from gateway: code=${code} reason=${reason}`);
    });
    gatewayClient.on("error", (err) => {
        logger.error(`Gateway connection error: ${err.message}`);
    });
    gatewayClient.on("reconnecting", (attempt, delay) => {
        logger.info(`Reconnecting to gateway (attempt ${attempt}, delay ${delay}ms)`);
    });
    // Set up machine label providers
    setMachineLabelProviders({
        getLabel: () => currentLabel,
        setLabel: async (label) => {
            currentLabel = label;
            await writeFile(labelPath, label, "utf-8");
            // Re-register with gateway to propagate the new label
            if (gatewayClient.connected) {
                await sendDaemonRegister(gatewayClient, label);
                logger.info("Re-sent daemon_register with updated label");
            }
        },
    });
    // Set up re-register providers for CLI-triggered project changes
    setReRegisterProviders({
        getGatewayClient: () => gatewayClient,
        getLabel: () => currentLabel,
    });
    // Connect to gateway
    gatewayClient.connect();
    // 9. Start File Watchers
    const watcherMap = startFileWatchers(gatewayClient);
    // Helper to create and start a watcher for a single project
    const createAndStartWatcher = (projectId, projectPath) => {
        // Don't create duplicate watchers
        if (watcherMap.has(projectId)) {
            logger.warn(`File watcher already exists for project ${projectId}`);
            return;
        }
        const watcher = new FileWatcher({
            projectId,
            projectPath,
            onFsChanged: (event) => {
                if (gatewayClient.connected) {
                    gatewayClient.send(event);
                }
            },
        });
        watcher.start();
        watcherMap.set(projectId, watcher);
        logger.info(`File watcher started for project ${projectId} at ${projectPath}`);
    };
    // Helper to stop a watcher for a project
    const stopProjectWatcher = async (projectId) => {
        const watcher = watcherMap.get(projectId);
        if (watcher) {
            await watcher.stop();
            watcherMap.delete(projectId);
            logger.info(`File watcher stopped for project ${projectId}`);
        }
    };
    // Set up project sync providers for project_added/project_removed messages (CLI)
    setProjectSyncProviders({
        getGatewayClient: () => gatewayClient,
        startWatcher: createAndStartWatcher,
        stopWatcher: stopProjectWatcher,
    });
    // Provide start-watcher callback to project-create handler (WebSocket)
    setProjectCreateStartWatcher(createAndStartWatcher);
    // Provide stop-watcher callback to project-delete handler (WebSocket)
    setProjectDeleteStopWatcher(stopProjectWatcher);
    // Set up status providers for /status endpoint
    let lastConnectedAt = null;
    let lastHeartbeatAt = null;
    gatewayClient.on("connected", () => {
        lastConnectedAt = new Date().toISOString();
        lastHeartbeatAt = lastConnectedAt;
    });
    // Track heartbeats - Gateway sends ping every 30s, Host auto-responds with pong
    gatewayClient.on("message", (msg) => {
        if (msg.type === "ping") {
            lastHeartbeatAt = new Date().toISOString();
        }
    });
    setStatusProviders({
        getMachineKey: () => {
            // Load from tokens synchronously - this runs rarely
            try {
                const authPath = join(getConfigDir(), "auth.json");
                const data = readFileSync(authPath, "utf-8");
                const tokens = JSON.parse(data);
                return tokens.machine_key ?? null;
            }
            catch {
                return null;
            }
        },
        getLoggedIn: () => {
            try {
                const authPath = join(getConfigDir(), "auth.json");
                return existsSync(authPath);
            }
            catch {
                return false;
            }
        },
        getUserInfo: () => {
            // Decode JWT to extract user info (email, username, provider)
            try {
                const authPath = join(getConfigDir(), "auth.json");
                const data = readFileSync(authPath, "utf-8");
                const tokens = JSON.parse(data);
                if (!tokens.access_token)
                    return null;
                // Decode JWT payload (base64url encoded, no verification needed for display)
                const parts = tokens.access_token.split(".");
                if (parts.length !== 3)
                    return null;
                const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
                return {
                    email: payload.email,
                    username: payload.user_metadata?.user_name ??
                        payload.user_metadata?.preferred_username ??
                        payload.user_metadata?.name,
                    provider: payload.app_metadata?.provider ?? payload.app_metadata?.providers?.[0],
                };
            }
            catch {
                return null;
            }
        },
        getGatewayState: async () => {
            // Active ping verification - sends WebSocket ping and waits for pong
            const connected = await gatewayClient.ping(3000);
            return {
                connected,
                lastConnectedAt,
                lastHeartbeatAt,
            };
        },
        isOpenCodeRunning: () => openCodeProcess !== null,
        getLastError: () => null,
        isWatcherRunning: (projectId) => watcherMap.has(projectId),
    });
    // Set terminal send function for stdout/exit events
    setTerminalSendFn((msg) => {
        if (gatewayClient.connected) {
            gatewayClient.send(msg);
        }
    });
    // Set up OpenCode SSE forwarding and supervisor if OpenCode is running
    if (openCodeProcess) {
        // Set up SSE send function to forward events through gateway
        setSseSendFn((msg) => {
            if (gatewayClient.connected) {
                gatewayClient.send(msg);
            }
        });
        // Start SSE subscription
        startSseSubscription(openCodeProcess.auth);
        // Start health check with restart callback
        startHealthCheck(async () => {
            logger.warn("OpenCode health check triggered restart");
            // The supervisor handles the actual restart
        });
        // Start supervisor to auto-restart on crashes
        supervise(openCodeProcess, {
            onRestarted: (auth) => {
                // Update auth and re-subscribe to SSE
                setOpenCodeAuth(auth);
                startSseSubscription(auth);
                logger.info("OpenCode restarted, SSE re-subscribed");
            },
        });
    }
    // Register shutdown handlers (reverse order of startup)
    registerShutdownHandler("file watchers", async () => {
        for (const watcher of watcherMap.values()) {
            await watcher.stop();
        }
    });
    registerShutdownHandler("message queues", () => {
        clearAllQueues();
    });
    // OpenCode shutdown handlers (reverse order: health check → SSE → supervisor)
    if (openCodeProcess) {
        registerShutdownHandler("opencode health check", () => {
            stopHealthCheck();
        });
        registerShutdownHandler("opencode sse", () => {
            stopSseSubscription();
        });
        registerShutdownHandler("opencode supervisor", async () => {
            await stopSupervisor();
            clearOpenCodeAuth();
        });
    }
    registerShutdownHandler("running commands", () => {
        killAllCommands();
    });
    registerShutdownHandler("terminal sessions", () => {
        killAllTerminals();
    });
    registerShutdownHandler("gateway connection", () => {
        router.detach();
        gatewayClient.disconnect();
    });
    // Set up signal handlers
    setupSignalHandlers();
    // 10. Ready
    logger.info("Daemon ready. Connected to gateway.");
}
/**
 * Register all message handlers on the router.
 */
function registerHandlers(router) {
    router.registerHandler("fs_list", handleFsList);
    router.registerHandler("fs_read", handleFsRead);
    router.registerHandler("fs_write", handleFsWrite);
    router.registerHandler("fs_create", handleFsCreate);
    router.registerHandler("fs_delete", handleFsDelete);
    router.registerHandler("fs_search", handleFsSearch);
    router.registerHandler("fs_replace", handleFsReplace);
    router.registerHandler("command_exec", handleCommandExec);
    router.registerHandler("command_cancel", handleCommandCancel);
    router.registerHandler("git_status", handleGitStatus);
    router.registerHandler("git_branches", handleGitBranches);
    router.registerHandler("git_branch_create", handleGitBranchCreate);
    router.registerHandler("git_checkout", handleGitCheckout);
    router.registerHandler("git_stage", handleGitStage);
    router.registerHandler("git_unstage", handleGitUnstage);
    router.registerHandler("git_commit", handleGitCommit);
    router.registerHandler("git_push", handleGitPush);
    router.registerHandler("git_pull", handleGitPull);
    router.registerHandler("git_file_base", handleGitFileBase);
    router.registerHandler("git_discard", handleGitDiscard);
    router.registerHandler("git_diff", handleGitDiff);
    router.registerHandler("agent_sessions_list", handleAgentSessionsList);
    router.registerHandler("agent_session_new", handleAgentSessionNew);
    router.registerHandler("agent_message", handleAgentMessage);
    router.registerHandler("agent_cancel", handleAgentCancel);
    router.registerHandler("agent_prompt_response", handleAgentPromptResponse);
    router.registerHandler("machines_list", handleMachinesList);
    router.registerHandler("projects_list", handleProjectsList);
    router.registerHandler("project_create", handleProjectCreate);
    router.registerHandler("project_delete", handleProjectDelete);
    router.registerHandler("terminal_create", handleTerminalCreate);
    router.registerHandler("terminal_stdin", handleTerminalStdin);
    router.registerHandler("terminal_resize", handleTerminalResize);
    router.registerHandler("terminal_kill", handleTerminalKill);
    router.registerHandler("permission_reply", handlePermissionReply);
}
/**
 * Start file watchers for all registered projects.
 */
function startFileWatchers(gatewayClient) {
    const projects = listProjects();
    const watchers = new Map();
    for (const project of projects) {
        const watcher = new FileWatcher({
            projectId: project.project_id,
            projectPath: project.path,
            onFsChanged: (event) => {
                if (gatewayClient.connected) {
                    gatewayClient.send(event);
                }
            },
        });
        watcher.start();
        watchers.set(project.project_id, watcher);
        logger.info(`File watcher started for project "${project.name}" at ${project.path}`);
    }
    return watchers;
}
/**
 * Set up process signal handlers for graceful shutdown.
 */
function setupSignalHandlers() {
    let signalReceived = false;
    const handleSignal = (signal) => {
        if (signalReceived)
            return;
        signalReceived = true;
        logger.info(`Received ${signal}, shutting down...`);
        void gracefulShutdown().then(() => {
            process.exit(0);
        });
    };
    process.on("SIGTERM", () => handleSignal("SIGTERM"));
    process.on("SIGINT", () => handleSignal("SIGINT"));
}
//# sourceMappingURL=daemon.js.map