/**
 * SMTP Client Command Handler
 * SMTP command sending and response parsing
 */

import { EventEmitter } from 'node:events';
import { SMTP_COMMANDS, SMTP_CODES, LINE_ENDINGS } from './constants.js';
import type { 
  ISmtpConnection, 
  ISmtpResponse, 
  ISmtpClientOptions,
  ISmtpCapabilities 
} from './interfaces.js';
import { 
  parseSmtpResponse, 
  parseEhloResponse, 
  formatCommand,
  isSuccessCode 
} from './utils/helpers.js';
import { logCommand, logDebug } from './utils/logging.js';

export class CommandHandler extends EventEmitter {
  private options: ISmtpClientOptions;
  private responseBuffer: string = '';
  private pendingCommand: { resolve: Function; reject: Function; command: string } | null = null;
  private commandTimeout: NodeJS.Timeout | null = null;
  
  constructor(options: ISmtpClientOptions) {
    super();
    this.options = options;
  }
  
  /**
   * Send EHLO command and parse capabilities
   */
  public async sendEhlo(connection: ISmtpConnection, domain?: string): Promise<ISmtpCapabilities> {
    const hostname = domain || this.options.domain || 'localhost';
    const command = `${SMTP_COMMANDS.EHLO} ${hostname}`;
    
    const response = await this.sendCommand(connection, command);
    
    if (!isSuccessCode(response.code)) {
      throw new Error(`EHLO failed: ${response.message}`);
    }
    
    const capabilities = parseEhloResponse(response.raw);
    connection.capabilities = capabilities;
    
    logDebug('EHLO capabilities parsed', this.options, { capabilities });
    return capabilities;
  }
  
  /**
   * Send MAIL FROM command
   */
  public async sendMailFrom(connection: ISmtpConnection, fromAddress: string): Promise<ISmtpResponse> {
    // Handle empty return path for bounce messages
    const command = fromAddress === '' 
      ? `${SMTP_COMMANDS.MAIL_FROM}:<>` 
      : `${SMTP_COMMANDS.MAIL_FROM}:<${fromAddress}>`;
    return this.sendCommand(connection, command);
  }
  
  /**
   * Send RCPT TO command
   */
  public async sendRcptTo(connection: ISmtpConnection, toAddress: string): Promise<ISmtpResponse> {
    const command = `${SMTP_COMMANDS.RCPT_TO}:<${toAddress}>`;
    return this.sendCommand(connection, command);
  }
  
  /**
   * Send DATA command
   */
  public async sendData(connection: ISmtpConnection): Promise<ISmtpResponse> {
    return this.sendCommand(connection, SMTP_COMMANDS.DATA);
  }
  
  /**
   * Send email data content
   */
  public async sendDataContent(connection: ISmtpConnection, emailData: string): Promise<ISmtpResponse> {
    // Normalize line endings to CRLF
    let data = emailData.replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n/g, '\r\n');
    
    // Ensure email data ends with CRLF
    if (!data.endsWith(LINE_ENDINGS.CRLF)) {
      data += LINE_ENDINGS.CRLF;
    }
    
    // Perform dot stuffing (escape lines starting with a dot)
    data = data.replace(/\r\n\./g, '\r\n..');
    
    // Add termination sequence
    data += '.' + LINE_ENDINGS.CRLF;
    
