import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { logger } from '../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from './classes.securitylogger.js';
import { LRUCache } from 'lru-cache';

/**
 * Reputation check result information
 */
export interface IReputationResult {
  score: number;          // 0 (worst) to 100 (best)
  isSpam: boolean;        // true if the IP is known for spam
  isProxy: boolean;       // true if the IP is a known proxy
  isTor: boolean;         // true if the IP is a known Tor exit node
  isVPN: boolean;         // true if the IP is a known VPN
  country?: string;       // Country code (if available)
  asn?: string;           // Autonomous System Number (if available)
  org?: string;           // Organization name (if available)
  blacklists?: string[];  // Names of blacklists that include this IP
  timestamp: number;      // When this result was created/retrieved
  error?: string;         // Error message if check failed
}

/**
 * Reputation threshold scores
 */
export enum ReputationThreshold {
  HIGH_RISK = 20,      // Score below this is considered high risk
  MEDIUM_RISK = 50,    // Score below this is considered medium risk
  LOW_RISK = 80        // Score below this is considered low risk (but not trusted)
}

/**
 * IP type classifications
 */
export enum IPType {
  RESIDENTIAL = 'residential',
  DATACENTER = 'datacenter',
  PROXY = 'proxy',
  TOR = 'tor',
  VPN = 'vpn',
  UNKNOWN = 'unknown'
}

/**
 * Options for the IP Reputation Checker
 */
export interface IIPReputationOptions {
  maxCacheSize?: number;  // Maximum number of IPs to cache (default: 10000)
  cacheTTL?: number;      // TTL for cache entries in ms (default: 24 hours)
  dnsblServers?: string[]; // List of DNSBL servers to check
  highRiskThreshold?: number; // Score below this is high risk
  mediumRiskThreshold?: number; // Score below this is medium risk
  lowRiskThreshold?: number; // Score below this is low risk
  enableLocalCache?: boolean; // Whether to persist cache to disk (default: true)
  enableDNSBL?: boolean; // Whether to use DNSBL checks (default: true)
  enableIPInfo?: boolean; // Whether to use IP info service (default: true)
}

/**
 * Class for checking IP reputation of inbound email senders
 */
export class IPReputationChecker {
  private static instance: IPReputationChecker;
  private reputationCache: LRUCache<string, IReputationResult>;
  private options: Required<IIPReputationOptions>;
  
  // Default DNSBL servers
  private static readonly DEFAULT_DNSBL_SERVERS = [
    'zen.spamhaus.org',         // Spamhaus
    'bl.spamcop.net',           // SpamCop
    'b.barracudacentral.org',   // Barracuda
    'spam.dnsbl.sorbs.net',     // SORBS
    'dnsbl.sorbs.net',          // SORBS (expanded)
    'cbl.abuseat.org',          // Composite Blocking List 
    'xbl.spamhaus.org',         // Spamhaus XBL
    'pbl.spamhaus.org',         // Spamhaus PBL
    'dnsbl-1.uceprotect.net',   // UCEPROTECT
    'psbl.surriel.com'          // PSBL
  ];
  
  // Default options
  private static readonly DEFAULT_OPTIONS: Required<IIPReputationOptions> = {
    maxCacheSize: 10000,
    cacheTTL: 24 * 60 * 60 * 1000, // 24 hours
    dnsblServers: IPReputationChecker.DEFAULT_DNSBL_SERVERS,
    highRiskThreshold: ReputationThreshold.HIGH_RISK,
    mediumRiskThreshold: ReputationThreshold.MEDIUM_RISK,
    lowRiskThreshold: ReputationThreshold.LOW_RISK,
    enableLocalCache: true,
    enableDNSBL: true,
    enableIPInfo: true
  };
  
  /**
   * Constructor for IPReputationChecker
   * @param options Configuration options
   */
  constructor(options: IIPReputationOptions = {}) {
    // Merge with default options
    this.options = {
      ...IPReputationChecker.DEFAULT_OPTIONS,
      ...options
    };
    
    // Initialize reputation cache
    this.reputationCache = new LRUCache<string, IReputationResult>({
      max: this.options.maxCacheSize,
      ttl: this.options.cacheTTL, // Cache TTL
    });
    
    // Load cache from disk if enabled
    if (this.options.enableLocalCache) {
      this.loadCache();
    }
  }
  
