# frozen_string_literal: true

require 'micro/attributes/with'

module Micro
  module Attributes
    module Features
      extend self

      module Name
        ALL = [
          DIFF = 'diff'.freeze,
          INITIALIZE = 'initialize'.freeze,
          KEYS_AS_SYMBOL = 'keys_as_symbol'.freeze,
          ACTIVEMODEL_VALIDATIONS = 'activemodel_validations'.freeze
        ].sort.freeze
      end

      module Options
        KEYS = [
          DIFF = 'Diff'.freeze,
          INIT = 'Init'.freeze,
          INIT_STRICT = 'InitStrict'.freeze,
          KEYS_AS_SYMBOL = 'KeysAsSymbol'.freeze,
          AM_VALIDATIONS = 'AMValidations'.freeze
        ].sort.freeze

        NAMES_TO_KEYS = {
          Name::DIFF => DIFF,
          Name::INITIALIZE => INIT,
          Name::KEYS_AS_SYMBOL => KEYS_AS_SYMBOL,
          Name::ACTIVEMODEL_VALIDATIONS => AM_VALIDATIONS
        }.freeze

        KEYS_TO_MODULES = {
          DIFF => With::Diff,
          INIT => With::Initialize,
          INIT_STRICT => With::StrictInitialize,
          KEYS_AS_SYMBOL => With::KeysAsSymbol,
          AM_VALIDATIONS => With::ActiveModelValidations
        }.freeze

        def self.fetch_key(arg)
          if arg.is_a?(Hash)
            INIT_STRICT if arg[:initialize] == :strict
          else
            name = String(arg)

            return name if KEYS_TO_MODULES.key?(name)

            NAMES_TO_KEYS[name]
          end
        end

        INVALID_NAME = [
          'Invalid feature name! Available options: ',
          Name::ALL.map { |feature_name| ":#{feature_name}" }.join(', ')
        ].join

        def self.fetch_keys(args)
          keys = Array(args).dup.map { |name| fetch_key(name) }

          raise ArgumentError, INVALID_NAME if keys.empty? || !(keys - KEYS).empty?

          yield(keys)
        end

        def self.remove_init_keys(keys, if_has_init_in:)
          keys.delete_if { |key| key == INIT || key == INIT_STRICT } if if_has_init_in.include?(INIT)
        end

        def self.without_keys(keys_to_exclude)
          (KEYS - keys_to_exclude).tap do |keys|
            remove_init_keys(keys, if_has_init_in: keys_to_exclude)
          end
        end

        def self.fetch_module_by_keys(keys)
          keys.delete_if { |key| key == INIT } if keys.include?(INIT_STRICT)

          option = keys.sort.join('_')

          KEYS_TO_MODULES.fetch(option) { With.const_get(option, false) }
        end
      end

      def all
        @all ||= self.with(Options::KEYS)
      end

      def with(names)
        Options.fetch_keys(names) do |keys|
          Options.fetch_module_by_keys(keys)
        end
      end

      def without(names)
        Options.fetch_keys(names) do |keys|
          keys = Options.without_keys(keys)

          keys.empty? ? ::Micro::Attributes : Options.fetch_module_by_keys(keys)
        end
      end
    end
  end
end