module Setup
  module ModelConfigurable
    extend ActiveSupport::Concern

    include ChangedIf

    included do
      after_destroy { config.destroy }

      changed_if { config.changed? }
    end

    def warnings
      @warnings ||= []
    end

    def configure
      super
      if config.changed?
        warnings.clear
        config.save
        config.errors.full_messages.each { |warn| warnings << warn }
      end
    end

    def config
      @_config ||=
        begin
          if new_record?
            self.class.config_model.new(self.class.relation_name => self)
          else
            self.class.config_model.find_or_initialize_by(self.class.relation_name => self)
          end
        end
    end

    module ClassMethods
      attr_reader :config_model, :relation_name, :foreign_key

      def inherited(subclass)
        super
        subclass.instance_variable_set(:@config_model, @config_model)
        subclass.instance_variable_set(:@relation_name, @relation_name)
        subclass.instance_variable_set(:@foreign_key, @foreign_key)
      end

      def config_with(model, options = {})
        @config_model = model
        config_fields = options[:only] || model.config_fields
        config_fields = [config_fields] unless config_fields.is_a?(Enumerable)
        config_fields = config_fields.to_a

        shared_configurable *config_fields

        relation = model.reflect_on_all_associations(:belongs_to).detect { |r| r.klass == self && r.inverse_of.nil? }

        fail "Belongs-To association config not found between #{model} and #{self}" unless relation
        fail "Belongs-To association config #{model}.#{relation.name} is autosave (it should not)" if relation.autosave

        @relation_name = relation.name
        @foreign_key = relation.foreign_key.to_sym

        delegate *config_model.config_fields.collect { |p| [p.to_sym, "#{p}=".to_sym] }.flatten, to: :config
      end

      def where(expression)
        config_criteria = nil
        if expression.is_a?(Hash)
          nil_configs = false
          config_options = {}
          config_model.config_fields.each do |field|
            if expression.key?(key = field.to_s) || expression.key?(key = field.to_sym)
              nil_configs ||= nil_option?(config_options[field] = expression.delete(key))
            end
          end
          if config_options.present?
            config_criteria = { :id.in => config_model.where(config_options).collect { |config| config[foreign_key] } }
            if nil_configs
              config_criteria = { '$or' => [config_criteria, { :id.nin => config_model.all.collect(&:"#{foreign_key}") }] }
            end
          end
        end
        q = super
        if config_criteria
          q = q.and(config_criteria)
        end
        q
      end

      def nil_option?(option)
        case option
        when nil
          true
        when Array
          option.any?(&:nil?)
        when Hash
          %w($in $or).any? { |op| (values = option[op] || option[op.to_sym]).is_a?(Array) && nil_option?(values) }
        else
          false
        end
      end

      def clear_config_for(tenant, ids)
        super
        tenant.switch do
          config_model.where(foreign_key.in => ids).delete_all
        end
      end
    end
  end
end
