import { createISocketClient, TracedEventEmitter, TracedSocket, } from "@superblocksteam/shared";
import { WebSocketServer } from "ws";
import { uploadContextToBucketeer } from "./ai-service/context-upload.js";
import { createSocketRecordingMiddleware } from "./ai-service/recording/recorders/socket-recorder.js";
import { getErrorMeta, getLogger } from "./util/logger.js";
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, checkAuthorization, path, tracer, }) {
        if (this.socketServer) {
            throw new Error("Socket server already started");
        }
        const wss = new WebSocketServer({ path, noServer: true });
        this.socketServer = wss;
        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: {
                    // Update widgets
                    setProperties: [fileSyncManager.handleSetProperties],
                    deleteProperties: [fileSyncManager.handleDeleteProperties],
                    createComponent: [fileSyncManager.handleCreateComponent],
                    deleteComponents: [fileSyncManager.handleDeleteComponents],
                    setProperty: [fileSyncManager.handleSetProperty],
                    batchUpdate: [fileSyncManager.handleBatchUpdate],
                    // 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, _context, peer) => {
                            await aiService?.handleAiGenerate(request, peer, peerId);
                        },
                    ],
                    aiGenerateWithQueue: [
                        async (request, _context, peer) => {
                            if (!aiService) {
                                throw new Error("AI service not available");
                            }
                            const { queueMode, ...aiRequest } = request;
                            return await aiService.handleAiGenerateWithQueue(aiRequest, peer, peerId, queueMode);
                        },
                    ],
                    aiRejectDraft: [
                        async (request, _context, peer) => {
                            await syncService?.discardLocalDraftChanges("ai:reject");
                            await aiService?.handleUserRejectedDraft(peer, request.silently);
                        },
                    ],
                    aiAcceptDraft: [
                        async (request, _context, peer) => {
                            const result = await syncService?.commitLocalDraftChanges("ai:accept");
                            await aiService?.handleUserAcceptedDraft(peer, 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,
                                                superblocksBaseUrl: aiService.superblocksBaseUrl,
                                                logger: getLogger(),
                                                jwt,
                                            });
                                        }
                                        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,
                                };
                            }
                            throw new Error("Failed to get commitId from commit");
                        },
                    ],
                    aiClearDraftState: [
                        async () => {
                            await syncService?.clearDraftsDirectory();
                        },
                    ],
                    aiCancel: [
                        async () => {
                            await aiService?.handleUserCanceled();
                        },
                    ],
                    aiInterpretPrompt: [
                        async (request, _context, peer) => {
                            await aiService?.handlePromptInterpretation(request, peer);
                        },
                    ],
                    aiExplainCode: [
                        async (request, _context, peer) => {
                            await aiService?.handleAiExplainCode(request, peer);
                        },
                    ],
                    aiSkipTestCase: [
                        async (request) => {
                            await aiService?.handleSkipTestCase(request.testRunId, request.testCaseId);
                        },
                    ],
                    aiRespondToToolPermission: [
                        async (request, _context, peer) => {
                            await aiService?.handleAiRespondToToolPermission(request, peer, peerId);
                        },
                    ],
                    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 };
                        },
                    ],
                    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"}`);
                        },
                    ],
                },
            }, middlewares, tracer, {
                onClose: () => {
                    console.log("Socket 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();
                }
            }
            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) {
                            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