  /**
   * Get the singleton instance of the checker
   * @param options Configuration options
   * @returns Singleton instance
   */
  public static getInstance(options: IIPReputationOptions = {}): IPReputationChecker {
    if (!IPReputationChecker.instance) {
      IPReputationChecker.instance = new IPReputationChecker(options);
    }
    return IPReputationChecker.instance;
  }
  
  /**
   * Check an IP address's reputation
   * @param ip IP address to check
   * @returns Reputation check result
   */
  public async checkReputation(ip: string): Promise<IReputationResult> {
    try {
      // Validate IP address format
      if (!this.isValidIPAddress(ip)) {
        logger.log('warn', `Invalid IP address format: ${ip}`);
        return this.createErrorResult(ip, 'Invalid IP address format');
      }
      
      // Check cache first
      const cachedResult = this.reputationCache.get(ip);
      if (cachedResult) {
        logger.log('info', `Using cached reputation data for IP ${ip}`, {
          score: cachedResult.score,
          isSpam: cachedResult.isSpam
        });
        return cachedResult;
      }
      
      // Initialize empty result
      const result: IReputationResult = {
        score: 100, // Start with perfect score
        isSpam: false,
        isProxy: false,
        isTor: false,
        isVPN: false,
        timestamp: Date.now()
      };
      
      // Check IP against DNS blacklists if enabled
      if (this.options.enableDNSBL) {
        const dnsblResult = await this.checkDNSBL(ip);
        
        // Update result with DNSBL information
        result.score -= dnsblResult.listCount * 10; // Subtract 10 points per blacklist
        result.isSpam = dnsblResult.listCount > 0;
        result.blacklists = dnsblResult.lists;
      }
      
      // Get additional IP information if enabled
      if (this.options.enableIPInfo) {
        const ipInfo = await this.getIPInfo(ip);
        
        // Update result with IP info
        result.country = ipInfo.country;
        result.asn = ipInfo.asn;
        result.org = ipInfo.org;
        
        // Adjust score based on IP type
        if (ipInfo.type === IPType.PROXY || ipInfo.type === IPType.TOR || ipInfo.type === IPType.VPN) {
          result.score -= 30; // Subtract 30 points for proxies, Tor, VPNs
          
          // Set proxy flags
          result.isProxy = ipInfo.type === IPType.PROXY;
          result.isTor = ipInfo.type === IPType.TOR;
          result.isVPN = ipInfo.type === IPType.VPN;
        }
      }
      
      // Ensure score is between 0 and 100
      result.score = Math.max(0, Math.min(100, result.score));
      
      // Update cache with result
      this.reputationCache.set(ip, result);
      
      // Save cache if enabled
      if (this.options.enableLocalCache) {
        this.saveCache();
      }
      
      // Log the reputation check
      this.logReputationCheck(ip, result);
      
      return result;
    } catch (error) {
      logger.log('error', `Error checking IP reputation for ${ip}: ${error.message}`, {
        ip,
        stack: error.stack
      });
      
      return this.createErrorResult(ip, error.message);
    }
  }
  
  /**
   * Check an IP against DNS blacklists
   * @param ip IP address to check
   * @returns DNSBL check results
   */
  private async checkDNSBL(ip: string): Promise<{
    listCount: number;
    lists: string[];
  }> {
    try {
      // Reverse the IP for DNSBL queries
      const reversedIP = this.reverseIP(ip);
      
      const results = await Promise.allSettled(
        this.options.dnsblServers.map(async (server) => {
          try {
            const lookupDomain = `${reversedIP}.${server}`;
            await plugins.dns.promises.resolve(lookupDomain);
            return server; // IP is listed in this DNSBL
          } catch (error) {
            if (error.code === 'ENOTFOUND') {
              return null; // IP is not listed in this DNSBL
            }
            throw error; // Other error
          }
        })
      );
      
      // Extract successful lookups (listed in DNSBL)
      const lists = results
        .filter((result): result is PromiseFulfilledResult<string> => 
          result.status === 'fulfilled' && result.value !== null
        )
        .map(result => result.value);
      
      return {
        listCount: lists.length,
        lists
      };
    } catch (error) {
      logger.log('error', `Error checking DNSBL for ${ip}: ${error.message}`);
      return {
        listCount: 0,
        lists: []
      };
    }
  }
  
