/**
 * gRPC Client for Clawx Agent Communication
 *
 * Replaces WebSocket client and SSE streaming with gRPC bidirectional streaming
 */
import * as grpc from '@grpc/grpc-js';
import { EventEmitter } from 'events';
import { log } from '../../utils/logger.js';
import { GrpcFrames } from './protocol.js';
import { getAgentServiceDefinition, encodePayload, decodePayload } from './proto-loader.js';
/** Convert internal GrpcAgentFrame to proto wire format */
function frameToProto(frame) {
    return {
        id: frame.id,
        type: frame.type,
        timestamp: frame.timestamp,
        fromAgentId: frame.fromAgentId,
        toAgentId: frame.toAgentId,
        parentOperationId: frame.parentOperationId || '',
        parentOperationType: frame.parentOperationType || '',
        payload: encodePayload(frame.payload),
        error: frame.error || '',
    };
}
/** Convert proto wire format to internal GrpcAgentFrame */
function protoToFrame(msg) {
    return {
        id: msg.id,
        type: msg.type,
        timestamp: typeof msg.timestamp === 'number' ? msg.timestamp : Number(msg.timestamp),
        fromAgentId: msg.fromAgentId,
        toAgentId: msg.toAgentId,
        parentOperationId: msg.parentOperationId || undefined,
        parentOperationType: msg.parentOperationType === 'chat' ? 'chat' : msg.parentOperationType === 'task' ? 'task' : undefined,
        payload: decodePayload(msg.payload),
        error: msg.error || undefined,
    };
}
export class GrpcClient extends EventEmitter {
    options;
    protoClient = null;
    call = null;
    connected = false;
    registered = false;
    heartbeatTimer = null;
    reconnectTimer = null;
    pendingFrames = [];
    constructor(options) {
        super();
        this.options = options;
        this.connect();
    }
    connect() {
        try {
            log.info(`[gRPC Client] ${this.options.agentId} connecting to ${this.options.serverAddress}...`);
            // Use proto-loaded service for typed client
            const AgentService = getAgentServiceDefinition();
            const address = this.options.serverAddress.replace(/^grpc:\/\//, '');
            this.protoClient = new AgentService(address, grpc.credentials.createInsecure());
            this.call = this.protoClient.connect();
            if (!this.call) {
                throw new Error('Failed to create gRPC stream');
            }
            this.setupCallHandlers();
            this.connected = true;
            this.emit('connected');
            // Register with server
            this.register();
        }
        catch (error) {
            this.handleError(error);
        }
    }
    setupCallHandlers() {
        if (!this.call)
            return;
        this.call.on('data', (data) => {
            try {
                const frame = protoToFrame(data);
                if (!frame.type) {
                    log.warn('[gRPC Client] Dropping malformed frame');
                    return;
                }
                this.handleFrame(frame);
            }
            catch (error) {
                log.error('[gRPC Client] Failed to parse frame:', error);
            }
        });
        this.call.on('error', (error) => {
            this.handleError(error);
        });
        this.call.on('end', () => {
            this.handleDisconnect();
        });
    }
    handleFrame(frame) {
        log.debug(`[gRPC Client] ${this.options.agentId} received ${frame.type} from ${frame.fromAgentId}`);
        // External handler
        this.options.onFrame?.(frame);
        this.emit('frame', frame);
        switch (frame.type) {
            case 'registered':
                this.handleRegistered(frame);
                break;
            case 'chat_message':
                this.emit('chat', {
                    from: frame.fromAgentId,
                    message: frame.payload?.message,
                    timestamp: frame.timestamp,
                    conversationId: frame.parentOperationId,
                });
                break;
            case 'agent_message_start':
            case 'agent_message_delta':
            case 'agent_message_end':
                this.emit('agentMessage', frame);
                break;
            case 'task_started':
            case 'task_progress':
            case 'tool_started':
            case 'tool_stdout':
            case 'tool_stderr':
            case 'tool_finished':
            case 'task_completed':
            case 'task_failed':
                this.emit('taskEvent', frame);
                break;
            case 'system':
                log.debug(`[gRPC Client] System message: ${frame.payload?.message}`);
                break;
            case 'error':
                log.error(`[gRPC Client] Error from server: ${frame.error}`);
                this.emit('error', new Error(frame.error || 'Unknown error'));
                break;
            case 'heartbeat':
                // Acknowledge heartbeat
                break;
            default:
                log.warn(`[gRPC Client] Unknown frame type: ${frame.type}`);
        }
    }
    handleRegistered(frame) {
        const payload = frame.payload;
        if (payload?.status === 'registered') {
            this.registered = true;
            log.info(`[gRPC Client] ${this.options.agentId} registered successfully`);
            this.options.onRegistered?.(this.options.agentId);
            this.emit('registered', this.options.agentId);
            this.startHeartbeat();
            // Send any pending frames
            this.flushPendingFrames();
        }
        else {
            log.error(`[gRPC Client] Registration rejected: ${payload?.message}`);
            this.emit('error', new Error(`Registration rejected: ${payload?.message}`));
        }
    }
    register() {
        const frame = GrpcFrames.createRegister(this.options.agentId, this.options.agentName, this.options.endpoint, this.options.persona, this.options.capabilities);
        this.sendFrame(frame);
    }
    startHeartbeat() {
        const interval = this.options.heartbeatInterval || 30000;
        this.heartbeatTimer = setInterval(() => {
            if (this.connected && this.registered) {
                this.sendHeartbeat();
            }
        }, interval);
    }
    sendHeartbeat() {
        const frame = GrpcFrames.createHeartbeat(this.options.agentId);
        this.sendFrame(frame);
    }
    handleError(error) {
        log.error(`[gRPC Client] ${this.options.agentId} error:`, error.message);
        this.options.onError?.(error);
        this.emit('error', error);
        this.handleDisconnect();
    }
    handleDisconnect() {
        if (!this.connected)
            return;
        this.connected = false;
        this.registered = false;
        this.call = null;
        this.stopHeartbeat();
        this.options.onDisconnected?.();
        this.emit('disconnected');
        // Schedule reconnection
        this.scheduleReconnect();
    }
    scheduleReconnect() {
        if (this.reconnectTimer) {
            clearTimeout(this.reconnectTimer);
        }
        const delay = this.options.reconnectDelay || 5000;
        this.reconnectTimer = setTimeout(() => {
            log.info(`[gRPC Client] ${this.options.agentId} attempting reconnect...`);
            this.connect();
        }, delay);
    }
    stopHeartbeat() {
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
    }
    flushPendingFrames() {
        while (this.pendingFrames.length > 0) {
            const frame = this.pendingFrames.shift();
            if (frame) {
                this.sendFrame(frame);
            }
        }
    }
    // Public API
    sendFrame(frame) {
        if (!this.connected || !this.call) {
            log.warn(`[gRPC Client] ${this.options.agentId} not connected, queuing frame`);
            this.pendingFrames.push(frame);
            return false;
        }
        if (!this.registered && frame.type !== 'register') {
            log.warn(`[gRPC Client] ${this.options.agentId} not registered, queuing frame`);
            this.pendingFrames.push(frame);
            return false;
        }
        try {
            this.call.write(frameToProto(frame));
            return true;
        }
        catch (error) {
            log.error(`[gRPC Client] ${this.options.agentId} failed to send frame:`, error);
            return false;
        }
    }
    sendChat(toAgentId, message, conversationId) {
        const frame = GrpcFrames.createChatMessage(this.options.agentId, toAgentId, message, conversationId);
        return this.sendFrame(frame);
    }
    sendAgentMessageStart(turnId, toAgentId, persona) {
        const frame = GrpcFrames.createAgentMessageStart(turnId, this.options.agentId, toAgentId, persona);
        return this.sendFrame(frame);
    }
    sendAgentMessageDelta(turnId, toAgentId, delta) {
        const frame = GrpcFrames.createAgentMessageDelta(turnId, this.options.agentId, toAgentId, delta);
        return this.sendFrame(frame);
    }
    sendAgentMessageEnd(turnId, toAgentId, finalMessage) {
        const frame = GrpcFrames.createAgentMessageEnd(turnId, this.options.agentId, toAgentId, finalMessage);
        return this.sendFrame(frame);
    }
    sendTaskStarted(taskId, toAgentId, tool, params) {
        const frame = GrpcFrames.createTaskStarted(taskId, this.options.agentId, toAgentId, tool, params);
        return this.sendFrame(frame);
    }
    sendTaskProgress(taskId, toAgentId, progress, message) {
        const frame = GrpcFrames.createTaskProgress(taskId, this.options.agentId, toAgentId, progress, message);
        return this.sendFrame(frame);
    }
    sendToolStarted(taskId, toAgentId, toolName, params, parentOperationType = 'task') {
        const frame = GrpcFrames.createToolStarted(taskId, this.options.agentId, toAgentId, toolName, params, parentOperationType);
        return this.sendFrame(frame);
    }
    sendToolStdout(taskId, toAgentId, data, parentOperationType = 'task') {
        const frame = GrpcFrames.createToolStdout(taskId, this.options.agentId, toAgentId, data, parentOperationType);
        return this.sendFrame(frame);
    }
    sendToolStderr(taskId, toAgentId, data, parentOperationType = 'task') {
        const frame = GrpcFrames.createToolStderr(taskId, this.options.agentId, toAgentId, data, parentOperationType);
        return this.sendFrame(frame);
    }
    sendToolFinished(taskId, toAgentId, result, parentOperationType = 'task') {
        const frame = GrpcFrames.createToolFinished(taskId, this.options.agentId, toAgentId, result, parentOperationType);
        return this.sendFrame(frame);
    }
    sendTaskCompleted(taskId, toAgentId, result) {
        const frame = GrpcFrames.createTaskCompleted(taskId, this.options.agentId, toAgentId, result);
        return this.sendFrame(frame);
    }
    sendTaskFailed(taskId, toAgentId, error) {
        const frame = GrpcFrames.createTaskFailed(taskId, this.options.agentId, toAgentId, error);
        return this.sendFrame(frame);
    }
    sendTaskCancelled(taskId, toAgentId, reason) {
        const frame = GrpcFrames.createTaskCancelled(taskId, this.options.agentId, toAgentId, reason);
        return this.sendFrame(frame);
    }
    disconnect() {
        this.connected = false;
        this.registered = false;
        this.stopHeartbeat();
        if (this.reconnectTimer) {
            clearTimeout(this.reconnectTimer);
            this.reconnectTimer = null;
        }
        if (this.call) {
            this.call.end();
            this.call = null;
        }
        if (this.protoClient) {
            this.protoClient.close();
            this.protoClient = null;
        }
        log.info(`[gRPC Client] ${this.options.agentId} disconnected`);
        this.emit('disconnected');
    }
    isConnected() {
        return this.connected;
    }
    isRegistered() {
        return this.registered;
    }
    getAgentId() {
        return this.options.agentId;
    }
    getAgentName() {
        return this.options.agentName;
    }
    getPersona() {
        return this.options.persona;
    }
}
//# sourceMappingURL=grpc-client.js.map