import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import type { MtaService } from './mta.classes.mta.js';

/**
 * Interface for DNS record information
 */
export interface IDnsRecord {
  name: string;
  type: string;
  value: string;
  ttl?: number;
  dnsSecEnabled?: boolean;
}

/**
 * Interface for DNS lookup options
 */
export interface IDnsLookupOptions {
  /** Cache time to live in milliseconds, 0 to disable caching */
  cacheTtl?: number;
  /** Timeout for DNS queries in milliseconds */
  timeout?: number;
}

/**
 * Interface for DNS verification result
 */
export interface IDnsVerificationResult {
  record: string;
  found: boolean;
  valid: boolean;
  value?: string;
  expectedValue?: string;
  error?: string;
}

/**
 * Manager for DNS-related operations, including record lookups, verification, and generation
 */
export class DNSManager {
  public mtaRef: MtaService;
  private cache: Map<string, { data: any; expires: number }> = new Map();
  private defaultOptions: IDnsLookupOptions = {
    cacheTtl: 300000, // 5 minutes
    timeout: 5000 // 5 seconds
  };

  constructor(mtaRefArg: MtaService, options?: IDnsLookupOptions) {
    this.mtaRef = mtaRefArg;
    
    if (options) {
      this.defaultOptions = {
        ...this.defaultOptions,
        ...options
      };
    }
    
    // Ensure the DNS records directory exists
    plugins.smartfile.fs.ensureDirSync(paths.dnsRecordsDir);
  }

  /**
   * Lookup MX records for a domain
   * @param domain Domain to look up
   * @param options Lookup options
   * @returns Array of MX records sorted by priority
   */
  public async lookupMx(domain: string, options?: IDnsLookupOptions): Promise<plugins.dns.MxRecord[]> {
    const lookupOptions = { ...this.defaultOptions, ...options };
    const cacheKey = `mx:${domain}`;
    
    // Check cache first
    const cached = this.getFromCache<plugins.dns.MxRecord[]>(cacheKey);
    if (cached) {
      return cached;
    }
    
    try {
      const records = await this.dnsResolveMx(domain, lookupOptions.timeout);
      
      // Sort by priority
      records.sort((a, b) => a.priority - b.priority);
      
      // Cache the result
      this.setInCache(cacheKey, records, lookupOptions.cacheTtl);
      
      return records;
    } catch (error) {
      console.error(`Error looking up MX records for ${domain}:`, error);
      throw new Error(`Failed to lookup MX records for ${domain}: ${error.message}`);
    }
  }

  /**
   * Lookup TXT records for a domain
   * @param domain Domain to look up
   * @param options Lookup options
   * @returns Array of TXT records
   */
  public async lookupTxt(domain: string, options?: IDnsLookupOptions): Promise<string[][]> {
    const lookupOptions = { ...this.defaultOptions, ...options };
    const cacheKey = `txt:${domain}`;
    
    // Check cache first
    const cached = this.getFromCache<string[][]>(cacheKey);
    if (cached) {
      return cached;
    }
    
    try {
      const records = await this.dnsResolveTxt(domain, lookupOptions.timeout);
      
      // Cache the result
      this.setInCache(cacheKey, records, lookupOptions.cacheTtl);
      
      return records;
    } catch (error) {
      console.error(`Error looking up TXT records for ${domain}:`, error);
      throw new Error(`Failed to lookup TXT records for ${domain}: ${error.message}`);
    }
  }

