module Ironfan
  class Provider
    class Ec2

      class SecurityGroup < Ironfan::Provider::Resource
        delegate :_dump, :authorize_group_and_owner, :authorize_port_range,
            :collection, :collection=, :connection, :connection=, :description,
            :description=, :destroy, :group_id, :group_id=, :identity,
            :identity=, :ip_permissions, :ip_permissions=, :name, :name=,
            :new_record?, :owner_id, :owner_id=, :reload, :requires,
            :requires_one, :revoke_group_and_owner, :revoke_port_range, :save,
            :symbolize_keys, :vpc_id, :vpc_id=, :wait_for,
          :to => :adaptee
        field :ensured,        :boolean,       :default => false

        def self.shared?()      true;   end
        def self.multiple?()    true;   end
        def self.resource_type()        :security_group;   end
        def self.expected_ids(computer)
          computer.server.cloud(:ec2).security_groups.keys.map{|k| k.to_s}.uniq
        end

        #
        # Discovery
        #
        def self.load!(cluster=nil)
          Ec2.connection.security_groups.each do |sg|
            remember SecurityGroup.new(:adaptee => sg) unless sg.blank?
          end
        end

        #
        # Manipulation
        #

        def self.create!(computer)
          return unless Ec2.applicable computer
 
          ensure_groups(computer)
          groups = self.expected_ids(computer)
          # Only handle groups that don't already exist
          groups.delete_if {|group| recall? group.to_s }
          return if groups.empty?

          Ironfan.step(computer.server.cluster_name, "creating security groups", :blue)
          groups.each do |group|
            Ironfan.step(group, "  creating #{group} security group", :blue)
            Ec2.connection.create_security_group(group.to_s,"Ironfan created group #{group}")
          end
          load! # Get the native groups via reload
        end

        def self.save!(computer)
          return unless Ec2.applicable computer

          create!(computer)            # Make sure the security groups exist
          security_groups = computer.server.cloud(:ec2).security_groups.values
          dsl_groups = security_groups.select do |dsl_group|
            not (recall? dsl_group or recall(dsl_group.name).ensured) and \
            not (dsl_group.range_authorizations +
                 dsl_group.group_authorized_by +
                 dsl_group.group_authorized).empty?
          end.compact
          return if dsl_groups.empty?

          Ironfan.step(computer.server.cluster_name, "ensuring security group permissions", :blue)
          dsl_groups.each do |dsl_group|
            dsl_group.group_authorized.each do |other_group|
              Ironfan.step(dsl_group.name, "  ensuring access from #{other_group}", :blue)
              options = {:group => "#{Ec2.aws_account_id}:#{other_group}"}
              safely_authorize(dsl_group.name,1..65535,options)
            end

            dsl_group.group_authorized_by.each do |other_group|
              Ironfan.step(dsl_group.name, "  ensuring access to #{other_group}", :blue)
              options = {:group => "#{Ec2.aws_account_id}:#{dsl_group.name}"}
              safely_authorize(other_group,1..65535,options)
            end

            dsl_group.range_authorizations.each do |range_auth|
              range, cidr, protocol = range_auth
              step_message = "  ensuring #{protocol} access from #{cidr} to #{range}"
              Ironfan.step(dsl_group.name, step_message, :blue)
              options = {:cidr_ip => cidr, :ip_protocol => protocol}
              safely_authorize(dsl_group.name,range,options)
            end
          end
        end

        #
        # Utility
        #
        def self.ensure_groups(computer)
          return unless Ec2.applicable computer
          # Ensure the security_groups include those for cluster & facet
          # FIXME: This violates the DSL's immutability; it should be
          #   something calculated from within the DSL construction
          Chef::Log.warn("CODE SMELL: violation of DSL immutability: #{caller}")
          cloud = computer.server.cloud(:ec2)
          c_group = cloud.security_group(computer.server.cluster_name)
          c_group.authorized_by_group(c_group.name)
          facet_name = "#{computer.server.cluster_name}-#{computer.server.facet_name}"
          cloud.security_group(facet_name)
        end

        # Try an authorization, ignoring duplicates (this is easier than correlating).
        # Do so for both TCP and UDP, unless only one is specified
        def self.safely_authorize(group_name,range,options)
          unless options[:ip_protocol]
            safely_authorize(group_name,range,options.merge(:ip_protocol => 'tcp'))
            safely_authorize(group_name,range,options.merge(:ip_protocol => 'udp'))
            return
          end

          fog_group = recall(group_name) or raise "unrecognized group: #{group_name}"
          begin
            fog_group.authorize_port_range(range,options)
          rescue Fog::Compute::AWS::Error => e      # InvalidPermission.Duplicate
            Chef::Log.debug("ignoring #{e}")
          end
        end
      end

    end
  end
end