module Dunlop::Ability
  extend ActiveSupport::Concern
  include CanCan::Ability

  included do
    attr_reader :user
    @allowed_authorization_classes ||= []
    @subject_aliases = {}
  end

  def self.has_permission_of_type(type, user)
    return true if user.admin?
    user.role_names.any?{|role| role =~ /^\w+-#{type}-/}
  end

  def self.any_permission_starts_with(expression, user)
    return true if user.admin?
    expression = "(#{expression.join('|')})" if expression.is_a?(Array)
    user.role_names.any?{|role| role =~ /^\w+-\w+-#{expression}/ }
  end

  def initialize(user)
    return unless user.present?
    @user = user
    setup_abilities
  end

  def has_permission_of_type(type)
    Dunlop::Ability.has_permission_of_type(type, user)
  end

  def any_permission_starts_with(expression)
    Dunlop::Ability.any_permission_starts_with(expression, user)
  end

  def setup_dunlop_abilities
    if user.admin?
      can :manage, :all
      # Extra permissions, not assigned
      # can :backtrace, Dunlop::LogEntry
      # can :report, :index
      # can :report, :system
    else
      setup_dunlop_class_based_abilities
      setup_subject_aliases
      can :do, :shit if rules.empty? # cancancan 1.15.0 bug, cannot handle empty rules
    end
  end

  def user_roles
    @user_roles ||= all_roles.select(&:user_role?)
  end

  def admin_roles
    @admin_roles ||= all_roles.select(&:admin_role?)
  end

  def all_roles
    return @all_roles if @all_roles
    roles = []
    user.role_names.each do |role_name|
      roles.push get_role_for_role_name(role_name)
    end
    user.role_names_admin.each do |role_name|
      roles.push get_role_for_role_name(role_name, role: Dunlop::Ability::AdminRole, verify_class: false)
    end
    @all_roles = roles.compact
  end

  def get_role_for_role_name(role_name, role: Dunlop::Ability::UserRole, verify_class: true)
    return unless role_name =~ /\w+-\w+-[\w:\/-]+/
    ability, type, class_name = role_name.split('-', 3)
    case type
      when 'app', 'adapter'
        type = 'class'
        class_name = "#{class_name.camelize}::Engine"
      when 'model'
        type = 'class'
        class_name = class_name.gsub('-', '_').camelize
    end
    return if verify_class and not self.class.allowed_authorization_classes.include?(class_name)
    role.new(ability, type, class_name)
  end

  def all_class_roles
    @all_class_roles ||= all_roles.select(&:class_role?)
  end

  def setup_dunlop_class_based_abilities
    all_class_roles.each do |role|
      can role.ability, role.target_class
    end

    # One usecase is to build a topmenu having the right menu structure based on the permissions. If there
    # is a permission on class A::B::C::D then there will be no permission on the A class, but I still have
    # to show the A toplevel menu, otherwise the user never arrives at the A::B::C::D class to perform it's
    # action through the UI. The current solution is to set an :index ability on the top level class A, so
    # in the _navigation_links.html.slim you can use
    #   can? :index, A
    # or even:
    #   can? :index, A::B
    # since a top level ability gives it to all subclasses (CanCanCan)
    all_class_roles.map{|role| role.class_name.split('::').first }.uniq.each do |top_level_class_name|
      can :index, top_level_class_name.constantize
    end
  end

  # This method should be implemented in the application app/models/ability.rb to implement custom authorization logic
  def setup_abilities
    setup_dunlop_abilities
  end

  def alternative_subjects(subject)
    result = super
    if subject.is_a?(Class) and subject_alias = self.class.subject_aliases.find{ |alias_from, alias_target| subject.ancestors.include?(alias_from) }.try(:last) # target
      result << subject_alias unless result.include?(subject_alias)
    end
    result
  end

  # Add the aliases to the rule's subjects
  def setup_subject_aliases
    rules.each do |rule|
      self.class.subject_aliases.each do |alias_from, alias_target|
        rule.subjects << alias_from if rule.subjects.include?(alias_target) and not rule.subjects.include?(alias_from)
      end
    end
  end

  module ClassMethods

    def alias_subject(*subjects)
      options = subjects.extract_options!
      subject_to = options[:to] or raise "alias_subject has no: ', to: TakePermissionsFrom' statement"
      subjects.each do |subject|
        subject_aliases[subject] = subject_to # shape of hash is to quick find the subject, subject_to less important
      end
    end

    # This method is a default implementation and meant to be overridden in de app/models/ability.rb class
    # for custom behaviour
    def allowed_authorization_classes
      @allowed_authorization_classes
    end

    def add_allowed_authorization_classes(classes)
      class_names = Array.wrap(classes).map{|c| c.is_a?(String) ? c : (c.try(:name) || c.to_s)}
      @allowed_authorization_classes |= class_names
    end

    def subject_aliases
      @subject_aliases
    end

    def add_dunlop_allowed_authorization_classes!
      result = ['Dunlop::Engine']
      if Dunlop.has_workflow?
        result += ['WorkflowInstance', 'WorkflowInstanceBatch']
        result += WorkflowInstance.possible_workflow_step_names.map{|name| name.to_s.classify }
      end
      result += SourceFile.classes.map(&:name) if Dunlop.has_source_files?
      result += TargetFile.classes.map(&:name) if Dunlop.has_target_files?
      @allowed_authorization_classes |= result
    end
  end
end
