import * as plugins from '../../../plugins.js';
import { TlsAlertLevel, TlsAlertDescription, TlsVersion } from '../utils/tls-utils.js';

/**
 * TlsAlert class for creating and sending TLS alert messages
 */
export class TlsAlert {
  // Use enum values from TlsAlertLevel
  static readonly LEVEL_WARNING = TlsAlertLevel.WARNING;
  static readonly LEVEL_FATAL = TlsAlertLevel.FATAL;

  // Use enum values from TlsAlertDescription
  static readonly CLOSE_NOTIFY = TlsAlertDescription.CLOSE_NOTIFY;
  static readonly UNEXPECTED_MESSAGE = TlsAlertDescription.UNEXPECTED_MESSAGE;
  static readonly BAD_RECORD_MAC = TlsAlertDescription.BAD_RECORD_MAC;
  static readonly DECRYPTION_FAILED = TlsAlertDescription.DECRYPTION_FAILED;
  static readonly RECORD_OVERFLOW = TlsAlertDescription.RECORD_OVERFLOW;
  static readonly DECOMPRESSION_FAILURE = TlsAlertDescription.DECOMPRESSION_FAILURE;
  static readonly HANDSHAKE_FAILURE = TlsAlertDescription.HANDSHAKE_FAILURE;
  static readonly NO_CERTIFICATE = TlsAlertDescription.NO_CERTIFICATE;
  static readonly BAD_CERTIFICATE = TlsAlertDescription.BAD_CERTIFICATE;
  static readonly UNSUPPORTED_CERTIFICATE = TlsAlertDescription.UNSUPPORTED_CERTIFICATE;
  static readonly CERTIFICATE_REVOKED = TlsAlertDescription.CERTIFICATE_REVOKED;
  static readonly CERTIFICATE_EXPIRED = TlsAlertDescription.CERTIFICATE_EXPIRED;
  static readonly CERTIFICATE_UNKNOWN = TlsAlertDescription.CERTIFICATE_UNKNOWN;
  static readonly ILLEGAL_PARAMETER = TlsAlertDescription.ILLEGAL_PARAMETER;
  static readonly UNKNOWN_CA = TlsAlertDescription.UNKNOWN_CA;
  static readonly ACCESS_DENIED = TlsAlertDescription.ACCESS_DENIED;
  static readonly DECODE_ERROR = TlsAlertDescription.DECODE_ERROR;
  static readonly DECRYPT_ERROR = TlsAlertDescription.DECRYPT_ERROR;
  static readonly EXPORT_RESTRICTION = TlsAlertDescription.EXPORT_RESTRICTION;
  static readonly PROTOCOL_VERSION = TlsAlertDescription.PROTOCOL_VERSION;
  static readonly INSUFFICIENT_SECURITY = TlsAlertDescription.INSUFFICIENT_SECURITY;
  static readonly INTERNAL_ERROR = TlsAlertDescription.INTERNAL_ERROR;
  static readonly INAPPROPRIATE_FALLBACK = TlsAlertDescription.INAPPROPRIATE_FALLBACK;
  static readonly USER_CANCELED = TlsAlertDescription.USER_CANCELED;
  static readonly NO_RENEGOTIATION = TlsAlertDescription.NO_RENEGOTIATION;
  static readonly MISSING_EXTENSION = TlsAlertDescription.MISSING_EXTENSION;
  static readonly UNSUPPORTED_EXTENSION = TlsAlertDescription.UNSUPPORTED_EXTENSION;
  static readonly CERTIFICATE_REQUIRED = TlsAlertDescription.CERTIFICATE_REQUIRED;
  static readonly UNRECOGNIZED_NAME = TlsAlertDescription.UNRECOGNIZED_NAME;
  static readonly BAD_CERTIFICATE_STATUS_RESPONSE = TlsAlertDescription.BAD_CERTIFICATE_STATUS_RESPONSE;
  static readonly BAD_CERTIFICATE_HASH_VALUE = TlsAlertDescription.BAD_CERTIFICATE_HASH_VALUE;
  static readonly UNKNOWN_PSK_IDENTITY = TlsAlertDescription.UNKNOWN_PSK_IDENTITY;
  static readonly CERTIFICATE_REQUIRED_1_3 = TlsAlertDescription.CERTIFICATE_REQUIRED_1_3;
  static readonly NO_APPLICATION_PROTOCOL = TlsAlertDescription.NO_APPLICATION_PROTOCOL;
  
