module Ironfan
  class Dsl
    class Component < Ironfan::Dsl
      include Gorillib::Builder
      include Gorillib::Concern
      include Ironfan::Plugin::Base; register_with Ironfan::Dsl::Compute

      field :cluster_name, Symbol
      field :facet_name, Symbol
      field :realm_name, Symbol
      field :name, Symbol

      def initialize(attrs, &blk)
        attrs.merge!(facet_name: (attrs[:owner].name unless attrs[:owner].nil? or not attrs[:owner].is_a?(Facet)),
                     cluster_name: (attrs[:owner].cluster_name unless attrs[:owner].nil?),
                     realm_name: (attrs[:owner].realm_name unless attrs[:owner].nil?))
        super attrs, &blk
      end

      def self.plugin_hook owner, attrs, plugin_name, full_name, &blk
        (this = new(attrs.merge(owner: owner, name: full_name), &blk))._project(owner)
        this
      end

      def announce_to node
        node.set['components']["#{cluster_name}-#{name}"]['name'] = name
      end

      def self.to_node
        super.tap do |node|
          node.set['cluster_name'] = cluster_name
        end
      end

      def self.from_node(node = NilCheckDelegate.new(nil))
        cluster_name = node['cluster_name'].to_s
        super(node).tap{|x| x.receive!(cluster_name: cluster_name,
                                       realm_name: cluster_name.split('_').first)}
      end

      def self.announce_name
        plugin_name
      end

      def announce_name
        self.class.announce_name
      end

      def _project(compute)
        compute.component name, self
        project(compute)
      end

      def realm_announcements
        (@@realm_announcements ||= {})
      end
      
      def realm_subscriptions component_name
        (@@realm_subscriptions ||= {})[component_name] ||= []
      end

      def announce(component_name)
        Chef::Log.debug("announced #{announce_name} for #{cluster_name}")
        realm_announcements[[realm_name, component_name]] = [cluster_name, facet_name]
        realm_subscriptions(component_name).each{|blk| blk.call(cluster_name, facet_name)}
      end

      def discover(component_name, &blk)
        if already_announced = realm_announcements[[realm_name, component_name]]
          yield *already_announced
        else
          Chef::Log.debug("#{cluster_name}: no one announced #{announce_name}. subscribing")
          realm_subscriptions(component_name) << blk
        end
      end
    end

    module Discovery
      include Gorillib::Builder
      extend Gorillib::Concern

      magic :server_cluster, Symbol
      magic :bidirectional, :boolean, default: false

      (@_dependencies ||= []) << Gorillib::Builder

      module ClassMethods
        def default_to_bidirectional default=true
          fields[:bidirectional].default = default
        end
      end

      def set_discovery compute, keys
        if server_cluster
          wire_to(compute, full_server_cluster, facet_name, keys)
        else
          # I'm defanging automatic discovery for now.
          raise StandardError.new("must explicitly specify a server_cluster for discovery")
          # discover(announce_name) do |cluster_name, facet_name|
          #   wire_to(compute, cluster_name, facet_name, keys)
          # end
        end
      end

      def wire_to(compute, cluster_name, facet_name, keys)
        discovery = {discovers: keys.reverse.inject(cluster_name){|hsh,key| {key => hsh}}}
        (compute.facet_role || compute.cluster_role).override_attributes(discovery)

        # FIXME: This is Ec2-specific and probably doesn't belong here.
        client_group_v = client_group(compute)
        server_group_v = server_group(cluster_name, facet_name)
        
        group_edge(compute.cloud(:ec2), client_group_v, server_group_v)
        group_edge(compute.cloud(:ec2), server_group_v, client_group_v) if bidirectional

        Chef::Log.debug("discovered #{announce_name} for #{cluster_name}: #{discovery}")
      end

      protected

      def client_group(compute)
        security_group(compute.cluster_name, (compute.name if compute.is_a?(Facet)))
      end
      
      def full_server_cluster
        "#{realm_name}_#{server_cluster}"
      end

      def group_edge(cloud, group_1, group_2)
        cloud.security_group(group_1).authorize_group(group_2)
        Chef::Log.debug("component.rb: allowing access from security group #{group_1} to #{group_2}")
      end

      def security_group(cluster_name, facet_name)
        facet_name ? [cluster_name, facet_name].join('-') : cluster_name
      end

      def server_group(cluster_name, facet_name)
        security_group(cluster_name, facet_name)
      end
    end

    module Announcement
      include Gorillib::Builder

      def _project(compute)
        announce announce_name
        super compute
      end
    end
    def to_manifest
      to_wire.reject{|k,_| _skip_fields.include? k}
    end

    def _skip_fields() skip_fields << :_type; end

    def skip_fields() [] end
  end
end