# frozen_string_literal: true

module Aleph
  module Persistence
    class Repository
      module Preloader
        def self.included(base)
          base.extend(ClassMethods)
        end

        module ClassMethods
          def belong_to(entity, repository:, on:)
            associate(entity, { repository: repository, on: on, kind: :belongs })
          end

          def have_one(entity, repository:, on:)
            associate(entity, { repository: repository, on: on, kind: :one })
          end

          def have_many(entity, repository:, on:)
            associate(entity, { repository: repository, on: on, kind: :many })
          end

          def associations
            @associations ||= {}
          end

          private

          def associate(entity, mapping)
            associations[entity] = mapping
          end
        end

        def preload(preloadable, *required)
          preloadable.is_a?(Array) ? preload_many(preloadable, required) : preload_one(preloadable, required)
        end

        def preload_one(preloadable, required)
          map_preloadables([preloadable])

          requireds = required.empty? ? associations : only_valid_associations(required)
          _preload(preloadables_mapping.keys, requireds)

          preloadable
        end

        def preload_many(preloadables, required)
          map_preloadables(preloadables)

          requireds = required.empty? ? associations : only_valid_associations(required)
          _preload(preloadables_mapping.keys, requireds)

          preloadables
        end

        private

        def _preload(ids, required)
          required.each do |association, mapping|
            repository = registry.send(mapping[:repository])

            send(mapping[:kind], association, repository, ids, **mapping)

          rescue NoMethodError => e
            raise(e) unless e.name.to_sym == mapping[:repository]
            invalid_repository!(e.name)
          end
        end

        def one(association, repository, ids, on:, **)
          filters = Hash[[[on, { operator: :in, values: ids }]]]

          repository.find_many_where(filters).each do |found|
            preloadable = preloadables_mapping[found[on]]

            next unless preloadable

            preloadable[association] ||= found
          end
        end

        def belongs(association, repository, ids, on:, **)
          belonging_ids = ids.map { |id| preloadables_mapping[id][on] }

          founds_mapping = repository.find_many(belonging_ids).each_with_object({}) do |found, map|
            map[found[:id]] ||= found
          end

          ids.each do |id|
            preloadables_mapping[id][association] = founds_mapping[preloadables_mapping[id][on]]
          end
        end

        def many(association, repository, ids, on:, **)
          filters = Hash[[[on, { operator: :in, values: ids }]]]
          pushable_hash = Hash.new { |h, k| h[k] = [] }

          founds_mapping = repository.find_many_where(filters).each_with_object(pushable_hash) do |found, map|
            map[found[on]] << found
          end

          ids.each do |id|
            preloadables_mapping[id][association] = founds_mapping[id]
          end
        end

        def associations
          self.class.associations
        end

        def map_preloadables(preloadables)
          @preloadables_mapping = preloadables.each_with_object({}) do |preloadable, map|
            map[preloadable[:id]] = preloadable
          end
        end

        def only_valid_associations(required)
          associations.slice(*required)
        end

        def invalid_repository!(name)
          msg = "No :#{name} repository available on registry"
          raise(Aleph::Persistence::Errors::RepositoryError, msg)
        end

        attr_reader :preloadables_mapping
      end
    end
  end
end