  /**
   * Get information about an IP address
   * @param ip IP address to check
   * @returns IP information
   */
  private async getIPInfo(ip: string): Promise<{
    country?: string;
    asn?: string;
    org?: string;
    type: IPType;
  }> {
    try {
      // In a real implementation, this would use an IP data service API
      // For this implementation, we'll use a simplified approach
      
      // Check if it's a known Tor exit node (simplified)
      const isTor = ip.startsWith('171.25.') || ip.startsWith('185.220.') || ip.startsWith('95.216.');
      
      // Check if it's a known VPN (simplified)
      const isVPN = ip.startsWith('185.156.') || ip.startsWith('37.120.');
      
      // Check if it's a known proxy (simplified)
      const isProxy = ip.startsWith('34.92.') || ip.startsWith('34.206.');
      
      // Determine IP type
      let type = IPType.UNKNOWN;
      if (isTor) {
        type = IPType.TOR;
      } else if (isVPN) {
        type = IPType.VPN;
      } else if (isProxy) {
        type = IPType.PROXY;
      } else {
        // Simple datacenters detection (major cloud providers)
        if (
          ip.startsWith('13.') || // AWS
          ip.startsWith('35.') || // Google Cloud
          ip.startsWith('52.') || // AWS
          ip.startsWith('34.') || // Google Cloud
          ip.startsWith('104.') // Various providers
        ) {
          type = IPType.DATACENTER;
        } else {
          type = IPType.RESIDENTIAL;
        }
      }
      
      // Return the information
      return {
        country: this.determineCountry(ip), // Simplified, would use geolocation service
        asn: 'AS12345', // Simplified, would look up real ASN
        org: this.determineOrg(ip), // Simplified, would use real org data
        type
      };
    } catch (error) {
      logger.log('error', `Error getting IP info for ${ip}: ${error.message}`);
      return {
        type: IPType.UNKNOWN
      };
    }
  }
  
  /**
   * Simplified method to determine country from IP
   * In a real implementation, this would use a geolocation database or service
   * @param ip IP address
   * @returns Country code
   */
  private determineCountry(ip: string): string {
    // Simplified mapping for demo purposes
    if (ip.startsWith('13.') || ip.startsWith('52.')) return 'US';
    if (ip.startsWith('35.') || ip.startsWith('34.')) return 'US';
    if (ip.startsWith('185.')) return 'NL';
    if (ip.startsWith('171.')) return 'DE';
    return 'XX'; // Unknown
  }
  
  /**
   * Simplified method to determine organization from IP
   * In a real implementation, this would use an IP-to-org database or service
   * @param ip IP address
   * @returns Organization name
   */
  private determineOrg(ip: string): string {
    // Simplified mapping for demo purposes
    if (ip.startsWith('13.') || ip.startsWith('52.')) return 'Amazon AWS';
    if (ip.startsWith('35.') || ip.startsWith('34.')) return 'Google Cloud';
    if (ip.startsWith('185.156.')) return 'NordVPN';
    if (ip.startsWith('37.120.')) return 'ExpressVPN';
    if (ip.startsWith('185.220.')) return 'Tor Exit Node';
    return 'Unknown';
  }
  
  /**
   * Reverse an IP address for DNSBL lookups (e.g., 1.2.3.4 -> 4.3.2.1)
   * @param ip IP address to reverse
   * @returns Reversed IP for DNSBL queries
   */
  private reverseIP(ip: string): string {
    return ip.split('.').reverse().join('.');
  }
  
