import { logger } from "../logger.js";
/**
 * Dispatches commands received from the onboarding service heartbeat.
 * Each command is executed asynchronously and independently.
 * Completed/failed acks are queued for the next heartbeat.
 */
export class CommandDispatcher {
    handlers = new Map();
    pendingAcks = [];
    inFlight = new Set();
    /** Commands that have completed but whose acks may not have reached the server yet. */
    completed = new Set();
    registerHandler(command, handler) {
        this.handlers.set(command, handler);
    }
    /**
     * Process pending commands from a heartbeat response.
     * Each command is dispatched independently (fire-and-forget).
     */
    dispatch(commands) {
        for (const cmd of commands) {
            if (this.inFlight.has(cmd.id) || this.completed.has(cmd.id))
                continue;
            const handler = this.handlers.get(cmd.command);
            if (!handler) {
                logger.warn(`[command-dispatcher] Unknown command: ${cmd.command} (id=${cmd.id})`);
                this.pendingAcks.push({
                    commandId: cmd.id,
                    status: "failed",
                    result: JSON.stringify({ error: `Unknown command: ${cmd.command}` }),
                });
                continue;
            }
            this.inFlight.add(cmd.id);
            logger.system(`[command-dispatcher] Executing command: ${cmd.command} (id=${cmd.id})`);
            void handler(cmd.id, cmd.payload)
                .then((result) => {
                logger.system(`[command-dispatcher] Command ${cmd.command} (id=${cmd.id}) ${result.status}`);
                this.pendingAcks.push({
                    commandId: cmd.id,
                    status: result.status,
                    result: result.result,
                });
                this.completed.add(cmd.id);
            })
                .catch((err) => {
                const errMsg = err instanceof Error ? err.message : String(err);
                logger.err(`[command-dispatcher] Command ${cmd.command} (id=${cmd.id}) threw: ${errMsg}`);
                this.pendingAcks.push({
                    commandId: cmd.id,
                    status: "failed",
                    result: JSON.stringify({ error: errMsg }),
                });
                this.completed.add(cmd.id);
            })
                .finally(() => {
                this.inFlight.delete(cmd.id);
            });
        }
    }
    /**
     * Drain all queued acks for inclusion in the next heartbeat.
     */
    drainAcks() {
        if (this.pendingAcks.length === 0)
            return [];
        return this.pendingAcks.splice(0);
    }
    /**
     * Notify the dispatcher that acks were successfully delivered to the server.
     * Clears the completed-set entries for those command IDs so they don't leak.
     */
    confirmAcksDelivered(acks) {
        for (const ack of acks) {
            this.completed.delete(ack.commandId);
        }
    }
    /**
     * Put acks back into the queue (e.g. when the heartbeat POST fails
     * and the acks were not delivered).
     */
    requeueAcks(acks) {
        this.pendingAcks.unshift(...acks);
    }
    /**
     * Queue a single ack (e.g. a synthetic ack for a command whose ack was
     * lost due to process restart).
     */
    queueAck(ack) {
        this.pendingAcks.push(ack);
    }
}
//# sourceMappingURL=command-dispatcher.js.map