import * as plugins from '../../plugins.js';
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
// MtaService reference removed
import type { Email } from '../core/classes.email.js';
import type { IDnsVerificationResult } from '../routing/classes.dnsmanager.js';

/**
 * SPF result qualifiers
 */
export enum SpfQualifier {
  PASS = '+',
  NEUTRAL = '?',
  SOFTFAIL = '~',
  FAIL = '-'
}

/**
 * SPF mechanism types
 */
export enum SpfMechanismType {
  ALL = 'all',
  INCLUDE = 'include',
  A = 'a',
  MX = 'mx',
  IP4 = 'ip4',
  IP6 = 'ip6',
  EXISTS = 'exists',
  REDIRECT = 'redirect',
  EXP = 'exp'
}

/**
 * SPF mechanism definition
 */
export interface SpfMechanism {
  qualifier: SpfQualifier;
  type: SpfMechanismType;
  value?: string;
}

/**
 * SPF record parsed data
 */
export interface SpfRecord {
  version: string;
  mechanisms: SpfMechanism[];
  modifiers: Record<string, string>;
}

/**
 * SPF verification result
 */
export interface SpfResult {
  result: 'pass' | 'neutral' | 'softfail' | 'fail' | 'temperror' | 'permerror' | 'none';
  explanation?: string;
  domain: string;
  ip: string;
  record?: string;
  error?: string;
}

/**
 * Maximum lookup limit for SPF records (prevent infinite loops)
 */
const MAX_SPF_LOOKUPS = 10;

/**
 * Class for verifying SPF records
 */
export class SpfVerifier {
  // DNS Manager reference for verifying records
  private dnsManager?: any;
  private lookupCount: number = 0;
  
  constructor(dnsManager?: any) {
    this.dnsManager = dnsManager;
  }
  
  /**
   * Parse SPF record from TXT record
   * @param record SPF TXT record
   * @returns Parsed SPF record or null if invalid
   */
  public parseSpfRecord(record: string): SpfRecord | null {
    if (!record.startsWith('v=spf1')) {
      return null;
    }
    
    try {
      const spfRecord: SpfRecord = {
        version: 'spf1',
        mechanisms: [],
        modifiers: {}
      };
      
      // Split into terms
      const terms = record.split(' ').filter(term => term.length > 0);
      
      // Skip version term
      for (let i = 1; i < terms.length; i++) {
        const term = terms[i];
        
        // Check if it's a modifier (name=value)
        if (term.includes('=')) {
          const [name, value] = term.split('=');
          spfRecord.modifiers[name] = value;
          continue;
        }
        
        // Parse as mechanism
        let qualifier = SpfQualifier.PASS; // Default is +
        let mechanismText = term;
        
        // Check for qualifier
        if (term.startsWith('+') || term.startsWith('-') || 
            term.startsWith('~') || term.startsWith('?')) {
          qualifier = term[0] as SpfQualifier;
          mechanismText = term.substring(1);
        }
        
        // Parse mechanism type and value
        const colonIndex = mechanismText.indexOf(':');
        let type: SpfMechanismType;
        let value: string | undefined;
        
        if (colonIndex !== -1) {
          type = mechanismText.substring(0, colonIndex) as SpfMechanismType;
          value = mechanismText.substring(colonIndex + 1);
        } else {
          type = mechanismText as SpfMechanismType;
        }
        
        spfRecord.mechanisms.push({ qualifier, type, value });
      }
      
      return spfRecord;
    } catch (error) {
      logger.log('error', `Error parsing SPF record: ${error.message}`, {
        record,
        error: error.message
      });
      return null;
    }
  }
  
