/**
 * SMTP Session Manager
 * Responsible for creating, managing, and cleaning up SMTP sessions
 */

import * as plugins from '../../../plugins.js';
import { SmtpState } from './interfaces.js';
import type { ISmtpSession, ISmtpEnvelope } from './interfaces.js';
import type { ISessionManager, ISessionEvents } 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 implements ISessionManager {
  /**
   * Map of socket ID to session
   */
  private sessions: Map<string, ISmtpSession> = new Map();
  
  /**
   * Map of socket to socket ID
   */
  private socketIds: Map<plugins.net.Socket | plugins.tls.TLSSocket, string> = new Map();
  
  /**
   * SMTP server options
   */
  private options: {
    socketTimeout: number;
    connectionTimeout: number;
    cleanupInterval: number;
  };
  
  /**
   * Event listeners
   */
  private eventListeners: {
    created?: Set<(session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void>;
    stateChanged?: Set<(session: ISmtpSession, previousState: SmtpState, newState: SmtpState) => void>;
    timeout?: Set<(session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void>;
    completed?: Set<(session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void>;
    error?: Set<(session: ISmtpSession, error: Error) => void>;
  } = {};
  
  /**
   * Timer for cleanup interval
   */
  private cleanupTimer: NodeJS.Timeout | null = null;
  
  /**
   * Creates a new session manager
   * @param options - Session manager options
   */
  constructor(options: {
    socketTimeout?: number;
    connectionTimeout?: number;
    cleanupInterval?: number;
  } = {}) {
    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
   */
  public createSession(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure: boolean): ISmtpSession {
    const sessionId = generateSessionId();
    const socketDetails = getSocketDetails(socket);
    
    // Create a new session
    const session: ISmtpSession = {
      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
   */
  public updateSessionState(session: ISmtpSession, newState: SmtpState): void {
    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
   */
  public updateSessionActivity(session: ISmtpSession): void {
    session.lastActivity = Date.now();
  }
  
  /**
   * Removes a session
   * @param socket - Client socket
   */
  public removeSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
    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
   */
  public getSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): ISmtpSession | undefined {
    const socketKey = this.socketIds.get(socket);
    if (!socketKey) {
      return undefined;
    }
    
    return this.sessions.get(socketKey);
  }
  
  /**
   * Cleans up idle sessions
   */
  public cleanupIdleSessions(): void {
    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: plugins.net.Socket | plugins.tls.TLSSocket | undefined;
        
        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
   */
  public getSessionCount(): number {
    return this.sessions.size;
  }
  
  /**
   * Clears all sessions (used when shutting down)
   */
  public clearAllSessions(): void {
    // 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
   */
  public on<K extends keyof ISessionEvents>(event: K, listener: ISessionEvents[K]): void {
    switch (event) {
      case 'created':
        if (!this.eventListeners.created) {
          this.eventListeners.created = new Set();
        }
        this.eventListeners.created.add(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void);
        break;
      case 'stateChanged':
        if (!this.eventListeners.stateChanged) {
          this.eventListeners.stateChanged = new Set();
        }
        this.eventListeners.stateChanged.add(listener as (session: ISmtpSession, previousState: SmtpState, newState: SmtpState) => void);
        break;
      case 'timeout':
        if (!this.eventListeners.timeout) {
          this.eventListeners.timeout = new Set();
        }
        this.eventListeners.timeout.add(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void);
        break;
      case 'completed':
        if (!this.eventListeners.completed) {
          this.eventListeners.completed = new Set();
        }
        this.eventListeners.completed.add(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void);
        break;
      case 'error':
        if (!this.eventListeners.error) {
          this.eventListeners.error = new Set();
        }
        this.eventListeners.error.add(listener as (session: ISmtpSession, error: Error) => void);
        break;
    }
  }
  
  /**
   * Remove an event listener
   * @param event - Event name
   * @param listener - Event listener function
   */
  public off<K extends keyof ISessionEvents>(event: K, listener: ISessionEvents[K]): void {
    switch (event) {
      case 'created':
        if (this.eventListeners.created) {
          this.eventListeners.created.delete(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void);
        }
        break;
      case 'stateChanged':
        if (this.eventListeners.stateChanged) {
          this.eventListeners.stateChanged.delete(listener as (session: ISmtpSession, previousState: SmtpState, newState: SmtpState) => void);
        }
        break;
      case 'timeout':
        if (this.eventListeners.timeout) {
          this.eventListeners.timeout.delete(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void);
        }
        break;
      case 'completed':
        if (this.eventListeners.completed) {
          this.eventListeners.completed.delete(listener as (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void);
        }
        break;
      case 'error':
        if (this.eventListeners.error) {
          this.eventListeners.error.delete(listener as (session: ISmtpSession, error: Error) => void);
        }
        break;
    }
  }
  
  /**
   * Emit an event to registered listeners
   * @param event - Event name
   * @param args - Event arguments
   */
  private emitEvent<K extends keyof ISessionEvents>(event: K, ...args: any[]): void {
    let listeners: Set<any> | undefined;
    
    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 as Function)(...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
   */
  private startCleanupTimer(): void {
    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
   */
  private stopCleanupTimer(): void {
    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
   */
  public replaceSocket(oldSocket: plugins.net.Socket | plugins.tls.TLSSocket, newSocket: plugins.net.Socket | plugins.tls.TLSSocket): boolean {
    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
   */
  private getSocketKey(socket: plugins.net.Socket | plugins.tls.TLSSocket): string {
    const details = getSocketDetails(socket);
    return `${details.remoteAddress}:${details.remotePort}-${Date.now()}`;
  }
  
  /**
   * Get all active sessions
   */
  public getAllSessions(): ISmtpSession[] {
    return Array.from(this.sessions.values());
  }
  
  /**
   * Update last activity for a session by socket
   */
  public updateLastActivity(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
    const session = this.getSession(socket);
    if (session) {
      this.updateSessionActivity(session);
    }
  }
  
  /**
   * Check for timed out sessions
   */
  public checkTimeouts(timeoutMs: number): ISmtpSession[] {
    const now = Date.now();
    const timedOutSessions: ISmtpSession[] = [];
    
    for (const session of this.sessions.values()) {
      if (now - session.lastActivity > timeoutMs) {
        timedOutSessions.push(session);
      }
    }
    
    return timedOutSessions;
  }
  
  /**
   * Clean up resources
   */
  public destroy(): void {
    // 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');
  }
}