/**
 * Session Isolation
 *
 * OpenClaw-inspired session scoping for multi-channel DM handling.
 *
 * Session scopes:
 * - per-channel-peer: Separate session per (channel, peer) pair
 *   Prevents cross-user context leakage
 * - per-account-channel-peer: Per (account, channel, peer) triple
 *   For multiple bot accounts on the same channel
 * - global: Single session for all peers (default for CLI)
 *
 * Usage:
 * ```typescript
 * const isolator = getSessionIsolator();
 *
 * // Get session key for a message
 * const sessionKey = isolator.getSessionKey(message);
 *
 * // Check if sessions are isolated
 * isolator.areIsolated(sessionKey1, sessionKey2); // true if different scopes
 * ```
 */
import { EventEmitter } from 'events';
// ============================================================================
// Default Configuration
// ============================================================================
export const DEFAULT_SESSION_ISOLATION_CONFIG = {
    defaultScope: 'per-channel-peer',
    maxSessions: 1000,
    sessionTtlMs: 24 * 60 * 60 * 1000, // 24 hours
    trackActivity: true,
};
// ============================================================================
// Session Isolator
// ============================================================================
export class SessionIsolator extends EventEmitter {
    config;
    sessions = new Map();
    cleanupTimer = null;
    constructor(config = {}) {
        super();
        this.config = { ...DEFAULT_SESSION_ISOLATION_CONFIG, ...config };
        // Start cleanup timer if TTL is set
        if (this.config.sessionTtlMs > 0) {
            this.cleanupTimer = setInterval(() => this.cleanupExpiredSessions(), Math.min(this.config.sessionTtlMs / 2, 60000));
        }
    }
    // ==========================================================================
    // Session Key Generation
    // ==========================================================================
    /**
     * Set or replace the identity linker used for canonical identity resolution
     */
    setIdentityLinker(linker) {
        this.config.identityLinker = linker;
    }
    /**
     * Generate session key for an inbound message.
     *
     * If an identity linker is configured, the sender's peer ID is resolved
     * to its canonical identity ID before computing the session key. This
     * allows the same person on different channels to share a session when
     * their identities are linked.
     */
    getSessionKey(message, accountId) {
        let peerId = message.sender.id;
        let channelType = message.channel.type;
        let channelId = message.channel.id;
        // Resolve canonical identity if linker is available
        if (this.config.identityLinker) {
            const canonical = this.config.identityLinker.resolve({
                channelType: message.channel.type,
                peerId: message.sender.id,
            });
            if (canonical) {
                // Use the canonical identity ID as the peer ID so that
                // linked identities across channels produce the same key.
                peerId = canonical.id;
                // Use the first identity's channel info as the canonical reference
                // so cross-channel identities converge to the same session.
                const primary = canonical.identities[0];
                if (primary) {
                    channelType = primary.channelType;
                    channelId = `canonical-${canonical.id}`;
                }
            }
        }
        const components = {
            accountId,
            channelType,
            channelId,
            peerId,
        };
        return this.computeSessionKey(components);
    }
    /**
     * Generate session key from explicit components
     */
    computeSessionKey(components) {
        const scope = this.getScopeForChannel(components.channelType);
        let key;
        switch (scope) {
            case 'global':
                key = 'global';
                break;
            case 'per-channel':
                key = `${components.channelType}:${components.channelId}`;
                break;
            case 'per-channel-peer':
                key = `${components.channelType}:${components.channelId}:${components.peerId}`;
                break;
            case 'per-account-channel-peer':
                key = `${components.accountId || 'default'}:${components.channelType}:${components.channelId}:${components.peerId}`;
                break;
        }
        // Track session
        if (!this.sessions.has(key)) {
            if (this.sessions.size >= this.config.maxSessions) {
                this.evictOldestSession();
            }
            const info = {
                key,
                components,
                scope,
                createdAt: new Date(),
                lastActivityAt: new Date(),
                messageCount: 0,
            };
            this.sessions.set(key, info);
            this.emit('session:created', info);
        }
        // Update activity
        const session = this.sessions.get(key);
        session.lastActivityAt = new Date();
        session.messageCount++;
        return key;
    }
    /**
     * Get the scope for a given channel type
     */
    getScopeForChannel(channelType) {
        return this.config.channelScopes?.[channelType] ?? this.config.defaultScope;
    }
    // ==========================================================================
    // Session Management
    // ==========================================================================
    /**
     * Check if two session keys are isolated from each other
     */
    areIsolated(key1, key2) {
        return key1 !== key2;
    }
    /**
     * Get session info
     */
    getSession(key) {
        return this.sessions.get(key);
    }
    /**
     * List all active sessions
     */
    listSessions() {
        return Array.from(this.sessions.values());
    }
    /**
     * List sessions for a specific channel
     */
    listSessionsForChannel(channelType) {
        return Array.from(this.sessions.values())
            .filter(s => s.components.channelType === channelType);
    }
    /**
     * List sessions for a specific peer across all channels
     */
    listSessionsForPeer(peerId) {
        return Array.from(this.sessions.values())
            .filter(s => s.components.peerId === peerId);
    }
    /**
     * Remove a session
     */
    removeSession(key) {
        const session = this.sessions.get(key);
        if (session) {
            this.sessions.delete(key);
            this.emit('session:removed', session);
            return true;
        }
        return false;
    }
    /**
     * Set scope for a specific channel
     */
    setChannelScope(channelType, scope) {
        if (!this.config.channelScopes) {
            this.config.channelScopes = {};
        }
        this.config.channelScopes[channelType] = scope;
        this.emit('scope:changed', channelType, scope);
    }
    // ==========================================================================
    // Cleanup
    // ==========================================================================
    /**
     * Clean up expired sessions
     */
    cleanupExpiredSessions() {
        if (this.config.sessionTtlMs <= 0)
            return;
        const now = Date.now();
        const expired = [];
        for (const [key, session] of this.sessions) {
            if (now - session.lastActivityAt.getTime() > this.config.sessionTtlMs) {
                expired.push(key);
            }
        }
        for (const key of expired) {
            this.removeSession(key);
        }
        if (expired.length > 0) {
            this.emit('sessions:cleaned', expired.length);
        }
    }
    /**
     * Evict the oldest session when at capacity
     */
    evictOldestSession() {
        let oldestKey = null;
        let oldestTime = Infinity;
        for (const [key, session] of this.sessions) {
            if (session.lastActivityAt.getTime() < oldestTime) {
                oldestTime = session.lastActivityAt.getTime();
                oldestKey = key;
            }
        }
        if (oldestKey) {
            this.removeSession(oldestKey);
        }
    }
    // ==========================================================================
    // Statistics
    // ==========================================================================
    /**
     * Get isolation statistics
     */
    getStats() {
        const sessionsByScope = {
            global: 0,
            'per-channel': 0,
            'per-channel-peer': 0,
            'per-account-channel-peer': 0,
        };
        const sessionsByChannel = {};
        let oldest = null;
        let newest = null;
        for (const session of this.sessions.values()) {
            sessionsByScope[session.scope]++;
            const ct = session.components.channelType;
            sessionsByChannel[ct] = (sessionsByChannel[ct] || 0) + 1;
            if (!oldest || session.createdAt < oldest)
                oldest = session.createdAt;
            if (!newest || session.createdAt > newest)
                newest = session.createdAt;
        }
        return {
            totalSessions: this.sessions.size,
            sessionsByScope,
            sessionsByChannel,
            oldestSession: oldest,
            newestSession: newest,
        };
    }
    // ==========================================================================
    // Lifecycle
    // ==========================================================================
    /**
     * Dispose the isolator
     */
    dispose() {
        if (this.cleanupTimer) {
            clearInterval(this.cleanupTimer);
            this.cleanupTimer = null;
        }
        this.sessions.clear();
        this.removeAllListeners();
    }
}
// ============================================================================
// Singleton
// ============================================================================
let isolatorInstance = null;
export function getSessionIsolator(config) {
    if (!isolatorInstance) {
        isolatorInstance = new SessionIsolator(config);
    }
    return isolatorInstance;
}
export function resetSessionIsolator() {
    if (isolatorInstance) {
        isolatorInstance.dispose();
    }
    isolatorInstance = null;
}
//# sourceMappingURL=session-isolation.js.map