import * as plugins from '../../plugins.js';

/**
 * Utility class for IP address operations
 */
export class IpUtils {
  /**
   * Check if the IP matches any of the glob patterns
   *
   * This method checks IP addresses against glob patterns and handles IPv4/IPv6 normalization.
   * It's used to implement IP filtering based on security configurations.
   *
   * @param ip - The IP address to check
   * @param patterns - Array of glob patterns
   * @returns true if IP matches any pattern, false otherwise
   */
  public static isGlobIPMatch(ip: string, patterns: string[]): boolean {
    if (!ip || !patterns || patterns.length === 0) return false;

    // Normalize the IP being checked
    const normalizedIPVariants = this.normalizeIP(ip);
    if (normalizedIPVariants.length === 0) return false;

    // Check each pattern
    for (const pattern of patterns) {
      // Handle CIDR notation
      if (pattern.includes('/')) {
        if (this.matchCIDR(ip, pattern)) {
          return true;
        }
        continue;
      }

      // Handle range notation
      if (pattern.includes('-') && !pattern.includes('*')) {
        if (this.matchIPRange(ip, pattern)) {
          return true;
        }
        continue;
      }

      // Expand shorthand patterns for glob matching
      let expandedPattern = pattern;
      if (pattern.includes('*') && !pattern.includes(':')) {
        const parts = pattern.split('.');
        while (parts.length < 4) {
          parts.push('*');
        }
        expandedPattern = parts.join('.');
      }

      // Normalize and check with minimatch
      const normalizedPatterns = this.normalizeIP(expandedPattern);
      
      for (const ipVariant of normalizedIPVariants) {
        for (const normalizedPattern of normalizedPatterns) {
          if (plugins.minimatch(ipVariant, normalizedPattern)) {
            return true;
          }
        }
      }
    }

    return false;
  }

  /**
   * Normalize IP addresses for consistent comparison
   * 
   * @param ip The IP address to normalize
   * @returns Array of normalized IP forms
   */
  public static normalizeIP(ip: string): string[] {
    if (!ip) return [];
    
    // Handle IPv4-mapped IPv6 addresses (::ffff:127.0.0.1)
    if (ip.startsWith('::ffff:')) {
      const ipv4 = ip.slice(7);
      return [ip, ipv4];
    }
    
    // Handle IPv4 addresses by also checking IPv4-mapped form
    if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
      return [ip, `::ffff:${ip}`];
    }
    