  /**
   * Find specific TXT record by subdomain and prefix
   * @param domain Base domain
   * @param subdomain Subdomain prefix (e.g., "dkim._domainkey")
   * @param prefix Record prefix to match (e.g., "v=DKIM1")
   * @param options Lookup options
   * @returns Matching TXT record or null if not found
   */
  public async findTxtRecord(
    domain: string,
    subdomain: string = '',
    prefix: string = '',
    options?: IDnsLookupOptions
  ): Promise<string | null> {
    const fullDomain = subdomain ? `${subdomain}.${domain}` : domain;
    
    try {
      const records = await this.lookupTxt(fullDomain, options);
      
      for (const recordArray of records) {
        // TXT records can be split into chunks, join them
        const record = recordArray.join('');
        
        if (!prefix || record.startsWith(prefix)) {
          return record;
        }
      }
      
      return null;
    } catch (error) {
      // Domain might not exist or no TXT records
      console.log(`No matching TXT record found for ${fullDomain} with prefix ${prefix}`);
      return null;
    }
  }

  /**
   * Verify if a domain has a valid SPF record
   * @param domain Domain to verify
   * @returns Verification result
   */
  public async verifySpfRecord(domain: string): Promise<IDnsVerificationResult> {
    const result: IDnsVerificationResult = {
      record: 'SPF',
      found: false,
      valid: false
    };
    
    try {
      const spfRecord = await this.findTxtRecord(domain, '', 'v=spf1');
      
      if (spfRecord) {
        result.found = true;
        result.value = spfRecord;
        
        // Basic validation - check if it contains all, include, ip4, ip6, or mx mechanisms
        const isValid = /v=spf1\s+([-~?+]?(all|include:|ip4:|ip6:|mx|a|exists:))/.test(spfRecord);
        result.valid = isValid;
        
        if (!isValid) {
          result.error = 'SPF record format is invalid';
        }
      } else {
        result.error = 'No SPF record found';
      }
    } catch (error) {
      result.error = `Error verifying SPF: ${error.message}`;
    }
    
    return result;
  }

  /**
   * Verify if a domain has a valid DKIM record
   * @param domain Domain to verify
   * @param selector DKIM selector (usually "mta" in our case)
   * @returns Verification result
   */
  public async verifyDkimRecord(domain: string, selector: string = 'mta'): Promise<IDnsVerificationResult> {
    const result: IDnsVerificationResult = {
      record: 'DKIM',
      found: false,
      valid: false
    };
    
    try {
      const dkimSelector = `${selector}._domainkey`;
      const dkimRecord = await this.findTxtRecord(domain, dkimSelector, 'v=DKIM1');
      
      if (dkimRecord) {
        result.found = true;
        result.value = dkimRecord;
        
        // Basic validation - check for required fields
        const hasP = dkimRecord.includes('p=');
        result.valid = dkimRecord.includes('v=DKIM1') && hasP;
        
        if (!result.valid) {
          result.error = 'DKIM record is missing required fields';
        } else if (dkimRecord.includes('p=') && !dkimRecord.match(/p=[a-zA-Z0-9+/]+/)) {
          result.valid = false;
          result.error = 'DKIM record has invalid public key format';
        }
      } else {
        result.error = `No DKIM record found for selector ${selector}`;
      }
    } catch (error) {
      result.error = `Error verifying DKIM: ${error.message}`;
    }
    
    return result;
  }

  /**
   * Verify if a domain has a valid DMARC record
   * @param domain Domain to verify
   * @returns Verification result
   */
  public async verifyDmarcRecord(domain: string): Promise<IDnsVerificationResult> {
    const result: IDnsVerificationResult = {
      record: 'DMARC',
      found: false,
      valid: false
    };
    
    try {
      const dmarcDomain = `_dmarc.${domain}`;
      const dmarcRecord = await this.findTxtRecord(dmarcDomain, '', 'v=DMARC1');
      
      if (dmarcRecord) {
        result.found = true;
        result.value = dmarcRecord;
        
        // Basic validation - check for required fields
        const hasPolicy = dmarcRecord.includes('p=');
        result.valid = dmarcRecord.includes('v=DMARC1') && hasPolicy;
        
        if (!result.valid) {
          result.error = 'DMARC record is missing required fields';
        }
      } else {
        result.error = 'No DMARC record found';
      }
    } catch (error) {
      result.error = `Error verifying DMARC: ${error.message}`;
    }
    
    return result;
  }

