import type { IMatcher, IIpMatchOptions } from '../types.js';

/**
 * IpMatcher provides comprehensive IP address matching functionality
 * Supporting exact matches, CIDR notation, ranges, and wildcards
 */
export class IpMatcher implements IMatcher<boolean, IIpMatchOptions> {
  /**
   * Check if a value is a valid IPv4 address
   */
  static isValidIpv4(ip: string): boolean {
    const parts = ip.split('.');
    if (parts.length !== 4) return false;
    
    return parts.every(part => {
      const num = parseInt(part, 10);
      return !isNaN(num) && num >= 0 && num <= 255 && part === num.toString();
    });
  }

  /**
   * Check if a value is a valid IPv6 address (simplified check)
   */
  static isValidIpv6(ip: string): boolean {
    // Basic IPv6 validation - can be enhanced
    const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|(([0-9a-fA-F]{1,4}:){1,7}|:):|(([0-9a-fA-F]{1,4}:){1,6}|::):[0-9a-fA-F]{1,4})$/;
    return ipv6Regex.test(ip);
  }

  /**
   * Convert IP address to numeric value for comparison
   */
  private static ipToNumber(ip: string): number {
    const parts = ip.split('.');
    return parts.reduce((acc, part, index) => {
      return acc + (parseInt(part, 10) << (8 * (3 - index)));
    }, 0);
  }

  /**
   * Match an IP against a CIDR notation pattern
   */
  static matchCidr(cidr: string, ip: string): boolean {
    const [range, bits] = cidr.split('/');
    if (!bits || !this.isValidIpv4(range) || !this.isValidIpv4(ip)) {
      return false;
    }

    const rangeMask = parseInt(bits, 10);
    if (isNaN(rangeMask) || rangeMask < 0 || rangeMask > 32) {
      return false;
    }

    const rangeNum = this.ipToNumber(range);
    const ipNum = this.ipToNumber(ip);
    const mask = (-1 << (32 - rangeMask)) >>> 0;

    return (rangeNum & mask) === (ipNum & mask);
  }

  /**
   * Match an IP against a wildcard pattern
   */
  static matchWildcard(pattern: string, ip: string): boolean {
    if (!this.isValidIpv4(ip)) return false;

    const patternParts = pattern.split('.');
    const ipParts = ip.split('.');

    if (patternParts.length !== 4) return false;

    return patternParts.every((part, index) => {
      if (part === '*') return true;
      return part === ipParts[index];
    });
  }

  /**
   * Match an IP against a range (e.g., "192.168.1.1-192.168.1.100")
   */
  static matchRange(range: string, ip: string): boolean {
    const [start, end] = range.split('-').map(s => s.trim());
    
    if (!start || !end || !this.isValidIpv4(start) || !this.isValidIpv4(end) || !this.isValidIpv4(ip)) {
      return false;
    }

    const startNum = this.ipToNumber(start);
    const endNum = this.ipToNumber(end);
    const ipNum = this.ipToNumber(ip);

    return ipNum >= startNum && ipNum <= endNum;
  }

  /**
   * Match an IP pattern against an IP address
   * Supports multiple formats:
   * - Exact match: "192.168.1.1"
   * - CIDR: "192.168.1.0/24"
   * - Wildcard: "192.168.1.*"
   * - Range: "192.168.1.1-192.168.1.100"
   */
  static match(
    pattern: string, 
    ip: string, 
    options: IIpMatchOptions = {}
  ): boolean {
    // Handle null/undefined cases
    if (!pattern || !ip) {
      return false;
    }

    // Normalize inputs
    const normalizedPattern = pattern.trim();
    const normalizedIp = ip.trim();

    // Extract IPv4 from IPv6-mapped addresses (::ffff:192.168.1.1)
    const ipv4Match = normalizedIp.match(/::ffff:(\d+\.\d+\.\d+\.\d+)/i);
    const testIp = ipv4Match ? ipv4Match[1] : normalizedIp;

    // Exact match
    if (normalizedPattern === testIp) {
      return true;
    }

    // CIDR notation
    if (options.allowCidr !== false && normalizedPattern.includes('/')) {
      return this.matchCidr(normalizedPattern, testIp);
    }

    // Wildcard matching
    if (normalizedPattern.includes('*')) {
      return this.matchWildcard(normalizedPattern, testIp);
    }

    // Range matching
    if (options.allowRanges !== false && normalizedPattern.includes('-')) {
      return this.matchRange(normalizedPattern, testIp);
    }

    return false;
  }

  /**
   * Check if an IP is authorized based on allow and block lists
   */
  static isAuthorized(
    ip: string,
    allowList: string[] = [],
    blockList: string[] = []
  ): boolean {
    // If IP is in block list, deny
    if (blockList.some(pattern => this.match(pattern, ip))) {
      return false;
    }

    // If allow list is empty, allow all (except blocked)
    if (allowList.length === 0) {
      return true;
    }

    // If allow list exists, IP must match
    return allowList.some(pattern => this.match(pattern, ip));
  }

  /**
   * Calculate the specificity of an IP pattern
   * Higher values mean more specific patterns
   */
  static calculateSpecificity(pattern: string): number {
    if (!pattern) return 0;

    let score = 0;
    
    // Exact IPs are most specific
    if (this.isValidIpv4(pattern) || this.isValidIpv6(pattern)) {
      score += 100;
    }
    
    // CIDR notation
    if (pattern.includes('/')) {
      const [, bits] = pattern.split('/');
      const maskBits = parseInt(bits, 10);
      if (!isNaN(maskBits)) {
        score += maskBits; // Higher mask = more specific
      }
    }
    
    // Wildcard patterns
    const wildcards = (pattern.match(/\*/g) || []).length;
    score -= wildcards * 20; // More wildcards = less specific
    
    // Range patterns are somewhat specific
    if (pattern.includes('-')) {
      score += 30;
    }
    
    return score;
  }

  /**
   * Instance method for interface compliance
   */
  match(pattern: string, ip: string, options?: IIpMatchOptions): boolean {
    return IpMatcher.match(pattern, ip, options);
  }
}