module Avo
  module Resources
    class ResourceManager
      attr_accessor :resources

      alias_method :all, :resources

      class << self
        def build
          instance = new
          instance.check_bad_resources
          instance
        end

        # Fetches the resources available to the application.
        # We have two ways of doing that.
        #
        # 1. Through eager loading.
        # We automatically eager load the resources directory and fetch the descendants from the scanned files.
        # This is the simple way to get started.
        #
        # 2. Manually, declared by the user.
        # We have this option to load the resources because when they are loaded automatically through eager loading,
        # those Resource classes and their methods may trigger loading other classes. And that may disrupt Rails booting process.
        # Ex: AdminResource may use self.model_class = User. That will trigger Ruby to load the User class and itself load
        # other classes in a chain reaction.
        # The scenario that comes up most often is when Rails boots, the routes are being computed which eager loads the resource files.
        # At that boot time some migration might have not been run yet, but Rails tries to access them through model associations,
        # and they are not available.
        #
        # To enable this feature add a `resources` array config in your Avo initializer.
        # config.resources = [
        #   "UserResource",
        #   "FishResource",
        # ]
        def fetch_resources
          # Mark the BaseResource as abstract so it doesn't get loaded by the resource manager
          # This is made here instead of in the BaseResource class because BaseResource class can be overridden
          # And we don't want to force the developer that overrides this class to add the abstract resource flag to the overridden class.
          Avo::BaseResource.abstract_resource!

          if Avo.configuration.resources.present?
            load_configured_resources
          else
            load_resources_namespace
          end

          # All descendants from Avo::Resources::Base except the internal abstract ones
          Base.descendants.reject { _1.is_abstract? }
        end

        def load_resources_namespace
          Rails.autoloaders.main.eager_load_namespace(Avo::Resources)
        end

        def load_configured_resources
          raise "Resources configuration must be an array" unless Avo.configuration.resources.is_a? Array

          Avo.configuration.resources.each do |resource|
            resource.to_s.safe_constantize
          end
        end
      end

      def initialize
        @resources = self.class.fetch_resources
      end

      def check_bad_resources
        resources.each do |resource|
          has_model = resource.model_class.present?

          unless has_model
            possible_model = resource.to_s.gsub "Avo::Resources::", ""
            possible_model = possible_model.gsub "Resource", ""

            Avo.error_manager.add({
              url: "https://docs.avohq.io/3.0/resources.html#self_model_class",
              target: "_blank",
              message: "#{resource} does not have a valid model assigned. It failed to find the #{possible_model} model. \n\r Please create that model or assign one using self.model_class = YOUR_MODEL"
            })
          end
        end
      end

      # Filters out the resources that are missing the model_class
      def valid_resources
        resources.select { |resource| resource.model_class.present? }.sort_by(&:name)
      end

      # Returns the Avo resource by camelized name
      #
      # get_resource_by_name('User') => instance of Avo::Resources::User
      def get_resource(resource)
        resource = "Avo::Resources::#{resource}" unless resource.to_s.starts_with?("Avo::Resources::")

        resources.find do |available_resource|
          resource.to_s == available_resource.to_s
        end
      end

      # Returns the Avo resource by singular snake_cased name
      #
      # get_resource_by_name('user') => instance of Avo::Resources::User
      def get_resource_by_name(name)
        get_resource name.singularize.camelize
      end

      # Returns the Avo resource by singular snake_cased name
      #
      # get_resource_by_name('z posts') => instance of Avo::Resources::ZPost
      def get_resource_by_plural_name(name)
        resources.find do |resource|
          resource.plural_name == name
        end
      end

      # Returns the Avo resource by singular snake_cased name
      # From all the resources that use the same model_class, it will fetch the first one in alphabetical order
      #
      # get_resource_by_name('User') => instance of Avo::Resources::User
      # get_resource_by_name(User) => instance of Avo::Resources::User

      def get_resource_by_model_class(klass)
        # Fetch the mappings imposed by the user.
        # If they are present, use those ones.
        mapping = get_mapping_for_model klass
        return get_resource(mapping) if mapping.present?

        valid_resources
          .find do |resource|
            resource.model_class.model_name.name == klass.to_s
          end
      end

      # Returns the Avo resource by singular snake_cased name
      #
      # get_resource_by_controller_name('delayed_backend_active_record_jobs') => instance of Avo::Resources::DelayedJob
      # get_resource_by_controller_name('users') => instance of Avo::Resources::User
      def get_resource_by_controller_name(name)
        valid_resources
          .find do |resource|
            resource.model_class.to_s.pluralize.underscore.tr("/", "_") == name.to_s
          end
      end

      # Returns the Avo resource by some name
      def guess_resource(name)
        get_resource_by_name(name.to_s) || get_resource_by_model_class(name)
      end

      # Returns the Rails model class by singular snake_cased name
      #
      # get_model_class_by_name('user') => User
      def get_model_class_by_name(name)
        name.to_s.camelize.singularize
      end

      def get_available_resources(user = nil)
        valid_resources
          .select do |resource|
            resource.authorization.class.authorize(
              user,
              resource.model_class,
              Avo.configuration.authorization_methods.stringify_keys["index"],
              policy_class: resource.authorization_policy,
              raise_exception: false
            )
          end
          .sort_by { |r| r.name }
      end

      def resources_for_navigation(user = nil)
        get_available_resources(user)
          .select do |resource|
            resource.visible_on_sidebar
          end
      end

      private

      def get_mapping_for_model(klass)
        (Avo.configuration.model_resource_mapping || {}).stringify_keys.transform_values(&:to_s)[klass.to_s]
      end
    end
  end
end
