# encoding: UTF-8
require 'log4r'
require 'ipaddr'
require 'uri'
require 'mauve/mauve_time'
require 'mauve/mauve_resolv'

module Mauve

  # A simple construct to match sources.
  #
  # One can ask if an IPv4, IPv6, hostname or url (match on hostname only) is
  # contained within a list.  If the query is not an IP address, it will be
  # converted into one as the checks are made.
  #
  # Note that the matching is greedy.  When a hostname maps to several IP
  # addresses and only one of tbhose is included in the list, a match 
  # will occur.
  #
  class SourceList 

    attr_reader :label, :list

    ## Default contructor.
    def initialize (label)
      @label            = label
      @last_resolved_at = nil
      @list = []
      @resolved_list = []
    end

    alias username label

    # Adds a source onto the list.
    #
    # The source can be a string, or array of strings.  Each one can be an IPv6
    # or IPv4 address or range, or a hostname.
    #
    # Hostnames can have *, or numeric ranges in their name.  A '*' represents
    # any character except ".".  A range can be specified as 1..4, meaning 1,
    # 2, 3 or 4.
    #
    # e.g.  1.2.3.4/24
    #       2001:dead::beef/64
    #       app1..10.my-customer.com
    #       *.db.my-customer.com
    #
    # Hostnames are also resolved into IP addresses, and re-resolved every 30
    # minutes.
    #
    # @param [String or Array] l The source(s) to add.
    # @return [SourceList]
    #
    def +(l)
      arr = [l].flatten.collect do |h|
        # "*"              means [^\.]+
        # "(\d+)\.\.(\d+)" is expanded to every integer between $1 and $2
        #                  joined by a pipe, e.g. 1..5 means 1|2|3|4|5
        #  "."              is literal, not a single-character match
        if h.is_a?(String) and (h =~ /[\[\]\*]/ or h =~ /(\d+)\.\.(\d+)/)
          Regexp.new(
              h.
              gsub(/(\d+)\.\.(\d+)/) { |a,b|
                ($1.to_i..$2.to_i).collect.join("|")
              }.
              gsub(/\./, "\\.").
              gsub(/\*/, "[0-9a-z\\-]+") +
              "\\.?$")
        elsif h.is_a?(String) and h =~ /^[0-9a-f\.:]+(\/\d+)?$/i
          IPAddr.new(h)
        elsif h.is_a?(String) or h.is_a?(Regexp)
          h
        else
          logger.warn "Cannot add #{h.inspect} to source list #{@label} as it is not a string or regular expression."
          nil
        end
      end.flatten.reject{|h| h.nil?}

      arr.each do |source|
        ##
        # I would use include? here, but IPAddr tries to convert "foreign"
        # classes to intgers, and RegExp doesn't have a to_i method..
        #
        if @list.any?{|e| source.is_a?(e.class) and source == e}
          logger.warn "#{source} is already on the #{self.label} list"
        else
          @list << source
        end
      end

      @resolved_list    = [] 
      @last_resolved_at = nil

      self
    end

    alias add_to_list +

    # @return [Log4r::Logger]
    def logger
      @logger ||= Log4r::Logger.new self.class.to_s
    end

    # 
    # Return whether or not a list contains a source.
    #
    # First the hostname is checked for a URI, using URI#parse, and then the
    # hostname is extracted from there.  If that fails, the original hostname
    # is used.
    #
    # Next we check against our list, including all IPs for any hostnames in
    # that list.
    #
    # If nothing is found, the hostname is then resolved to its IPs, and we
    # check to see if those IPs are in our list. 
    #
    # @param [String] host The host to look for.
    # @return [Boolean]
    def includes?(host)
      #
      # Redo resolution every thirty minutes
      #
      resolve if @resolved_list.empty? or @last_resolved_at.nil? or (Time.now - 1800) > @last_resolved_at

      #
      # Pick out hostnames from URIs.
      #
      if host =~ /^[a-z][a-z0-9+-]+:\/\//
        begin      
          uri = URI.parse(host)
          host = uri.host unless uri.host.nil?
        rescue URI::InvalidURIError => ex
          # ugh
          logger.warn "Did not recognise URI #{host}"
        end
      end

      return true if @resolved_list.any? do |l|
        case l
          when String
            host == l
          when Regexp
            host =~ l
          when IPAddr 
            begin
              l.include?(IPAddr.new(host))
            rescue ArgumentError
              # rescue random IPAddr argument errors
              false
            end
          else
            false
        end
      end

      return false unless @resolved_list.any?{|l| l.is_a?(IPAddr)}

      ips = MauveResolv.get_ips_for(host).collect{|i| IPAddr.new(i)}

      return false if ips.empty?

      return @resolved_list.select{|i| i.is_a?(IPAddr)}.any? do |list_ip| 
        ips.any?{|ip| list_ip.include?(ip)}
      end
      
      return false
    end

    # 
    # Resolve all hostnames in the list to IP addresses.
    #
    # @return [Array] The new list.
    #
    def resolve
      @last_resolved_at = Time.now

      new_list = @list.collect do |host| 
        if host.is_a?(String)
          [host] + MauveResolv.get_ips_for(host).collect{|i| IPAddr.new(i)}
        else
          host
        end
      end
      @resolved_list = new_list.flatten
    end
  end

end