  /**
   * Check all email authentication records (SPF, DKIM, DMARC) for a domain
   * @param domain Domain to check
   * @param dkimSelector DKIM selector
   * @returns Object with verification results for each record type
   */
  public async verifyEmailAuthRecords(domain: string, dkimSelector: string = 'mta'): Promise<{
    spf: IDnsVerificationResult;
    dkim: IDnsVerificationResult;
    dmarc: IDnsVerificationResult;
  }> {
    const [spf, dkim, dmarc] = await Promise.all([
      this.verifySpfRecord(domain),
      this.verifyDkimRecord(domain, dkimSelector),
      this.verifyDmarcRecord(domain)
    ]);
    
    return { spf, dkim, dmarc };
  }

  /**
   * Generate a recommended SPF record for a domain
   * @param domain Domain name
   * @param options Configuration options for the SPF record
   * @returns Generated SPF record
   */
  public generateSpfRecord(domain: string, options: {
    includeMx?: boolean;
    includeA?: boolean;
    includeIps?: string[];
    includeSpf?: string[];
    policy?: 'none' | 'neutral' | 'softfail' | 'fail' | 'reject';
  } = {}): IDnsRecord {
    const {
      includeMx = true,
      includeA = true,
      includeIps = [],
      includeSpf = [],
      policy = 'softfail'
    } = options;
    
    let value = 'v=spf1';
    
    if (includeMx) {
      value += ' mx';
    }
    
    if (includeA) {
      value += ' a';
    }
    
    // Add IP addresses
    for (const ip of includeIps) {
      if (ip.includes(':')) {
        value += ` ip6:${ip}`;
      } else {
        value += ` ip4:${ip}`;
      }
    }
    
    // Add includes
    for (const include of includeSpf) {
      value += ` include:${include}`;
    }
    
    // Add policy
    const policyMap = {
      'none': '?all',
      'neutral': '~all',
      'softfail': '~all',
      'fail': '-all',
      'reject': '-all'
    };
    
    value += ` ${policyMap[policy]}`;
    
    return {
      name: domain,
      type: 'TXT',
      value: value
    };
  }

  /**
   * Generate a recommended DMARC record for a domain
   * @param domain Domain name
   * @param options Configuration options for the DMARC record
   * @returns Generated DMARC record
   */
  public generateDmarcRecord(domain: string, options: {
    policy?: 'none' | 'quarantine' | 'reject';
    subdomainPolicy?: 'none' | 'quarantine' | 'reject';
    pct?: number;
    rua?: string;
    ruf?: string;
    daysInterval?: number;
  } = {}): IDnsRecord {
    const {
      policy = 'none',
      subdomainPolicy,
      pct = 100,
      rua,
      ruf,
      daysInterval = 1
    } = options;
    
    let value = 'v=DMARC1; p=' + policy;
    
    if (subdomainPolicy) {
      value += `; sp=${subdomainPolicy}`;
    }
    
    if (pct !== 100) {
      value += `; pct=${pct}`;
    }
    
    if (rua) {
      value += `; rua=mailto:${rua}`;
    }
    
    if (ruf) {
      value += `; ruf=mailto:${ruf}`;
    }
    
    if (daysInterval !== 1) {
      value += `; ri=${daysInterval * 86400}`;
    }
    
    // Add reporting format and ADKIM/ASPF alignment
    value += '; fo=1; adkim=r; aspf=r';
    
    return {
      name: `_dmarc.${domain}`,
      type: 'TXT',
      value: value
    };
  }