  /**
   * Create a TLS alert buffer with the specified level and description code
   *
   * @param level Alert level (warning or fatal)
   * @param description Alert description code
   * @param tlsVersion TLS version bytes (default is TLS 1.2: 0x0303)
   * @returns Buffer containing the TLS alert message
   */
  static create(
    level: number,
    description: number,
    tlsVersion: [number, number] = [TlsVersion.TLS1_2[0], TlsVersion.TLS1_2[1]]
  ): Buffer {
    return Buffer.from([
      0x15, // Alert record type
      tlsVersion[0],
      tlsVersion[1], // TLS version (default to TLS 1.2: 0x0303)
      0x00,
      0x02, // Length
      level, // Alert level
      description, // Alert description
    ]);
  }
  
  /**
   * Create a warning-level TLS alert
   * 
   * @param description Alert description code
   * @returns Buffer containing the warning-level TLS alert message
   */
  static createWarning(description: number): Buffer {
    return this.create(this.LEVEL_WARNING, description);
  }
  
  /**
   * Create a fatal-level TLS alert
   * 
   * @param description Alert description code
   * @returns Buffer containing the fatal-level TLS alert message
   */
  static createFatal(description: number): Buffer {
    return this.create(this.LEVEL_FATAL, description);
  }
  
  /**
   * Send a TLS alert to a socket and optionally close the connection
   * 
   * @param socket The socket to send the alert to
   * @param level Alert level (warning or fatal)
   * @param description Alert description code
   * @param closeAfterSend Whether to close the connection after sending the alert
   * @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
   * @returns Promise that resolves when the alert has been sent
   */
  static async send(
    socket: plugins.net.Socket,
    level: number,
    description: number,
    closeAfterSend: boolean = false,
    closeDelay: number = 200
  ): Promise<void> {
    const alert = this.create(level, description);
    
    return new Promise<void>((resolve, reject) => {
      try {
        // Ensure the alert is written as a single packet
        socket.cork();
        const writeSuccessful = socket.write(alert, (err) => {
          if (err) {
            reject(err);
            return;
          }
          
          if (closeAfterSend) {
            setTimeout(() => {
              socket.end();
              resolve();
            }, closeDelay);
          } else {
            resolve();
          }
        });
        socket.uncork();
        
        // If write wasn't successful immediately, wait for drain
        if (!writeSuccessful && !closeAfterSend) {
          socket.once('drain', () => {
            resolve();
          });
        }
      } catch (err) {
        reject(err);
      }
    });
  }
  
  /**
   * Pre-defined TLS alert messages
   */
  static readonly alerts = {
    // Warning level alerts
    closeNotify: TlsAlert.createWarning(TlsAlert.CLOSE_NOTIFY),
    unsupportedExtension: TlsAlert.createWarning(TlsAlert.UNSUPPORTED_EXTENSION),
    certificateRequired: TlsAlert.createWarning(TlsAlert.CERTIFICATE_REQUIRED),
    unrecognizedName: TlsAlert.createWarning(TlsAlert.UNRECOGNIZED_NAME),
    noRenegotiation: TlsAlert.createWarning(TlsAlert.NO_RENEGOTIATION),
    userCanceled: TlsAlert.createWarning(TlsAlert.USER_CANCELED),
    
    // Warning level alerts for session resumption
    certificateExpiredWarning: TlsAlert.createWarning(TlsAlert.CERTIFICATE_EXPIRED),
    handshakeFailureWarning: TlsAlert.createWarning(TlsAlert.HANDSHAKE_FAILURE),
    insufficientSecurityWarning: TlsAlert.createWarning(TlsAlert.INSUFFICIENT_SECURITY),
    
    // Fatal level alerts
    unexpectedMessage: TlsAlert.createFatal(TlsAlert.UNEXPECTED_MESSAGE),
    badRecordMac: TlsAlert.createFatal(TlsAlert.BAD_RECORD_MAC),
    recordOverflow: TlsAlert.createFatal(TlsAlert.RECORD_OVERFLOW),
    handshakeFailure: TlsAlert.createFatal(TlsAlert.HANDSHAKE_FAILURE),
    badCertificate: TlsAlert.createFatal(TlsAlert.BAD_CERTIFICATE),
    certificateExpired: TlsAlert.createFatal(TlsAlert.CERTIFICATE_EXPIRED),
    certificateUnknown: TlsAlert.createFatal(TlsAlert.CERTIFICATE_UNKNOWN),
    illegalParameter: TlsAlert.createFatal(TlsAlert.ILLEGAL_PARAMETER),
    unknownCA: TlsAlert.createFatal(TlsAlert.UNKNOWN_CA),
    accessDenied: TlsAlert.createFatal(TlsAlert.ACCESS_DENIED),
    decodeError: TlsAlert.createFatal(TlsAlert.DECODE_ERROR),
    decryptError: TlsAlert.createFatal(TlsAlert.DECRYPT_ERROR),
    protocolVersion: TlsAlert.createFatal(TlsAlert.PROTOCOL_VERSION),
    insufficientSecurity: TlsAlert.createFatal(TlsAlert.INSUFFICIENT_SECURITY),
    internalError: TlsAlert.createFatal(TlsAlert.INTERNAL_ERROR),
    unrecognizedNameFatal: TlsAlert.createFatal(TlsAlert.UNRECOGNIZED_NAME),
  };
  
