require 'rubygems'
require 'fog/aws'

module Rubber
  module Dns

    class Aws < Base

      attr_accessor :client

      def initialize(env)
        super(env)
        creds = { :provider => 'aws', :aws_access_key_id => env.access_key, :aws_secret_access_key => env.access_secret }
        @client = ::Fog::DNS.new(creds)
      end
      
      def normalize_name(name, domain)
        domain = domain.gsub(/\.$/, "")

        if name
          name = name.gsub(/\.$/, "")
          name = name.gsub(/.?#{domain}$/, "")
          # Route 53 encodes asterisks in their ASCII octal representation.
          name = name.gsub("\\052", "*")
        end
        
        return name, domain
      end
      
      def denormalize_name(name, domain)
        if ! name || name.strip.empty?
          name = "#{domain}"
        else
          name = "#{name}.#{domain}"
        end

        name = "#{name}." 
        domain = "#{domain}."
        
        return name, domain
      end
      
      # Convert from fog/aws model to rubber option hash that represents a dns record
      def host_to_opts(host)
        opts = {}
        
        opts[:host] ||= host.name || ''
        opts[:domain] ||= host.zone.domain
        opts[:host], opts[:domain] = normalize_name(opts[:host], opts[:domain])
        
        opts[:type] ||= host.type
        opts[:ttl] ||= host.ttl.to_i if host.ttl
        
        opts[:data] ||= []
        if host.type =~ /MX/i
          host.value.each do |val|
            parts = val.split(" ")
            opts[:data] << {'priority' => parts[0], 'value' => parts[1]}
          end
        elsif ! host.alias_target.nil?
          # Convert from camel-case to snake-case for Route 53 ALIAS records
          # so the match the rubber config format.
          opts[:data] << {
            'hosted_zone_id' => host.alias_target['HostedZoneId'],
            'dns_name' => host.alias_target['DNSName'].split('.')[0..-1].join('.')
          }
          # Route 53 ALIAS records do not have a TTL, so delete the rubber-supplied default value.
          opts.delete(:ttl)
        else
          opts[:data].concat(Array(host.value))
        end

        return opts
      end

      # Convert from rubber option hash that represents a dns record to fog/aws model 
      def opts_to_host(opts)
        host = {}
        host[:name], domain = denormalize_name(opts[:host], opts[:domain])
        
        host[:type] =  opts[:type]
        host[:ttl] = opts[:ttl] if opts[:ttl]

        if opts[:data]
          # Route 53 requires the priority to be munged with the data value.
          if host[:type] =~ /MX/i
            host[:value] = opts[:data].collect {|o| "#{o[:priority]} #{o[:value]}"}
          elsif opts[:data].first.is_a?(Hash)
            # Route 53 allows creation of ALIAS records, which will always be
            # a Hash in the DNS config.  ALIAS records cannot have a TTL.
            host[:alias_target] = opts[:data].first
            host.delete(:value)
            host.delete(:ttl)
          else
            host[:value] = opts[:data]
          end
        end

        return host
      end

      def find_or_create_zone(domain)
        zone = @client.zones.all.find {|z| z.domain =~ /^#{domain}\.?/}
        if ! zone
          zone = @client.zones.create(:domain => domain)
        end
        return zone
      end
      
      def all_hosts(zone)
        hosts = []
        opts = {}
        has_more = true
        
        while has_more
          all_hosts = zone.records.all(opts)
          has_more = all_hosts.is_truncated
          opts = {:name => all_hosts.next_record_name,
                  :type => all_hosts.next_record_type
          }
          hosts.concat(all_hosts)
        end
        
        return hosts        
      end
      
      
      def find_hosts(opts = {})
        opts = setup_opts(opts, [:host, :domain])

        zone = find_or_create_zone(opts[:domain])
        host = opts_to_host(opts)
        
        if opts[:host] && opts[:host] != '*'
          found_host = zone.records.all(:name => host[:name], :type => host[:type], :max_items => 1).first
          found_host = nil if found_host && found_host.name != "#{host[:name]}." && found_host.type != host[:type]
          hosts = Array(found_host)
        else
          hosts = all_hosts(zone)
        end
        
        hosts = hosts.select {|h| h.name == host[:name] } if opts.has_key?(:host) && opts[:host] != '*'
        hosts = hosts.select {|h| h.type == host[:type] } if opts.has_key?(:type) && opts[:type] != '*'
        
        hosts
      end

      def find_host_records(opts = {})
        hosts = find_hosts(opts)
        result = hosts.collect {|h| host_to_opts(h).merge(:domain => opts[:domain]) }

        result
      end

      def create_host_record(opts = {})
        opts = setup_opts(opts, [:host, :data, :domain, :type, :ttl])
        zone = find_or_create_zone(opts[:domain])
        zone.records.create(opts_to_host(opts))
      end

      def destroy_host_record(opts = {})
        opts = setup_opts(opts, [:host, :domain])

        find_hosts(opts).each do |h|
          h.destroy || raise("Failed to destroy #{h.hostname}")
        end
      end

      def update_host_record(old_opts={}, new_opts={})
        old_opts = setup_opts(old_opts, [:host, :domain, :type])
        new_opts = setup_opts(new_opts, [:host, :domain, :type, :data])
        new_host = opts_to_host(new_opts)

        host = find_hosts(old_opts).first
        result = host.modify(new_host)
        result || raise("Failed to update host #{host.name}, #{host.errors.full_messages.join(', ')}")
      end
      
    end

  end
end
