module Ironfan
  class Provider
    class Ec2

      class SecurityGroup < Ironfan::Provider::Resource

        WIDE_OPEN = (-1..-1)

        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

        def self.shared?()      true;   end
        def self.multiple?()    true;   end
        def self.resource_type()        :security_group;   end
        def self.expected_ids(computer)
          return unless computer.server
          ec2 = computer.server.cloud(:ec2)

          server_groups = computer.server.security_groups
          cloud_groups = ec2.security_groups

          result = []
          [server_groups, cloud_groups].each do |container|
            container.keys.each { |name| result.push( group_name_with_vpc(name,ec2.vpc) )}
          end
          return result.uniq

        end

        def name()
          self.class.group_name_with_vpc(adaptee.name, adaptee.vpc_id)
        end

        #
        # Discovery
        #
        def self.load!(cluster=nil)
          Ec2.connection.security_groups.reject { |raw| raw.blank? }.each do |raw|
            sg =  SecurityGroup.new(:adaptee => raw)
            remember sg
            remember(sg, :id => sg.name.gsub( /^vpc-[^:]+:/, '') )
          end
        end

        def receive_adaptee(obj)
          obj = Ec2.connection.security_groups.new(obj) if obj.is_a?(Hash)
          super
        end

        def to_s
          if ip_permissions.present?
            perm_str = ip_permissions.map{|perm|
              "%s:%s-%s (%s | %s)" % [
                perm['ipProtocol'], perm['fromPort'], perm['toPort'],
                perm['groups'  ].map{|el| el['groupName'] }.join(','),
                perm['ipRanges'].map{|el| el['cidrIp']    }.join(','),
              ]
            }
            return "<%-15s %-12s %-25s %s>" % [ self.class.handle, group_id, name, perm_str]
          else
            return "<%-15s %-12s %s>" % [ self.class.handle, group_id, name ]
          end
        end

        #
        # Manipulation
        #

        def self.prepare!(computers)

          # Create any groups that don't yet exist, and ensure any authorizations
          # that are required for those groups
          cluster_name             = nil
          groups_to_create         = [ ]
          authorizations_to_ensure = [ ]

          computers.each{|comp| ensure_groups(comp) if Ec2.applicable(comp) } # Add facet and cluster security groups for the computer

          # First, deduce the list of all groups to which at least one instance belongs
          # We'll use this later to decide whether to create groups, or authorize access,
          # using a VPC security group or an EC2 security group.
          groups_that_should_exist = computers.map{|comp| expected_ids(comp) }.flatten.compact.sort.uniq
          groups_to_create << groups_that_should_exist

          computers.select { |computer| Ec2.applicable computer }.each do |computer|
            cloud           = computer.server.cloud(:ec2)
            cluster_name    = computer.server.cluster_name

            # Iterate over all of the security group information, keeping track of
            # any groups that must exist and any authorizations that must be ensured
            [computer.server.security_groups, cloud.security_groups].each do |container|
              container.values.each do |dsl_group|
                
                groups_to_create << dsl_group.name

                groups_to_create << dsl_group.group_authorized.map do |other_group|
                  most_appropriate_group_name(other_group, cloud.vpc)
                end
                
                groups_to_create << dsl_group.group_authorized_by.map do |other_group|
                  most_appropriate_group_name(other_group, cloud.vpc)
                end
                
                authorizations_to_ensure << dsl_group.group_authorized.map do |other_group|
                  {
                    :grantor      => most_appropriate_group_name(dsl_group.name, cloud.vpc),
                    :grantee      => most_appropriate_group_name(other_group, cloud.vpc),
                    :grantee_type => :group,
                    :range        => WIDE_OPEN,
                  }
                end
                
                authorizations_to_ensure << dsl_group.group_authorized_by.map do |other_group|
                  {
                    :grantor      => most_appropriate_group_name(other_group, cloud.vpc),
                    :grantee      => most_appropriate_group_name(dsl_group.name, cloud.vpc),
                    :grantee_type => :group,
                    :range        => WIDE_OPEN,
                  }
                end
                
                authorizations_to_ensure << dsl_group.range_authorizations.map do |range_auth|
                  range, cidr, protocol = range_auth
                  {
                    :grantor      => group_name_with_vpc(dsl_group.name, cloud.vpc),
                    :grantee      => { :cidr_ip => cidr, :ip_protocol => protocol },
                    :grantee_type => :cidr,
                    :range        => range,
                  }
                end
              end
            end
          end
          groups_to_create         = groups_to_create.flatten.uniq.reject { |group| recall? group.to_s }.sort
          authorizations_to_ensure = authorizations_to_ensure.flatten.uniq.sort { |a,b| a[:grantor] <=> b[:grantor] }

          Ironfan.step(cluster_name, "creating security groups", :blue) unless groups_to_create.empty?
          groups_to_create.each do |group|
            if group =~ /\//
              Ironfan.step(group, "  assuming that owner/group pair #{group} already exists", :blue)
            else
              Ironfan.step(group, "  creating #{group} security group", :blue)
              begin
                tokens    = group.to_s.split(':')
                group_id  = tokens.pop
                vpc_id    = tokens.pop
                Ec2.connection.create_security_group(group_id,"Ironfan created group #{group_id}",vpc_id)
              rescue Fog::Compute::AWS::Error => e # InvalidPermission.Duplicate
                case e.message
                when /SecurityGroupLimitExceeded/
                  raise e
                else
                  Chef::Log.info("ignoring security group error: #{e}")
                end
              end
            end
          end

          # Re-load everything so that we have a @@known list of security groups to manipulate
          load! unless groups_to_create.empty?

          # Now make sure that all required authorizations are present
          Ironfan.step(cluster_name, "ensuring security group permissions", :blue) unless authorizations_to_ensure.empty?
          authorizations_to_ensure.each do |auth|
            grantor_fog = recall(auth[:grantor])
            if :group == auth[:grantee_type]
              if fog_grantee = recall(auth[:grantee])
                options = { :group => fog_grantee.group_id }
              elsif auth[:grantee] =~ /\//
                options = { :group_alias => auth[:grantee] }
              else
                raise "Don't know what to do with authorization grantee #{auth[:grantee]}"
              end
              message = "  ensuring access from #{auth[:grantee]} to #{auth[:grantor]}"
            else
              options = auth[:grantee]
              message = "  ensuring #{auth[:grantee][:ip_protocol]} access from #{auth[:grantee][:cidr_ip]} to #{auth[:range]}"
            end
            Ironfan.step(auth[:grantor], message, :blue)
            safely_authorize(grantor_fog, auth[:range], options)
          end
        end

        def self.group_name_with_vpc(name,vpc_id=nil)
          vpc_id.nil? ? name.to_s : "#{vpc_id}:#{name.to_s}"
        end

        def self.most_appropriate_group_name(group, vpc_id)
          vpc_id.present? ? group_name_with_vpc(group, vpc_id) : group
        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
          Ironfan.todo("CODE SMELL: violation of DSL immutability: #{caller}")
          server = computer.server
          cluster_name = "#{computer.server.realm_name}-#{computer.server.cluster_name}"
          server.security_group computer.server.realm_name
          realm_group = server.security_group cluster_name
          realm_group.authorized_by_group realm_group.name
          facet_name = "#{computer.server.realm_name}-#{computer.server.cluster_name}-#{computer.server.facet_name}"
          server.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(fog_group,range,options)

          if options[:group_alias]
            # In this case, we must first extract the group name
            # before recursively calling this function with it.

            owner, group = options.delete(:group_alias).split(/\//)
            Chef::Log.debug("authorizing group alias #{options[:group_alias].inspect} to group #{fog_group.name}")
            group_id = Ec2.connection.security_groups.get(group).group_id
            safely_authorize(fog_group, range, options.merge(group: group_id))
          elsif options[:ip_protocol]
            # In this case, we've received the ip_protocol. With or
            # without a group name, we have enough information to open
            # the security group.

            Chef::Log.debug("authorizing to #{fog_group.name} with options #{options.inspect}")
            self.patiently(fog_group.name, Fog::Compute::AWS::Error, :ignore => Proc.new { |e| e.message =~ /Duplicate/ }) do
              fog_group.authorize_port_range(range,options)
            end
          else
            # Without an IP protocol, we'll open all of the relevant
            # ones. On non-VPC, that means tcp, udp, and icmp. On VPC,
            # that means -1 for all protocols.

            Chef::Log.debug([
                "didn't receive ip_protocol for authorization to #{fog_group.name} ",
                "with options #{options.inspect}. assuming all protocols"
              ].join)
            if fog_group.vpc_id.nil?
              # Non-VPC does not support -1 for all protocols, so
              # we'll need to do each protocol indendently. If we
              # haven't received an ip_protocol, we'll assume the user
              # meant to open everything.
              safely_authorize(fog_group, 1..65535, options.merge(:ip_protocol => 'tcp'))
              safely_authorize(fog_group, 1..65535, options.merge(:ip_protocol => 'udp'))
              safely_authorize(fog_group, -1..-1,   options.merge(:ip_protocol => 'icmp'))
            else
              # In VPC, we should use only one rule to conserve rules.
              safely_authorize(fog_group,range,options.merge(:ip_protocol => -1))
            end
          end
        end
      end
    end
  end
end