/**
 * gRPC Server for Clawx Agent Communication
 *
 * Replaces WebSocket server 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 { v4 as uuidv4 } from 'uuid';
import { GrpcFrames } from './protocol.js';
import { getAgentServiceDefinition, getHealthServiceDefinition, 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,
    };
}
function sanitizeGrpcPayload(value) {
    try {
        return JSON.parse(JSON.stringify(value, (_key, current) => {
            if (typeof current === 'function')
                return undefined;
            if (current && typeof current === 'object') {
                const ctor = current.constructor?.name;
                if (ctor === 'GrpcServer' || ctor === 'ServerResponse' || ctor === 'IncomingMessage' || ctor === 'Socket' || ctor === 'TreeNode') {
                    return undefined;
                }
            }
            return current;
        }));
    }
    catch {
        return value;
    }
}
export class GrpcServer extends EventEmitter {
    options;
    server;
    agents = new Map();
    clients = new Map();
    constructor(options) {
        super();
        this.options = options;
        this.server = new grpc.Server();
        this.setupService();
    }
    setupService() {
        // Load proto-defined service
        const AgentService = getAgentServiceDefinition();
        const HealthService = getHealthServiceDefinition();
        // Agent service implementation
        const agentServiceImpl = {
            // Bidirectional stream — primary transport
            connect: (call) => {
                const clientId = uuidv4();
                log.info(`[gRPC] New client connection: ${clientId}`);
                this.clients.set(clientId, { agentId: 'unknown', call });
                call.on('data', (data) => {
                    try {
                        const frame = protoToFrame(data);
                        this.handleFrame(frame, call, clientId);
                    }
                    catch (error) {
                        log.error('[gRPC] Failed to parse frame:', error);
                        this.sendError(call, 'Failed to parse frame', clientId);
                    }
                });
                call.on('error', (error) => {
                    log.error(`[gRPC] Client ${clientId} error:`, error);
                    this.handleDisconnection(clientId);
                });
                call.on('end', () => {
                    log.info(`[gRPC] Client ${clientId} connection ended`);
                    this.handleDisconnection(clientId);
                });
                // Send welcome message
                call.write(frameToProto(GrpcFrames.createSystemMessage('unknown', 'Connected to Clawx gRPC server', { clientId, serverPort: this.options.port })));
            },
            // Structured RPC: Register
            register: (call, callback) => {
                const req = call.request;
                const persona = decodePayload(req.persona);
                try {
                    const agentId = req.agentId;
                    const agentName = req.agentName;
                    if (this.agents.has(agentId)) {
                        const existing = this.agents.get(agentId);
                        existing.lastSeen = Date.now();
                    }
                    this.options.onAgentRegistered?.({
                        id: agentId,
                        name: agentName,
                        type: 'local',
                        status: 'idle',
                        capabilities: req.capabilities || [],
                        endpoint: req.endpoint || '',
                        workspace: '',
                        created: Date.now(),
                        lastHeartbeat: Date.now(),
                    });
                    callback(null, {
                        agentId,
                        status: 'registered',
                        message: `Welcome ${agentName}`,
                        masterEndpoint: `grpc://localhost:${this.options.port}`,
                    });
                }
                catch (error) {
                    callback(null, {
                        agentId: req.agentId || 'unknown',
                        status: 'rejected',
                        message: error instanceof Error ? error.message : 'Registration failed',
                    });
                }
            },
            // Structured RPC: Heartbeat
            heartbeat: (call, callback) => {
                const agentId = call.request.agentId;
                this.handleHeartbeat(agentId);
                callback(null, { ok: true, serverTime: Date.now() });
            },
            // Structured RPC: ExecuteTask (server-streaming)
            executeTask: (call) => {
                const req = call.request;
                const taskId = req.taskId;
                const tool = req.tool;
                const params = decodePayload(req.params);
                const context = decodePayload(req.context);
                // Subscribe to task events and forward to this stream
                const onFrame = (frame) => {
                    if (frame.parentOperationId === taskId) {
                        call.write({
                            taskId,
                            type: frame.type,
                            payload: encodePayload(frame.payload),
                            error: frame.error || '',
                        });
                        // End stream on terminal events
                        if (frame.type === 'task_completed' || frame.type === 'task_failed' || frame.type === 'task_cancelled') {
                            this.removeListener('frame', onFrame);
                            call.end();
                        }
                    }
                };
                this.on('frame', onFrame);
                call.on('cancelled', () => {
                    this.removeListener('frame', onFrame);
                });
                // The actual task dispatch happens via sendTask() — this RPC just streams results
                log.debug(`[gRPC] ExecuteTask stream opened for task ${taskId}`);
            },
            // Structured RPC: CancelTask
            cancelTask: (call, callback) => {
                const { taskId, reason } = call.request;
                // Broadcast cancellation frame
                const frame = GrpcFrames.createTaskCancelled(taskId, 'server', 'broadcast', reason);
                this.broadcastFrame(frame);
                callback(null, { ok: true, message: `Task ${taskId} cancelled` });
            },
            // Structured RPC: SendChat (server-streaming)
            sendChat: (call) => {
                const req = call.request;
                const conversationId = req.conversationId;
                const onFrame = (frame) => {
                    if (frame.parentOperationId === conversationId) {
                        call.write({
                            conversationId,
                            type: frame.type,
                            payload: encodePayload(frame.payload),
                        });
                        if (frame.type === 'agent_message_end') {
                            this.removeListener('frame', onFrame);
                            call.end();
                        }
                    }
                };
                this.on('frame', onFrame);
                call.on('cancelled', () => {
                    this.removeListener('frame', onFrame);
                });
            },
        };
        // Health service implementation
        const healthServiceImpl = {
            check: (_call, callback) => {
                callback(null, {
                    status: 'SERVING',
                    agentId: '',
                    agentName: '',
                    connectedAgents: this.agents.size,
                });
            },
        };
        this.server.addService(AgentService.service, agentServiceImpl);
        this.server.addService(HealthService.service, healthServiceImpl);
    }
    handleFrame(frame, call, clientId) {
        log.debug(`[gRPC] ${frame.type} from ${frame.fromAgentId} to ${frame.toAgentId}`);
        // Update client-agent mapping
        if (frame.fromAgentId !== 'unknown' && frame.fromAgentId !== 'server') {
            const client = this.clients.get(clientId);
            if (client && client.agentId === 'unknown') {
                client.agentId = frame.fromAgentId;
            }
        }
        switch (frame.type) {
            case 'register':
                this.handleRegistration(frame, call, clientId);
                break;
            case 'heartbeat':
                this.handleHeartbeat(frame.fromAgentId);
                break;
            case 'agent_message_start':
            case 'agent_message_delta':
            case 'agent_message_end':
            case 'task_started':
            case 'task_progress':
            case 'tool_started':
            case 'tool_stdout':
            case 'tool_stderr':
            case 'tool_finished':
            case 'task_completed':
            case 'task_failed':
            case 'task_cancelled':
            case 'chat_message':
                this.routeFrame(frame);
                break;
            default:
                log.warn(`[gRPC] Unknown frame type: ${frame.type}`);
        }
        // Emit frame for external handlers
        this.options.onFrame?.(frame);
        this.emit('frame', frame);
    }
    handleRegistration(frame, call, clientId) {
        try {
            const payload = frame.payload;
            if (!payload || !payload.agentId || !payload.agentName) {
                throw new Error('Missing agentId or agentName in registration');
            }
            const agentId = payload.agentId;
            const agentName = payload.agentName;
            const persona = payload.persona;
            const capabilities = payload.capabilities || [];
            const endpoint = payload.endpoint || `http://localhost:${this.options.port + 1}`;
            // Check if agent already registered (reconnection is normal)
            if (this.agents.has(agentId)) {
                log.debug(`[gRPC] Agent ${agentId} reconnected, updating connection`);
                const existingAgent = this.agents.get(agentId);
                existingAgent.call = call;
                existingAgent.lastSeen = Date.now();
            }
            else {
                // Register new agent
                const agent = {
                    id: agentId,
                    name: agentName,
                    call,
                    persona,
                    capabilities,
                    endpoint,
                    lastSeen: Date.now(),
                    subscriptions: new Map(),
                };
                this.agents.set(agentId, agent);
                log.info(`[gRPC] Agent registered: ${agentName} (${agentId})`);
                // Update client mapping
                const client = this.clients.get(clientId);
                if (client) {
                    client.agentId = agentId;
                }
                // Notify external handler
                this.options.onAgentRegistered?.({
                    id: agentId,
                    name: agentName,
                    type: 'local',
                    status: 'idle',
                    capabilities,
                    endpoint,
                    workspace: '', // Workspace not known at registration time
                    created: Date.now(),
                    lastHeartbeat: Date.now(),
                });
                this.emit('agentRegistered', agentId);
            }
            // Send registration confirmation
            call.write(frameToProto(GrpcFrames.createRegistered(agentId, 'registered', `Welcome ${agentName}`, `grpc://localhost:${this.options.port}`)));
        }
        catch (error) {
            log.error('[gRPC] Registration failed:', error);
            call.write(frameToProto(GrpcFrames.createRegistered(frame.payload?.agentId || 'unknown', 'rejected', error instanceof Error ? error.message : 'Registration failed')));
        }
    }
    handleHeartbeat(agentId) {
        const agent = this.agents.get(agentId);
        if (agent) {
            agent.lastSeen = Date.now();
        }
    }
    routeFrame(frame) {
        const { toAgentId, fromAgentId } = frame;
        if (toAgentId === 'broadcast') {
            this.broadcastFrame(frame, fromAgentId);
        }
        else if (toAgentId === 'server') {
            // Handle server-bound frames (e.g., task results to master)
            this.emit('serverFrame', frame);
        }
        else {
            this.sendToAgent(toAgentId, frame);
        }
    }
    sendToAgent(agentId, frame) {
        const agent = this.agents.get(agentId);
        if (!agent) {
            log.warn(`[gRPC] Agent ${agentId} not found`);
            return false;
        }
        try {
            agent.call.write(frameToProto(frame));
            return true;
        }
        catch (error) {
            log.error(`[gRPC] Failed to send to ${agentId}:`, error);
            return false;
        }
    }
    broadcastFrame(frame, excludeAgentId) {
        let count = 0;
        this.agents.forEach((agent, agentId) => {
            if (agentId !== excludeAgentId) {
                if (this.sendToAgent(agentId, frame)) {
                    count++;
                }
            }
        });
        log.debug(`[gRPC] Broadcast from ${excludeAgentId || 'server'} to ${count} agents`);
    }
    sendError(call, error, clientId) {
        try {
            call.write(frameToProto(GrpcFrames.createErrorMessage(clientId, error)));
        }
        catch (err) {
            log.error('[gRPC] Failed to send error:', err);
        }
    }
    handleDisconnection(clientId) {
        const client = this.clients.get(clientId);
        if (!client)
            return;
        const agentId = client.agentId;
        this.clients.delete(clientId);
        if (agentId !== 'unknown' && this.agents.has(agentId)) {
            // Check if this was the last connection for this agent
            let hasOtherConnections = false;
            for (const [cid, c] of this.clients.entries()) {
                if (cid !== clientId && c.agentId === agentId) {
                    hasOtherConnections = true;
                    break;
                }
            }
            if (!hasOtherConnections) {
                this.agents.delete(agentId);
                log.info(`[gRPC] Agent disconnected: ${agentId}`);
                this.options.onAgentDisconnected?.(agentId);
                this.emit('agentDisconnected', agentId);
            }
        }
    }
    // Public API
    async start() {
        return new Promise((resolve, reject) => {
            this.server.bindAsync(`127.0.0.1:${this.options.port}`, grpc.ServerCredentials.createInsecure(), (error, port) => {
                if (error) {
                    reject(error);
                    return;
                }
                this.server.start();
                log.info(`[gRPC] Server started on port ${port}`);
                resolve();
            });
        });
    }
    async stop() {
        return new Promise((resolve) => {
            this.server.tryShutdown(() => {
                log.info('[gRPC] Server stopped');
                resolve();
            });
        });
    }
    sendFrame(frame) {
        return this.sendToAgent(frame.toAgentId, frame);
    }
    sendChat(fromAgentId, toAgentId, message, conversationId, extraPayload) {
        const frame = GrpcFrames.createChatMessage(fromAgentId, toAgentId, message, conversationId, extraPayload);
        return this.sendFrame(frame);
    }
    sendTask(fromAgentId, toAgentId, taskId, tool, params, context) {
        const safeParams = sanitizeGrpcPayload({
            ...params,
            __context: context,
        });
        const frame = GrpcFrames.createTaskStarted(taskId, fromAgentId, toAgentId, tool, safeParams);
        return this.sendFrame(frame);
    }
    cancelTask(fromAgentId, toAgentId, taskId, reason) {
        const frame = GrpcFrames.createTaskCancelled(taskId, fromAgentId, toAgentId, reason);
        return this.sendFrame(frame);
    }
    broadcast(fromAgentId, message) {
        const frame = GrpcFrames.createChatMessage(fromAgentId, 'broadcast', message);
        let count = 0;
        this.agents.forEach((agent, agentId) => {
            if (agentId !== fromAgentId) {
                if (this.sendToAgent(agentId, frame)) {
                    count++;
                }
            }
        });
        return count;
    }
    getConnectedAgents() {
        const agents = [];
        this.agents.forEach(agent => {
            agents.push({
                id: agent.id,
                name: agent.name,
                persona: agent.persona,
                capabilities: agent.capabilities,
            });
        });
        return agents;
    }
    getAgentCount() {
        return this.agents.size;
    }
    isAgentConnected(agentId) {
        return this.agents.has(agentId);
    }
}
//# sourceMappingURL=grpc-server.js.map