  /**
   * Create an error result for when reputation check fails
   * @param ip IP address
   * @param errorMessage Error message
   * @returns Error result
   */
  private createErrorResult(ip: string, errorMessage: string): IReputationResult {
    return {
      score: 50, // Neutral score for errors
      isSpam: false,
      isProxy: false,
      isTor: false,
      isVPN: false,
      timestamp: Date.now(),
      error: errorMessage
    };
  }
  
  /**
   * Validate IP address format
   * @param ip IP address to validate
   * @returns Whether the IP is valid
   */
  private isValidIPAddress(ip: string): boolean {
    // IPv4 regex pattern
    const ipv4Pattern = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
    return ipv4Pattern.test(ip);
  }
  
  /**
   * Log reputation check to security logger
   * @param ip IP address
   * @param result Reputation result
   */
  private logReputationCheck(ip: string, result: IReputationResult): void {
    // Determine log level based on reputation score
    let logLevel = SecurityLogLevel.INFO;
    if (result.score < this.options.highRiskThreshold) {
      logLevel = SecurityLogLevel.WARN;
    } else if (result.score < this.options.mediumRiskThreshold) {
      logLevel = SecurityLogLevel.INFO;
    }
    
    // Log the check
    SecurityLogger.getInstance().logEvent({
      level: logLevel,
      type: SecurityEventType.IP_REPUTATION,
      message: `IP reputation check ${result.isSpam ? 'flagged spam' : 'completed'} for ${ip}`,
      ipAddress: ip,
      details: {
        score: result.score,
        isSpam: result.isSpam,
        isProxy: result.isProxy,
        isTor: result.isTor,
        isVPN: result.isVPN,
        country: result.country,
        blacklists: result.blacklists
      },
      success: !result.isSpam
    });
  }
  
  /**
   * Save cache to disk
   */
  private saveCache(): void {
    try {
      // Convert cache entries to serializable array
      const entries = Array.from(this.reputationCache.entries()).map(([ip, data]) => ({
        ip,
        data
      }));
      
      // Only save if we have entries
      if (entries.length === 0) {
        return;
      }
      
      // Ensure directory exists
      const cacheDir = plugins.path.join(paths.dataDir, 'security');
      plugins.smartfile.fs.ensureDirSync(cacheDir);
      
      // Save to file
      const cacheFile = plugins.path.join(cacheDir, 'ip_reputation_cache.json');
      plugins.smartfile.memory.toFsSync(
        JSON.stringify(entries),
        cacheFile
      );
      
      logger.log('info', `Saved ${entries.length} IP reputation cache entries to disk`);
    } catch (error) {
      logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
    }
  }
  
  /**
   * Load cache from disk
   */
  private loadCache(): void {
    try {
      const cacheFile = plugins.path.join(paths.dataDir, 'security', 'ip_reputation_cache.json');
      
      // Check if file exists
      if (!plugins.fs.existsSync(cacheFile)) {
        return;
      }
      
      // Read and parse cache
      const cacheData = plugins.fs.readFileSync(cacheFile, 'utf8');
      const entries = JSON.parse(cacheData);
      
      // Validate and filter entries
      const now = Date.now();
      const validEntries = entries.filter(entry => {
        const age = now - entry.data.timestamp;
        return age < this.options.cacheTTL; // Only load entries that haven't expired
      });
      
      // Restore cache
      for (const entry of validEntries) {
        this.reputationCache.set(entry.ip, entry.data);
      }
      
      logger.log('info', `Loaded ${validEntries.length} IP reputation cache entries from disk`);
    } catch (error) {
      logger.log('error', `Failed to load IP reputation cache: ${error.message}`);
    }
  }
  
  /**
   * Get the risk level for a reputation score
   * @param score Reputation score (0-100)
   * @returns Risk level description
   */
  public static getRiskLevel(score: number): 'high' | 'medium' | 'low' | 'trusted' {
    if (score < ReputationThreshold.HIGH_RISK) {
      return 'high';
    } else if (score < ReputationThreshold.MEDIUM_RISK) {
      return 'medium';
    } else if (score < ReputationThreshold.LOW_RISK) {
      return 'low';
    } else {
      return 'trusted';
    }
  }
}