module Volt
  class Model
    # The permissions module provides helpers for working with Volt permissions.
    module Permissions
      module ClassMethods
        # Own by user requires a logged in user (Volt.current_user) to save a model.  If
        # the user is not logged in, an validation error will occur.  Once created
        # the user can not be changed.
        #
        # @param key [Symbol] the name of the attribute to store
        def own_by_user(key = :user_id)
          relation, pattern = key.to_s, /_id$/
          if relation.match(pattern)
            belongs_to key.to_s.gsub(pattern, '')
          else
            raise "You tried to auto associate a model using #{key}, but #{key} "\
                  "does not end in `_id`"
          end          # When the model is created, assign it the user_id (if the user is logged in)
          on(:new) do
            # Only assign the user_id if there isn't already one and the user is logged in.
            if _user_id.nil? && !(user_id = Volt.current_user_id).nil?
              send(:"_#{key}=", user_id)
            end
          end

          on(:create, :update) do
            # Don't allow the key to be changed
            deny(key)
          end

          # Setup a validation that requires a user_id
          validate do
            # Lookup directly in @attributes to optimize and prevent the need
            # for a nil model.
            unless @attributes[:user_id]
              # Show an error that the user is not logged in
              next { key => ['requires a logged in user'] }
            end
          end
        end

        # permissions takes a block and yields
        def permissions(*actions, &block)
          # Store the permissions block so we can run it in validations
          self.__permissions__ ||= {}

          # if no action was specified, assume all actions
          actions += [:create, :read, :update, :delete] if actions.size == 0

          actions.each do |action|
            # Add to an array of proc's for each action
            (self.__permissions__[action] ||= []) << block
          end

          validate do
            action = new? ? :create : :update
            run_permissions(action)
          end
        end
      end

      def self.included(base)
        base.send(:extend, ClassMethods)
        base.class_attribute :__permissions__
      end

      def allow(*fields)
        if @__allow_fields
          if @__allow_fields != true
            if fields.size == 0
              # No field's were passed, this means we deny all
              @__allow_fields = true
            else
              # Fields were specified, add them to the list
              @__allow_fields += fields.map(&:to_sym)
            end
          end
        else
          fail 'allow should be called inside of a permissions block'
        end
      end

      def deny(*fields)
        if @__deny_fields
          if @__deny_fields != true
            if fields.size == 0
              # No field's were passed, this means we deny all
              @__deny_fields = true
            else
              # Fields were specified, add them to the list
              @__deny_fields += fields.map(&:to_sym)
            end
          end
        else
          fail 'deny should be called inside of a permissions block'
        end
      end

      # owner? can be called on a model to check if the currently logged
      # in user (```Volt.current_user```) is the owner of this instance.
      #
      # @param key [Symbol] the name of the attribute where the user_id is stored
      def owner?(key = :user_id)
        # Lookup the original user_id
        owner_id = was(key) || send(:"_#{key}")
        !owner_id.nil? && owner_id == Volt.current_user_id
      end

      # Returns boolean if the model can be deleted
      def can_delete?
        action_allowed?(:delete)
      end

      # Checks the read permissions
      def can_read?
        action_allowed?(:read)
      end

      def can_create?
        action_allowed?(:create)
      end

      # Checks if any denies are in place for an action (read or delete)
      def action_allowed?(action_name)
        # TODO: this does some unnecessary work
        compute_allow_and_deny(action_name)

        deny = @__deny_fields == true || (@__deny_fields && @__deny_fields.size > 0)

        clear_allow_and_deny

        !deny
      end

      # Return the list of allowed fields
      def allow_and_deny_fields(action_name)
        compute_allow_and_deny(action_name)

        result = [@__allow_fields, @__deny_fields]

        clear_allow_and_deny

        result
      end

      # Filter fields returns the attributes with any denied or not allowed fields
      # removed based on the current user.
      #
      # Run with Volt.as_user(...) to change the user
      def filtered_attributes
        # Run the read permission check
        allow, deny = allow_and_deny_fields(:read)

        result = nil

        if allow && allow != true && allow.size > 0
          # always keep id
          allow << :id

          # Only keep fields in the allow list
          result = @attributes.select { |key| allow.include?(key) }
        elsif deny == true
          # Only keep id
          # TODO: Should this be a full reject?
          result = @attributes.reject { |key| key != :id }
        elsif deny && deny.size > 0
          # Reject any in the deny list
          result = @attributes.reject { |key| deny.include?(key) }
        else
          result = @attributes
        end

        # Deeply filter any nested models
        return result.map do |key, value|
          if value.is_a?(Model)
            value = value.filtered_attributes
          end

          [key, value]
        end.to_h
      end

      private

      def run_permissions(action_name = nil)
        compute_allow_and_deny(action_name)

        errors = {}

        if @__allow_fields == true
          # Allow all fields
        elsif @__allow_fields && @__allow_fields.size > 0
          # Deny all not specified in the allow list
          changed_attributes.keys.each do |field_name|
            unless @__allow_fields.include?(field_name)
              add_error_if_changed(errors, field_name)
            end
          end
        end

        if @__deny_fields == true
          # Don't allow any field changes
          changed_attributes.keys.each do |field_name|
            add_error_if_changed(errors, field_name)
          end
        elsif @__deny_fields
          # Allow all except the denied
          @__deny_fields.each do |field_name|
            add_error_if_changed(errors, field_name) if changed?(field_name)
          end
        end

        clear_allow_and_deny

        errors
      end

      def clear_allow_and_deny
        @__deny_fields = nil
        @__allow_fields = nil
      end

      # Run through the permission blocks for the action name, acumulate
      # all allow/deny fields.
      def compute_allow_and_deny(action_name)
        @__deny_fields = []
        @__allow_fields = []

        # Skip permissions can be run on the server to ignore the permissions
        return if Volt.in_mode?(:skip_permissions)

        # Run the permission blocks
        action_name ||= new? ? :create : :update

        # Run each of the permission blocks for this action
        permissions = self.class.__permissions__
        if permissions && (blocks = permissions[action_name])
          blocks.each do |block|
            # Call the block, pass the action name
            instance_exec(action_name, &block)
          end
        end
      end

      def add_error_if_changed(errors, field_name)
        if changed?(field_name)
          (errors[field_name] ||= []) << 'can not be changed'
        end
      end
    end
  end
end