    return this.sendRawData(connection, data);
  }
  
  /**
   * Send RSET command
   */
  public async sendRset(connection: ISmtpConnection): Promise<ISmtpResponse> {
    return this.sendCommand(connection, SMTP_COMMANDS.RSET);
  }
  
  /**
   * Send NOOP command
   */
  public async sendNoop(connection: ISmtpConnection): Promise<ISmtpResponse> {
    return this.sendCommand(connection, SMTP_COMMANDS.NOOP);
  }
  
  /**
   * Send QUIT command
   */
  public async sendQuit(connection: ISmtpConnection): Promise<ISmtpResponse> {
    return this.sendCommand(connection, SMTP_COMMANDS.QUIT);
  }
  
  /**
   * Send STARTTLS command
   */
  public async sendStartTls(connection: ISmtpConnection): Promise<ISmtpResponse> {
    return this.sendCommand(connection, SMTP_COMMANDS.STARTTLS);
  }
  
  /**
   * Send AUTH command
   */
  public async sendAuth(connection: ISmtpConnection, method: string, credentials?: string): Promise<ISmtpResponse> {
    const command = credentials ? 
      `${SMTP_COMMANDS.AUTH} ${method} ${credentials}` : 
      `${SMTP_COMMANDS.AUTH} ${method}`;
    return this.sendCommand(connection, command);
  }
  
  /**
   * Send a generic SMTP command
   */
  public async sendCommand(connection: ISmtpConnection, command: string): Promise<ISmtpResponse> {
    return new Promise((resolve, reject) => {
      if (this.pendingCommand) {
        reject(new Error('Another command is already pending'));
        return;
      }
      
      this.pendingCommand = { resolve, reject, command };
      
      // Set command timeout
      const timeout = 30000; // 30 seconds
      this.commandTimeout = setTimeout(() => {
        this.pendingCommand = null;
        this.commandTimeout = null;
        reject(new Error(`Command timeout: ${command}`));
      }, timeout);
      
      // Set up data handler
      const dataHandler = (data: Buffer) => {
        this.handleIncomingData(data.toString());
      };
      
      connection.socket.on('data', dataHandler);
      
      // Clean up function
      const cleanup = () => {
        connection.socket.removeListener('data', dataHandler);
        if (this.commandTimeout) {
          clearTimeout(this.commandTimeout);
          this.commandTimeout = null;
        }
      };
      
      // Send command
      const formattedCommand = command.endsWith(LINE_ENDINGS.CRLF) ? command : formatCommand(command);
      
      logCommand(command, undefined, this.options);
      logDebug(`Sending command: ${command}`, this.options);
      
      connection.socket.write(formattedCommand, (error) => {
        if (error) {
          cleanup();
          this.pendingCommand = null;
          reject(error);
        }
      });
      
      // Override resolve/reject to include cleanup
      const originalResolve = resolve;
      const originalReject = reject;
      
      this.pendingCommand.resolve = (response: ISmtpResponse) => {
        cleanup();
        this.pendingCommand = null;
        logCommand(command, response, this.options);
        originalResolve(response);
      };
      
      this.pendingCommand.reject = (error: Error) => {
        cleanup();
        this.pendingCommand = null;
        originalReject(error);
      };
    });
  }
  
  /**
   * Send raw data without command formatting
   */
  public async sendRawData(connection: ISmtpConnection, data: string): Promise<ISmtpResponse> {
    return new Promise((resolve, reject) => {
      if (this.pendingCommand) {
        reject(new Error('Another command is already pending'));
        return;
      }
      
      this.pendingCommand = { resolve, reject, command: 'DATA_CONTENT' };
      
      // Set data timeout
      const timeout = 60000; // 60 seconds for data
      this.commandTimeout = setTimeout(() => {
        this.pendingCommand = null;
        this.commandTimeout = null;
        reject(new Error('Data transmission timeout'));
      }, timeout);
      
      // Set up data handler
      const dataHandler = (chunk: Buffer) => {
        this.handleIncomingData(chunk.toString());
      };
      
      connection.socket.on('data', dataHandler);
      
      // Clean up function
      const cleanup = () => {
        connection.socket.removeListener('data', dataHandler);
        if (this.commandTimeout) {
          clearTimeout(this.commandTimeout);
          this.commandTimeout = null;
        }
      };
      
      // Override resolve/reject to include cleanup
      const originalResolve = resolve;
      const originalReject = reject;
      
      this.pendingCommand.resolve = (response: ISmtpResponse) => {
        cleanup();
        this.pendingCommand = null;
        originalResolve(response);
      };
      
      this.pendingCommand.reject = (error: Error) => {
        cleanup();
        this.pendingCommand = null;
        originalReject(error);
      };
      
      // Send data
      connection.socket.write(data, (error) => {
        if (error) {
          cleanup();
          this.pendingCommand = null;
          reject(error);
        }
      });
    });
  }
  
  /**
   * Wait for server greeting
   */
  public async waitForGreeting(connection: ISmtpConnection): Promise<ISmtpResponse> {
    return new Promise((resolve, reject) => {
      const timeout = 30000; // 30 seconds
      let timeoutHandler: NodeJS.Timeout;
      
      const dataHandler = (data: Buffer) => {
        this.responseBuffer += data.toString();
        
        if (this.isCompleteResponse(this.responseBuffer)) {
          clearTimeout(timeoutHandler);
          connection.socket.removeListener('data', dataHandler);
          
          const response = parseSmtpResponse(this.responseBuffer);
          this.responseBuffer = '';
          
          if (isSuccessCode(response.code)) {
            resolve(response);
          } else {
            reject(new Error(`Server greeting failed: ${response.message}`));
          }
        }
      };
      
      timeoutHandler = setTimeout(() => {
        connection.socket.removeListener('data', dataHandler);
        reject(new Error('Greeting timeout'));
      }, timeout);
      
      connection.socket.on('data', dataHandler);
    });
  }
  
  private handleIncomingData(data: string): void {
    if (!this.pendingCommand) {
      return;
    }
    
    this.responseBuffer += data;
    
    if (this.isCompleteResponse(this.responseBuffer)) {
      const response = parseSmtpResponse(this.responseBuffer);
      this.responseBuffer = '';
      
      if (isSuccessCode(response.code) || (response.code >= 300 && response.code < 400) || response.code >= 400) {
        this.pendingCommand.resolve(response);
      } else {
        this.pendingCommand.reject(new Error(`Command failed: ${response.message}`));
      }
    }
  }
  
  private isCompleteResponse(buffer: string): boolean {
    // Check if we have a complete response
    const lines = buffer.split(/\r?\n/);
    
    if (lines.length < 1) {
      return false;
    }
    
    // Check the last non-empty line
    for (let i = lines.length - 1; i >= 0; i--) {
      const line = lines[i].trim();
      if (line.length > 0) {
        // Response is complete if line starts with "XXX " (space after code)
        return /^\d{3} /.test(line);
      }
    }
    
    return false;
  }
}