"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SocketManager = void 0;
exports.runPythonScript = runPythonScript;
const socket_io_1 = require("socket.io");
const Run_1 = require("../dao/Run");
const trpc_1 = require("../../../shared/src/types/trpc");
const child_process_1 = require("child_process");
const messageForm_1 = require("../../../shared/src/types/messageForm");
const InputRequest_1 = require("../dao/InputRequest");
const FridayAppMessage_1 = require("../dao/FridayAppMessage");
const dayjs_1 = __importDefault(require("dayjs"));
const src_1 = require("../../../shared/src");
const ReplyingStateManager_1 = require("../services/ReplyingStateManager");
const fs = __importStar(require("node:fs"));
const friday_1 = require("../../../shared/src/config/friday");
const Trace_1 = require("../dao/Trace");
class SocketManager {
    static close() {
        if (this.io) {
            this.io.close();
        }
    }
    static sendInterruptSignalToFriday() {
        return __awaiter(this, void 0, void 0, function* () {
            this.io.of('/friday').emit(trpc_1.SocketEvents.server.interruptReply);
        });
    }
    static init(httpServer) {
        this.io = new socket_io_1.Server(httpServer, {
            cors: {
                origin: '*',
            },
            maxHttpBufferSize: Infinity,
        });
        const fridayNamespace = this.io.of('/friday');
        fridayNamespace.on('connection', (socket) => {
            console.debug(`${socket.id}: Friday app client connected`);
            socket.on('disconnect', () => {
                console.debug(`${socket.id}: Friday app client disconnected`);
            });
        });
        // Python client connection
        const pythonNamespace = this.io.of('/python');
        pythonNamespace.on('connection', (socket) => {
            const runId = socket.handshake.auth.run_id;
            console.debug(`${socket.id}: Python client connected`);
            socket.on('disconnect', () => __awaiter(this, void 0, void 0, function* () {
                // Delete the input requests
                yield InputRequest_1.InputRequestDao.deleteInputRequestsByRunId(runId);
                this.changeRunStatusAndTriggerEvents(runId, messageForm_1.Status.DONE).catch((error) => {
                    console.error(error);
                    throw error;
                });
            }));
        });
        const clientNamespace = this.io.of('/client');
        clientNamespace.on('connection', (socket) => {
            console.debug('Client connected');
            socket.on(trpc_1.SocketEvents.client.joinProjectListRoom, () => {
                socket.join(trpc_1.SocketRoomName.ProjectListRoom);
                console.debug(`${socket.id}: joined room: ${trpc_1.SocketRoomName.ProjectListRoom}`);
                Run_1.RunDao.getAllProjects()
                    .then((projects) => {
                    // Push projects to the client
                    socket.emit(trpc_1.SocketEvents.server.pushProjects, projects);
                })
                    .catch((error) => {
                    console.error(error);
                    throw error;
                });
            });
            socket.on(trpc_1.SocketEvents.client.joinProjectRoom, (project, callback) => __awaiter(this, void 0, void 0, function* () {
                const projectExist = yield Run_1.RunDao.doesProjectExist(project);
                if (!projectExist) {
                    callback({
                        success: false,
                        message: `Project ${project} not found`,
                    });
                }
                else {
                    socket.join(`project-${project}`);
                    console.debug(`${socket.id}: joined room: project-${project}`);
                    // Return runs to this socket/client
                    Run_1.RunDao.getAllProjectRuns(project)
                        .then((runs) => {
                        // Push runs to the client
                        socket.emit(trpc_1.SocketEvents.server.pushRunsData, runs);
                    })
                        .catch((error) => {
                        console.error(error);
                        throw error;
                    });
                }
            }));
            socket.on(trpc_1.SocketEvents.client.joinRunRoom, (runId, callback) => __awaiter(this, void 0, void 0, function* () {
                const runExist = yield Run_1.RunDao.doesRunExist(runId);
                if (!runExist) {
                    callback({
                        success: false,
                        message: `Run ${runId} not found`,
                    });
                }
                else {
                    socket.join(`run-${runId}`);
                    console.debug(`${socket.id}: joined room: run-${runId}`);
                    // Return run data, input requests and messages to this socket/client
                    Run_1.RunDao.getRunData(runId)
                        .then((data) => {
                        socket.emit(trpc_1.SocketEvents.server.pushRunData, data.runData);
                        socket.emit(trpc_1.SocketEvents.server.pushInputRequests, data.inputRequests);
                        socket.emit(trpc_1.SocketEvents.server.pushMessages, data.messages);
                        socket.emit(trpc_1.SocketEvents.server.pushSpans, data.spans);
                    })
                        .catch((error) => {
                        console.error(error);
                        throw error;
                    });
                    // Return model invocation data
                    Trace_1.SpanDao.getModelInvocationData(runId).then((data) => {
                        socket.emit(trpc_1.SocketEvents.server.pushModelInvocationData, data);
                    });
                }
            }));
            socket.on(trpc_1.SocketEvents.client.sendUserInputToServer, (requestId, blocksInput, structuredInput, callback) => __awaiter(this, void 0, void 0, function* () {
                const inputRequest = yield InputRequest_1.InputRequestDao.getInputRequestByRequestId(requestId);
                if (inputRequest === null) {
                    callback({
                        success: false,
                        message: `Input request ${requestId} not found`,
                    });
                }
                else {
                    const runId = inputRequest.runId;
                    yield InputRequest_1.InputRequestDao.deleteInputRequest(requestId);
                    // If input requests are empty, change the run status to finished
                    const res = yield Run_1.RunDao.getRunData(runId);
                    if (res.inputRequests.length === 0) {
                        this.changeRunStatusAndTriggerEvents(runId, messageForm_1.Status.RUNNING).catch((error) => {
                            console.error(error);
                            throw error;
                        });
                    }
                    // Emit the input to the python client
                    this.io
                        .of('/python')
                        .emit(trpc_1.SocketEvents.server.forwardUserInput, requestId, blocksInput, structuredInput);
                }
            }));
            socket.on(trpc_1.SocketEvents.client.joinOverviewRoom, () => __awaiter(this, void 0, void 0, function* () {
                socket.join(trpc_1.SocketRoomName.OverviewRoom);
                console.debug(`${socket.id}: joined room: ${trpc_1.SocketRoomName.OverviewRoom}`);
                // Return current overview data
                const res = yield this._getOverViewData();
                socket.emit(trpc_1.SocketEvents.server.pushOverviewData, res);
            }));
            socket.on(trpc_1.SocketEvents.client.leaveRoom, (room) => {
                socket.leave(room);
                console.debug(`${socket.id}: left room: ${room}`);
            });
            socket.on(trpc_1.SocketEvents.client.deleteProjects, (projects, callback) => __awaiter(this, void 0, void 0, function* () {
                try {
                    yield Run_1.RunDao.deleteProjects(projects);
                    callback({
                        success: true,
                        message: `Success: ${projects.length} project deleted`,
                    });
                    // Update projectListRoom, overviewRoom,
                    this.broadcastOverviewDataToDashboardRoom();
                    this.broadcastRunToProjectListRoom();
                }
                catch (error) {
                    callback({
                        success: false,
                        message: `Error: ${error}`,
                    });
                }
            }));
            socket.on(trpc_1.SocketEvents.client.deleteRuns, (runIds, callback) => __awaiter(this, void 0, void 0, function* () {
                try {
                    const nDelete = yield Run_1.RunDao.deleteRuns(runIds);
                    callback({
                        success: nDelete === runIds.length,
                        message: `Deleted ${nDelete} runs`,
                    });
                    // Update data to overviewRoom, projectRoom
                    this.broadcastOverviewDataToDashboardRoom();
                    this.broadcastRunToProjectListRoom();
                }
                catch (error) {
                    callback({
                        success: false,
                        message: `Failed to delete runs: ${error}`,
                    });
                }
            }));
            // friday app
            socket.on(trpc_1.SocketEvents.client.getFridayConfig, (callback) => __awaiter(this, void 0, void 0, function* () {
                console.debug(`${socket.id}: getFridayConfig`);
                try {
                    // Send config to the client
                    const fridayConfig = friday_1.FridayConfigManager.getInstance().getConfig();
                    callback({
                        success: true,
                        data: fridayConfig,
                        message: 'Get Friday config successfully',
                    });
                }
                catch (error) {
                    console.error(error);
                    callback({
                        success: false,
                        data: null,
                        message: `Failed to get Friday config: ${error}`,
                    });
                }
            }));
            socket.on(trpc_1.SocketEvents.client.saveFridayConfig, (config, callback) => __awaiter(this, void 0, void 0, function* () {
                const fridayConfigManager = friday_1.FridayConfigManager.getInstance();
                // Save the config to the file
                fridayConfigManager.updateConfig(config);
                callback({
                    success: true,
                    message: 'Save Friday config successfully',
                });
            }));
            socket.on(trpc_1.SocketEvents.client.installFridayRequirements, (pythonEnv, callback) => __awaiter(this, void 0, void 0, function* () {
                const fridayConfigManager = friday_1.FridayConfigManager.getInstance();
                const res = yield fridayConfigManager.installRequirements(pythonEnv);
                callback(res);
            }));
            socket.on(trpc_1.SocketEvents.client.joinFridayAppRoom, (callback) => __awaiter(this, void 0, void 0, function* () {
                console.debug(`${socket.id}: joined room: ${trpc_1.SocketRoomName.FridayAppRoom}`);
                socket.join(trpc_1.SocketRoomName.FridayAppRoom);
                const replyingManager = ReplyingStateManager_1.ReplyingStateManager.getInstance();
                // Push the replying state
                socket.emit(trpc_1.SocketEvents.server.pushReplyingState, replyingManager.getReplyingState());
                // Push the replies to the client
                FridayAppMessage_1.FridayAppMessageDao.getRepliesBefore(undefined, 100)
                    .then((res) => {
                    socket.emit(trpc_1.SocketEvents.server.pushReplies, res.replies, res.hasMore);
                })
                    .catch((error) => {
                    console.error(error);
                    callback({
                        success: false,
                        message: `Failed to get replies: ${error}`,
                    });
                });
            }));
            socket.on(trpc_1.SocketEvents.client.verifyFridayConfig, (pythonEnv, callback) => __awaiter(this, void 0, void 0, function* () {
                console.debug(`${socket.id}: verifyPythonEnv:`, pythonEnv);
                // Verify if python exists
                const result = friday_1.FridayConfigManager.getInstance().verifyPythonEnv(pythonEnv);
                callback(result);
            }));
            socket.on(trpc_1.SocketEvents.client.sendUserInputToFridayApp, (name, role, content, callback) => __awaiter(this, void 0, void 0, function* () {
                const replyingManager = ReplyingStateManager_1.ReplyingStateManager.getInstance();
                const replyId = crypto.randomUUID();
                const msgId = crypto.randomUUID();
                FridayAppMessage_1.FridayAppMessageDao.saveReplyMessage(replyId, {
                    id: msgId,
                    name: name,
                    role: role,
                    content: content,
                    metadata: {},
                    timestamp: (0, dayjs_1.default)().format('YYYY-MM-DD HH:mm:ss.SSS'),
                }, true)
                    .then((reply) => {
                    this.broadcastReplyToFridayAppRoom(reply);
                })
                    .catch((error) => {
                    console.error(error);
                    callback({
                        success: false,
                        message: `Failed to add reply: ${error}`,
                    });
                });
                // Send the message to the python client
                // TODO: move somewhere
                const config = src_1.ConfigManager.getInstance().getConfig();
                // Broad the replying state to the Friday app room
                replyingManager.setReplyingState(true);
                this.broadcastReplyingStateToFridayAppRoom();
                const fridayConfigManager = friday_1.FridayConfigManager.getInstance();
                const fridayConfig = fridayConfigManager.getConfig();
                if (!fridayConfig) {
                    callback({
                        success: false,
                        message: 'Friday config not found. Please set it up first.',
                    });
                    return;
                }
                // Prepare the arguments for the Python script
                const mainScriptPath = fridayConfig.mainScriptPath
                    ? fridayConfig.mainScriptPath
                    : fridayConfigManager.getDefaultMainScriptPath();
                const args = [
                    mainScriptPath,
                    '--query',
                    JSON.stringify(content),
                    '--studio_url',
                    `http://localhost:${config.port}`,
                ];
                console.log(fridayConfig);
                for (const [key, value] of Object.entries(fridayConfig)) {
                    if (key !== 'pythonEnv' && key !== 'mainScriptPath') {
                        args.push(`--${key}`, value);
                    }
                }
                runPythonScript(fridayConfig.pythonEnv, args)
                    .then((result) => {
                    if (result.success) {
                        console.debug('[PYTHON SCRIPT OUTPUT]:', result.data);
                    }
                    else {
                        console.error('[PYTHON SCRIPT ERROR]:', result.error);
                        callback({
                            success: false,
                            message: result.error,
                        });
                    }
                })
                    .catch((error) => {
                    console.error('[PYTHON SCRIPT ERROR]:', error);
                    callback({
                        success: false,
                        message: error,
                    });
                })
                    .finally(() => {
                    replyingManager.setReplyingState(false);
                    this.broadcastReplyingStateToFridayAppRoom();
                });
            }));
            socket.on(trpc_1.SocketEvents.client.interruptReplyOfFridayApp, () => __awaiter(this, void 0, void 0, function* () {
                yield this.sendInterruptSignalToFriday();
            }));
            socket.on(trpc_1.SocketEvents.client.cleanHistoryOfFridayApp, () => __awaiter(this, void 0, void 0, function* () {
                FridayAppMessage_1.FridayAppMessageDao.cleanHistoryMessages().then(() => {
                    const filePath = src_1.PATHS.getFridayDialogHistoryPath();
                    if (fs.existsSync(filePath)) {
                        fs.unlinkSync(filePath);
                        console.debug(`Deleted file: ${filePath}`);
                    }
                    else {
                        console.warn(`File not found: ${filePath}`);
                    }
                    // TODO: 告知client
                    this.broadcastReplyToFridayAppRoom(undefined, true);
                });
            }));
            socket.on('disconnect', () => {
                console.debug('Client disconnected');
            });
        });
    }
    /*
     * Emit events to the project list room.
     */
    static broadcastRunToProjectListRoom() {
        Run_1.RunDao.getAllProjects()
            .then((projects) => {
            // Push projects to the client
            this.io
                .of('/client')
                .to(trpc_1.SocketRoomName.ProjectListRoom)
                .emit(trpc_1.SocketEvents.server.pushProjects, projects);
        })
            .catch((error) => {
            console.error(error);
            throw error;
        });
    }
    static broadcastRunToProjectRoom(project) {
        Run_1.RunDao.getAllProjectRuns(project)
            .then((runs) => {
            // Push runs to the client
            this.io
                .of('/client')
                .to(`project-${project}`)
                .emit(trpc_1.SocketEvents.server.pushRunsData, runs);
        })
            .catch((error) => {
            console.error(error);
            throw error;
        });
    }
    /*
     * Emit events to the run room.
     */
    static broadcastMessageToRunRoom(runId, msgForm) {
        this.io
            .of('/client')
            .to(`run-${runId}`)
            .emit(trpc_1.SocketEvents.server.pushMessages, [
            Object.assign({ id: msgForm.id, runId: msgForm.runId, replyId: msgForm.replyId }, msgForm.msg),
        ]);
    }
    static broadcastSpanDataToRunRoom(spanDataArray) {
        // Group spans by runId
        const groupedSpans = {};
        spanDataArray.forEach((spanData) => {
            if (!groupedSpans[spanData.runId]) {
                groupedSpans[spanData.runId] = [];
            }
            groupedSpans[spanData.runId].push(spanData);
        });
        // Send grouped spans to each run room
        for (const runId in groupedSpans) {
            this.io
                .of('/client')
                .to(`run-${runId}`)
                .emit(trpc_1.SocketEvents.server.pushSpans, groupedSpans[runId]);
            this.broadcastModelInvocationDataToRunRoom(runId);
        }
    }
    static broadcastInputRequestToRunRoom(runId, inputRequest) {
        this.io
            .of('/client')
            .to(`run-${runId}`)
            .emit(trpc_1.SocketEvents.server.pushInputRequests, [inputRequest]);
    }
    static broadcastRunDataToRunRoom(runId, runData) {
        this.io
            .of('/client')
            .to(`run-${runId}`)
            .emit(trpc_1.SocketEvents.server.pushRunData, runData);
    }
    static clearInputRequestsToRunRoom(runId) {
        this.io
            .of('/client')
            .to(`run-${runId}`)
            .emit(trpc_1.SocketEvents.server.clearInputRequests);
    }
    static changeRunStatusAndTriggerEvents(runId, newStatus) {
        return __awaiter(this, void 0, void 0, function* () {
            const runExist = yield Run_1.RunDao.doesRunExist(runId);
            if (runExist) {
                // Update the run status to "finished"
                yield Run_1.RunDao.changeRunStatus(runId, newStatus);
                // Find the project by runId
                const res = yield Run_1.RunDao.getRunData(runId);
                const project = res.runData.project;
                // Broadcast projects to all clients in the ProjectList room
                this.broadcastRunToProjectListRoom();
                // Broadcast runs to all clients in the project room
                this.broadcastRunToProjectRoom(project);
                // Broadcast run data to all clients in the run room
                this.broadcastRunDataToRunRoom(runId, res.runData);
                if (newStatus === messageForm_1.Status.DONE) {
                    // Clear the input requests for all clients in the run room
                    this.clearInputRequestsToRunRoom(runId);
                }
            }
        });
    }
    static broadcastOverviewDataToDashboardRoom() {
        this._getOverViewData()
            .then((res) => {
            this.io
                .of('/client')
                .to(trpc_1.SocketRoomName.OverviewRoom)
                .emit(trpc_1.SocketEvents.server.pushOverviewData, res);
        })
            .catch((error) => {
            console.error(error);
            throw error;
        });
    }
    static _getOverViewData() {
        return __awaiter(this, void 0, void 0, function* () {
            const res1 = yield Run_1.RunDao.getRunViewData();
            const res2 = yield Trace_1.SpanDao.getModelInvocationViewData();
            return Object.assign(Object.assign({}, res1), res2);
        });
    }
    static broadcastModelInvocationDataToRunRoom(runId) {
        Trace_1.SpanDao.getModelInvocationData(runId).then((data) => {
            this.io
                .of('/client')
                .to(`run-${runId}`)
                .emit(trpc_1.SocketEvents.server.pushModelInvocationData, data);
        });
    }
    static broadcastReplyToFridayAppRoom(reply, override = false) {
        this.io
            .of('/client')
            .to(trpc_1.SocketRoomName.FridayAppRoom)
            .emit(trpc_1.SocketEvents.server.pushReplies, reply === undefined ? [] : [reply], false, override);
    }
    static broadcastReplyingStateToFridayAppRoom() {
        const replyingManager = ReplyingStateManager_1.ReplyingStateManager.getInstance();
        this.io
            .of('/client')
            .to(trpc_1.SocketRoomName.FridayAppRoom)
            .emit(trpc_1.SocketEvents.server.pushReplyingState, replyingManager.getReplyingState());
    }
}
exports.SocketManager = SocketManager;
function runPythonScript(pythonEnv, commands) {
    return __awaiter(this, void 0, void 0, function* () {
        return new Promise((resolve) => {
            console.debug('The execute command:', pythonEnv, commands);
            const pythonProcess = (0, child_process_1.spawn)(pythonEnv, commands, {
                env: Object.assign(Object.assign({}, process.env), { FORCE_COLOR: '0' }),
            });
            let output = '';
            let errorOutput = '';
            // 收集标准输出
            pythonProcess.stdout.on('data', (data) => {
                output += data.toString();
            });
            // 收集错误输出
            pythonProcess.stderr.on('data', (data) => {
                errorOutput += data.toString();
            });
            // 进程结束时处理结果
            pythonProcess.on('close', (code) => {
                if (code === 0) {
                    resolve({
                        success: true,
                        data: output.trim(),
                    });
                }
                else {
                    resolve({
                        success: false,
                        error: errorOutput.trim(),
                    });
                }
            });
        });
    });
}
