import * as net from 'net';

/**
 * TlsAlert class for managing TLS alert messages
 */
export class TlsAlert {
  // TLS Alert Levels
  static readonly LEVEL_WARNING = 0x01;
  static readonly LEVEL_FATAL = 0x02;
  
  // TLS Alert Description Codes - RFC 8446 (TLS 1.3) / RFC 5246 (TLS 1.2)
  static readonly CLOSE_NOTIFY = 0x00;
  static readonly UNEXPECTED_MESSAGE = 0x0A;
  static readonly BAD_RECORD_MAC = 0x14;
  static readonly DECRYPTION_FAILED = 0x15; // TLS 1.0 only
  static readonly RECORD_OVERFLOW = 0x16;
  static readonly DECOMPRESSION_FAILURE = 0x1E; // TLS 1.2 and below
  static readonly HANDSHAKE_FAILURE = 0x28;
  static readonly NO_CERTIFICATE = 0x29; // SSLv3 only
  static readonly BAD_CERTIFICATE = 0x2A;
  static readonly UNSUPPORTED_CERTIFICATE = 0x2B;
  static readonly CERTIFICATE_REVOKED = 0x2C;
  static readonly CERTIFICATE_EXPIRED = 0x2F;
  static readonly CERTIFICATE_UNKNOWN = 0x30;
  static readonly ILLEGAL_PARAMETER = 0x2F;
  static readonly UNKNOWN_CA = 0x30;
  static readonly ACCESS_DENIED = 0x31;
  static readonly DECODE_ERROR = 0x32;
  static readonly DECRYPT_ERROR = 0x33;
  static readonly EXPORT_RESTRICTION = 0x3C; // TLS 1.0 only
  static readonly PROTOCOL_VERSION = 0x46;
  static readonly INSUFFICIENT_SECURITY = 0x47;
  static readonly INTERNAL_ERROR = 0x50;
  static readonly INAPPROPRIATE_FALLBACK = 0x56;
  static readonly USER_CANCELED = 0x5A;
  static readonly NO_RENEGOTIATION = 0x64; // TLS 1.2 and below
  static readonly MISSING_EXTENSION = 0x6D; // TLS 1.3
  static readonly UNSUPPORTED_EXTENSION = 0x6E; // TLS 1.3
  static readonly CERTIFICATE_REQUIRED = 0x6F; // TLS 1.3
  static readonly UNRECOGNIZED_NAME = 0x70;
  static readonly BAD_CERTIFICATE_STATUS_RESPONSE = 0x71;
  static readonly BAD_CERTIFICATE_HASH_VALUE = 0x72; // TLS 1.2 and below
  static readonly UNKNOWN_PSK_IDENTITY = 0x73;
  static readonly CERTIFICATE_REQUIRED_1_3 = 0x74; // TLS 1.3
  static readonly NO_APPLICATION_PROTOCOL = 0x78;
  
  /**
   * 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] = [0x03, 0x03]
  ): 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: 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: 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: 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: 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: 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: net.Socket, 
    description: number, 
    closeDelay: number = 100
  ): Promise<void> {
    return this.send(socket, this.LEVEL_FATAL, description, true, closeDelay);
  }
}