/**
 * SMTP Session Manager
 * Responsible for creating, managing, and cleaning up SMTP sessions
 */
import * as plugins from '../../../plugins.js';
import { SmtpState } from './interfaces.js';
import { SMTP_DEFAULTS } from './constants.js';
import { generateSessionId, getSocketDetails } from './utils/helpers.js';
import { SmtpLogger } from './utils/logging.js';
/**
 * Manager for SMTP sessions
 * Handles session creation, tracking, timeout management, and cleanup
 */
export class SessionManager {
    /**
     * Map of socket ID to session
     */
    sessions = new Map();
    /**
     * Map of socket to socket ID
     */
    socketIds = new Map();
    /**
     * SMTP server options
     */
    options;
    /**
     * Event listeners
     */
    eventListeners = {};
    /**
     * Timer for cleanup interval
     */
    cleanupTimer = null;
    /**
     * Creates a new session manager
     * @param options - Session manager options
     */
    constructor(options = {}) {
        this.options = {
            socketTimeout: options.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT,
            connectionTimeout: options.connectionTimeout || SMTP_DEFAULTS.CONNECTION_TIMEOUT,
            cleanupInterval: options.cleanupInterval || SMTP_DEFAULTS.CLEANUP_INTERVAL
        };
        // Start the cleanup timer
        this.startCleanupTimer();
    }
    /**
     * Creates a new session for a socket connection
     * @param socket - Client socket
     * @param secure - Whether the connection is secure (TLS)
     * @returns New SMTP session
     */
    createSession(socket, secure) {
        const sessionId = generateSessionId();
        const socketDetails = getSocketDetails(socket);
        // Create a new session
        const session = {
            id: sessionId,
            state: SmtpState.GREETING,
            clientHostname: '',
            mailFrom: '',
            rcptTo: [],
            emailData: '',
            emailDataChunks: [],
            emailDataSize: 0,
            useTLS: secure || false,
            connectionEnded: false,
            remoteAddress: socketDetails.remoteAddress,
            remotePort: socketDetails.remotePort,
            createdAt: new Date(),
            secure: secure || false,
            authenticated: false,
            envelope: {
                mailFrom: { address: '', args: {} },
                rcptTo: []
            },
            lastActivity: Date.now()
        };
        // Store session with unique ID
        const socketKey = this.getSocketKey(socket);
        this.socketIds.set(socket, socketKey);
        this.sessions.set(socketKey, session);
        // Set socket timeout
        socket.setTimeout(this.options.socketTimeout);
        // Emit session created event
        this.emitEvent('created', session, socket);
        // Log session creation
        SmtpLogger.info(`Created SMTP session ${sessionId}`, {
            sessionId,
            remoteAddress: session.remoteAddress,
            remotePort: socketDetails.remotePort,
            secure: session.secure
        });
        return session;
    }
    /**
     * Updates the session state
     * @param session - SMTP session
     * @param newState - New state
     */
    updateSessionState(session, newState) {
        if (session.state === newState) {
            return;
        }
        const previousState = session.state;
        session.state = newState;
        // Update activity timestamp
        this.updateSessionActivity(session);
        // Emit state changed event
        this.emitEvent('stateChanged', session, previousState, newState);
        // Log state change
        SmtpLogger.debug(`Session ${session.id} state changed from ${previousState} to ${newState}`, {
            sessionId: session.id,
            previousState,
            newState,
            remoteAddress: session.remoteAddress
        });
    }
    /**
     * Updates the session's last activity timestamp
     * @param session - SMTP session
     */
    updateSessionActivity(session) {
        session.lastActivity = Date.now();
    }
    /**
     * Removes a session
     * @param socket - Client socket
     */
    removeSession(socket) {
        const socketKey = this.socketIds.get(socket);
        if (!socketKey) {
            return;
        }
        const session = this.sessions.get(socketKey);
        if (session) {
            // Mark the session as ended
            session.connectionEnded = true;
            // Clear any data timeout if it exists
            if (session.dataTimeoutId) {
                clearTimeout(session.dataTimeoutId);
                session.dataTimeoutId = undefined;
            }
            // Emit session completed event
            this.emitEvent('completed', session, socket);
            // Log session removal
            SmtpLogger.info(`Removed SMTP session ${session.id}`, {
                sessionId: session.id,
                remoteAddress: session.remoteAddress,
                finalState: session.state
            });
        }
        // Remove from maps
        this.sessions.delete(socketKey);
        this.socketIds.delete(socket);
    }
    /**
     * Gets a session for a socket
     * @param socket - Client socket
     * @returns SMTP session or undefined if not found
     */
    getSession(socket) {
        const socketKey = this.socketIds.get(socket);
        if (!socketKey) {
            return undefined;
        }
        return this.sessions.get(socketKey);
    }
    /**
     * Cleans up idle sessions
     */
    cleanupIdleSessions() {
        const now = Date.now();
        let timedOutCount = 0;
        for (const [socketKey, session] of this.sessions.entries()) {
            if (session.connectionEnded) {
                // Session already marked as ended, but still in map
                this.sessions.delete(socketKey);
                continue;
            }
            // Calculate how long the session has been idle
            const lastActivity = session.lastActivity || 0;
            const idleTime = now - lastActivity;
            // Use appropriate timeout based on session state
            const timeout = session.state === SmtpState.DATA_RECEIVING
                ? this.options.socketTimeout * 2 // Double timeout for data receiving
                : session.state === SmtpState.GREETING
                    ? this.options.connectionTimeout // Initial connection timeout
                    : this.options.socketTimeout; // Standard timeout for other states
            // Check if session has timed out
            if (idleTime > timeout) {
                // Find the socket for this session
                let timedOutSocket;
                for (const [socket, key] of this.socketIds.entries()) {
                    if (key === socketKey) {
                        timedOutSocket = socket;
                        break;
                    }
                }
                if (timedOutSocket) {
                    // Emit timeout event
                    this.emitEvent('timeout', session, timedOutSocket);
                    // Log timeout
                    SmtpLogger.warn(`Session ${session.id} timed out after ${Math.round(idleTime / 1000)}s of inactivity`, {
                        sessionId: session.id,
                        remoteAddress: session.remoteAddress,
                        state: session.state,
                        idleTime
                    });
                    // End the socket connection
                    try {
                        timedOutSocket.end();
                    }
                    catch (error) {
                        SmtpLogger.error(`Error ending timed out socket: ${error instanceof Error ? error.message : String(error)}`, {
                            sessionId: session.id,
                            remoteAddress: session.remoteAddress,
                            error: error instanceof Error ? error : new Error(String(error))
                        });
                    }
                    // Remove from maps
                    this.sessions.delete(socketKey);
                    this.socketIds.delete(timedOutSocket);
                    timedOutCount++;
                }
            }
        }
        if (timedOutCount > 0) {
            SmtpLogger.info(`Cleaned up ${timedOutCount} timed out sessions`, {
                totalSessions: this.sessions.size
            });
        }
    }
    /**
     * Gets the current number of active sessions
     * @returns Number of active sessions
     */
    getSessionCount() {
        return this.sessions.size;
    }
    /**
     * Clears all sessions (used when shutting down)
     */
    clearAllSessions() {
        // Log the action
        SmtpLogger.info(`Clearing all sessions (count: ${this.sessions.size})`);
        // Clear the sessions and socket IDs maps
        this.sessions.clear();
        this.socketIds.clear();
        // Stop the cleanup timer
        this.stopCleanupTimer();
    }
    /**
     * Register an event listener
     * @param event - Event name
     * @param listener - Event listener function
     */
    on(event, listener) {
        switch (event) {
            case 'created':
                if (!this.eventListeners.created) {
                    this.eventListeners.created = new Set();
                }
                this.eventListeners.created.add(listener);
                break;
            case 'stateChanged':
                if (!this.eventListeners.stateChanged) {
                    this.eventListeners.stateChanged = new Set();
                }
                this.eventListeners.stateChanged.add(listener);
                break;
            case 'timeout':
                if (!this.eventListeners.timeout) {
                    this.eventListeners.timeout = new Set();
                }
                this.eventListeners.timeout.add(listener);
                break;
            case 'completed':
                if (!this.eventListeners.completed) {
                    this.eventListeners.completed = new Set();
                }
                this.eventListeners.completed.add(listener);
                break;
            case 'error':
                if (!this.eventListeners.error) {
                    this.eventListeners.error = new Set();
                }
                this.eventListeners.error.add(listener);
                break;
        }
    }
    /**
     * Remove an event listener
     * @param event - Event name
     * @param listener - Event listener function
     */
    off(event, listener) {
        switch (event) {
            case 'created':
                if (this.eventListeners.created) {
                    this.eventListeners.created.delete(listener);
                }
                break;
            case 'stateChanged':
                if (this.eventListeners.stateChanged) {
                    this.eventListeners.stateChanged.delete(listener);
                }
                break;
            case 'timeout':
                if (this.eventListeners.timeout) {
                    this.eventListeners.timeout.delete(listener);
                }
                break;
            case 'completed':
                if (this.eventListeners.completed) {
                    this.eventListeners.completed.delete(listener);
                }
                break;
            case 'error':
                if (this.eventListeners.error) {
                    this.eventListeners.error.delete(listener);
                }
                break;
        }
    }
    /**
     * Emit an event to registered listeners
     * @param event - Event name
     * @param args - Event arguments
     */
    emitEvent(event, ...args) {
        let listeners;
        switch (event) {
            case 'created':
                listeners = this.eventListeners.created;
                break;
            case 'stateChanged':
                listeners = this.eventListeners.stateChanged;
                break;
            case 'timeout':
                listeners = this.eventListeners.timeout;
                break;
            case 'completed':
                listeners = this.eventListeners.completed;
                break;
            case 'error':
                listeners = this.eventListeners.error;
                break;
        }
        if (!listeners) {
            return;
        }
        for (const listener of listeners) {
            try {
                listener(...args);
            }
            catch (error) {
                SmtpLogger.error(`Error in session event listener for ${String(event)}: ${error instanceof Error ? error.message : String(error)}`, {
                    error: error instanceof Error ? error : new Error(String(error))
                });
            }
        }
    }
    /**
     * Start the cleanup timer
     */
    startCleanupTimer() {
        if (this.cleanupTimer) {
            return;
        }
        this.cleanupTimer = setInterval(() => {
            this.cleanupIdleSessions();
        }, this.options.cleanupInterval);
        // Prevent the timer from keeping the process alive
        if (this.cleanupTimer.unref) {
            this.cleanupTimer.unref();
        }
    }
    /**
     * Stop the cleanup timer
     */
    stopCleanupTimer() {
        if (this.cleanupTimer) {
            clearInterval(this.cleanupTimer);
            this.cleanupTimer = null;
        }
    }
    /**
     * Replace socket mapping for STARTTLS upgrades
     * @param oldSocket - Original plain socket
     * @param newSocket - New TLS socket
     * @returns Whether the replacement was successful
     */
    replaceSocket(oldSocket, newSocket) {
        const socketKey = this.socketIds.get(oldSocket);
        if (!socketKey) {
            SmtpLogger.warn('Cannot replace socket - original socket not found in session manager');
            return false;
        }
        const session = this.sessions.get(socketKey);
        if (!session) {
            SmtpLogger.warn('Cannot replace socket - session not found for socket key');
            return false;
        }
        // Remove old socket mapping
        this.socketIds.delete(oldSocket);
        // Add new socket mapping
        this.socketIds.set(newSocket, socketKey);
        // Set socket timeout for new socket
        newSocket.setTimeout(this.options.socketTimeout);
        SmtpLogger.info(`Socket replaced for session ${session.id} (STARTTLS upgrade)`, {
            sessionId: session.id,
            remoteAddress: session.remoteAddress,
            oldSocketType: oldSocket.constructor.name,
            newSocketType: newSocket.constructor.name
        });
        return true;
    }
    /**
     * Gets a unique key for a socket
     * @param socket - Client socket
     * @returns Socket key
     */
    getSocketKey(socket) {
        const details = getSocketDetails(socket);
        return `${details.remoteAddress}:${details.remotePort}-${Date.now()}`;
    }
    /**
     * Get all active sessions
     */
    getAllSessions() {
        return Array.from(this.sessions.values());
    }
    /**
     * Update last activity for a session by socket
     */
    updateLastActivity(socket) {
        const session = this.getSession(socket);
        if (session) {
            this.updateSessionActivity(session);
        }
    }
    /**
     * Check for timed out sessions
     */
    checkTimeouts(timeoutMs) {
        const now = Date.now();
        const timedOutSessions = [];
        for (const session of this.sessions.values()) {
            if (now - session.lastActivity > timeoutMs) {
                timedOutSessions.push(session);
            }
        }
        return timedOutSessions;
    }
    /**
     * Clean up resources
     */
    destroy() {
        // Clear the cleanup timer
        if (this.cleanupTimer) {
            clearInterval(this.cleanupTimer);
            this.cleanupTimer = null;
        }
        // Clear all sessions
        this.clearAllSessions();
        // Clear event listeners
        this.eventListeners = {};
        SmtpLogger.debug('SessionManager destroyed');
    }
}
//# sourceMappingURL=data:application/json;base64, 