# frozen_string_literal: true

module IAmICan
  module Dynamic
    extend self

    def scopes
      #
      # Generate scopes of each specified i_am_i_can association
      #
      # scope :with_stored_roles, -> { includes(:stored_roles) }
      #
      # Usage:
      #   User.with_stored_roles(role_id = 1)
      #   User.with_stored_roles?(role_id = 1)
      proc do |keys|
        keys.each do |k|
          scope :"with_#{_reflect_of(k)}", -> (ids = nil) do
            break includes(_reflect_of(k)) unless ids
            includes(_reflect_of(k)).where(
                i_am_i_can.send("#{k}_class").underscore.pluralize => { id: ids })
          end

          define_singleton_method :"with_#{_reflect_of(k)}?" do |ids|
            send(:"with_#{_reflect_of(k)}", ids).present?
          end
        end
      end
    end

    def class_reflections
      #
      # Extend each associated querying to a class method that returns ActiveRecord::Relation
      #
      # Suppose: in UserRole model,
      #   has_and_belongs_to_many :related_users
      #
      # It will do like this:
      #   def self.related_users
      #     User.with_stored_roles.where(user_roles: { id: self.ids })
      #   end
      #
      # Usage:
      #   UserRole.all.related_users
      #
      proc do
        %w[ subject role role_group permission ].each do |k|
          next if _reflect_of(k).blank?
          define_singleton_method _reflect_of(k) do
            model = i_am_i_can.send("#{k}_model")
            raise NoMethodError unless (reflect_name = model._reflect_of(i_am_i_can.act))

            model.send("with_#{reflect_name}").where(
                self.name.underscore.pluralize => { id: self.ids }
            )
          end
        end
      end
    end

    def assignment_helpers
      #
      # Generate methods for each Content of Assignment
      #
      # Example for a subject model called User, which `has_and_belongs_to_many :stored_roles`.
      # You call the proc below by given contents [:role], then:
      #
      proc { |contents| contents.each do |content|
        content_cls = i_am_i_can.send("#{content}_class") rescue next
        _plural = '_' + content.to_s.pluralize
        __plural = send("_#{_plural}")

        # _stored_roles_exec
        #   Add / Remove (by passing action :cancel) roles to a user instance
        define_method "_#{_reflect_of(content)}_exec" do |action = :assign, instances = [ ], **conditions|
          collection, objects = send(_plural), [ ]
          records = conditions.present? ? content_cls.constantize.where(conditions) : [ ]

          case action
          when :assign  then collection << objects = (records + instances).uniq - collection
          when :cancel  then collection.destroy objects = (records + instances).uniq & collection
          when :replace then send("#{__plural}=", objects = (records + instances).uniq) # collection=
          end
          objects
        end
        #
        alias_method "_stored#{_plural}_exec", "_#{_reflect_of(content)}_exec"
      end }
    end

    def definition_helpers
      #
      # Generate class methods for each Content of Definition
      #
      # Example for a subject model called User,
      #   which `has_many_temporary_roles` and `has_and_belongs_to_many :stored_roles`.
      # You call the proc below by given contents [:role], then:
      #
      proc { |contents| contents.each do |content|
        content_cls = i_am_i_can.send("#{content}_class") rescue next
        _plural = '_' + content.to_s.pluralize

        # _create_roles
        #    Define and store roles of Subject
        define_singleton_method "_create#{_plural}" do |objects|
          # Role.create([{ name: .. }]).reject { the roles that validation failed }
          content_cls.constantize.create(objects)
              .reject {|record| record.new_record? }
        end
      end }
    end
  end
end