# frozen_string_literal: true

require_relative "constants"
require_relative "env"

module Hanami
  # The Hanami app is a singular slice tasked with managing the core components of the app and
  # coordinating overall app boot.
  #
  # For smaller apps, the app may be the only slice present, whereas larger apps may consist of many
  # slices, with the app reserved for holding a small number of shared components only.
  #
  # @see Slice
  #
  # @api public
  # @since 2.0.0
  class App < Slice
    @_mutex = Mutex.new

    # @api private
    # @since 2.0.0
    def self.inherited(subclass)
      super

      Hanami.app = subclass

      subclass.extend(ClassMethods)

      @_mutex.synchronize do
        subclass.class_eval do
          @config = Hanami::Config.new(app_name: slice_name, env: Hanami.env)
          Hanami::Env.load
        end
      end
    end

    # App class interface
    module ClassMethods
      # Returns the app's config.
      #
      # @return [Hanami::Config]
      #
      # @api public
      # @since 2.0.0
      attr_reader :config

      # Returns the app's {SliceName}.
      #
      # @return [Hanami::SliceName]
      #
      # @see Slice::ClassMethods#slice_name
      #
      # @api public
      # @since 2.0.0
      def app_name
        slice_name
      end

      # Prepares the $LOAD_PATH based on the app's configured root, prepending the `lib/` directory
      # if it exists. If the lib directory is already added, this will do nothing.
      #
      # In ordinary circumstances, you should never have to call this method: this method is called
      # immediately upon subclassing {Hanami::App}, as a convenicence to put lib/ (under the default
      # root of `Dir.pwd`) on the load path automatically. This is helpful if you need to require
      # files inside the subclass body for performing certain app configuration steps.
      #
      # If you change your app's `config.root` and you need to require files from its `lib/`
      # directory within your {App} subclass body, you should call {.prepare_load_path} explicitly
      # after setting the new root.
      #
      # Otherwise, this method is called again as part of the app {.prepare} step, so if you've
      # changed your app's root and do _not_ need to require files within your {App} subclass body,
      # then you don't need to call this method.
      #
      # @example
      #   module MyApp
      #     class App < Hanami::App
      #       config.root = Pathname(__dir__).join("../src")
      #       prepare_load_path
      #
      #       # You can make requires for your files here
      #     end
      #   end
      #
      # @return [self]
      #
      # @api public
      # @since 2.0.0
      def prepare_load_path
        if (lib_path = root.join(LIB_DIR)).directory?
          path = lib_path.realpath.to_s
          $LOAD_PATH.prepend(path) unless $LOAD_PATH.include?(path)
        end

        self
      end

      private

      def prepare_all
        prepare_load_path

        # Make app-wide notifications available as early as possible
        container.use(:notifications)

        # Ensure all basic slice preparation is complete before we make adjustments below (which
        # rely on the basic prepare steps having already run)
        super

        # Run specific prepare steps for the app slice. Note also that some standard steps have been
        # skipped via the empty method overrides below.
        prepare_app_component_dirs
        prepare_app_providers
      end

      # Skip standard slice prepare steps that do not apply to the app
      def prepare_container_component_dirs; end
      def prepare_container_imports; end

      # rubocop:disable Metrics/AbcSize

      def prepare_app_component_dirs
        # Component files in both `app/` and `app/lib/` define classes in the
        # app's namespace

        if root.join(APP_DIR, LIB_DIR).directory?
          container.config.component_dirs.add(File.join(APP_DIR, LIB_DIR)) do |dir|
            dir.namespaces.add_root(key: nil, const: app_name.name)
          end
        end

        # When auto-registering components in `app/`, ignore files in `app/lib/` (these will be
        # auto-registered as above), as well as the configured no_auto_register_paths
        no_auto_register_paths = ([LIB_DIR] + config.no_auto_register_paths)
          .map { |path|
            path.end_with?(File::SEPARATOR) ? path : "#{path}#{File::SEPARATOR}"
          }

        if root.join(APP_DIR).directory?
          container.config.component_dirs.add(APP_DIR) do |dir|
            dir.namespaces.add_root(key: nil, const: app_name.name)
            dir.auto_register = -> component {
              relative_path = component.file_path.relative_path_from(root.join(APP_DIR)).to_s
              !relative_path.start_with?(*no_auto_register_paths)
            }
          end
        end
      end

      def prepare_app_providers
        require_relative "providers/inflector"
        register_provider(:inflector, source: Hanami::Providers::Inflector)

        # Allow logger to be replaced by users with a manual provider, for advanced cases
        unless container.providers[:logger]
          require_relative "providers/logger"
          register_provider(:logger, source: Hanami::Providers::Logger)
        end

        if Hanami.bundled?("rack")
          require_relative "providers/rack"
          register_provider(:rack, source: Hanami::Providers::Rack, namespace: true)
        end

        if Hanami.bundled?("hanami-db")
          register_provider(:db_logging, source: Hanami::Providers::DBLogging)
        end
      end

      def prepare_autoloader
        autoloader.tag = "hanami.app.#{slice_name.name}"

        # Component dirs are automatically pushed to the autoloader by dry-system's zeitwerk plugin.
        # This method adds other dirs that are not otherwise configured as component dirs.

        # Autoload classes from `lib/[app_namespace]/`
        if root.join(LIB_DIR, app_name.name).directory?
          autoloader.push_dir(root.join(LIB_DIR, app_name.name), namespace: namespace)
        end

        autoloader.setup
      end

      # rubocop:enable Metrics/AbcSize
    end
  end
end
