module Katello
  module Concerns
    module Api::V2::Authorization
      extend ActiveSupport::Concern

      # The purpose of this module is to protect a controller from a user creating or updating some association
      # when they do not have permissions to view the associated items.  An example would be adding random repository ids to
      # content view.
      # To support this, within the controller define a method such as:
      #     def filtered_associations
      #       {
      #         :component_ids => Katello::ContentViewVersion,
      #         :repository_ids => Katello::Repository
      #       }
      #     end
      # This assumes that the parameters are 'wrapped'.  So the above in the content_views_controller, actually looks at
      #  a subhash of 'content_view'

      included do
        before_action :check_association_ids, :only => [:create, :update]
      end

      def find_authorized_katello_resource
        found_entity = nil
        ::Foreman::AccessControl.permissions_for_controller_action(path_to_authenticate).each do |permission|
          next unless found_entity.blank?
          finder_scope = permission&.finder_scope
          if finder_scope
            found_entity = resource_class.send(finder_scope).find_by(:id => params[:id])
          end
        end
        throw_resource_not_found if found_entity.blank?
        instance_variable_set("@#{resource_name}", found_entity)
      end

      def find_unauthorized_katello_resource
        instance_variable_set("@#{resource_name}", resource_class.find_by(id: params[:id]))
        throw_resource_not_found if instance_variable_get("@#{resource_name}").nil?
      end

      def throw_resource_not_found(name: resource_name, id: params[:id])
        perms_message = "Potential missing permissions: " +
          missing_permissions.map(&:name).join(', ')
        fail HttpErrors::NotFound, _("Could not find %{name} resource with id %{id}. %{perms_message}") % {id: id, name: name, perms_message: perms_message}
      end

      def missing_permissions
        missing_perms = ::Foreman::AccessControl.permissions_for_controller_action(path_to_authenticate)

        # promote_or_remove_content_views_to_environments has a special relationship to promote_or_remove_content_views
        if path_to_authenticate["controller"] == "katello/api/v2/content_view_versions" &&
            path_to_authenticate["action"].in?(["promote", "remove_from_environment", "remove", "republish_repositories", "verify_checksum"])
          missing_perms << ::Permission.find_by(name: "promote_or_remove_content_views_to_environments")
        end
        missing_perms
      end

      def throw_resources_not_found(name:, expected_ids: [])
        resources = yield
        found_ids = resources.map(&:id)
        missing_ids = expected_ids.map(&:to_i) - found_ids

        if missing_ids.any?
          fail HttpErrors::NotFound, _("Could not find %{name} resources with ids %{ids}") % {ids: missing_ids.join(', '), name: name}
        end
      end

      def check_association_ids
        if filtered_associations
          wrapped_params = params[self._wrapper_options.name]
          find_param_arrays(wrapped_params).each do |key_path|
            if (model_class = filtered_associations.with_indifferent_access.dig(*key_path))
              param_ids = wrapped_params.dig(*key_path)
              filtered_ids = model_class.readable.where(:id => param_ids).pluck(:id)
              if (unfound_ids = param_ids_missing(param_ids, filtered_ids)).any?
                fail HttpErrors::NotFound, _("One or more ids (%{ids}) were not found for %{assoc}.  You may not have permissions to see them.") %
                    {ids: unfound_ids, assoc: key_path.last}
              end
            else
              fail _("Unfiltered params array: %s.") % key_path
            end
          end
        else
          Rails.logger.warn("#{self.class.name} may has unprotected associations, see controllers/katello/api/v2/authorization.rb for details.") if ENV['RAILS_ENV'] == 'development'
        end
      end

      def filtered_associations
        #should return {} when supported by all controllers
        nil
      end

      def param_ids_missing(param_ids, filtered_ids)
        param_ids.map(&:to_i).uniq - filtered_ids.map(&:to_i).uniq
      end

      #returns an array of list of keys pointing to an array in a params hash i.e.:
      # {"a"=> {"b" => [3]}}  =>  [["a", "b"]]
      def find_param_arrays(hash = params)
        list_of_paths = []
        hash.each do |key, value|
          case value
          when ActionController::Parameters, Hash
            list_of_paths += find_param_arrays(value).compact.map { |inner_keys| [key] + inner_keys }
          when Array
            list_of_paths << [key]
          end
        end
        list_of_paths.compact
      end
    end
  end
end
