import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from './classes.securitylogger.js';
import { LRUCache } from 'lru-cache';
import { CachedIPReputation } from '../db/documents/classes.cached.ip.reputation.js';

/**
 * 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 database (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 | undefined;
  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 persisted reputations into in-memory cache
    if (this.options.enableLocalCache) {
      this.loadCacheFromDb().catch((error: unknown) => {
        logger.log('error', `Failed to load IP reputation cache during initialization: ${(error as Error).message}`);
      });
    }
  }

  /**
   * 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;
  }

  /**
   * Reset the singleton instance (for shutdown/testing)
   */
  public static resetInstance(): void {
    IPReputationChecker.instance = undefined;
  }

  /**
   * 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 in-memory LRU cache first (fast path)
      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 in-memory LRU cache
      this.reputationCache.set(ip, result);

      // Persist to database if enabled (fire and forget)
      if (this.options.enableLocalCache) {
        this.persistReputationToDb(ip, result).catch((error: unknown) => {
          logger.log('error', `Failed to persist IP reputation for ${ip}: ${(error as Error).message}`);
        });
      }

      // Log the reputation check
      this.logReputationCheck(ip, result);

      return result;
    } catch (error: unknown) {
      logger.log('error', `Error checking IP reputation for ${ip}: ${(error as Error).message}`, {
        ip,
        stack: (error as Error).stack
      });

      return this.createErrorResult(ip, (error as 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: unknown) {
            if ((error as any).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: unknown) {
      logger.log('error', `Error checking DNSBL for ${ip}: ${(error as 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: unknown) {
      logger.log('error', `Error getting IP info for ${ip}: ${(error as 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
    });
  }

  /**
   * Persist a single IP reputation result to the database via CachedIPReputation
   */
  private async persistReputationToDb(ip: string, result: IReputationResult): Promise<void> {
    try {
      const data = {
        score: result.score,
        isSpam: result.isSpam,
        isProxy: result.isProxy,
        isTor: result.isTor,
        isVPN: result.isVPN,
        country: result.country,
        asn: result.asn,
        org: result.org,
        blacklists: result.blacklists,
      };

      const existing = await CachedIPReputation.findByIP(ip);
      if (existing) {
        existing.updateReputation(data);
        await existing.save();
      } else {
        const doc = CachedIPReputation.fromReputationData(ip, data);
        await doc.save();
      }
    } catch (error: unknown) {
      logger.log('error', `Failed to persist IP reputation for ${ip}: ${(error as Error).message}`);
    }
  }

  /**
   * Load persisted reputations from CachedIPReputation documents into the in-memory LRU cache
   */
  private async loadCacheFromDb(): Promise<void> {
    try {
      const docs = await CachedIPReputation.getInstances({});
      let loadedCount = 0;

      for (const doc of docs) {
        // Skip expired documents
        if (doc.isExpired()) {
          continue;
        }

        const result: IReputationResult = {
          score: doc.score,
          isSpam: doc.isSpam,
          isProxy: doc.isProxy,
          isTor: doc.isTor,
          isVPN: doc.isVPN,
          country: doc.country || undefined,
          asn: doc.asn || undefined,
          org: doc.org || undefined,
          blacklists: doc.blacklists || [],
          timestamp: doc.lastAccessedAt?.getTime() ?? doc.createdAt?.getTime() ?? Date.now(),
        };

        this.reputationCache.set(doc.ipAddress, result);
        loadedCount++;
      }

      if (loadedCount > 0) {
        logger.log('info', `Loaded ${loadedCount} IP reputation cache entries from database`);
      }
    } catch (error: unknown) {
      logger.log('error', `Failed to load IP reputation cache from database: ${(error as 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';
    }
  }
}