  /**
   * Check if IP is in CIDR range
   * @param ip IP address to check
   * @param cidr CIDR range
   * @returns Whether the IP is in the CIDR range
   */
  private isIpInCidr(ip: string, cidr: string): boolean {
    try {
      const ipAddress = plugins.ip.Address4.parse(ip);
      return ipAddress.isInSubnet(new plugins.ip.Address4(cidr));
    } catch (error) {
      // Try IPv6
      try {
        const ipAddress = plugins.ip.Address6.parse(ip);
        return ipAddress.isInSubnet(new plugins.ip.Address6(cidr));
      } catch (e) {
        return false;
      }
    }
  }
  
  /**
   * Check if a domain has the specified IP in its A or AAAA records
   * @param domain Domain to check
   * @param ip IP address to check
   * @returns Whether the domain resolves to the IP
   */
  private async isDomainResolvingToIp(domain: string, ip: string): Promise<boolean> {
    try {
      // First try IPv4
      const ipv4Addresses = await plugins.dns.promises.resolve4(domain);
      if (ipv4Addresses.includes(ip)) {
        return true;
      }
      
      // Then try IPv6
      const ipv6Addresses = await plugins.dns.promises.resolve6(domain);
      if (ipv6Addresses.includes(ip)) {
        return true;
      }
      
      return false;
    } catch (error) {
      return false;
    }
  }
  
  /**
   * Verify SPF for a given email with IP and helo domain
   * @param email Email to verify
   * @param ip Sender IP address
   * @param heloDomain HELO/EHLO domain used by sender
   * @returns SPF verification result
   */
  public async verify(
    email: Email,
    ip: string,
    heloDomain: string
  ): Promise<SpfResult> {
    const securityLogger = SecurityLogger.getInstance();
    
    // Reset lookup count
    this.lookupCount = 0;
    
    // Get domain from envelope from (return-path)
    const domain = email.getEnvelopeFrom().split('@')[1] || '';
    
    if (!domain) {
      return {
        result: 'permerror',
        explanation: 'No envelope from domain',
        domain: '',
        ip
      };
    }
    
    try {
      // Look up SPF record
      const spfVerificationResult = this.dnsManager ? 
        await this.dnsManager.verifySpfRecord(domain) :
        { found: false, valid: false, error: 'DNS Manager not available' };
      
      if (!spfVerificationResult.found) {
        return {
          result: 'none',
          explanation: 'No SPF record found',
          domain,
          ip
        };
      }
      
      if (!spfVerificationResult.valid) {
        return {
          result: 'permerror',
          explanation: 'Invalid SPF record',
          domain,
          ip,
          record: spfVerificationResult.value
        };
      }
      
      // Parse SPF record
      const spfRecord = this.parseSpfRecord(spfVerificationResult.value);
      
      if (!spfRecord) {
        return {
          result: 'permerror',
          explanation: 'Failed to parse SPF record',
          domain,
          ip,
          record: spfVerificationResult.value
        };
      }
      
      // Check SPF record
      const result = await this.checkSpfRecord(spfRecord, domain, ip);
      
      // Log the result
      const spfLogLevel = result.result === 'pass' ? 
        SecurityLogLevel.INFO : 
        (result.result === 'fail' ? SecurityLogLevel.WARN : SecurityLogLevel.INFO);
      
      securityLogger.logEvent({
        level: spfLogLevel,
        type: SecurityEventType.SPF,
        message: `SPF ${result.result} for ${domain} from IP ${ip}`,
        domain,
        details: {
          ip,
          heloDomain,
          result: result.result,
          explanation: result.explanation,
          record: spfVerificationResult.value
        },
        success: result.result === 'pass'
      });
      
      return {
        ...result,
        domain,
        ip,
        record: spfVerificationResult.value
      };
    } catch (error) {
      // Log error
      logger.log('error', `SPF verification error: ${error.message}`, {
        domain,
        ip,
        error: error.message
      });
      
      securityLogger.logEvent({
        level: SecurityLogLevel.ERROR,
        type: SecurityEventType.SPF,
        message: `SPF verification error for ${domain}`,
        domain,
        details: {
          ip,
          error: error.message
        },
        success: false
      });
      
      return {
        result: 'temperror',
        explanation: `Error verifying SPF: ${error.message}`,
        domain,
        ip,
        error: error.message
      };
    }
  }
  