    return [ip];
  }

  /**
   * Check if an IP is authorized using security rules
   *
   * @param ip - The IP address to check
   * @param allowedIPs - Array of allowed IP patterns
   * @param blockedIPs - Array of blocked IP patterns
   * @returns true if IP is authorized, false if blocked
   */
  public static isIPAuthorized(ip: string, allowedIPs: string[] = [], blockedIPs: string[] = []): boolean {
    // Skip IP validation if no rules are defined
    if (!ip || (allowedIPs.length === 0 && blockedIPs.length === 0)) {
      return true;
    }

    // First check if IP is blocked - blocked IPs take precedence
    if (blockedIPs.length > 0 && this.isGlobIPMatch(ip, blockedIPs)) {
      return false;
    }

    // Then check if IP is allowed (if no allowed IPs are specified, all non-blocked IPs are allowed)
    return allowedIPs.length === 0 || this.isGlobIPMatch(ip, allowedIPs);
  }

  /**
   * Check if an IP address is a private network address
   * 
   * @param ip The IP address to check
   * @returns true if the IP is a private network address, false otherwise
   */
  public static isPrivateIP(ip: string): boolean {
    if (!ip) return false;

    // Handle IPv4-mapped IPv6 addresses
    if (ip.startsWith('::ffff:')) {
      ip = ip.slice(7);
    }

    // Check IPv4 private ranges
    if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
      const parts = ip.split('.').map(Number);
      
      // Check common private ranges
      // 10.0.0.0/8
      if (parts[0] === 10) return true;
      
      // 172.16.0.0/12
      if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
      
      // 192.168.0.0/16
      if (parts[0] === 192 && parts[1] === 168) return true;
      
      // 127.0.0.0/8 (localhost)
      if (parts[0] === 127) return true;
      
      return false;
    }
    
    // IPv6 local addresses
    return ip === '::1' || ip.startsWith('fc00:') || ip.startsWith('fd00:') || ip.startsWith('fe80:');
  }

  /**
   * Check if an IP address is a public network address
   * 
   * @param ip The IP address to check
   * @returns true if the IP is a public network address, false otherwise
   */
  public static isPublicIP(ip: string): boolean {
    return !this.isPrivateIP(ip);
  }

  /**
   * Check if an IP matches a CIDR notation
   * 
   * @param ip The IP address to check
   * @param cidr The CIDR notation (e.g., "192.168.1.0/24")
   * @returns true if IP is within the CIDR range
   */
  private static matchCIDR(ip: string, cidr: string): boolean {
    if (!cidr.includes('/')) return false;
    
    const [networkAddr, prefixStr] = cidr.split('/');
    const prefix = parseInt(prefixStr, 10);
    
    // Handle IPv4-mapped IPv6 in the IP being checked
    let checkIP = ip;
    if (checkIP.startsWith('::ffff:')) {
      checkIP = checkIP.slice(7);
    }
    
    // Handle IPv6 CIDR
    if (networkAddr.includes(':')) {
      // TODO: Implement IPv6 CIDR matching
      return false;
    }
    
    // IPv4 CIDR matching
    if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(checkIP)) return false;
    if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(networkAddr)) return false;
    if (isNaN(prefix) || prefix < 0 || prefix > 32) return false;
    
    const ipParts = checkIP.split('.').map(Number);
    const netParts = networkAddr.split('.').map(Number);
    
    // Validate IP parts
    for (const part of [...ipParts, ...netParts]) {
      if (part < 0 || part > 255) return false;
    }
    
    // Convert to 32-bit integers
    const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3];
    const netNum = (netParts[0] << 24) | (netParts[1] << 16) | (netParts[2] << 8) | netParts[3];
    
    // Create mask
    const mask = (-1 << (32 - prefix)) >>> 0;
    
    // Check if IP is in network range
    return (ipNum & mask) === (netNum & mask);
  }

  /**
   * Check if an IP matches a range notation
   * 
   * @param ip The IP address to check
   * @param range The range notation (e.g., "192.168.1.1-192.168.1.100")
   * @returns true if IP is within the range
   */
  private static matchIPRange(ip: string, range: string): boolean {
    if (!range.includes('-')) return false;
    
    const [startIP, endIP] = range.split('-').map(s => s.trim());
    
    // Handle IPv4-mapped IPv6 in the IP being checked
    let checkIP = ip;
    if (checkIP.startsWith('::ffff:')) {
      checkIP = checkIP.slice(7);
    }
    
    // Only handle IPv4 for now
    if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(checkIP)) return false;
    if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(startIP)) return false;
    if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(endIP)) return false;
    
    const ipParts = checkIP.split('.').map(Number);
    const startParts = startIP.split('.').map(Number);
    const endParts = endIP.split('.').map(Number);
    
    // Validate parts
    for (const part of [...ipParts, ...startParts, ...endParts]) {
      if (part < 0 || part > 255) return false;
    }
    
    // Convert to 32-bit integers for comparison
    const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3];
    const startNum = (startParts[0] << 24) | (startParts[1] << 16) | (startParts[2] << 8) | startParts[3];
    const endNum = (endParts[0] << 24) | (endParts[1] << 16) | (endParts[2] << 8) | endParts[3];
    
    // Convert to unsigned for proper comparison
    const ipUnsigned = ipNum >>> 0;
    const startUnsigned = startNum >>> 0;
    const endUnsigned = endNum >>> 0;
    
    return ipUnsigned >= startUnsigned && ipUnsigned <= endUnsigned;
  }

  /**
   * Convert a subnet CIDR to an IP range for filtering
   * 
   * @param cidr The CIDR notation (e.g., "192.168.1.0/24")
   * @returns Array of glob patterns that match the CIDR range
   */
  public static cidrToGlobPatterns(cidr: string): string[] {
    if (!cidr || !cidr.includes('/')) return [];
    
    const [ipPart, prefixPart] = cidr.split('/');
    const prefix = parseInt(prefixPart, 10);
    
    if (isNaN(prefix) || prefix < 0 || prefix > 32) return [];
    
    // For IPv4 only for now
    if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(ipPart)) return [];
    
    const ipParts = ipPart.split('.').map(Number);
    const fullMask = Math.pow(2, 32 - prefix) - 1;
    
    // Convert IP to a numeric value
    const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3];
    
    // Calculate network address (IP & ~fullMask)
    const networkNum = ipNum & ~fullMask;
    
    // For large ranges, return wildcard patterns
    if (prefix <= 8) {
      return [`${(networkNum >>> 24) & 255}.*.*.*`];
    } else if (prefix <= 16) {
      return [`${(networkNum >>> 24) & 255}.${(networkNum >>> 16) & 255}.*.*`];
    } else if (prefix <= 24) {
      return [`${(networkNum >>> 24) & 255}.${(networkNum >>> 16) & 255}.${(networkNum >>> 8) & 255}.*`];
    }
    
    // For small ranges, create individual IP patterns
    const patterns = [];
    const maxAddresses = Math.min(256, Math.pow(2, 32 - prefix));
    
    for (let i = 0; i < maxAddresses; i++) {
      const currentIpNum = networkNum + i;
      patterns.push(
        `${(currentIpNum >>> 24) & 255}.${(currentIpNum >>> 16) & 255}.${(currentIpNum >>> 8) & 255}.${currentIpNum & 255}`
      );
    }
    
    return patterns;
  }
}