  /**
   * Utility method to send a warning-level unrecognized_name alert
   * Specifically designed for SNI issues to encourage the client to retry with SNI
   * 
   * @param socket The socket to send the alert to
   * @returns Promise that resolves when the alert has been sent
   */
  static async sendSniRequired(socket: plugins.net.Socket): Promise<void> {
    return this.send(socket, this.LEVEL_WARNING, this.UNRECOGNIZED_NAME);
  }
  
  /**
   * Utility method to send a close_notify alert and close the connection
   * 
   * @param socket The socket to send the alert to
   * @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
   * @returns Promise that resolves when the alert has been sent and the connection closed
   */
  static async sendCloseNotify(socket: plugins.net.Socket, closeDelay: number = 200): Promise<void> {
    return this.send(socket, this.LEVEL_WARNING, this.CLOSE_NOTIFY, true, closeDelay);
  }
  
  /**
   * Utility method to send a certificate_expired alert to force new TLS session
   * 
   * @param socket The socket to send the alert to
   * @param fatal Whether to send as a fatal alert (default: false)
   * @param closeAfterSend Whether to close the connection after sending the alert (default: true)
   * @param closeDelay Milliseconds to wait before closing the connection (default: 200ms)
   * @returns Promise that resolves when the alert has been sent
   */
  static async sendCertificateExpired(
    socket: plugins.net.Socket,
    fatal: boolean = false,
    closeAfterSend: boolean = true,
    closeDelay: number = 200
  ): Promise<void> {
    const level = fatal ? this.LEVEL_FATAL : this.LEVEL_WARNING;
    return this.send(socket, level, this.CERTIFICATE_EXPIRED, closeAfterSend, closeDelay);
  }
  
  /**
   * Send a sequence of alerts to force SNI from clients
   * This combines multiple alerts to ensure maximum browser compatibility
   * 
   * @param socket The socket to send the alerts to
   * @returns Promise that resolves when all alerts have been sent
   */
  static async sendForceSniSequence(socket: plugins.net.Socket): Promise<void> {
    try {
      // Send unrecognized_name (warning)
      socket.cork();
      socket.write(this.alerts.unrecognizedName);
      socket.uncork();
      
      // Give the socket time to send the alert
      return new Promise((resolve) => {
        setTimeout(resolve, 50);
      });
    } catch (err) {
      return Promise.reject(err);
    }
  }
  
  /**
   * Send a fatal level alert that immediately terminates the connection
   * 
   * @param socket The socket to send the alert to
   * @param description Alert description code
   * @param closeDelay Milliseconds to wait before closing the connection (default: 100ms)
   * @returns Promise that resolves when the alert has been sent and the connection closed
   */
  static async sendFatalAndClose(
    socket: plugins.net.Socket,
    description: number, 
    closeDelay: number = 100
  ): Promise<void> {
    return this.send(socket, this.LEVEL_FATAL, description, true, closeDelay);
  }
}