  /**
   * Check SPF record against IP address
   * @param spfRecord Parsed SPF record
   * @param domain Domain being checked
   * @param ip IP address to check
   * @returns SPF result
   */
  private async checkSpfRecord(
    spfRecord: SpfRecord,
    domain: string,
    ip: string
  ): Promise<SpfResult> {
    // Check for 'redirect' modifier
    if (spfRecord.modifiers.redirect) {
      this.lookupCount++;
      
      if (this.lookupCount > MAX_SPF_LOOKUPS) {
        return {
          result: 'permerror',
          explanation: 'Too many DNS lookups',
          domain,
          ip
        };
      }
      
      // Handle redirect
      const redirectDomain = spfRecord.modifiers.redirect;
      const redirectResult = this.dnsManager ? 
        await this.dnsManager.verifySpfRecord(redirectDomain) :
        { found: false, valid: false, error: 'DNS Manager not available' };
      
      if (!redirectResult.found || !redirectResult.valid) {
        return {
          result: 'permerror',
          explanation: `Invalid redirect to ${redirectDomain}`,
          domain,
          ip
        };
      }
      
      const redirectRecord = this.parseSpfRecord(redirectResult.value);
      
      if (!redirectRecord) {
        return {
          result: 'permerror',
          explanation: `Failed to parse redirect record from ${redirectDomain}`,
          domain,
          ip
        };
      }
      
      return this.checkSpfRecord(redirectRecord, redirectDomain, ip);
    }
    
    // Check each mechanism in order
    for (const mechanism of spfRecord.mechanisms) {
      let matched = false;
      
      switch (mechanism.type) {
        case SpfMechanismType.ALL:
          matched = true;
          break;
          
        case SpfMechanismType.IP4:
          if (mechanism.value) {
            matched = this.isIpInCidr(ip, mechanism.value);
          }
          break;
          
        case SpfMechanismType.IP6:
          if (mechanism.value) {
            matched = this.isIpInCidr(ip, mechanism.value);
          }
          break;
          
        case SpfMechanismType.A:
          this.lookupCount++;
          
          if (this.lookupCount > MAX_SPF_LOOKUPS) {
            return {
              result: 'permerror',
              explanation: 'Too many DNS lookups',
              domain,
              ip
            };
          }
          
          // Check if domain has A/AAAA record matching IP
          const checkDomain = mechanism.value || domain;
          matched = await this.isDomainResolvingToIp(checkDomain, ip);
          break;
          
        case SpfMechanismType.MX:
          this.lookupCount++;
          
          if (this.lookupCount > MAX_SPF_LOOKUPS) {
            return {
              result: 'permerror',
              explanation: 'Too many DNS lookups',
              domain,
              ip
            };
          }
          
          // Check MX records
          const mxDomain = mechanism.value || domain;
          
          try {
            const mxRecords = await plugins.dns.promises.resolveMx(mxDomain);
            
            for (const mx of mxRecords) {
              // Check if this MX record's IP matches
              const mxMatches = await this.isDomainResolvingToIp(mx.exchange, ip);
              
              if (mxMatches) {
                matched = true;
                break;
              }
            }
          } catch (error) {
            // No MX records or error
            matched = false;
          }
          break;
          
        case SpfMechanismType.INCLUDE:
          if (!mechanism.value) {
            continue;
          }
          
          this.lookupCount++;
          
          if (this.lookupCount > MAX_SPF_LOOKUPS) {
            return {
              result: 'permerror',
              explanation: 'Too many DNS lookups',
              domain,
              ip
            };
          }
          
          // Check included domain's SPF record
          const includeDomain = mechanism.value;
          const includeResult = this.dnsManager ? 
            await this.dnsManager.verifySpfRecord(includeDomain) :
            { found: false, valid: false, error: 'DNS Manager not available' };
          
          if (!includeResult.found || !includeResult.valid) {
            continue; // Skip this mechanism
          }
          
          const includeRecord = this.parseSpfRecord(includeResult.value);
          
          if (!includeRecord) {
            continue; // Skip this mechanism
          }
          
          // Recursively check the included SPF record
          const includeCheck = await this.checkSpfRecord(includeRecord, includeDomain, ip);
          
          // Include mechanism matches if the result is "pass"
          matched = includeCheck.result === 'pass';
          break;
          
        case SpfMechanismType.EXISTS:
          if (!mechanism.value) {
            continue;
          }
          
          this.lookupCount++;
          
          if (this.lookupCount > MAX_SPF_LOOKUPS) {
            return {
              result: 'permerror',
              explanation: 'Too many DNS lookups',
              domain,
              ip
            };
          }
          
          // Check if domain exists (has any A record)
          try {
            await plugins.dns.promises.resolve(mechanism.value, 'A');
            matched = true;
          } catch (error) {
            matched = false;
          }
          break;
      }
      
      // If this mechanism matched, return its result
      if (matched) {
        switch (mechanism.qualifier) {
          case SpfQualifier.PASS:
            return {
              result: 'pass',
              explanation: `Matched ${mechanism.type}${mechanism.value ? ':' + mechanism.value : ''}`,
              domain,
              ip
            };
          case SpfQualifier.FAIL:
            return {
              result: 'fail',
              explanation: `Matched ${mechanism.type}${mechanism.value ? ':' + mechanism.value : ''}`,
              domain,
              ip
            };
          case SpfQualifier.SOFTFAIL:
            return {
              result: 'softfail',
              explanation: `Matched ${mechanism.type}${mechanism.value ? ':' + mechanism.value : ''}`,
              domain,
              ip
            };
          case SpfQualifier.NEUTRAL:
            return {
              result: 'neutral',
              explanation: `Matched ${mechanism.type}${mechanism.value ? ':' + mechanism.value : ''}`,
              domain,
              ip
            };
        }
      }
    }
    
    // If no mechanism matched, default to neutral
    return {
      result: 'neutral',
      explanation: 'No matching mechanism found',
      domain,
      ip
    };
  }
  
