"use strict";
// =============================================================================
// Pure Agent - QQ Bot Platform Adapter
// Ported from Hermes gateway/platforms/qqbot/adapter.py
// Discord-like WebSocket Gateway + REST API
// =============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.QQAdapter = void 0;
const config_1 = require("../config");
const base_1 = require("./base");
const logger_1 = require("../../logger");
const platform_registry_1 = require("../platform-registry");
// ---------------------------------------------------------------------------
// QQ Bot Gateway opcodes (Discord-like)
// ---------------------------------------------------------------------------
var QQOpcode;
(function (QQOpcode) {
    QQOpcode[QQOpcode["DISPATCH"] = 0] = "DISPATCH";
    QQOpcode[QQOpcode["HEARTBEAT"] = 1] = "HEARTBEAT";
    QQOpcode[QQOpcode["IDENTIFY"] = 2] = "IDENTIFY";
    QQOpcode[QQOpcode["RESUME"] = 6] = "RESUME";
    QQOpcode[QQOpcode["RECONNECT"] = 7] = "RECONNECT";
    QQOpcode[QQOpcode["INVALID_SESSION"] = 9] = "INVALID_SESSION";
    QQOpcode[QQOpcode["HELLO"] = 10] = "HELLO";
    QQOpcode[QQOpcode["HEARTBEAT_ACK"] = 11] = "HEARTBEAT_ACK";
})(QQOpcode || (QQOpcode = {}));
// QQ message intents
const INTENTS = (1 << 25) | (1 << 30) | (1 << 12) | (1 << 26);
// ---------------------------------------------------------------------------
// QQAdapter
// ---------------------------------------------------------------------------
class QQAdapter extends base_1.BasePlatformAdapter {
    _appId;
    _clientSecret;
    _accessToken = '';
    _tokenExpiresAt = 0;
    _ws = null;
    _heartbeatInterval = 41250; // default 41.25s
    _heartbeatTimer = null;
    _sessionId = '';
    _lastSeq = null;
    _gatewayUrl = '';
    _msgSeqCounter = 0;
    _markdownSupport;
    constructor(config) {
        super(config_1.Platform.QQBOT, config);
        this._appId = config.extra?.app_id || process.env.QQ_APP_ID || '';
        this._clientSecret = config.extra?.client_secret || process.env.QQ_CLIENT_SECRET || '';
        this._markdownSupport = config.extra?.markdown_support !== false;
    }
    // -------------------------------------------------------------------------
    // Connection
    // -------------------------------------------------------------------------
    async connect() {
        if (!this._appId || !this._clientSecret) {
            (0, logger_1.getLogger)().error('QQ Bot: missing app_id or client_secret');
            return false;
        }
        try {
            // Get access token
            await this._refreshToken();
            // Get gateway URL
            await this._connectGateway();
            this._connected = true;
            (0, logger_1.getLogger)().info(`QQ Bot: connected (app: ${this._appId})`);
            return true;
        }
        catch (err) {
            (0, logger_1.getLogger)().error(`QQ Bot: connection failed: ${err.message}`);
            return false;
        }
    }
    async disconnect() {
        this._connected = false;
        if (this._heartbeatTimer) {
            clearInterval(this._heartbeatTimer);
            this._heartbeatTimer = null;
        }
        if (this._ws) {
            this._ws.close();
            this._ws = null;
        }
        (0, logger_1.getLogger)().info('QQ Bot: disconnected');
    }
    // -------------------------------------------------------------------------
    // Gateway connection
    // -------------------------------------------------------------------------
    async _connectGateway() {
        // Get gateway URL
        const response = await fetch('https://api.sgroup.qq.com/gateway', {
            headers: { 'Authorization': `QQBot ${this._accessToken}` },
            signal: AbortSignal.timeout(10000),
        });
        if (!response.ok)
            throw new Error(`Gateway HTTP ${response.status}`);
        const data = await response.json();
        this._gatewayUrl = data.url;
        await this._connectWebSocket();
    }
    async _connectWebSocket() {
        return new Promise((resolve, reject) => {
            const ws = new WebSocket(this._gatewayUrl);
            this._ws = ws;
            const timeout = setTimeout(() => {
                ws.close();
                reject(new Error('WebSocket connect timeout'));
            }, 15000);
            ws.onopen = () => {
                (0, logger_1.getLogger)().debug('QQ Bot: WebSocket connected, waiting for Hello');
            };
            ws.onmessage = (event) => {
                try {
                    const frame = JSON.parse(typeof event.data === 'string' ? event.data : event.data.toString());
                    this._onGatewayFrame(frame, resolve, timeout);
                }
                catch (err) {
                    (0, logger_1.getLogger)().warn('QQ Bot: failed to parse gateway frame');
                }
            };
            ws.onclose = (event) => {
                this._connected = false;
                if (this._heartbeatTimer) {
                    clearInterval(this._heartbeatTimer);
                    this._heartbeatTimer = null;
                }
                (0, logger_1.getLogger)().info(`QQ Bot: WebSocket closed (code: ${event.code})`);
                // Auto-reconnect unless fatal
                if (event.code !== 4004 && event.code !== 4010 && event.code !== 4011) {
                    setTimeout(() => {
                        if (!this._connected && this._appId) {
                            this.connect().catch(() => { });
                        }
                    }, 5000);
                }
            };
            ws.onerror = (err) => {
                (0, logger_1.getLogger)().error(`QQ Bot: WebSocket error: ${err}`);
            };
        });
    }
    // -------------------------------------------------------------------------
    // Gateway frame processing
    // -------------------------------------------------------------------------
    _onGatewayFrame(frame, resolveConnect, connectTimeout) {
        const op = frame.op;
        const d = frame.d;
        const s = frame.s;
        const t = frame.t;
        if (s !== null && s !== undefined)
            this._lastSeq = s;
        switch (op) {
            case QQOpcode.HELLO: {
                // Hello: get heartbeat interval and identify
                this._heartbeatInterval = d.heartbeat_interval;
                // Start heartbeat
                this._startHeartbeat();
                // Identify or Resume
                if (this._sessionId) {
                    this._sendFrame(QQOpcode.RESUME, {
                        token: `QQBot ${this._accessToken}`,
                        session_id: this._sessionId,
                        seq: this._lastSeq,
                    });
                }
                else {
                    this._sendFrame(QQOpcode.IDENTIFY, {
                        token: `QQBot ${this._accessToken}`,
                        intents: INTENTS,
                        shard: [0, 1],
                        properties: {},
                    });
                }
                break;
            }
            case QQOpcode.DISPATCH: {
                // Event dispatch
                if (t === 'READY') {
                    this._sessionId = d.session_id;
                    (0, logger_1.getLogger)().info(`QQ Bot: READY, session=${this._sessionId}`);
                    if (resolveConnect) {
                        clearTimeout(connectTimeout);
                        resolveConnect(true);
                    }
                }
                else {
                    this._onDispatch(t, d).catch(err => {
                        (0, logger_1.getLogger)().error(`QQ Bot dispatch error: ${err.message}`);
                    });
                }
                break;
            }
            case QQOpcode.HEARTBEAT: {
                this._sendFrame(QQOpcode.HEARTBEAT, this._lastSeq);
                break;
            }
            case QQOpcode.HEARTBEAT_ACK: {
                // Heartbeat acknowledged
                break;
            }
            case QQOpcode.RECONNECT: {
                (0, logger_1.getLogger)().info('QQ Bot: server requested reconnect');
                this._ws?.close();
                break;
            }
            case QQOpcode.INVALID_SESSION: {
                (0, logger_1.getLogger)().warn('QQ Bot: invalid session, re-identifying');
                this._sessionId = '';
                this._lastSeq = null;
                break;
            }
        }
    }
    // -------------------------------------------------------------------------
    // Event dispatch
    // -------------------------------------------------------------------------
    async _onDispatch(eventType, data) {
        switch (eventType) {
            case 'C2C_MESSAGE_CREATE':
                await this._handleC2CMessage(data);
                break;
            case 'GROUP_AT_MESSAGE_CREATE':
                await this._handleGroupMessage(data);
                break;
            case 'DIRECT_MESSAGE_CREATE':
                await this._handleDMMessage(data);
                break;
            case 'INTERACTION_CREATE':
                (0, logger_1.getLogger)().debug(`QQ Bot: interaction event`);
                break;
        }
    }
    async _handleC2CMessage(data) {
        const text = data.content || '';
        const attachments = data.attachments || [];
        // Check for voice attachment
        let voiceText = '';
        const voiceAtt = attachments.find((a) => a.content_type?.startsWith('audio/') || a.asr_refer_text);
        if (voiceAtt) {
            voiceText = voiceAtt.asr_refer_text || '';
        }
        const event = {
            text: voiceText || text,
            messageType: voiceAtt ? base_1.MessageType.VOICE : base_1.MessageType.TEXT,
            platform: config_1.Platform.QQBOT,
            chatId: data.author?.user_openid || '',
            chatType: 'dm',
            userId: data.author?.user_openid,
            messageId: data.id,
            timestamp: data.timestamp ? new Date(data.timestamp).getTime() : Date.now(),
            rawMessage: data,
        };
        await this.handleMessage(event);
    }
    async _handleGroupMessage(data) {
        let text = data.content || '';
        // Strip @mention
        text = text.replace(/@\S+\s*/g, '').trim();
        const event = {
            text,
            messageType: base_1.MessageType.TEXT,
            platform: config_1.Platform.QQBOT,
            chatId: data.group_openid || '',
            chatType: 'group',
            userId: data.author?.member_openid,
            messageId: data.id,
            timestamp: data.timestamp ? new Date(data.timestamp).getTime() : Date.now(),
            rawMessage: data,
        };
        await this.handleMessage(event);
    }
    async _handleDMMessage(data) {
        const event = {
            text: data.content || '',
            messageType: base_1.MessageType.TEXT,
            platform: config_1.Platform.QQBOT,
            chatId: data.guild_id || '',
            chatType: 'dm',
            userId: data.author?.id,
            messageId: data.id,
            timestamp: data.timestamp ? new Date(data.timestamp).getTime() : Date.now(),
            rawMessage: data,
        };
        await this.handleMessage(event);
    }
    // -------------------------------------------------------------------------
    // Send message
    // -------------------------------------------------------------------------
    async send(chatId, content, replyTo) {
        const token = await this._ensureToken();
        const truncated = content.slice(0, 2000);
        // Determine chat type from the last message event
        // For now, try C2C first, then group
        const endpoints = [
            { url: `https://api.sgroup.qq.com/v2/users/${chatId}/messages`, type: 'c2c' },
            { url: `https://api.sgroup.qq.com/v2/groups/${chatId}/messages`, type: 'group' },
        ];
        for (const endpoint of endpoints) {
            try {
                const msgSeq = ++this._msgSeqCounter;
                const body = {
                    msg_type: this._markdownSupport ? 2 : 0,
                    msg_seq: msgSeq,
                };
                if (this._markdownSupport) {
                    body.markdown = { content: truncated };
                }
                else {
                    body.content = truncated;
                }
                if (replyTo)
                    body.msg_id = replyTo;
                const response = await fetch(endpoint.url, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `QQBot ${token}`,
                    },
                    body: JSON.stringify(body),
                    signal: AbortSignal.timeout(15000),
                });
                if (response.ok) {
                    const result = await response.json().catch(() => ({}));
                    return { success: true, messageId: result.id };
                }
                // 400 on C2C might mean it's a group chat, try next
                if (response.status === 400 && endpoint.type === 'c2c')
                    continue;
                const errBody = await response.json().catch(() => ({}));
                return { success: false, error: errBody.message || `HTTP ${response.status}` };
            }
            catch (err) {
                if (endpoint.type === 'c2c')
                    continue; // Try group endpoint
                return { success: false, error: err.message };
            }
        }
        return { success: false, error: 'Failed to send via any endpoint' };
    }
    async getChatInfo(chatId) {
        return { name: `qq:${chatId}`, type: 'dm' };
    }
    // -------------------------------------------------------------------------
    // Token management
    // -------------------------------------------------------------------------
    async _refreshToken() {
        const response = await fetch('https://bots.qq.com/app/getAppAccessToken', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ appId: this._appId, clientSecret: this._clientSecret }),
            signal: AbortSignal.timeout(10000),
        });
        if (!response.ok)
            throw new Error(`QQ token HTTP ${response.status}`);
        const result = await response.json();
        this._accessToken = result.access_token;
        this._tokenExpiresAt = Date.now() + (parseInt(result.expires_in) - 60) * 1000;
    }
    async _ensureToken() {
        if (Date.now() >= this._tokenExpiresAt) {
            await this._refreshToken();
        }
        return this._accessToken;
    }
    // -------------------------------------------------------------------------
    // Heartbeat
    // -------------------------------------------------------------------------
    _startHeartbeat() {
        if (this._heartbeatTimer)
            clearInterval(this._heartbeatTimer);
        this._heartbeatTimer = setInterval(() => {
            this._sendFrame(QQOpcode.HEARTBEAT, this._lastSeq);
        }, this._heartbeatInterval);
    }
    // -------------------------------------------------------------------------
    // Gateway frame sending
    // -------------------------------------------------------------------------
    _sendFrame(op, d) {
        if (this._ws && this._ws.readyState === WebSocket.OPEN) {
            this._ws.send(JSON.stringify({ op, d }));
        }
    }
}
exports.QQAdapter = QQAdapter;
// Auto-register
platform_registry_1.platformRegistry.register({
    name: 'qqbot',
    label: 'QQ Bot',
    factory: (config) => new QQAdapter(config),
    checkFn: () => !!(process.env.QQ_APP_ID || process.env.QQ_CLIENT_SECRET),
    requiredEnv: ['QQ_APP_ID', 'QQ_CLIENT_SECRET'],
    installHint: 'Set QQ_APP_ID and QQ_CLIENT_SECRET environment variables (from QQ Bot platform)',
    emoji: '🐧',
    maxMessageLength: 2000,
});
//# sourceMappingURL=qqbot.js.map