# frozen_string_literal: true

module ActsAsFavoritor
  module Favoritor
    def self.included(base)
      base.extend(ClassMethods)
    end

    module ClassMethods
      def acts_as_favoritor
        if ActsAsFavoritor.configuration&.cache
          serialize :favoritor_score, Hash
          serialize :favoritor_total, Hash
        end

        has_many :favorites, as: :favoritor, dependent: :destroy

        extend ActsAsFavoritor::FavoritorLib
        include ActsAsFavoritor::Favoritor::InstanceMethods
      end
    end

    # rubocop:disable Metrics/ModuleLength
    module InstanceMethods
      # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
      def method_missing(method, *args)
        if method.to_s[/favorited_(.+)/]
          favorited_by_type($1.singularize.classify)
        elsif ActsAsFavoritor.configuration.cache &&
              method.to_s[/favoritor_(.+)_score/]
          favoritor_score[$1.singularize.classify]
        elsif ActsAsFavoritor.configuration.cache &&
              method.to_s[/favoritor_(.+)_total/]
          favoritor_total[$1.singularize.classify]
        else
          super
        end
      end
      # rubocop:enable Metrics/AbcSize, Metrics/MethodLength

      def respond_to_missing?(method, include_private = false)
        super || method.to_s[/favorited_(.+)/] ||
          method.to_s[/favoritor_(.+)_score/] ||
          method.to_s[/favoritor_(.+)_total/]
      end

      def favorite(favoritable,
                   scopes: [ActsAsFavoritor.configuration.default_scope])
        self.class.build_result_for_scopes scopes do |scope|
          return nil if self == favoritable

          inc_cache(scope) if ActsAsFavoritor.configuration.cache

          favorites.for_favoritable(favoritable).send("#{scope}_list")
                   .first_or_create!
        end
      end

      def unfavorite(favoritable,
                     scopes: [ActsAsFavoritor.configuration.default_scope])
        self.class.build_result_for_scopes scopes do |scope|
          favorite_record = get_favorite(favoritable, scope)
          return nil unless favorite_record.present?

          dec_cache(scope) if ActsAsFavoritor.configuration.cache
          favorite_record.destroy!
        end
      end

      def favorited?(favoritable,
                     scopes: [ActsAsFavoritor.configuration.default_scope])
        self.class.build_result_for_scopes scopes do |scope|
          Favorite.unblocked.send("#{scope}_list").for_favoritor(self)
                  .for_favoritable(favoritable).size.positive?
        end
      end

      def all_favorites(scopes: [ActsAsFavoritor.configuration.default_scope])
        self.class.build_result_for_scopes scopes do |scope|
          favorites.unblocked.send("#{scope}_list")
        end
      end

      def all_favorited(scopes: [ActsAsFavoritor.configuration.default_scope])
        self.class.build_result_for_scopes scopes do |scope|
          favorites.unblocked.send("#{scope}_list").includes(:favoritable)
                   .map(&:favoritable)
        end
      end

      def favorites_by_type(favoritable_type,
                            scopes: [ActsAsFavoritor.configuration
                                                    .default_scope])
        self.class.build_result_for_scopes scopes do |scope|
          favorites.unblocked.send("#{scope}_list")
                   .for_favoritable_type(favoritable_type)
        end
      end

      def favorited_by_type(favoritable_type,
                            scopes: [ActsAsFavoritor.configuration
                                                    .default_scope])
        self.class.build_result_for_scopes scopes do |scope|
          favorites.unblocked.send("#{scope}_list").includes(:favoritable)
                   .for_favoritable_type(favoritable_type).map(&:favoritable)
        end
      end

      def blocked_by?(favoritable,
                      scopes: [ActsAsFavoritor.configuration.default_scope])
        self.class.build_result_for_scopes scopes do |scope|
          Favorite.blocked.send("#{scope}_list").for_favoritor(self)
                  .for_favoritable(favoritable).size.positive?
        end
      end

      def blocked_by(scopes: [ActsAsFavoritor.configuration.default_scope])
        self.class.build_result_for_scopes scopes do |scope|
          favorites.includes(:favoritable).blocked.send("#{scope}_list")
                   .map(&:favoritable)
        end
      end

      private

      def get_favorite(favoritable, scope)
        favorites.unblocked.send("#{scope}_list").for_favoritable(favoritable)
                 .first
      end

      # rubocop:disable Metrics/AbcSize
      def inc_cache(scope)
        favoritor_score[scope] = (favoritor_score[scope] || 0) + 1
        favoritor_total[scope] = (favoritor_total[scope] || 0) + 1
        save!

        favoritable.favoritable_score[scope] =
          (favoritable.favoritable_score[scope] || 0) + 1
        favoritable.favoritable_total[scope] =
          (favoritable.favoritable_total[scope] || 0) + 1
        favoritable.save!
      end

      def dec_cache(scope)
        favoritor_score[scope] = (favoritor_score[scope] || 0) - 1
        favoritor_score.delete(scope) unless favoritor_score[scope].positive?
        save!

        favoritable.favoritable_score[scope] =
          (favoritable.favoritable_score[scope] || 0) - 1
        # rubocop:disable Metrics/LineLength
        favoritable.favoritable_score.delete(scope) unless favoritable.favoritable_score[scope].positive?
        # rubocop:enable Metrics/LineLength
        favoritable.save!
      end
      # rubocop:enable Metrics/AbcSize
    end
    # rubocop:enable Metrics/ModuleLength
  end
end