module LucidPolicy
  module Mixin
    def self.included(base)
      base.instance_exec do
        def policy_for(a_class)
          raise LucidPolicy::Exception, "policy_for #{a_class} can only be used once within #{self}!" if @the_class
          @the_class = a_class
          unless a_class.methods.include?(:authorization_rules)
            a_class.define_singleton_method(:authorization_rules) do
              @authorization_rules ||= {}
            end
          end
          unless a_class.method_defined?(:authorized?)
            a_class.define_method(:authorized?) do |*class_method_props|
              target_class = class_method_props[0]
              raise "At least the class must be given!" unless target_class
              target_method = class_method_props[1]
              props = class_method_props[2..-1]

              result = :deny
              self.class.authorization_rules.each_value do |rules|
                condition_result = true
                rules[:conditions].each do |condition|
                  condition_result = condition.call(self, target_class, target_method, *props, &condition)
                  break unless condition_result == true
                end

                if condition_result == true
                  result = if rules[:classes].key?(target_class)
                             if target_method && rules[:classes][target_class].key?(:methods) &&
                                rules[:classes][target_class][:methods].key?(target_method)
                               rules[:classes][target_class][:methods][target_method]
                             else
                               rules[:classes][target_class][:default]
                             end
                           else
                             rules[:others]
                           end

                  if result.class == Proc
                    policy_helper = Isomorfeus::Policy::Helper.new
                    policy_helper.instance_exec(self, target_class, target_method, *props, &result)
                    result = policy_helper.result
                  end
                end
                break if result == :allow
              end
              result == :allow ? true : false
            end
          end
          unless a_class.method_defined?(:authorized!)
            a_class.define_method(:authorized!) do |*class_method_props|
              return true if authorized?(*class_method_props)
              raise LucidPolicy::Exception, "#{self} not authorized to call #{class_method_props}"
            end
          end
          @the_class.authorization_rules[self.to_s] = { classes: {}, conditions: [], others: :deny }
        end

        def all
          :others
        end

        def allow(*classes_and_methods)
          _raise_allow_deny_first if @refine_used
          _allow_or_deny(:allow, *classes_and_methods)
        end

        def deny(*classes_and_methods)
          _raise_allow_deny_first if @refine_used
          _allow_or_deny(:deny, *classes_and_methods)
        end

        def others
          :others
        end

        def refine(*classes_and_methods, &block)
          @refine_used = true
          _allow_or_deny(nil, *classes_and_methods, &block)
        end

        def with_condition(&block)
          _raise_policy_first unless @the_class
          @the_class.authorization_rules[self.to_s][:conditions] << block
        end

        private

        def _raise_policy_first
          raise LucidPolicy::Exception, "'allow' or 'deny' must appear before 'refine'"
        end

        def _raise_policy_first
          raise LucidPolicy::Exception, "policy_for Class must be specified first"
        end

        def _allow_or_deny(allow_or_deny, *classes_and_methods, &block)
          _raise_policy_first unless @the_class
          rule_hash = @the_class.authorization_rules[self.to_s]
          allow_or_deny_or_block = block_given? ? block : allow_or_deny.to_sym

          target_classes = []
          target_methods = []

          if classes_and_methods.first == :others
            rule_hash[:others] = allow_or_deny_or_block
            return
          end

          classes_and_methods.each do |class_or_method|
            if (class_or_method.class == String || class_or_method.class == Symbol) && class_or_method.to_s[0].downcase == class_or_method.to_s[0]
              target_methods << class_or_method.to_sym
            else
              target_classes << class_or_method
            end
          end

          target_classes.each do |target_class|
            rule_hash[:classes][target_class] = {} unless rule_hash[:classes].key?(target_class)
            if allow_or_deny && target_methods.empty?
              rule_hash[:classes][target_class][:default] = allow_or_deny_or_block
            else
              rule_hash[:classes][target_class][:default] = :deny unless rule_hash[:classes][target_class].key?(:default)
              rule_hash[:classes][target_class][:methods] = {} unless rule_hash[:classes][target_class].key?(:methods)
              target_methods.each do |target_method|
                rule_hash[:classes][target_class][:methods][target_method] = allow_or_deny_or_block
              end
            end
          end
        end
      end
    end
  end
end