/**
 * Multi-Channel Support
 *
 * Inspired by OpenClaw's multi-platform messaging support.
 * Provides unified interface for Telegram, Discord, Slack, and more.
 */
import { EventEmitter } from 'events';
import { logger } from '../utils/logger.js';
import { getSessionIsolator } from './session-isolation.js';
import { getIdentityLinker } from './identity-links.js';
// ============================================================================
// Session Key Helper
// ============================================================================
/**
 * Generate a session key for an inbound message using the SessionIsolator.
 *
 * This is a convenience function that uses the singleton SessionIsolator
 * to compute a session key from the message's channel and sender info.
 * Returns undefined if session isolation is not available.
 *
 * @param message - The inbound message to generate a key for
 * @param accountId - Optional bot account ID for multi-account setups
 * @returns The session key string, or undefined if isolation fails
 */
export function getSessionKey(message, accountId) {
    try {
        const isolator = getSessionIsolator();
        return isolator.getSessionKey(message, accountId);
    }
    catch {
        return undefined;
    }
}
// ============================================================================
// Identity Resolution Helper
// ============================================================================
/**
 * Resolve the canonical identity for an inbound message's sender.
 *
 * Uses the singleton IdentityLinker to look up cross-channel identity links.
 * Returns null if no canonical identity exists for the sender.
 *
 * @param message - The inbound message whose sender to resolve
 * @returns The canonical identity, or null if no link exists
 */
export function getCanonicalIdentity(message) {
    try {
        const linker = getIdentityLinker();
        return linker.resolve({
            channelType: message.channel.type,
            peerId: message.sender.id,
        });
    }
    catch {
        return null;
    }
}
// ============================================================================
// Peer Routing Helper
// ============================================================================
import { getPeerRouter } from './peer-routing.js';
/**
 * Resolve the best route for an inbound message using the singleton PeerRouter.
 *
 * This is a convenience function similar to `getSessionKey()` and
 * `checkDMPairing()`. It delegates to the singleton PeerRouter and
 * returns the resolved route (or null if no route matches).
 *
 * @param message - The inbound message to resolve a route for
 * @param accountId - Optional bot account ID for multi-account setups
 * @returns The resolved route with agent config, or null if no route matches
 */
export function resolveRoute(message, accountId) {
    try {
        const router = getPeerRouter();
        return router.resolve(message, accountId);
    }
    catch {
        return null;
    }
}
/**
 * Get the effective agent configuration for an inbound message.
 *
 * Resolves the route and merges the matched agent config with defaults.
 * Always returns a config object (falls back to default if no route matches).
 *
 * @param message - The inbound message to get config for
 * @param accountId - Optional bot account ID for multi-account setups
 * @returns The effective agent configuration
 */
export function getRouteAgentConfig(message, accountId) {
    try {
        const router = getPeerRouter();
        return router.getAgentConfig(message, accountId);
    }
    catch {
        return {};
    }
}
// ============================================================================
// Base Channel Class
// ============================================================================
/**
 * Abstract base class for channel implementations
 */
