/*
 * Libre WebUI
 * Copyright (C) 2025 Kroonen AI, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import WebSocket from 'ws';
import { randomUUID } from 'crypto';
// ---------------------------------------------------------------------------
// OpenClawSessionService
// ---------------------------------------------------------------------------
class OpenClawSessionService {
    constructor() {
        this.ws = null;
        this.pending = new Map();
        this.gatewayUrl = '';
        this.gatewayToken = '';
        this.sessionKey = 'main';
        this.connected = false;
        this.connectNonce = null;
        this.reconnectTimer = null;
        this.backoffMs = 800;
        this.listeners = new Set();
        this.closing = false;
    }
    // -------------------------------------------------------------------------
    // Public API
    // -------------------------------------------------------------------------
    /** Configure and connect to the OpenClaw gateway WebSocket. */
    connect(opts) {
        // Derive WS URL from HTTP URL
        const httpUrl = opts.gatewayUrl.replace(/\/v1\/chat\/completions$/, '');
        this.gatewayUrl = httpUrl
            .replace(/^http:/, 'ws:')
            .replace(/^https:/, 'wss:');
        this.gatewayToken = opts.token;
        this.sessionKey = opts.sessionKey ?? 'main';
        this.closing = false;
        this.doConnect();
    }
    /** Disconnect from the gateway. */
    disconnect() {
        this.closing = true;
        if (this.reconnectTimer) {
            clearTimeout(this.reconnectTimer);
            this.reconnectTimer = null;
        }
        this.ws?.close();
        this.ws = null;
        this.connected = false;
        this.flushPending(new Error('disconnected'));
    }
    /** Subscribe to session events (chat deltas, tool calls, etc.). */
    subscribe(cb) {
        this.listeners.add(cb);
        return () => this.listeners.delete(cb);
    }
    /** Send a chat message into the agent session. Returns the runId. */
    async sendMessage(message, sessionKey) {
        const key = sessionKey ?? this.sessionKey;
        const idempotencyKey = randomUUID();
        const result = (await this.request('chat.send', {
            sessionKey: key,
            message,
            deliver: false,
            idempotencyKey,
        }));
        return {
            runId: result?.runId ?? idempotencyKey,
            status: result?.status ?? 'started',
        };
    }
    /** Abort the current run. */
    async abort(sessionKey, runId) {
        const params = {
            sessionKey: sessionKey ?? this.sessionKey,
        };
        if (runId)
            params.runId = runId;
        await this.request('chat.abort', params);
    }
    /** Fetch chat history. */
    async history(sessionKey, limit = 200) {
        const result = (await this.request('chat.history', {
            sessionKey: sessionKey ?? this.sessionKey,
            limit,
        }));
        return { messages: result?.messages ?? [] };
    }
    get isConnected() {
        return this.connected;
    }
    // -------------------------------------------------------------------------
    // WebSocket lifecycle
    // -------------------------------------------------------------------------
    doConnect() {
        if (this.closing)
            return;
        console.log(`[OpenClawSession] Connecting to gateway: ${this.gatewayUrl}`);
        try {
            this.ws = new WebSocket(this.gatewayUrl);
        }
        catch (err) {
            console.error('[OpenClawSession] Failed to create WebSocket:', err);
            this.scheduleReconnect();
            return;
        }
        this.ws.on('open', () => {
            console.log('[OpenClawSession] WebSocket open, waiting for challenge…');
        });
        this.ws.on('message', (data) => {
            this.handleMessage(String(data));
        });
        this.ws.on('close', (code, reason) => {
            const reasonStr = reason?.toString() ?? '';
            console.log(`[OpenClawSession] WebSocket closed (${code}): ${reasonStr}`);
            this.ws = null;
            this.connected = false;
            this.flushPending(new Error(`gateway closed (${code}): ${reasonStr}`));
            this.emit('disconnected', { code, reason: reasonStr });
            this.scheduleReconnect();
        });
        this.ws.on('error', (err) => {
            console.error('[OpenClawSession] WebSocket error:', err.message);
        });
    }
    scheduleReconnect() {
        if (this.closing)
            return;
        const delay = this.backoffMs;
        this.backoffMs = Math.min(this.backoffMs * 1.7, 15000);
        this.reconnectTimer = setTimeout(() => this.doConnect(), delay);
    }
    // -------------------------------------------------------------------------
    // Protocol handling
    // -------------------------------------------------------------------------
    handleMessage(raw) {
        let frame;
        try {
            frame = JSON.parse(raw);
        }
        catch {
            return;
        }
        if (frame.type === 'event') {
            this.handleEvent(frame);
            return;
        }
        if (frame.type === 'res') {
            const pending = this.pending.get(frame.id);
            if (!pending)
                return;
            this.pending.delete(frame.id);
            clearTimeout(pending.timer);
            if (frame.ok) {
                pending.resolve(frame.payload);
            }
            else {
                pending.reject(new Error(frame.error?.message ?? 'request failed'));
            }
        }
    }
    handleEvent(frame) {
        // Handle connect challenge
        if (frame.event === 'connect.challenge') {
            const payload = frame.payload;
            if (payload?.nonce) {
                this.connectNonce = payload.nonce;
                this.sendConnect();
            }
            return;
        }
        // Handle chat events — forward to listeners
        if (frame.event === 'chat') {
            this.emit('chat', frame.payload);
            return;
        }
        // Handle agent events (tool streaming)
        if (frame.event === 'agent') {
            const payload = frame.payload;
            if (payload?.stream === 'tool' && payload.data) {
                const toolEvent = {
                    toolCallId: payload.data.toolCallId ?? '',
                    name: payload.data.name ?? 'tool',
                    phase: payload.data.phase ?? '',
                    args: payload.data.args,
                    result: payload.data.result,
                    partialResult: payload.data.partialResult,
                };
                this.emit('tool', toolEvent);
            }
            return;
        }
    }
    async sendConnect() {
        const params = {
            minProtocol: 3,
            maxProtocol: 3,
            client: {
                id: 'gateway-client',
                version: '1.0.0',
                platform: 'node',
                mode: 'backend',
            },
            role: 'operator',
            scopes: ['operator.read', 'operator.write'],
            caps: [],
            auth: { token: this.gatewayToken },
            userAgent: 'libre-webui/1.0.0',
            locale: 'en-US',
        };
        try {
            const result = (await this.request('connect', params));
            console.log('[OpenClawSession] Connected to gateway, protocol:', result?.protocol);
            this.connected = true;
            this.backoffMs = 800;
            this.emit('connected', {});
        }
        catch (err) {
            console.error('[OpenClawSession] Connect failed:', err);
            this.ws?.close(4008, 'connect failed');
        }
    }
    // -------------------------------------------------------------------------
    // Request/response
    // -------------------------------------------------------------------------
    request(method, params) {
        return new Promise((resolve, reject) => {
            if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
                return reject(new Error('gateway not connected'));
            }
            const id = randomUUID();
            const timer = setTimeout(() => {
                this.pending.delete(id);
                reject(new Error(`request timeout: ${method}`));
            }, 30000);
            this.pending.set(id, { resolve, reject, timer });
            const frame = {
                type: 'req',
                id,
                method,
                params,
            };
            this.ws.send(JSON.stringify(frame));
        });
    }
    flushPending(err) {
        for (const [, p] of this.pending) {
            clearTimeout(p.timer);
            p.reject(err);
        }
        this.pending.clear();
    }
    // -------------------------------------------------------------------------
    // Event emission
    // -------------------------------------------------------------------------
    emit(type, data) {
        for (const cb of this.listeners) {
            try {
                cb(type, data);
            }
            catch (err) {
                console.error('[OpenClawSession] Listener error:', err);
            }
        }
    }
}
// ---------------------------------------------------------------------------
// Helpers to extract text from gateway chat message format
// ---------------------------------------------------------------------------
export function extractTextFromMessage(message) {
    if (!message || typeof message !== 'object')
        return null;
    const msg = message;
    // Direct text field
    if (typeof msg.text === 'string')
        return msg.text;
    // content field — string or array
    const content = msg.content;
    if (typeof content === 'string')
        return content;
    if (Array.isArray(content)) {
        const parts = content
            .filter((block) => typeof block === 'object' &&
            block !== null &&
            block.type === 'text' &&
            typeof block.text === 'string')
            .map((block) => block.text);
        if (parts.length > 0)
            return parts.join('\n');
    }
    return null;
}
// Singleton
const openclawSessionService = new OpenClawSessionService();
export default openclawSessionService;
//# sourceMappingURL=openclawSessionService.js.map