/**
 * WebSocket 客户端模块
 * 负责与调度中心建立和维护 WebSocket 连接
 */
import WebSocket from 'ws';
import { EventEmitter } from 'node:events';
import { logger } from './logger.js';
const DEFAULT_HEARTBEAT_TIMEOUT_MS = 9000;
export class WebSocketClient extends EventEmitter {
    ws = null;
    config;
    agentId = null;
    heartbeatTimer = null;
    heartbeatTimeoutTimer = null;
    reconnectAttempts = 0;
    reconnectTimer = null;
    isManualClose = false;
    websocketFactory;
    constructor(config, websocketFactory) {
        super();
        this.config = config;
        this.websocketFactory = websocketFactory ?? ((url) => new WebSocket(url));
    }
    /**
       * 建立 WebSocket 连接
       * 主流程：准备连接 -> 创建连接 -> 设置事件监听
       */
    connect(agentId) {
        return new Promise((resolve, reject) => {
            this.prepareConnection(agentId);
            const wsUrl = this.buildWebSocketUrl(agentId);
            logger.info(`[WebSocket] 正在连接: ${this.maskRuntimeTokenInUrl(wsUrl)}`);
            const { rejectOnce, resolveOnce } = this.createSettledHandlers(resolve, reject);
            try {
                this.ws = this.websocketFactory(wsUrl);
                this.setupWebSocketEventHandlers(this.ws, rejectOnce, resolveOnce);
            }
            catch (error) {
                rejectOnce(error instanceof Error ? error : new Error(String(error)));
            }
        });
    }
    /**
     * 准备连接前的状态清理
     */
    prepareConnection(agentId) {
        this.agentId = agentId;
        this.isManualClose = false;
        if (this.ws && this.ws.readyState !== WebSocket.CLOSED) {
            this.ws.removeAllListeners();
            this.ws.terminate();
        }
    }
    /**
     * 构建 WebSocket URL
     */
    buildWebSocketUrl(agentId) {
        const url = new URL(this.config.serverUrl);
        url.searchParams.set('agentId', agentId);
        const runtimeAccessToken = this.config.runtimeAccessToken?.trim();
        if (runtimeAccessToken) {
            url.searchParams.set('runtimeAccessToken', runtimeAccessToken);
        }
        return url.toString();
    }
    maskRuntimeTokenInUrl(url) {
        try {
            const parsedUrl = new URL(url);
            if (parsedUrl.searchParams.has('runtimeAccessToken')) {
                parsedUrl.searchParams.set('runtimeAccessToken', '***');
            }
            return parsedUrl.toString();
        }
        catch {
            return url.replace(/([?&]runtimeAccessToken=)[^&]*/u, '$1***');
        }
    }
    /**
     * 创建一次性 resolve/reject 处理器
     */
    createSettledHandlers(resolve, reject) {
        let settled = false;
        const rejectOnce = (error) => {
            if (!settled) {
                settled = true;
                reject(error);
            }
        };
        const resolveOnce = () => {
            if (!settled) {
                settled = true;
                resolve();
            }
        };
        return { rejectOnce, resolveOnce };
    }
    /**
     * 设置 WebSocket 事件监听器
     */
    setupWebSocketEventHandlers(ws, rejectOnce, resolveOnce) {
        let opened = false;
        ws.on('open', () => {
            opened = true;
            this.handleWebSocketOpen(resolveOnce);
        });
        ws.on('message', (data) => this.handleWebSocketMessage(data));
        ws.on('pong', () => this.markHeartbeatReceived());
        ws.on('close', (code, reason) => this.handleWebSocketClose(code, reason, opened, rejectOnce));
        ws.on('error', (error) => this.handleWebSocketError(error, opened, rejectOnce));
    }
    /**
     * 处理 WebSocket 打开事件
     */
    handleWebSocketOpen(resolveOnce) {
        logger.info('[WebSocket] 连接已建立');
        this.reconnectAttempts = 0;
        this.clearReconnectTimer();
        this.startHeartbeat();
        this.emit('connected');
        resolveOnce();
    }
    /**
     * 处理 WebSocket 消息
     */
    handleWebSocketMessage(data) {
        try {
            const message = JSON.parse(data.toString());
            this.acknowledgeReliableMessage(message);
            this.handleMessage(message);
        }
        catch (error) {
            logger.warn('[WebSocket] 消息解析失败:', error);
        }
    }
    acknowledgeReliableMessage(message) {
        const payload = message.data;
        if (!payload || typeof payload !== 'object') {
            return;
        }
        const data = payload;
        const messageId = String(data._msgId || '').trim();
        const requireAck = data._requireAck === true || data._requireAck === 'true';
        if (!messageId || !requireAck) {
            return;
        }
        this.send({
            type: 'MESSAGE_ACK',
            data: { msgId: messageId },
            timestamp: new Date().toISOString(),
        });
    }
    /**
     * 处理 WebSocket 关闭事件
     */
    handleWebSocketClose(code, reason, opened, rejectOnce) {
        const closeReason = reason.toString();
        logger.warn(`[WebSocket] 连接关闭: code=${code}, reason=${closeReason}`);
        this.stopHeartbeat();
        this.clearHeartbeatTimeout();
        this.emit('disconnected', code, reason);
        if (!opened) {
            rejectOnce(new Error(`WebSocket 连接在建立前关闭: code=${code}, reason=${closeReason}`));
        }
        if (!this.isManualClose) {
            if (code === 1008) {
                logger.warn('[WebSocket] MCP session runtime token 已失效或不匹配；不会降级刷新 bridge 级 token');
            }
            this.scheduleReconnect();
        }
    }
    /**
     * 处理 WebSocket 错误事件
     */
    handleWebSocketError(error, opened, rejectOnce) {
        logger.error('[WebSocket] 连接错误:', error.message);
        this.emit('error', error);
        if (!opened) {
            rejectOnce(error);
        }
    }
    /**
     * 断开 WebSocket 连接
     */
    disconnect() {
        this.isManualClose = true;
        this.stopHeartbeat();
        this.clearHeartbeatTimeout();
        this.clearReconnectTimer();
        if (this.ws) {
            if (this.ws.readyState === WebSocket.OPEN) {
                this.ws.close(1000, 'Agent logout');
            }
            this.ws = null;
        }
        this.agentId = null;
        logger.info('[WebSocket] 已断开连接');
    }
    /**
     * 发送消息
     */
    send(message) {
        if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
            logger.warn('[WebSocket] 连接未建立，无法发送消息');
            return false;
        }
        try {
            this.ws.send(JSON.stringify(message));
            return true;
        }
        catch (error) {
            logger.error('[WebSocket] 发送消息失败:', error);
            return false;
        }
    }
    /**
     * 上报 MCP Node.js 进程启动后的运行日志。
     */
    sendMcpNodeLog(data) {
        return this.send({
            type: 'MCP_NODE_LOG',
            data,
            timestamp: new Date().toISOString(),
        });
    }
    /**
     * 是否已连接
     */
    isConnected() {
        return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
    }
    /**
     * 处理接收到的消息
     */
    handleMessage(message) {
        switch (message.type) {
            case 'GROUP_MSG':
                this.emit('groupMessage', message.data);
                break;
            case 'DM_MSG':
                this.emit('directMessage', message.data);
                break;
            case 'ONLINE_CHANGE':
                this.emit('onlineChange', message.data);
                break;
            case 'HEARTBEAT':
                this.markHeartbeatReceived();
                break;
            case 'PROFILE_INFO':
                logger.info('[WebSocket] 收到 PROFILE_INFO 消息');
                this.emit('profileInfo', message.data);
                break;
            case 'AGENT_STATUS':
                logger.info('[WebSocket] 收到 AGENT_STATUS 消息');
                this.emit('agentStatus', message.data);
                break;
            case 'GROUP_READ_RECEIPT':
            case 'DM_READ_RECEIPT':
            case 'TERMINAL_OUTPUT':
            case 'TERMINAL_STATUS':
                break;
            default:
                logger.warn('[WebSocket] 未知消息类型:', message.type);
        }
    }
    /**
     * 启动心跳
     */
    startHeartbeat() {
        this.stopHeartbeat();
        this.heartbeatTimer = setInterval(() => {
            if (this.isConnected()) {
                this.ensureHeartbeatTimeout();
                this.sendProtocolPing();
                this.send({
                    type: 'HEARTBEAT',
                    data: 'ping',
                    timestamp: new Date().toISOString()
                });
            }
        }, this.config.heartbeatInterval);
    }
    /**
     * 停止心跳
     */
    stopHeartbeat() {
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
        this.clearHeartbeatTimeout();
    }
    sendProtocolPing() {
        if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
            return;
        }
        try {
            this.ws.ping();
        }
        catch (error) {
            logger.warn('[WebSocket] 协议层 ping 发送失败，终止连接并重连:', error instanceof Error ? error.message : String(error));
            this.ws.terminate();
        }
    }
    markHeartbeatReceived() {
        this.clearHeartbeatTimeout();
    }
    ensureHeartbeatTimeout() {
        if (this.heartbeatTimeoutTimer) {
            return;
        }
        this.heartbeatTimeoutTimer = setTimeout(() => {
            this.heartbeatTimeoutTimer = null;
            if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
                return;
            }
            logger.warn(`[WebSocket] ${this.resolveHeartbeatTimeout()}ms 未收到心跳响应，终止连接并触发重连`);
            this.ws.terminate();
        }, this.resolveHeartbeatTimeout());
    }
    clearHeartbeatTimeout() {
        if (this.heartbeatTimeoutTimer) {
            clearTimeout(this.heartbeatTimeoutTimer);
            this.heartbeatTimeoutTimer = null;
        }
    }
    resolveHeartbeatTimeout() {
        return this.config.heartbeatTimeout ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
    }
    /**
     * 安排重连
     */
    scheduleReconnect() {
        const hasRetryLimit = this.config.maxReconnectAttempts !== -1;
        if (hasRetryLimit && this.reconnectAttempts >= this.config.maxReconnectAttempts) {
            logger.error('[WebSocket] 重连次数已达上限，放弃重连');
            this.emit('reconnectFailed');
            return;
        }
        if (this.reconnectTimer) {
            return;
        }
        this.reconnectAttempts++;
        // 首次重连也走配置的重连间隔，给服务端和前端一个稳定窗口来感知离线状态。
        // 否则 0ms 立即重连会让 agent 在 DB 中几乎瞬间从 offline 又变回 online，
        // 页面上看起来就像“从未离线”。
        const delay = this.config.reconnectInterval;
        const retryInfo = hasRetryLimit
            ? `第 ${this.reconnectAttempts}/${this.config.maxReconnectAttempts} 次`
            : `第 ${this.reconnectAttempts} 次（无限重试模式）`;
        logger.warn(`[WebSocket] ${delay}ms 后尝试${retryInfo}重连...`);
        this.reconnectTimer = setTimeout(() => {
            this.reconnectTimer = null;
            if (this.agentId) {
                this.connect(this.agentId).catch((error) => {
                    logger.warn('[WebSocket] 重连失败，继续重试:', error instanceof Error ? error.message : String(error));
                    this.scheduleReconnect();
                });
            }
        }, delay);
    }
    /**
     * 清除重连定时器
     */
    clearReconnectTimer() {
        if (this.reconnectTimer) {
            clearTimeout(this.reconnectTimer);
            this.reconnectTimer = null;
        }
    }
}
//# sourceMappingURL=websocket-client.js.map