export class BaseChannel extends EventEmitter {
    type;
    config;
    status;
    constructor(type, config) {
        super();
        this.type = type;
        this.config = config;
        this.status = {
            type,
            connected: false,
            authenticated: false,
        };
    }
    /**
     * Get channel status
     */
    getStatus() {
        return { ...this.status };
    }
    /**
     * Check if user is allowed
     */
    isUserAllowed(userId) {
        if (!this.config.allowedUsers || this.config.allowedUsers.length === 0) {
            return true;
        }
        return this.config.allowedUsers.includes(userId);
    }
    /**
     * Check if channel is allowed
     */
    isChannelAllowed(channelId) {
        if (!this.config.allowedChannels || this.config.allowedChannels.length === 0) {
            return true;
        }
        return this.config.allowedChannels.includes(channelId);
    }
    /**
     * Parse command from message
     */
    parseCommand(message) {
        if (message.content.startsWith('/')) {
            const parts = message.content.slice(1).split(/\s+/);
            return {
                ...message,
                isCommand: true,
                commandName: parts[0],
                commandArgs: parts.slice(1),
            };
        }
        return message;
    }
    /**
     * Format message for sending
     */
    formatMessage(content, parseMode) {
        if (parseMode === 'plain') {
            // Strip markdown/html
            return content
                .replace(/[*_`~]/g, '')
                .replace(/<[^>]+>/g, '');
        }
        return content;
    }
}
// ============================================================================
// Mock Channel (for testing)
// ============================================================================
export class MockChannel extends BaseChannel {
    messages = [];
    sentMessages = [];
    constructor(config = {}) {
        const channelType = config.type || 'cli';
        super(channelType, {
            type: channelType,
            enabled: true,
            ...config,
        });
    }
    async connect() {
        this.status.connected = true;
        this.status.authenticated = true;
        this.emit('connected', this.type);
    }
    async disconnect() {
        this.status.connected = false;
        this.emit('disconnected', this.type);
    }
    async send(message) {
        this.sentMessages.push(message);
        return {
            success: true,
            messageId: `mock-${Date.now()}`,
            timestamp: new Date(),
        };
    }
    /**
     * Simulate receiving a message
     */
    simulateMessage(content, options = {}) {
        const message = {
            id: `msg-${Date.now()}`,
            channel: {
                id: 'mock-channel',
                type: 'cli',
                name: 'Mock Channel',
            },
            sender: {
                id: 'mock-user',
                username: 'testuser',
                displayName: 'Test User',
            },
            content,
            contentType: 'text',
            timestamp: new Date(),
            ...options,
        };
        const parsed = this.parseCommand(message);
        this.messages.push(parsed);
        this.emit('message', parsed);
        if (parsed.isCommand) {
            this.emit('command', parsed);
        }
        return parsed;
    }
    /**
     * Get received messages
     */
    getMessages() {
        return [...this.messages];
    }
    /**
     * Get sent messages
     */
    getSentMessages() {
        return [...this.sentMessages];
    }
    /**
     * Clear messages
     */
    clear() {
        this.messages = [];
        this.sentMessages = [];
    }
}
// ============================================================================
// Channel Manager
// ============================================================================
/**
 * Manages multiple channels
 */
export class ChannelManager extends EventEmitter {
    channels = new Map();
    messageHandlers = [];
    /**
     * Register a channel
     */
    registerChannel(channel) {
        this.channels.set(channel.type, channel);
        // Forward events
        channel.on('message', (message) => {
            this.emit('message', message, channel);
            this.handleMessage(message, channel);
        });
        channel.on('command', (message) => {
            this.emit('command', message, channel);
        });
        channel.on('connected', (type) => {
            this.emit('channel-connected', type);
        });
        channel.on('disconnected', (type, error) => {
            this.emit('channel-disconnected', type, error);
        });
        channel.on('error', (type, error) => {
            this.emit('channel-error', type, error);
        });
    }
    /**
     * Unregister a channel
     */
    unregisterChannel(type) {
        const channel = this.channels.get(type);
        if (channel) {
            channel.removeAllListeners();
            this.channels.delete(type);
        }
    }
    /**
     * Get a channel
     */
    getChannel(type) {
        return this.channels.get(type);
    }
    /**
     * Get all channels
     */
    getAllChannels() {
        return Array.from(this.channels.values());
    }
    /**
     * Connect all channels
     */
    async connectAll() {
        const promises = Array.from(this.channels.values()).map(c => c.connect());
        await Promise.all(promises);
    }
    /**
     * Disconnect all channels
     */
    async disconnectAll() {
        const promises = Array.from(this.channels.values()).map(c => c.disconnect());
        await Promise.all(promises);
    }
    /**
     * Get status of all channels
     */
    getStatus() {
        const status = {};
        for (const [type, channel] of this.channels) {
            status[type] = channel.getStatus();
        }
        return status;
    }
    /**
     * Send to a specific channel
     */
    async send(type, message) {
        const channel = this.channels.get(type);
        if (!channel) {
            return {
                success: false,
                error: `Channel ${type} not found`,
                timestamp: new Date(),
            };
        }
        return channel.send(message);
    }
    /**
     * Broadcast to all connected channels
     */
    async broadcast(message) {
        const results = new Map();
        for (const [type, channel] of this.channels) {
            if (channel.getStatus().connected) {
                const result = await channel.send({
                    ...message,
                    channelId: '*', // Will be handled by each channel
                });
                results.set(type, result);
            }
        }
        return results;
    }
    /**
     * Add message handler
     */
    onMessage(handler) {
        this.messageHandlers.push(handler);
    }
    /**
     * Handle incoming message
     */
    async handleMessage(message, channel) {
        for (const handler of this.messageHandlers) {
            try {
                await handler(message, channel);
            }
            catch (error) {
                this.emit('error', channel.type, error);
            }
        }
    }
    // =========================================================================
    // Proactive Messaging (Phase 4 - Iteration 4)
    // =========================================================================
    outgoingQueue = [];
    processingQueue = false;
    /**
     * Send a message to a user across channels with priority
     */
    async sendToUser(channelType, channelId, content, priority = 'normal') {
        const priorityMap = { low: 0, normal: 1, high: 2, urgent: 3 };
        const message = {
            channelId,
            content,
            silent: priority === 'low',
        };
        this.outgoingQueue.push({ type: channelType, message, priority: priorityMap[priority] });
        // Sort by priority descending
        this.outgoingQueue.sort((a, b) => b.priority - a.priority);
        return this.processQueue();
    }
    /**
     * Process the outgoing message queue
     */
    async processQueue() {
        if (this.processingQueue || this.outgoingQueue.length === 0) {
            return { success: true, timestamp: new Date() };
        }
        this.processingQueue = true;
        let lastResult = { success: true, timestamp: new Date() };
        try {
            while (this.outgoingQueue.length > 0) {
                const item = this.outgoingQueue.shift();
                lastResult = await this.send(item.type, item.message);
                this.emit('message:sent', { type: item.type, result: lastResult });
            }
        }
        finally {
            this.processingQueue = false;
        }
        return lastResult;
    }
    /**
     * Shutdown
     */
    async shutdown() {
        await this.disconnectAll();
        this.channels.clear();
        this.messageHandlers = [];
        this.removeAllListeners();
    }
}
// ============================================================================
// Singleton
// ============================================================================
let managerInstance = null;
export function getChannelManager() {
    if (!managerInstance) {
        managerInstance = new ChannelManager();
    }
    return managerInstance;
}
export function resetChannelManager() {
    if (managerInstance) {
        managerInstance.shutdown().catch((err) => {
            logger.debug('Channel manager shutdown error (ignored)', { error: err instanceof Error ? err.message : String(err) });
        });
        managerInstance = null;
    }
}
// ============================================================================
// Re-exports
// ============================================================================
// Telegram
export { TelegramChannel } from './telegram/index.js';
// Discord
export { DiscordChannel } from './discord/index.js';
// Slack
export { SlackChannel } from './slack/index.js';
// Webhook Server
export { WebhookServer, getWebhookServer, resetWebhookServer, } from './webhook-server.js';
export { DEFAULT_SESSION_ISOLATION_CONFIG, SessionIsolator, getSessionIsolator, resetSessionIsolator, } from './session-isolation.js';
export { IdentityLinker, getIdentityLinker, resetIdentityLinker, } from './identity-links.js';
export { PeerRouter, getPeerRouter, resetPeerRouter, } from './peer-routing.js';
export { DEFAULT_DM_PAIRING_CONFIG, DMPairingManager, getDMPairing, resetDMPairing, } from './dm-pairing.js';
// ============================================================================
// Lane Queue Helpers
// ============================================================================
import { LaneQueue } from '../concurrency/lane-queue.js';
/** Dedicated LaneQueue instance for channel message processing */
let channelLaneQueue = null;
/**
 * Get the singleton LaneQueue for channel message processing.
 *
 * This is separate from the agent executor's lane queue so that
 * channel-level serialization and tool-level serialization each
 * have their own queue configurations.
 */
export function getChannelLaneQueue() {
    if (!channelLaneQueue) {
        channelLaneQueue = new LaneQueue({
            maxParallel: 3,
            defaultTimeout: 120000, // 2 minutes for message processing
            maxPending: 200,
        });
    }
    return channelLaneQueue;
}
/**
 * Reset the channel lane queue (for testing).
 */
export function resetChannelLaneQueue() {
    if (channelLaneQueue) {
        channelLaneQueue.clear();
    }
    channelLaneQueue = null;
}
/**
 * Enqueue a message handler for serial execution within a session.
 *
 * Messages for the same session key are processed one at a time
 * (serial by default). Different sessions run in parallel.
 *
 * @param sessionKey - The session key used as the lane ID
 * @param handler - Async function to process the message
 * @param options - Optional task options (e.g., timeout, priority)
 * @returns Promise that resolves with the handler's return value
 */
export function enqueueMessage(sessionKey, handler, options) {
    const queue = getChannelLaneQueue();
    return queue.enqueue(sessionKey, handler, {
        category: 'channel-message',
        ...options,
    });
}
// ============================================================================
// DM Pairing Helper
// ============================================================================
import { getDMPairing } from './dm-pairing.js';
/**
 * Check if a DM sender is approved via the pairing system.
 *
 * When DM pairing is disabled (the default), this always returns
 * `{ approved: true }` so callers can skip gating logic.
 *
 * @param message - The inbound message to check
 * @returns PairingStatus indicating approval state and pairing code if needed
 */
export async function checkDMPairing(message) {
    const pairing = getDMPairing();
    // If pairing is not enabled, always approve
    if (!pairing.requiresPairing(message.channel.type)) {
        return {
            approved: true,
            senderId: message.sender.id,
            channelType: message.channel.type,
        };
    }
    // Only gate DM messages, not group messages
    if (!message.channel.isDM) {
        return {
            approved: true,
            senderId: message.sender.id,
            channelType: message.channel.type,
        };
    }
    return pairing.checkSender(message);
}
//# sourceMappingURL=index.js.map