  /**
   * Save DNS record recommendations to a file
   * @param domain Domain name
   * @param records DNS records to save
   */
  public async saveDnsRecommendations(domain: string, records: IDnsRecord[]): Promise<void> {
    try {
      const filePath = plugins.path.join(paths.dnsRecordsDir, `${domain}.recommendations.json`);
      plugins.smartfile.memory.toFsSync(JSON.stringify(records, null, 2), filePath);
      console.log(`DNS recommendations for ${domain} saved to ${filePath}`);
    } catch (error) {
      console.error(`Error saving DNS recommendations for ${domain}:`, error);
    }
  }

  /**
   * Get cache key value
   * @param key Cache key
   * @returns Cached value or undefined if not found or expired
   */
  private getFromCache<T>(key: string): T | undefined {
    const cached = this.cache.get(key);
    
    if (cached && cached.expires > Date.now()) {
      return cached.data as T;
    }
    
    // Remove expired entry
    if (cached) {
      this.cache.delete(key);
    }
    
    return undefined;
  }

  /**
   * Set cache key value
   * @param key Cache key
   * @param data Data to cache
   * @param ttl TTL in milliseconds
   */
  private setInCache<T>(key: string, data: T, ttl: number = this.defaultOptions.cacheTtl): void {
    if (ttl <= 0) return; // Don't cache if TTL is disabled
    
    this.cache.set(key, {
      data,
      expires: Date.now() + ttl
    });
  }

  /**
   * Clear the DNS cache
   * @param key Optional specific key to clear, or all cache if not provided
   */
  public clearCache(key?: string): void {
    if (key) {
      this.cache.delete(key);
    } else {
      this.cache.clear();
    }
  }

  /**
   * Promise-based wrapper for dns.resolveMx
   * @param domain Domain to resolve
   * @param timeout Timeout in milliseconds
   * @returns Promise resolving to MX records
   */
  private dnsResolveMx(domain: string, timeout: number = 5000): Promise<plugins.dns.MxRecord[]> {
    return new Promise((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        reject(new Error(`DNS MX lookup timeout for ${domain}`));
      }, timeout);
      
      plugins.dns.resolveMx(domain, (err, addresses) => {
        clearTimeout(timeoutId);
        
        if (err) {
          reject(err);
        } else {
          resolve(addresses);
        }
      });
    });
  }

  /**
   * Promise-based wrapper for dns.resolveTxt
   * @param domain Domain to resolve
   * @param timeout Timeout in milliseconds
   * @returns Promise resolving to TXT records
   */
  private dnsResolveTxt(domain: string, timeout: number = 5000): Promise<string[][]> {
    return new Promise((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        reject(new Error(`DNS TXT lookup timeout for ${domain}`));
      }, timeout);
      
      plugins.dns.resolveTxt(domain, (err, records) => {
        clearTimeout(timeoutId);
        
        if (err) {
          reject(err);
        } else {
          resolve(records);
        }
      });
    });
  }

  /**
   * Generate all recommended DNS records for proper email authentication
   * @param domain Domain to generate records for
   * @returns Array of recommended DNS records
   */
  public async generateAllRecommendedRecords(domain: string): Promise<IDnsRecord[]> {
    const records: IDnsRecord[] = [];
    
    // Get DKIM record (already created by DKIMCreator)
    try {
      // Now using the public method
      const dkimRecord = await this.mtaRef.dkimCreator.getDNSRecordForDomain(domain);
      records.push(dkimRecord);
    } catch (error) {
      console.error(`Error getting DKIM record for ${domain}:`, error);
    }
    
    // Generate SPF record
    const spfRecord = this.generateSpfRecord(domain, {
      includeMx: true,
      includeA: true,
      policy: 'softfail'
    });
    records.push(spfRecord);
    
    // Generate DMARC record
    const dmarcRecord = this.generateDmarcRecord(domain, {
      policy: 'none', // Start with monitoring mode
      rua: `dmarc@${domain}` // Replace with appropriate report address
    });
    records.push(dmarcRecord);
    
    // Save recommendations
    await this.saveDnsRecommendations(domain, records);
    
    return records;
  }
}