/**
 * SMTP Client Authentication Handler
 * Authentication mechanisms implementation
 */

import { AUTH_METHODS } from './constants.js';
import type { 
  ISmtpConnection, 
  ISmtpAuthOptions, 
  ISmtpClientOptions,
  ISmtpResponse,
  IOAuth2Options
} from './interfaces.js';
import { 
  encodeAuthPlain, 
  encodeAuthLogin, 
  generateOAuth2String,
  isSuccessCode 
} from './utils/helpers.js';
import { logAuthentication, logDebug } from './utils/logging.js';
import type { CommandHandler } from './command-handler.js';

export class AuthHandler {
  private options: ISmtpClientOptions;
  private commandHandler: CommandHandler;
  
  constructor(options: ISmtpClientOptions, commandHandler: CommandHandler) {
    this.options = options;
    this.commandHandler = commandHandler;
  }
  
  /**
   * Authenticate using the configured method
   */
  public async authenticate(connection: ISmtpConnection): Promise<void> {
    if (!this.options.auth) {
      logDebug('No authentication configured', this.options);
      return;
    }
    
    const authOptions = this.options.auth;
    const capabilities = connection.capabilities;
    
    if (!capabilities || capabilities.authMethods.size === 0) {
      throw new Error('Server does not support authentication');
    }
    
    // Determine authentication method
    const method = this.selectAuthMethod(authOptions, capabilities.authMethods);
    
    logAuthentication('start', method, this.options);
    
    try {
      switch (method) {
        case AUTH_METHODS.PLAIN:
          await this.authenticatePlain(connection, authOptions);
          break;
        case AUTH_METHODS.LOGIN:
          await this.authenticateLogin(connection, authOptions);
          break;
        case AUTH_METHODS.OAUTH2:
          await this.authenticateOAuth2(connection, authOptions);
          break;
        default:
          throw new Error(`Unsupported authentication method: ${method}`);
      }
      
      logAuthentication('success', method, this.options);
    } catch (error) {
      logAuthentication('failure', method, this.options, { error });
      throw error;
    }
  }
  
  /**
   * Authenticate using AUTH PLAIN
   */
  private async authenticatePlain(connection: ISmtpConnection, auth: ISmtpAuthOptions): Promise<void> {
    if (!auth.user || !auth.pass) {
      throw new Error('Username and password required for PLAIN authentication');
    }
    
    const credentials = encodeAuthPlain(auth.user, auth.pass);
    const response = await this.commandHandler.sendAuth(connection, AUTH_METHODS.PLAIN, credentials);
    
    if (!isSuccessCode(response.code)) {
      throw new Error(`PLAIN authentication failed: ${response.message}`);
    }
  }
  
  /**
   * Authenticate using AUTH LOGIN
   */
  private async authenticateLogin(connection: ISmtpConnection, auth: ISmtpAuthOptions): Promise<void> {
    if (!auth.user || !auth.pass) {
      throw new Error('Username and password required for LOGIN authentication');
    }
    
    // Step 1: Send AUTH LOGIN
    let response = await this.commandHandler.sendAuth(connection, AUTH_METHODS.LOGIN);
    
    if (response.code !== 334) {
      throw new Error(`LOGIN authentication initiation failed: ${response.message}`);
    }
    
    // Step 2: Send username
    const encodedUser = encodeAuthLogin(auth.user);
    response = await this.commandHandler.sendCommand(connection, encodedUser);
    
    if (response.code !== 334) {
      throw new Error(`LOGIN username failed: ${response.message}`);
    }
    
    // Step 3: Send password
    const encodedPass = encodeAuthLogin(auth.pass);
    response = await this.commandHandler.sendCommand(connection, encodedPass);
    
    if (!isSuccessCode(response.code)) {
      throw new Error(`LOGIN password failed: ${response.message}`);
    }
  }
  
