import { cp, mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { WebSocketServer } from "ws";
import { createISocketClient, DEFAULT_NON_GIT_BRANCH, SUPERBLOCKS_LIVE_GIT_BRANCH, TracedEventEmitter, TracedSocket, } from "@superblocksteam/shared";
import { uploadContextToBucketeer } from "./ai-service/context-upload.js";
import { createSocketRecordingMiddleware } from "./ai-service/recording/recorders/socket-recorder.js";
import { GIT_LOG_FORMAT, parseGitLog } from "./git-log.js";
import { GIT_OPERATION_BUSY_MESSAGE, } from "./git-operation-lock.js";
import { ensureAzureDevOpsWorkflowFile } from "./git-service/azure-devops-workflow.js";
import { ensureBitbucketWorkflowFile } from "./git-service/bitbucket-workflow.js";
import { ensureGithubWorkflowFile } from "./git-service/github-workflow.js";
import { ensureGitlabWorkflowFile } from "./git-service/gitlab-workflow.js";
import { GitConflictError } from "./git-service/index.js";
import { doesRemoteLiveBranchExist, ensureLiveBranchCheckedOut, pushLiveBranchWithAutoRecreate, rollbackLastCommit, } from "./git-service/live-branch.js";
import { getSyncCountsAgainstDefaultBranch } from "./git-service/status-sync.js";
import { getErrorMeta, getLogger } from "./util/logger.js";
const SB_WORKTREE_SNAPSHOTS_DIR = join(tmpdir(), "sb-worktree-snapshots");
let _socketManager;
// Global flag for Clark log forwarding to browser console.
// Defaults to enabled for internal dev (SUPERBLOCKS_CLI_ENV=local), can be toggled via ?clark_logs=true query param.
let _clarkVerboseLogsEnabled = process.env.SUPERBLOCKS_CLI_ENV === "local";
export function isClarkVerboseLogsEnabled() {
    return _clarkVerboseLogsEnabled;
}
export function setClarkVerboseLogsEnabled(enabled) {
    _clarkVerboseLogsEnabled = enabled;
}
export function createSocketManager(tracer) {
    _socketManager?.dispose();
    _socketManager = new SocketManager(tracer);
    return _socketManager;
}
export function disposeSocketManager() {
    _socketManager?.dispose();
    _socketManager = undefined;
}
class SocketManager extends TracedEventEmitter {
    peerIds = new Map();
    editorSockets = new Set();
    librarySockets = new Set();
    _disposer = () => { };
    constructor(tracer) {
        super(tracer);
    }
    socketServer;
    startSocketServer({ server, fileSyncManager, syncService, lockService, aiService, gitService, gitBootstrapFailed, activateGitService, checkAuthorization, path, tracer, gitOperationLock, superblocksBaseUrl, }) {
        if (this.socketServer) {
            throw new Error("Socket server already started");
        }
        const wss = new WebSocketServer({ path, noServer: true });
        this.socketServer = wss;
        let activeGitService = gitService;
        // Sticky guard. Set to true by `gitDisconnect` and deliberately NOT
        // cleared when it finishes: any callers of `getActiveGitService()` that
        // started before disconnect but resolve after it completes must still
        // return `undefined` so they can't resurrect a git service (and re-assert
        // `superblocks/live`) by assigning the activated value and running
        // `ensureLiveBranchCheckedOut`. The only legitimate path that flips this
        // back to false is the `gitActivate` RPC, which represents an explicit
        // user-driven reconnect; automatic paths (editor-reconnect's
        // fire-and-forget `void activateGitService()`) must not override a manual
        // disconnect.
        let disconnectInProgress = false;
        const getActiveGitService = async () => {
            if (disconnectInProgress) {
                return undefined;
            }
            if (activeGitService) {
                await ensureLiveBranchCheckedOut(activeGitService);
                if (disconnectInProgress) {
                    return undefined;
                }
                aiService?.setRuntimeGitServices(activeGitService, syncService, gitOperationLock);
                return activeGitService;
            }
            if (!activateGitService) {
                return undefined;
            }
            // Post-connect git activation may race with server-side persistence.
            // Retry briefly so users don't need a manual dev server restart.
            const maxAttempts = 5;
            for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
                if (disconnectInProgress) {
                    return undefined;
                }
                try {
                    const activated = await activateGitService();
                    if (disconnectInProgress) {
                        return undefined;
                    }
                    activeGitService = activated;
                    if (activeGitService) {
                        await ensureLiveBranchCheckedOut(activeGitService);
                        if (disconnectInProgress) {
                            return undefined;
                        }
                        aiService?.setRuntimeGitServices(activeGitService, syncService, gitOperationLock);
                        return activeGitService;
                    }
                }
                catch (error) {
                    getLogger().warn("[git] activation attempt failed", {
                        gitCategory: "setup",
                        gitOperation: "activate",
                        gitOutcome: "failed",
                        gitAttempt: attempt,
                        gitMaxAttempts: maxAttempts,
                        ...getErrorMeta(error),
                    });
                    if (attempt === maxAttempts) {
                        getLogger().error(`[git] activation attempts exhausted (${attempt}/${maxAttempts})`, getErrorMeta(error));
                        throw error;
                    }
                }
                if (attempt < maxAttempts) {
                    await new Promise((resolve) => {
                        setTimeout(resolve, 750);
                    });
                }
            }
            return undefined;
        };
        async function runWithGitLock(fn) {
            if (!gitOperationLock) {
                return fn();
            }
            const result = gitOperationLock.tryAcquire();
            if (!result.acquired) {
                throw new Error(GIT_OPERATION_BUSY_MESSAGE);
            }
            try {
                return await fn();
            }
            finally {
                result.release();
            }
        }
        const handleUpgrade = (req, socket, head) => {
            if (!req.url) {
                return;
            }
            const url = new URL(req.url, "http://localhost");
            // TODO: some sort of authorization? this socket shouldnt be open outside of superblocks
            if (url.pathname === path) {
                let isEditor = false;
                if (req.headers["sec-websocket-protocol"] === "editor") {
                    isEditor = true;
                }
                let isUpgraded = false;
                wss.handleUpgrade(req, socket, head, (ws) => {
                    if (isUpgraded) {
                        return;
                    }
                    isUpgraded = true;
                    const peerId = url.searchParams.get("peerId");
                    if (!peerId) {
                        getLogger().error("No peerId in upgrade request");
                        ws.close(1008, "No peerId found");
                        return;
                    }
                    if (url.searchParams.get("applicationId") !== syncService?.applicationId) {
                        getLogger().error("Application ID mismatch", {
                            error: {
                                kind: "ValidationError",
                                message: `Expected application ID ${syncService?.applicationId}, received ${url.searchParams.get("applicationId")}`,
                            },
                        });
                        ws.close(1008, "Application ID mismatch");
                        return;
                    }
                    wss.emit("connection", ws, isEditor, url.searchParams.get("peerId"), url.searchParams.get("userId"));
                });
            }
            else {
                // Noop
            }
        };
        server.on("upgrade", handleUpgrade);
        this._disposer = () => {
            server.removeListener("upgrade", handleUpgrade);
            wss.close();
        };
        wss.on("connection", (ws, isEditor, peerId, userId) => {
            const authMiddleware = async (_params, ctx, _peer, next) => {
                const result = await checkAuthorization(ctx.peerAuthorization);
                if (!result.success) {
                    throw new Error(result.error);
                }
                return await next();
            };
            // Create recording middleware if AI service is available
            const recordingMiddleware = aiService
                ? createSocketRecordingMiddleware(aiService.getRecordingManager())
                : undefined;
            // Build middleware array: recording first (for debugging), then auth
            const middlewares = [recordingMiddleware, authMiddleware].filter((m) => m != null);
            const socket = createISocketClient(new TracedSocket(ws, {
                editor: {
                    // Renames
                    renameElement: [fileSyncManager.handleRenameElement],
                    renameApi: [fileSyncManager.handleRenameApi],
                    // Update APIs
                    apiUpdate: [fileSyncManager.handleUpdateApi],
                    apiDelete: [fileSyncManager.handleDeleteApi],
                    // Ensure files are synced
                    ensureFilesSynced: [
                        async () => {
                            if (!syncService) {
                                throw new Error("Syncing is disabled, cannot ensure files are synced");
                            }
                            const result = await syncService.uploadDirectoryNowIfNeeded("requestedFilesSync");
                            return result.directoryHash;
                        },
                    ],
                    // AI + draft handling
                    aiGenerate: [
                        async (request) => {
                            await aiService?.handleAiGenerate(request);
                        },
                    ],
                    aiGenerateWithQueue: [
                        async (request) => {
                            if (!aiService) {
                                throw new Error("AI service not available");
                            }
                            const { queueMode, ...aiRequest } = request;
                            return await aiService.handleAiGenerateWithQueue(aiRequest, queueMode);
                        },
                    ],
                    aiTriggerMigrationTurn: [
                        async (request) => {
                            if (!aiService) {
                                throw new Error("AI service not available");
                            }
                            return await aiService.handleAiTriggerMigrationTurn(request);
                        },
                    ],
                    aiRejectDraft: [
                        async (request) => {
                            await syncService?.discardLocalDraftChanges("ai:reject");
                            await aiService?.handleUserRejectedDraft(request.silently);
                        },
                    ],
                    aiAcceptDraft: [
                        async (request, _context) => {
                            getLogger().info("[aiAcceptDraft] accept draft start", {
                                applicationId: syncService?.applicationId,
                                branchName: syncService?.branchName,
                                silently: request.silently,
                            });
                            const result = await syncService?.commitLocalDraftChanges("ai:accept");
                            getLogger().info("[aiAcceptDraft] draft commit completed", {
                                applicationId: syncService?.applicationId,
                                branchName: syncService?.branchName,
                                commitId: result?.commitId,
                                hasCommitId: Boolean(result?.commitId),
                            });
                            await aiService?.handleUserAcceptedDraft(request.silently);
                            getLogger().info("[aiAcceptDraft] accepted draft chat state handled", {
                                applicationId: syncService?.applicationId,
                                branchName: syncService?.branchName,
                                commitId: result?.commitId,
                                silently: request.silently,
                            });
                            if (result?.commitId) {
                                // Upload context file to bucketeer asynchronously (fire-and-forget)
                                if (aiService && syncService) {
                                    void (async () => {
                                        try {
                                            // Get contextId using the same pattern as other context operations
                                            // This gets userId from clark.context.jwt
                                            const contextId = aiService.getContextId();
                                            if (!contextId) {
                                                getLogger().warn("[aiAcceptDraft] No contextId available (JWT missing or no userId), skipping context upload");
                                                return;
                                            }
                                            // Get JWT from clark context (same source as userId)
                                            // We need to access it via a method or use peer authorization as fallback
                                            const jwt = aiService.getClarkJwt() ||
                                                _context.peerAuthorization?.replace("Bearer ", "");
                                            if (!jwt) {
                                                getLogger().warn("[aiAcceptDraft] No JWT found, skipping context upload");
                                                return;
                                            }
                                            await uploadContextToBucketeer({
                                                applicationId: syncService.applicationId,
                                                commitId: result.commitId,
                                                branchName: syncService.branchName,
                                                appRootDirPath: aiService.appRootDirPath,
                                                userId: contextId.userId,
                                                useServerUpload: aiService.isAiFileUploadEnabled,
                                                superblocksBaseUrl: aiService.superblocksBaseUrl,
                                                logger: getLogger(),
                                                jwt,
                                            });
                                            getLogger().info("[aiAcceptDraft] context upload completed", {
                                                applicationId: syncService.applicationId,
                                                branchName: syncService.branchName,
                                                commitId: result.commitId,
                                                contextUserId: contextId.userId,
                                            });
                                        }
                                        catch (error) {
                                            getLogger().warn("[aiAcceptDraft] Failed to upload context to bucketeer (non-blocking)", getErrorMeta(error));
                                            // Don't throw - this is fire-and-forget
                                        }
                                    })();
                                }
                                return {
                                    commitId: result.commitId,
                                };
                            }
                            getLogger().info("[aiAcceptDraft] No new checkpoint created for ai:accept (no hash changes from last checkpoint)");
                            return { commitId: "" };
                        },
                    ],
                    aiClearDraftState: [
                        async () => {
                            await syncService?.clearDraftsDirectory();
                        },
                    ],
                    aiCancel: [
                        async () => {
                            await aiService?.handleUserCanceled();
                            await syncService?.reportDraftStateToServer(false);
                        },
                    ],
                    aiNotifyIntegrationSetupClosed: [
                        async () => {
                            await aiService?.handleIntegrationSetupClosed();
                        },
                    ],
                    aiInterpretPrompt: [
                        async (request) => {
                            await aiService?.handlePromptInterpretation(request);
                        },
                    ],
                    aiExplainCode: [
                        async (request) => {
                            await aiService?.handleAiExplainCode(request);
                        },
                    ],
                    aiSummarizeApiUsage: [
                        async (request) => {
                            await aiService?.handleAiSummarizeApiUsage(request);
                        },
                    ],
                    aiSummarizeError: [
                        async (request) => {
                            await aiService?.handleAiSummarizeError(request);
                        },
                    ],
                    aiSkipTestCase: [
                        async (request) => {
                            await aiService?.handleSkipTestCase(request.testRunId, request.testCaseId);
                        },
                    ],
                    aiRespondToToolPermission: [
                        async (request) => {
                            await aiService?.handleAiRespondToToolPermission(request);
                        },
                    ],
                    aiLabelAttachment: [
                        async (request) => {
                            if (!aiService) {
                                throw new Error("AI service not available");
                            }
                            return await aiService.handleAiLabelAttachment(request);
                        },
                    ],
                    aiChatGetMessages: [
                        async (request) => {
                            const messages = await aiService?.chatSessionStore.getMessages({
                                force: request?.force,
                            });
                            return {
                                messages: messages ?? [],
                            };
                        },
                    ],
                    aiNewChat: [
                        async () => {
                            return aiService?.handleNewChat();
                        },
                    ],
                    aiGetAppFacts: [
                        async () => {
                            if (!aiService) {
                                throw new Error("AI service not available");
                            }
                            const facts = await aiService.getAppFacts();
                            return { facts };
                        },
                    ],
                    aiDeleteAppFact: [
                        async (request) => {
                            if (!aiService) {
                                throw new Error("AI service not available");
                            }
                            await aiService.deleteAppFact(request.path);
                        },
                    ],
                    aiToggleAppFact: [
                        async (request) => {
                            if (!aiService) {
                                throw new Error("AI service not available");
                            }
                            const fact = await aiService.toggleAppFact(request.path, request.isEnabled);
                            return { fact };
                        },
                    ],
                    aiMoveAppKnowledge: [
                        async (request) => {
                            if (!aiService) {
                                throw new Error("AI service not available");
                            }
                            return await aiService.moveAppFact(request);
                        },
                    ],
                    aiRestoreToCommit: [
                        async (request, _context) => {
                            // Update clark.context.jwt from socket if missing (for restore operations)
                            if (aiService && _context.peerAuthorization) {
                                const jwt = _context.peerAuthorization.replace("Bearer ", "");
                                if (!aiService.getClarkJwt() && jwt) {
                                    aiService.updateClarkJwt(jwt);
                                }
                            }
                            await aiService?.handleRestoreToCommit(request.sinceTimestamp, request.commitId, request.applicationId, request.branchName);
                        },
                    ],
                    handleRuntimeError: [
                        async (request) => {
                            aiService?.handleRuntimeError(request);
                        },
                    ],
                    handleClearRuntimeError: [
                        async (request) => {
                            aiService?.handleClearRuntimeError(request.id);
                        },
                    ],
                    handleClearRuntimeErrors: [
                        async (request) => {
                            aiService?.handleClearRuntimeErrors(request.before);
                        },
                    ],
                    handleConsoleLog: [
                        async (request) => {
                            aiService?.handleConsoleLog(request);
                        },
                    ],
                    handleApiRunRecord: [
                        async (request) => {
                            aiService?.handleApiRunRecord(request.apiId, request.run);
                        },
                    ],
                    handleJudgeEvaluation: [
                        async (request) => {
                            if (!aiService) {
                                throw new Error("AI service not available");
                            }
                            return await aiService.handleJudgeEvaluation(request);
                        },
                    ],
                    ping: [
                        async (ping) => {
                            if (ping?.isUserActive !== undefined) {
                                if (ping.isUserActive) {
                                    void lockService?.restartInactivityTimer();
                                    lockService?.markActivity(ping.isUserActive);
                                }
                            }
                        },
                    ],
                    setClarkLogsEnabled: [
                        async (request) => {
                            setClarkVerboseLogsEnabled(request.enabled);
                            getLogger().info(`Clark log forwarding ${request.enabled ? "enabled" : "disabled"}`);
                        },
                    ],
                    gitCommitLocal: [
                        async (request) => runWithGitLock(async () => {
                            try {
                                const readyGitService = await getActiveGitService();
                                if (!readyGitService) {
                                    throw new Error("Git is not configured for this application");
                                }
                                await readyGitService
                                    .fetch("origin", SUPERBLOCKS_LIVE_GIT_BRANCH)
                                    .catch((fetchErr) => {
                                    const msg = fetchErr instanceof Error
                                        ? fetchErr.message
                                        : String(fetchErr);
                                    if (!msg.includes("couldn't find remote ref")) {
                                        throw fetchErr;
                                    }
                                });
                                const resolvedBaseUrl = superblocksBaseUrl || aiService?.superblocksBaseUrl;
                                await ensureGithubWorkflowFile(readyGitService, resolvedBaseUrl).catch((error) => {
                                    getLogger().warn("[git] gitCommitLocal failed to ensure GitHub workflow file", {
                                        gitCategory: "setup",
                                        gitOperation: "ensure-github-workflow-file",
                                        gitOutcome: "failed",
                                        ...getErrorMeta(error),
                                    });
                                });
                                await ensureAzureDevOpsWorkflowFile(readyGitService, resolvedBaseUrl).catch((error) => {
                                    getLogger().warn("[git] gitCommitLocal failed to ensure Azure DevOps workflow file", {
                                        gitCategory: "setup",
                                        gitOperation: "ensure-azure-devops-workflow-file",
                                        gitOutcome: "failed",
                                        ...getErrorMeta(error),
                                    });
                                });
                                await ensureGitlabWorkflowFile(readyGitService, resolvedBaseUrl).catch((error) => {
                                    getLogger().warn("[git] gitCommitLocal failed to ensure GitLab workflow file", {
                                        gitCategory: "setup",
                                        gitOperation: "ensure-gitlab-workflow-file",
                                        gitOutcome: "failed",
                                        ...getErrorMeta(error),
                                    });
                                });
                                await ensureBitbucketWorkflowFile(readyGitService, resolvedBaseUrl).catch((error) => {
                                    getLogger().warn("[git] gitCommitLocal failed to ensure Bitbucket workflow file", {
                                        gitCategory: "setup",
                                        gitOperation: "ensure-bitbucket-workflow-file",
                                        gitOutcome: "failed",
                                        ...getErrorMeta(error),
                                    });
                                });
                                await readyGitService.add(".");
                                const result = await readyGitService.commit(request.message);
                                return { sha: result.commit };
                            }
                            catch (error) {
                                getLogger().error("[git] gitCommitLocal failed", getErrorMeta(error));
                                throw error;
                            }
                        }),
                    ],
                    gitPushLiveBranch: [
                        async () => runWithGitLock(async () => {
                            try {
                                const readyGitService = await getActiveGitService();
                                if (!readyGitService) {
                                    throw new Error("Git is not configured for this application");
                                }
                                await pushLiveBranchWithAutoRecreate(readyGitService);
                            }
                            catch (error) {
                                getLogger().error("[git] gitPushLiveBranch failed", getErrorMeta(error));
                                throw error;
                            }
                        }),
                    ],
                    gitRollbackLastCommit: [
                        async (request) => runWithGitLock(async () => {
                            const readyGitService = await getActiveGitService();
                            if (!readyGitService) {
                                throw new Error("Git is not configured for this application");
                            }
                            return rollbackLastCommit({
                                gitService: readyGitService,
                                expectedSha: request.expectedSha,
                            });
                        }),
                    ],
                    gitPull: [
                        async (_request, _context) => runWithGitLock(async () => {
                            const readyGitService = await getActiveGitService();
                            if (!readyGitService) {
                                throw new Error("Git is not configured for this application");
                            }
                            await readyGitService.fetch();
                            const status = await readyGitService.status();
                            const hasLocalChanges = status.files.length > 0;
                            if (hasLocalChanges) {
                                await readyGitService.add(".");
                                await readyGitService.stash();
                            }
                            let hasConflicts = false;
                            let merged = false;
                            try {
                                const mergeResult = await readyGitService.mergeRef(`origin/${SUPERBLOCKS_LIVE_GIT_BRANCH}`);
                                merged = !mergeResult.alreadyUpToDate;
                                if (merged) {
                                    const unmerged = await readyGitService.getConflictedFiles();
                                    if (unmerged.length > 0) {
                                        hasConflicts = true;
                                    }
                                }
                            }
                            catch (mergeError) {
                                if (!(mergeError instanceof GitConflictError)) {
                                    getLogger().error("[git] gitPull: merge failed (non-conflict), aborting", getErrorMeta(mergeError));
                                    try {
                                        await readyGitService.raw(["merge", "--abort"]);
                                    }
                                    catch (abortErr) {
                                        getLogger().warn(`[git] gitPull: merge --abort failed: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
                                    }
                                    if (hasLocalChanges) {
                                        try {
                                            await readyGitService.stashPop();
                                        }
                                        catch (popErr) {
                                            getLogger().warn(`[git] gitPull: stash pop after merge abort failed: ${popErr instanceof Error ? popErr.message : String(popErr)}`);
                                        }
                                    }
                                    throw mergeError;
                                }
                                merged = true;
                                hasConflicts = true;
                            }
                            if (hasLocalChanges) {
                                try {
                                    await readyGitService.stashPop();
                                }
                                catch (popError) {
                                    if (popError instanceof GitConflictError) {
                                        hasConflicts = true;
                                    }
                                    else {
                                        getLogger().error("[git] gitPull: stash pop failed (non-conflict)", getErrorMeta(popError));
                                        throw popError;
                                    }
                                }
                                if (!hasConflicts) {
                                    const unmerged = await readyGitService.getConflictedFiles();
                                    if (unmerged.length > 0) {
                                        hasConflicts = true;
                                    }
                                }
                            }
                            if (hasConflicts) {
                                const conflictedFiles = await readyGitService.getConflictedFiles();
                                getLogger().warn("[git] pull conflict detected", {
                                    gitCategory: "pull",
                                    gitOperation: "pull",
                                    gitOutcome: "conflict",
                                    branch: SUPERBLOCKS_LIVE_GIT_BRANCH,
                                    conflictedFileCount: conflictedFiles.length,
                                    conflictedFiles,
                                });
                                return { merged: false, conflictedFiles };
                            }
                            return { merged };
                        }),
                    ],
                    gitSyncStatus: [
                        async () => {
                            const readyGitService = await getActiveGitService();
                            if (!readyGitService) {
                                return gitBootstrapFailed
                                    ? { bootstrapFailed: true }
                                    : null;
                            }
                            let hasUncommittedChanges = false;
                            try {
                                const status = await readyGitService.status();
                                hasUncommittedChanges = status.files.length > 0;
                            }
                            catch (statusErr) {
                                getLogger().warn("[git] gitSyncStatus status failed", {
                                    gitCategory: "status",
                                    gitOperation: "syncStatus",
                                    gitOutcome: "failed",
                                    gitErrorKind: "status_failed",
                                    ...getErrorMeta(statusErr),
                                });
                                return null;
                            }
                            let ahead = 0;
                            let behind = 0;
                            try {
                                await readyGitService.fetch("origin", SUPERBLOCKS_LIVE_GIT_BRANCH);
                                const counts = await readyGitService.revList("HEAD", `origin/${SUPERBLOCKS_LIVE_GIT_BRANCH}`);
                                ahead = counts.ahead;
                                behind = counts.behind;
                            }
                            catch (fetchErr) {
                                getLogger().warn("[git] gitSyncStatus fetch/revList failed", {
                                    gitCategory: "status",
                                    gitOperation: "syncStatus",
                                    gitOutcome: "failed",
                                    gitErrorKind: "fetch_or_rev_list_failed",
                                    branch: SUPERBLOCKS_LIVE_GIT_BRANCH,
                                    remote: "origin",
                                    ...getErrorMeta(fetchErr),
                                });
                            }
                            let aheadOfDefaultBranch = false;
                            try {
                                const defaultCounts = await getSyncCountsAgainstDefaultBranch({
                                    gitService: readyGitService,
                                    leftRef: "HEAD",
                                });
                                aheadOfDefaultBranch = defaultCounts.ahead > 0;
                            }
                            catch {
                                // Optional: enable Open PR when branch has commits vs default.
                            }
                            const remoteLiveBranchExists = await doesRemoteLiveBranchExist(readyGitService);
                            const isLiveBranchMissing = remoteLiveBranchExists === false;
                            return {
                                ahead,
                                behind,
                                hasUncommittedChanges,
                                aheadOfDefaultBranch,
                                isLiveBranchMissing,
                            };
                        },
                    ],
                    gitChangedFiles: [
                        async () => {
                            const readyGitService = await getActiveGitService();
                            if (!readyGitService)
                                return [];
                            try {
                                const status = await readyGitService.status();
                                return status.files.map((f) => ({
                                    path: f.path,
                                    status: f.working_dir === "D" || f.index === "D"
                                        ? "deleted"
                                        : f.working_dir === "?" || f.index === "A"
                                            ? "added"
                                            : "modified",
                                }));
                            }
                            catch (err) {
                                getLogger().warn("[git] gitChangedFiles status failed", {
                                    gitCategory: "status",
                                    gitOperation: "changed-files",
                                    gitOutcome: "failed",
                                    gitErrorKind: "status_failed",
                                    ...getErrorMeta(err),
                                });
                                return [];
                            }
                        },
                    ],
                    gitActivate: [
                        async () => runWithGitLock(async () => {
                            // `gitActivate` is fired on explicit user-driven
                            // (re)connect; it is the sole path allowed to clear the
                            // sticky `disconnectInProgress` flag set by
                            // `gitDisconnect`. Without this reset, a user who
                            // disconnects and then reconnects in the same process
                            // would be stuck with git permanently disabled.
                            disconnectInProgress = false;
                            const service = await getActiveGitService();
                            return { activated: !!service };
                        }),
                    ],
                    gitDisconnect: [
                        async () => runWithGitLock(async () => {
                            // Atomically deactivate git BEFORE any teardown work so
                            // that concurrent callers of getActiveGitService() can't
                            // grab the old service and force `superblocks/live` back
                            // via ensureLiveBranchCheckedOut() while we're switching
                            // branches and removing `.git`.
                            const serviceToDisconnect = activeGitService;
                            activeGitService = undefined;
                            disconnectInProgress = true;
                            // Ensure DBFS sync context is no longer tied to
                            // `superblocks/live` immediately after native git disconnect.
                            syncService?.setBranchName(DEFAULT_NON_GIT_BRANCH);
                            // Native git disconnect should move runtime lock context back
                            // to default branch before clients reconnect.
                            if (lockService) {
                                const wasLockedBeforeDisconnect = lockService.isLocked;
                                try {
                                    await lockService.switchBranch(DEFAULT_NON_GIT_BRANCH, {
                                        reacquireLock: wasLockedBeforeDisconnect,
                                    });
                                }
                                catch (err) {
                                    getLogger().warn("[git] gitDisconnect failed to switch lock branch back to main", {
                                        gitCategory: "setup",
                                        gitOperation: "disconnect",
                                        gitOutcome: "failed",
                                        gitErrorKind: "switch_lock_branch_failed",
                                        ...getErrorMeta(err),
                                    });
                                    try {
                                        await lockService.switchBranch(DEFAULT_NON_GIT_BRANCH, {
                                            reacquireLock: false,
                                        });
                                        if (wasLockedBeforeDisconnect) {
                                            await lockService.acquireLock();
                                        }
                                    }
                                    catch (fallbackErr) {
                                        getLogger().warn("[git] gitDisconnect fallback lock reset to main failed", {
                                            gitCategory: "setup",
                                            gitOperation: "disconnect",
                                            gitOutcome: "failed",
                                            gitErrorKind: "fallback_lock_reset_failed",
                                            ...getErrorMeta(fallbackErr),
                                        });
                                    }
                                }
                            }
                            if (serviceToDisconnect) {
                                const gitDir = join(serviceToDisconnect.workDir, ".git");
                                let checkoutBranchBeforeDisconnect = DEFAULT_NON_GIT_BRANCH;
                                try {
                                    checkoutBranchBeforeDisconnect =
                                        await serviceToDisconnect.getDefaultBranch();
                                }
                                catch (defaultBranchErr) {
                                    getLogger().warn("[git] gitDisconnect failed to detect remote default branch before cleanup; falling back to non-git baseline branch", {
                                        gitCategory: "setup",
                                        gitOperation: "disconnect",
                                        gitOutcome: "failed",
                                        gitErrorKind: "default_branch_detection_failed",
                                        ...getErrorMeta(defaultBranchErr),
                                    });
                                }
                                try {
                                    await serviceToDisconnect.checkoutOrCreate(checkoutBranchBeforeDisconnect);
                                }
                                catch (checkoutErr) {
                                    getLogger().warn("[git] gitDisconnect failed to checkout branch before removing git metadata", {
                                        gitCategory: "setup",
                                        gitOperation: "disconnect",
                                        gitOutcome: "failed",
                                        gitErrorKind: "checkout_disconnect_branch_failed",
                                        checkoutBranchBeforeDisconnect,
                                        ...getErrorMeta(checkoutErr),
                                    });
                                }
                                try {
                                    await rm(gitDir, { recursive: true, force: true });
                                }
                                catch (rmErr) {
                                    getLogger().warn("[git] gitDisconnect failed to remove .git directory; manual cleanup may be required", {
                                        gitCategory: "setup",
                                        gitOperation: "disconnect",
                                        gitOutcome: "failed",
                                        gitErrorKind: "git_dir_removal_failed",
                                        gitDir,
                                        ...getErrorMeta(rmErr),
                                    });
                                }
                            }
                        }),
                    ],
                    gitDefaultBranch: [
                        async () => {
                            const readyGitService = await getActiveGitService();
                            if (!readyGitService)
                                return null;
                            try {
                                return await readyGitService.getDefaultBranch();
                            }
                            catch (err) {
                                getLogger().warn("[git] gitDefaultBranch failed", {
                                    gitCategory: "branch",
                                    gitOperation: "default-branch",
                                    gitOutcome: "failed",
                                    gitErrorKind: "default_branch_detection_failed",
                                    ...getErrorMeta(err),
                                });
                                return null;
                            }
                        },
                    ],
                    gitBranchLog: [
                        async (request) => {
                            const readyGitService = await getActiveGitService();
                            if (!readyGitService)
                                return [];
                            const limit = (request && "limit" in request
                                ? request.limit
                                : undefined) ?? 50;
                            const remote = request && "remote" in request
                                ? (request.remote ?? true)
                                : true;
                            let branch;
                            if (request && "branch" in request && request.branch) {
                                branch = request.branch;
                            }
                            else {
                                try {
                                    branch = await readyGitService.getDefaultBranch();
                                }
                                catch (defaultBranchErr) {
                                    getLogger().warn("[git] gitBranchLog default branch detection failed", {
                                        gitCategory: "branch",
                                        gitOperation: "branch-log",
                                        gitOutcome: "failed",
                                        gitErrorKind: "branch_log_default_branch_detection_failed",
                                        remote: "origin",
                                        ...getErrorMeta(defaultBranchErr),
                                    });
                                    const status = await readyGitService
                                        .status()
                                        .catch(() => null);
                                    const fallbackBranch = status?.current?.trim();
                                    if (!fallbackBranch) {
                                        return [];
                                    }
                                    branch = fallbackBranch;
                                }
                            }
                            const logRef = remote ? `origin/${branch}` : branch;
                            if (remote) {
                                try {
                                    await readyGitService.fetch("origin", branch);
                                }
                                catch (fetchErr) {
                                    getLogger().warn("[git] gitBranchLog fetch failed", {
                                        gitCategory: "branch",
                                        gitOperation: "branch-log",
                                        gitOutcome: "failed",
                                        gitErrorKind: "branch_log_fetch_failed",
                                        branch,
                                        remote: "origin",
                                        ...getErrorMeta(fetchErr),
                                    });
                                }
                            }
                            const raw = await readyGitService.raw([
                                "log",
                                logRef,
                                `--format=${GIT_LOG_FORMAT}`,
                                `-n`,
                                String(limit),
                            ]);
                            return parseGitLog(raw);
                        },
                    ],
                    gitPrepareMainCommit: [
                        async (request) => runWithGitLock(async () => {
                            const readyGitService = await getActiveGitService();
                            if (!readyGitService) {
                                throw new Error("Git service is not available");
                            }
                            if (!syncService) {
                                throw new Error("Sync service is not available");
                            }
                            const { sha } = request;
                            if (!sha || !/^[0-9a-f]{5,40}$/i.test(sha)) {
                                throw new Error("sha must be a valid git SHA");
                            }
                            let defaultBranch = null;
                            try {
                                defaultBranch =
                                    await readyGitService.getDefaultBranch();
                            }
                            catch (defaultBranchErr) {
                                getLogger().warn("[git] gitPrepareMainCommit default branch detection failed", {
                                    gitCategory: "build",
                                    gitOperation: "prepare-main-commit",
                                    gitOutcome: "failed",
                                    gitErrorKind: "prepare_main_commit_default_branch_detection_failed",
                                    remote: "origin",
                                    ...getErrorMeta(defaultBranchErr),
                                });
                            }
                            if (defaultBranch) {
                                try {
                                    await readyGitService.fetch("origin", defaultBranch);
                                }
                                catch (fetchErr) {
                                    getLogger().warn("[git] gitPrepareMainCommit fetch failed", {
                                        gitCategory: "build",
                                        gitOperation: "prepare-main-commit",
                                        gitOutcome: "failed",
                                        gitErrorKind: "prepare_main_commit_fetch_failed",
                                        defaultBranch,
                                        remote: "origin",
                                        ...getErrorMeta(fetchErr),
                                    });
                                }
                            }
                            const worktreeBase = await mkdtemp(join(tmpdir(), "sb-main-deploy-"));
                            const worktreePath = join(worktreeBase, "worktree");
                            try {
                                await readyGitService.raw([
                                    "worktree",
                                    "add",
                                    "--detach",
                                    worktreePath,
                                    sha,
                                ]);
                                // Remove .git metadata from worktree so it doesn't affect the hash
                                await rm(join(worktreePath, ".git"), {
                                    force: true,
                                    recursive: true,
                                });
                                const { directoryHash } = await syncService.uploadSnapshotToDbfs(worktreePath);
                                // Persist the worktree snapshot so the SABS build can
                                // use the commit-specific files instead of the live sandbox.
                                const snapshotPath = join(SB_WORKTREE_SNAPSHOTS_DIR, encodeURIComponent(directoryHash));
                                await mkdir(SB_WORKTREE_SNAPSHOTS_DIR, {
                                    recursive: true,
                                });
                                await rm(snapshotPath, {
                                    recursive: true,
                                    force: true,
                                });
                                await cp(worktreePath, snapshotPath, {
                                    recursive: true,
                                });
                                return { directoryHash, sha };
                            }
                            finally {
                                try {
                                    await readyGitService.raw([
                                        "worktree",
                                        "remove",
                                        "--force",
                                        worktreePath,
                                    ]);
                                }
                                catch (cleanupErr) {
                                    getLogger().warn("[git] gitPrepareMainCommit worktree cleanup failed", {
                                        gitCategory: "build",
                                        gitOperation: "prepare-main-commit",
                                        gitOutcome: "failed",
                                        gitErrorKind: "worktree_cleanup_failed",
                                        worktreePath,
                                        ...getErrorMeta(cleanupErr),
                                    });
                                }
                                await rm(worktreeBase, {
                                    recursive: true,
                                    force: true,
                                });
                            }
                        }),
                    ],
                },
            }, middlewares, tracer, {
                onClose: () => {
                    getLogger().info("[socket-manager] TracedSocket connection closed");
                },
                logger: {
                    error: (message) => {
                        getLogger().error(message ?? "");
                    },
                    info: (message) => {
                        getLogger().info(message ?? "");
                    },
                    warn: (message) => {
                        getLogger().warn(message ?? "");
                    },
                },
            }));
            // we are storing all of the sockets for a given peerId (editor+library pair of connections)
            // which lets us handle synchronization of operations between the two
            let existingSockets = this.peerIds.get(peerId);
            if (!existingSockets) {
                existingSockets = new Set();
                this.peerIds.set(peerId, existingSockets);
            }
            if (isEditor) {
                this.editorSockets.add(socket);
                this.emit("connect:editor", socket, peerId);
                if (userId) {
                    lockService?.addConnectedPeerIdToUser(userId, peerId);
                    // Mark user as active on connection and restart inactivity timer
                    lockService?.markActivity(true);
                    void lockService?.restartInactivityTimer();
                }
                // When a new editor connects after git was enabled, the lock may
                // still be on 'main'. Trigger git activation which calls
                // ensureRuntimeDbfsBranchConsistency → lockService.switchBranch
                // to transfer the lock to superblocks/live.
                if (activateGitService) {
                    void activateGitService().catch((err) => {
                        getLogger().warn(`[socket-manager] post-connect git activation failed: ${err instanceof Error ? err.message : String(err)}`);
                    });
                }
            }
            else {
                this.librarySockets.add(socket);
                this.emit("connect:library", socket);
            }
            existingSockets.add(socket);
            ws.on("err", (e) => {
                getLogger().error("Socket error", getErrorMeta(e));
            });
            ws.on("close", (code, reason) => {
                this.peerIds.get(peerId)?.delete(socket);
                getLogger().info(`Socket from ${isEditor ? "editor" : "library"} closed code: ${code} reason: ${reason}`);
                if (isEditor) {
                    this.editorSockets.delete(socket);
                    if (userId) {
                        lockService?.removeConnectedPeerIdFromUser(userId, peerId);
                        if (lockService?.connectedUsers.length === 0) {
                            if (aiService?.isBusy()) {
                                getLogger().info("[socket-manager] All users disconnected but AI service is busy, deferring cache cleanup until generation completes");
                                let cleaned = false;
                                const deferredCleanup = () => {
                                    if (cleaned)
                                        return;
                                    cleaned = true;
                                    // Only clean up if no users have reconnected in the meantime
                                    if (lockService?.connectedUsers.length === 0) {
                                        getLogger().info("[socket-manager] Generation ended with no connected users, cleaning up AI service cache");
                                        aiService
                                            .removeIntegrationCache()
                                            .catch((error) => {
                                            getLogger().warn(`Error removing integration cache after deferred cleanup: ${error}`);
                                        });
                                    }
                                    else {
                                        getLogger().info("[socket-manager] Generation ended but users have reconnected, skipping cache cleanup");
                                    }
                                };
                                aiService.once("generationCompleted", deferredCleanup);
                                aiService.once("generationFailed", deferredCleanup);
                            }
                            else {
                                getLogger().info("[socket-manager] All users disconnected, cleaning up AI service cache");
                                aiService
                                    ?.removeIntegrationCache()
                                    .catch((error) => {
                                    getLogger().warn(`Error removing integration cache after disconnect: ${error}`);
                                });
                            }
                        }
                    }
                    this.emit("disconnect:editor", peerId);
                }
                else {
                    this.librarySockets.delete(socket);
                }
            });
        });
    }
    callEditorClients(fn) {
        for (const socket of this.editorSockets) {
            fn(socket).catch(errorHandler);
        }
    }
    callLibraryClients(fn) {
        for (const socket of this.librarySockets) {
            fn(socket).catch(errorHandler);
        }
    }
    getAnyLibraryClient() {
        return Array.from(this.librarySockets).at(0);
    }
    getEditorClients() {
        return this.editorSockets;
    }
    closeAllClients() {
        for (const socket of this.editorSockets) {
            socket.close();
        }
        for (const socket of this.librarySockets) {
            socket.close();
        }
    }
    dispose() {
        this._disposer();
    }
}
function errorHandler(err) {
    // ignore if a method is not callable or doesnt exist, just log it so we can debug and fix
    // otherwise we can crash the vite process due to unhandled promise rejection
    if (err != null &&
        typeof err === "object" &&
        "code" in err &&
        // 2: unknown method
        // 3: exception in message handler
        // 8: connection closed by client while waiting for response
        // see: https://github.com/superblocksteam/superblocks/blob/code-mode/packages/shared/src/socket/socket.ts
        (err.code === 2 || err.code === 3 || err.code === 8 || err.code === 10)) {
        // Connection closed (code 8) is expected when users close browser tabs - log as INFO
        if (err.code === 8 || err.code === 10) {
            getLogger().info(`[socket] Connection closed`, getErrorMeta(err));
        }
        else {
            getLogger().error(`[socket] ${err.message}`, getErrorMeta(err));
        }
    }
    else {
        throw err;
    }
}
//# sourceMappingURL=socket-manager.js.map