  /**
   * Check if email passes SPF verification
   * @param email Email to verify
   * @param ip Sender IP address
   * @param heloDomain HELO/EHLO domain used by sender
   * @returns Whether email passes SPF
   */
  public async verifyAndApply(
    email: Email,
    ip: string,
    heloDomain: string
  ): Promise<boolean> {
    const result = await this.verify(email, ip, heloDomain);
    
    // Add headers
    email.headers['Received-SPF'] = `${result.result} (${result.domain}: ${result.explanation}) client-ip=${ip}; envelope-from=${email.getEnvelopeFrom()}; helo=${heloDomain};`;
    
    // Apply policy based on result
    switch (result.result) {
      case 'fail':
        // Fail - mark as spam
        email.mightBeSpam = true;
        logger.log('warn', `SPF failed for ${result.domain} from ${ip}: ${result.explanation}`);
        return false;
        
      case 'softfail':
        // Soft fail - accept but mark as suspicious
        email.mightBeSpam = true;
        logger.log('info', `SPF softfailed for ${result.domain} from ${ip}: ${result.explanation}`);
        return true;
        
      case 'neutral':
      case 'none':
        // Neutral or none - accept but note in headers
        logger.log('info', `SPF ${result.result} for ${result.domain} from ${ip}: ${result.explanation}`);
        return true;
        
      case 'pass':
        // Pass - accept
        logger.log('info', `SPF passed for ${result.domain} from ${ip}: ${result.explanation}`);
        return true;
        
      case 'temperror':
      case 'permerror':
        // Temporary or permanent error - log but accept
        logger.log('error', `SPF error for ${result.domain} from ${ip}: ${result.explanation}`);
        return true;
        
      default:
        return true;
    }
  }
}