require 'set'
module Rolypoly
  class RoleGatekeeper
    attr_reader :roles
    def initialize(roles, actions, resource = nil)
      self.roles = Set.new Array(roles).map(&:to_s)
      self.actions = Set.new Array(actions).map(&:to_s)
      self.resource = resource
      self.all_actions = false
      self.public = false
    end

    def initialize_copy(other)
      @roles = @roles.dup
      @actions = @actions.dup
    end

    # on(resource).allow(*roles).to_access(*actions)
    def allow(*roles)
      to(*roles)
      self
    end

    # on(resource).restrict(*actions).to(*roles)
    def restrict(*actions)
      to_access(*actions)
      self
    end

    # allow(*roles).on(resource).to_access(*actions)
    def on(resource)
      self.resource = resource
      self
    end

    # restrict(*actions).to *roles
    def to(*roles)
      self.roles = self.roles.merge roles.flatten.compact.map(&:to_s)
    end

    # make actions public basically
    # restrict(:index).to_none
    def to_none
      self.public = true
    end

    # allow(*roles).to_access *actions
    def to_access(*actions)
      self.actions = self.actions.merge actions.flatten.compact.map(&:to_s)
    end

    # allow role access to all actions
    # allow(*roles).to_all
    def to_all
      self.all_actions = true
    end

    def allow?(current_roles, action, options = {})
      action?(action) && role?(current_roles, options)
    end

    def allowed_roles(current_roles, action, options = {})
      return [] if public? || !action?(action)
      match_roles(current_roles, options)
    end

    def all_public
      self.public = true
      self.all_actions = true
    end

    def role?(check_roles, options = {})
      return true if public?
      required_resource = find_required_resource(options)

      Array(check_roles).any? do |check_role|
        allowed_role?(check_role) && allowed_resource?(check_role, required_resource)
      end
    end

    private def require_resource?
      !!resource
    end

    private def allowed_resource?(check_role, required_resource)
      return true unless require_resource?

      case required_resource
      when ->(r) { r.is_a?(Hash) && r.keys == [:resource_type] }
        check_role.respond_to?(:resource_type) &&
          check_role.resource_type == required_resource[:resource_type]
      when Hash, ->(r) { type_id_resource?(r) }
        check_role.respond_to?(:resource?) &&
          check_role.resource?(required_resource)
      else
        check_role.respond_to?(:resource?) &&
          Array(required_resource).any? {|r| check_role.resource?(r) }
      end
    end

    private def type_id_resource?(required_resource)
      required_resource.is_a?(Array) && %w(String Symbol).include?(required_resource.first.class.name)
    end

    private def find_required_resource(options = {})
      return resource unless %w(String Symbol).include?(resource.class.to_s)

      options[resource]
    end

    def action?(check_actions)
      check_actions = Set.new Array(check_actions).map(&:to_s)
      all_actions? || !(check_actions & actions).empty?
    end

    def public?
      !!public
    end

    protected # self.attr= gets mad
    attr_writer :roles
    attr_accessor :actions
    attr_accessor :all_actions
    attr_accessor :public
    attr_accessor :resource

    private def match_roles(check_roles, options = {})
      required_resource = find_required_resource(options)

      Array(check_roles).select do |check_role|
        allowed_role?(check_role) && allowed_resource?(check_role, required_resource)
      end
    end

    private def allowed_role?(role_object)
      role_string = role_object.respond_to?(:to_role_string) ? role_object.to_role_string : role_object.to_s

      roles.include?(role_string.to_s)
    end

    def all_actions?
      !!all_actions
    end
    private :all_actions?
  end
end
