# frozen_string_literal: true

require_relative "constants"

module Hanami
  # @api private
  class SliceRegistrar
    VALID_SLICE_NAME_RE = /^[a-z][a-z0-9_]*$/
    SLICE_DELIMITER = CONTAINER_KEY_DELIMITER

    attr_reader :parent, :slices
    private :parent, :slices

    def initialize(parent)
      @parent = parent
      @slices = {}
    end

    def register(name, slice_class = nil, &block)
      unless name.to_s =~ VALID_SLICE_NAME_RE
        raise ArgumentError, "slice name #{name.inspect} must be lowercase alphanumeric text and underscores only"
      end

      return unless filter_slice_names([name]).any?

      if slices.key?(name.to_sym)
        raise SliceLoadError, "Slice '#{name}' is already registered"
      end

      slice = slice_class || build_slice(name, &block)

      configure_slice(name, slice)

      slices[name.to_sym] = slice
    end

    def [](name)
      slices.fetch(name) do
        raise SliceLoadError, "Slice '#{name}' not found"
      end
    end

    def freeze
      slices.freeze
      super
    end

    def load_slices
      slice_configs = root.join(CONFIG_DIR, SLICES_DIR).glob("*#{RB_EXT}")
        .map { _1.basename(RB_EXT) }

      slice_dirs = root.join(SLICES_DIR).glob("*")
        .select(&:directory?)
        .map { _1.basename }

      (slice_dirs + slice_configs).uniq.sort
        .then { filter_slice_names(_1) }
        .each(&method(:load_slice))

      self
    end

    def each(&block)
      slices.each_value(&block)
    end

    def keys
      slices.keys
    end

    def to_a
      slices.values
    end

    def with_nested
      to_a.flat_map { |slice|
        # Return nested slices first so that their more specific namespaces may be picked up first
        # by SliceConfigurable#slice_for
        slice.slices.with_nested + [slice]
      }
    end

    private

    def root
      parent.root
    end

    def inflector
      parent.inflector
    end

    def parent_slice_namespace
      parent.eql?(parent.app) ? Object : parent.namespace
    end

    # Runs when a slice file has been found inside the app at `config/slices/[slice_name].rb`,
    # or when a slice directory exists at `slices/[slice_name]`.
    #
    # If a slice definition file is found by `find_slice_require_path`, then `load_slice` will
    # require the file before registering the slice class.
    #
    # If a slice class is not found, registering the slice will generate the slice class.
    def load_slice(slice_name)
      slice_require_path = find_slice_require_path(slice_name)
      require slice_require_path if slice_require_path

      slice_class =
        begin
          inflector.constantize("#{slice_module_name(slice_name)}#{MODULE_DELIMITER}Slice")
        rescue NameError => e
          raise e unless e.name.to_s == inflector.camelize(slice_name) || e.name.to_s == :Slice
        end

      register(slice_name, slice_class)
    end

    # Finds the path to the slice's definition file, if it exists, in the following order:
    #
    # 1. `config/slices/[slice_name].rb`
    # 2. `slices/[parent_slice_name]/config/[slice_name].rb` (unless parent is the app)
    # 3. `slices/[slice_name]/config/slice.rb`
    #
    # If the slice is nested under another slice then it will look in the following order:
    #
    # 1. `config/slices/[parent_slice_name]/[slice_name].rb`
    # 2. `slices/[parent_slice_name]/config/[slice_name].rb`
    # 3. `slices/[parent_slice_name]/[slice_name]/config/slice.rb`
    def find_slice_require_path(slice_name)
      app_slice_file_path = [slice_name]
      app_slice_file_path.prepend(parent.slice_name) unless parent.eql?(parent.app)
      ancestors = [
        parent.app.root.join(CONFIG_DIR, SLICES_DIR, app_slice_file_path.join(File::SEPARATOR)),
        parent.root.join(CONFIG_DIR, SLICES_DIR, slice_name),
        root.join(SLICES_DIR, slice_name, CONFIG_DIR, "slice")
      ]

      ancestors
        .uniq
        .find { _1.sub_ext(RB_EXT).file? }
        &.to_s
    end

    def build_slice(slice_name, &block)
      slice_module =
        begin
          inflector.constantize(slice_module_name(slice_name))
        rescue NameError
          parent_slice_namespace.const_set(inflector.camelize(slice_name), Module.new)
        end

      slice_module.const_set(:Slice, Class.new(Hanami::Slice, &block))
    end

    def slice_module_name(slice_name)
      inflector.camelize("#{parent_slice_namespace.name}#{PATH_DELIMITER}#{slice_name}")
    end

    def configure_slice(slice_name, slice)
      slice.instance_variable_set(:@parent, parent)

      # Slices require a root, so provide a sensible default based on the slice's parent
      slice.config.root ||= root.join(SLICES_DIR, slice_name.to_s)

      slice.config.slices = child_slice_names(slice_name, parent.config.slices)
    end

    # Returns a filtered array of slice names based on the parent's `config.slices`
    #
    # This works with both singular slice names (e.g. `"admin"`) as well as dot-delimited nested
    # slice names (e.g. `"admin.shop"`).
    #
    # It will consider only the base names of the slices (since in this case, a parent slice must be
    # loaded in order for its children to be loaded).
    #
    # @example
    #   parent.config.slices # => ["admin.shop"]
    #   filter_slice_names(["admin", "main"]) # => ["admin"]
    #
    #   parent.config.slices # => ["admin"]
    #   filter_slice_names(["admin", "main"]) # => ["admin"]
    def filter_slice_names(slice_names)
      slice_names = slice_names.map(&:to_s)

      if parent.config.slices
        slice_names & parent.config.slices.map { base_slice_name(_1) }
      else
        slice_names
      end
    end

    # Returns the base slice name from an (optionally) dot-delimited nested slice name.
    #
    # @example
    #   base_slice_name("admin") # => "admin"
    #   base_slice_name("admin.users") # => "admin"
    def base_slice_name(name)
      name.to_s.split(SLICE_DELIMITER).first
    end

    # Returns an array of slice names specific to the given child slice.
    #
    # @example
    #   child_local_slice_names("admin", ["main", "admin.users"]) # => ["users"]
    def child_slice_names(parent_slice_name, slice_names)
      slice_names
        &.select { |name|
          name.include?(SLICE_DELIMITER) && name.split(SLICE_DELIMITER)[0] == parent_slice_name.to_s
        }
        &.map { |name|
          name.split(SLICE_DELIMITER)[1..].join(SLICE_DELIMITER) # which version of Ruby supports this?
        }
    end
  end
end
