class IpComparison

  MATCH = :ip_match
  PARTIAL_MATCH = :partial_geo_match
  NO_MATCH = :no_ip_match

  GEO_COUNTRY = 'country'
  GEO_REGION = 'region'
  GEO_CITY = 'city'
  GEO_ISP = 'isp'

  AN_IPV4_WITHIN_TEXT =
    Regexp.new(
      Resolv::IPv4::Regex.to_s.
        # replace ruby stdlib resolver's IPv4 regex start/end
        # operators with word-boundary operators
        gsub(/\\A|\\z/, '\b')
    )

  def self.compare_ips(candidate_ip, ips_to_match)
    is_exact_match = ips_to_match.include?(candidate_ip)

    return IpComparisonResult.new(MATCH) if is_exact_match

    ips_to_match.each do |ip_to_match|
      next unless partial_ip_match_by_prefix(candidate_ip, ip_to_match)
      result = partial_ip_match_by_geo(candidate_ip, ip_to_match)

      return result if result.matching?
    end

    return IpComparisonResult.new(NO_MATCH)
  end

  # per spec, we consider an IP to be partially matching another if the first two octets
  # are equal
  def self.partial_ip_match_by_prefix(first_ip, second_ip)
    first_octets = first_ip.split('.')
    second_octets = second_ip.split('.')

    return false unless first_octets.length == second_octets.length

    return first_octets[0] == second_octets[0] && first_octets[1] == second_octets[1]
  end

  def self.partial_ip_match_by_geo(first_ip, second_ip)
    first_lookup = Maxmind.lookup(first_ip)
    second_lookup = Maxmind.lookup(second_ip)

    return IpComparisonResult.new(NO_MATCH) unless first_lookup.found? && second_lookup.found?

    first_region = first_lookup.subdivisions.most_specific.name
    second_region = second_lookup.subdivisions.most_specific.name

    first_city = first_lookup.city.name
    second_city = second_lookup.city.name

    first_isp_lookup = Maxmind.isp_lookup(first_ip)
    second_isp_lookup = Maxmind.isp_lookup(second_ip)

    country_is_matching = first_lookup.country.name == second_lookup.country.name
    region_is_matching = first_region == second_region
    city_is_matching = first_city == second_city
    isp_is_matching = first_isp_lookup['isp'] == second_isp_lookup['isp']

    region_missing = !first_region && !second_region
    city_missing = !first_city && !second_city

    matches = {}

    matches['Country'] = GEO_COUNTRY if country_is_matching
    matches['Region'] = GEO_REGION if region_is_matching
    matches['City'] = GEO_CITY if city_is_matching
    matches['ISP'] = GEO_ISP if isp_is_matching

    result = IpComparisonResult.new(PARTIAL_MATCH, matches)

    # if the geo lookup fully matches, we don't care if the ISP does not match
    return result if country_is_matching && region_is_matching && city_is_matching
    # if country, region, and ISP match, we don't care if the city is missing or doesn't match
    return result if country_is_matching && region_is_matching && (!city_is_matching || city_missing) && isp_is_matching
    # if country, city, and ISP match, it's still a match if the region is missing
    return result if country_is_matching && region_missing && city_is_matching && isp_is_matching

    return IpComparisonResult.new(NO_MATCH, matches)
  end

  def self.geo_lookup_ip(ip_address)
    geo_lookup = Maxmind.lookup(ip_address)
    isp_lookup = Maxmind.isp_lookup(ip_address)

    return IpGeo.from_maxmind(geo_lookup, isp_lookup)
  end

  def self.ips_from_text(text)
    return [] if text.nil?

    text.scan(AN_IPV4_WITHIN_TEXT).map do |ip_address_by_octet|
      ip_address_by_octet.join('.')
    end
  end
end
