# frozen_string_literal: true
module IdentityCache
  module ConfigurationDSL
    extend ActiveSupport::Concern

    included do |base|
      base.class_attribute(:cache_indexes)
      base.class_attribute(:cached_has_manys)
      base.class_attribute(:cached_has_ones)

      base.cached_has_manys = {}
      base.cached_has_ones = {}
      base.cache_indexes = []
    end

    module ClassMethods
      # Will cache an association to the class including IdentityCache.
      # The embed option, if set, will make IdentityCache keep the association
      # values in the same cache entry as the parent.
      #
      # Embedded associations are more effective in offloading database work,
      # however they will increase the size of the cache entries and make the
      # whole entry expire when any of the embedded members change.
      #
      # == Example:
      #   class Product
      #     include IdentityCache
      #     has_many :options
      #     has_many :orders
      #     cache_has_many :options, embed: :ids
      #     cache_has_many :orders
      #   end
      #
      # == Parameters
      # +association+ Name of the association being cached as a symbol
      #
      # == Options
      #
      # * embed: If `true`, IdentityCache will embed the associated records
      #   in the cache entries for this model, as well as all the embedded
      #   associations for the associated record recursively.
      #   If `:ids` (the default), it will only embed the ids for the associated
      #   records.
      def cache_has_many(association, embed: :ids)
        ensure_base_model
        check_association_for_caching(association)
        reflection = reflect_on_association(association)
        association_class = case embed
        when :ids
          Cached::Reference::HasMany
        when true
          Cached::Recursive::HasMany
        else
          raise NotImplementedError
        end

        cached_has_manys[association] = association_class.new(
          association,
          reflection: reflection,
        ).tap(&:build)
      end

      # Will cache an association to the class including IdentityCache.
      # IdentityCache will keep the association values in the same cache entry
      # as the parent.
      #
      # == Example:
      #   class Product
      #     cache_has_one :store, embed: true
      #     cache_has_one :vendor, embed: :id
      #   end
      #
      # == Parameters
      # +association+ Symbol with the name of the association being cached
      #
      # == Options
      #
      # * embed: If `true`, IdentityCache will embed the associated record
      #   in the cache entries for this model, as well as all the embedded
      #   associations for the associated record recursively.
      #   If `:id`, it will only embed the id for the associated record.
      def cache_has_one(association, embed:)
        ensure_base_model
        check_association_for_caching(association)
        reflection = reflect_on_association(association)
        association_class = case embed
        when :id
          Cached::Reference::HasOne
        when true
          Cached::Recursive::HasOne
        else
          raise NotImplementedError
        end

        cached_has_ones[association] = association_class.new(
          association,
          reflection: reflection,
        ).tap(&:build)
      end

      # Will cache a single attribute on its own blob, it will add a
      # fetch_attribute_by_id (or the value of the by option).
      #
      # == Example:
      #   class Product
      #     include IdentityCache
      #     cache_attribute :quantity, by: :name
      #     cache_attribute :quantity, by: [:name, :vendor]
      #   end
      #
      # == Parameters
      # +attribute+ Symbol with the name of the attribute being cached
      #
      # == Options
      #
      # * by: Other attribute or attributes in the model to keep values indexed. Default is :id
      # * unique: if the index would only have unique values. Default is true
      def cache_attribute(attribute, by: :id, unique: true)
        cache_attribute_by_alias(attribute, alias_name: attribute, by: by, unique: unique)
      end

      private

      def cache_attribute_by_alias(attribute_or_proc, alias_name:, by:, unique:)
        ensure_base_model
        fields = Array(by)

        klass = fields.one? ? Cached::AttributeByOne : Cached::AttributeByMulti
        cached_attribute = klass.new(self, attribute_or_proc, alias_name, fields, unique)
        cached_attribute.build
        cache_indexes.push(cached_attribute)
      end

      def ensure_base_model
        if self != cached_model
          raise DerivedModelError, <<~MSG.squish
            IdentityCache class methods must be called on the same
            model that includes IdentityCache
          MSG
        end
      end

      def check_association_for_caching(association)
        unless (association_reflection = reflect_on_association(association))
          raise AssociationError, "Association named '#{association}' was not found on #{self.class}"
        end
        if association_reflection.options[:through]
          raise UnsupportedAssociationError, "caching through associations isn't supported"
        end
      end
    end
  end
end