  /**
   * Authenticate using OAuth2
   */
  private async authenticateOAuth2(connection: ISmtpConnection, auth: ISmtpAuthOptions): Promise<void> {
    if (!auth.oauth2) {
      throw new Error('OAuth2 configuration required for OAUTH2 authentication');
    }
    
    let accessToken = auth.oauth2.accessToken;
    
    // Refresh token if needed
    if (!accessToken || this.isTokenExpired(auth.oauth2)) {
      accessToken = await this.refreshOAuth2Token(auth.oauth2);
    }
    
    const authString = generateOAuth2String(auth.oauth2.user, accessToken);
    const response = await this.commandHandler.sendAuth(connection, AUTH_METHODS.OAUTH2, authString);
    
    if (!isSuccessCode(response.code)) {
      throw new Error(`OAUTH2 authentication failed: ${response.message}`);
    }
  }
  
  /**
   * Select appropriate authentication method
   */
  private selectAuthMethod(auth: ISmtpAuthOptions, serverMethods: Set<string>): string {
    // If method is explicitly specified, use it
    if (auth.method && auth.method !== 'AUTO') {
      const method = auth.method === 'OAUTH2' ? AUTH_METHODS.OAUTH2 : auth.method;
      if (serverMethods.has(method)) {
        return method;
      }
      throw new Error(`Requested authentication method ${auth.method} not supported by server`);
    }
    
    // Auto-select based on available credentials and server support
    if (auth.oauth2 && serverMethods.has(AUTH_METHODS.OAUTH2)) {
      return AUTH_METHODS.OAUTH2;
    }
    
    if (auth.user && auth.pass) {
      // Prefer PLAIN over LOGIN for simplicity
      if (serverMethods.has(AUTH_METHODS.PLAIN)) {
        return AUTH_METHODS.PLAIN;
      }
      if (serverMethods.has(AUTH_METHODS.LOGIN)) {
        return AUTH_METHODS.LOGIN;
      }
    }
    
    throw new Error('No compatible authentication method found');
  }
  
  /**
   * Check if OAuth2 token is expired
   */
  private isTokenExpired(oauth2: IOAuth2Options): boolean {
    if (!oauth2.expires) {
      return false; // No expiry information, assume valid
    }
    
    const now = Date.now();
    const buffer = 300000; // 5 minutes buffer
    
    return oauth2.expires < (now + buffer);
  }
  
  /**
   * Refresh OAuth2 access token
   */
  private async refreshOAuth2Token(oauth2: IOAuth2Options): Promise<string> {
    // This is a simplified implementation
    // In a real implementation, you would make an HTTP request to the OAuth2 provider
    logDebug('OAuth2 token refresh required', this.options);
    
    if (!oauth2.refreshToken) {
      throw new Error('Refresh token required for OAuth2 token refresh');
    }
    
    // TODO: Implement actual OAuth2 token refresh
    // For now, throw an error to indicate this needs to be implemented
    throw new Error('OAuth2 token refresh not implemented. Please provide a valid access token.');
  }
  
  /**
   * Validate authentication configuration
   */
  public validateAuthConfig(auth: ISmtpAuthOptions): string[] {
    const errors: string[] = [];
    
    if (auth.method === 'OAUTH2' || auth.oauth2) {
      if (!auth.oauth2) {
        errors.push('OAuth2 configuration required when using OAUTH2 method');
      } else {
        if (!auth.oauth2.user) errors.push('OAuth2 user required');
        if (!auth.oauth2.clientId) errors.push('OAuth2 clientId required');
        if (!auth.oauth2.clientSecret) errors.push('OAuth2 clientSecret required');
        if (!auth.oauth2.refreshToken && !auth.oauth2.accessToken) {
          errors.push('OAuth2 refreshToken or accessToken required');
        }
      }
    } else if (auth.method === 'PLAIN' || auth.method === 'LOGIN' || (!auth.method && (auth.user || auth.pass))) {
      if (!auth.user) errors.push('Username required for basic authentication');
      if (!auth.pass) errors.push('Password required for basic authentication');
    }
    
    return errors;
  }
}