# frozen_string_literal: true

module Spree
  class Product < Spree::Base
    module Scopes
      def self.prepended(base)
        base.class_eval do
          cattr_accessor :search_scopes do
            []
          end

          def self.add_search_scope(name, &block)
            singleton_class.send(:define_method, name.to_sym, &block)
            search_scopes << name.to_sym
          end

          def self.property_conditions(property)
            properties = Property.table_name
            case property
            when String   then { "#{properties}.name" => property }
            when Property then { "#{properties}.id" => property.id }
            else { "#{properties}.id" => property.to_i }
            end
          end

          scope :ascend_by_updated_at, -> { order(updated_at: :asc) }
          scope :descend_by_updated_at, -> { order(updated_at: :desc) }
          scope :ascend_by_name, -> { order(name: :asc) }
          scope :descend_by_name, -> { order(name: :desc) }

          add_search_scope :ascend_by_master_price do
            joins(master: :prices).select('spree_products.* , spree_prices.amount')
                                         .order(Spree::Price.arel_table[:amount].asc)
          end

          add_search_scope :descend_by_master_price do
            joins(master: :prices).select('spree_products.* , spree_prices.amount')
                                         .order(Spree::Price.arel_table[:amount].desc)
          end

          add_search_scope :price_between do |low, high|
            joins(master: :prices).where(Price.table_name => { amount: low..high })
          end

          add_search_scope :master_price_lte do |price|
            joins(master: :prices).where("#{price_table_name}.amount <= ?", price)
          end

          add_search_scope :master_price_gte do |price|
            joins(master: :prices).where("#{price_table_name}.amount >= ?", price)
          end

          # This scope selects products in taxon AND all its descendants
          # If you need products only within one taxon use
          #
          #   Spree::Product.joins(:taxons).where(Taxon.table_name => { id: taxon.id })
          #
          # If you're using count on the result of this scope, you must use the
          # `:distinct` option as well:
          #
          #   Spree::Product.in_taxon(taxon).count(distinct: true)
          #
          # This is so that the count query is distinct'd:
          #
          #   SELECT COUNT(DISTINCT "spree_products"."id") ...
          #
          #   vs.
          #
          #   SELECT COUNT(*) ...
          add_search_scope :in_taxon do |taxon|
            includes(:classifications)
              .where('spree_products_taxons.taxon_id' => taxon.self_and_descendants.pluck(:id))
              .order(Spree::Classification.arel_table[:position].asc)
          end

          # This scope selects products in all taxons AND all its descendants
          # If you need products only within one taxon use
          #
          #   Spree::Product.taxons_id_eq([x,y])
          add_search_scope :in_taxons do |*taxons|
            taxons = get_taxons(taxons)
            taxons.first ? prepare_taxon_conditions(taxons) : where(nil)
          end

          # a scope that finds all products having property specified by name, object or id
          add_search_scope :with_property do |property|
            joins(:properties).where(property_conditions(property))
          end

          # a simple test for product with a certain property-value pairing
          # note that it can test for properties with NULL values, but not for absent values
          add_search_scope :with_property_value do |property, value|
            joins(:properties)
              .where("#{Spree::ProductProperty.table_name}.value = ?", value)
              .where(property_conditions(property))
          end

          add_search_scope :with_option do |option|
            option_types = Spree::OptionType.table_name
            conditions = case option
                         when String     then { "#{option_types}.name" => option }
                         when OptionType then { "#{option_types}.id" => option.id }
                         else { "#{option_types}.id" => option.to_i }
            end

            joins(:option_types).where(conditions)
          end

          add_search_scope :with_option_value do |option, value|
            option_values = Spree::OptionValue.table_name
            option_type_id = case option
                             when String then Spree::OptionType.find_by(name: option) || option.to_i
                             when Spree::OptionType then option.id
                             else option.to_i
            end

            conditions = "#{option_values}.name = ? AND #{option_values}.option_type_id = ?", value, option_type_id
            group('spree_products.id').joins(variants_including_master: :option_values).where(conditions)
          end

          # Finds all products which have either:
          # 1) have an option value with the name matching the one given
          # 2) have a product property with a value matching the one given
          add_search_scope :with do |value|
            includes(variants_including_master: :option_values).
              includes(:product_properties).
              where("#{Spree::OptionValue.table_name}.name = ? OR #{Spree::ProductProperty.table_name}.value = ?", value, value)
          end

          # Finds all products that have a name containing the given words.
          add_search_scope :in_name do |words|
            like_any([:name], prepare_words(words))
          end

          # Finds all products that have a name or meta_keywords containing the given words.
          add_search_scope :in_name_or_keywords do |words|
            like_any([:name, :meta_keywords], prepare_words(words))
          end

          # Finds all products that have a name, description, meta_description or meta_keywords containing the given keywords.
          add_search_scope :in_name_or_description do |words|
            like_any([:name, :description, :meta_description, :meta_keywords], prepare_words(words))
          end

          # Finds all products that have the ids matching the given collection of ids.
          # Alternatively, you could use find(collection_of_ids), but that would raise an exception if one product couldn't be found
          add_search_scope :with_ids do |*ids|
            where(id: ids)
          end

          # Sorts products from most popular (popularity is extracted from how many
          # times use has put product in cart, not completed orders)
          #
          # there is alternative faster and more elegant solution, it has small drawback though,
          # it doesn stack with other scopes :/
          #
          # joins: "LEFT OUTER JOIN (SELECT line_items.variant_id as vid, COUNT(*) as cnt FROM line_items GROUP BY line_items.variant_id) AS popularity_count ON variants.id = vid",
          # order: 'COALESCE(cnt, 0) DESC'
          add_search_scope :descend_by_popularity do
            joins(:master).
              order(Arel.sql(%{
           COALESCE((
             SELECT
               COUNT(#{Spree::LineItem.quoted_table_name}.id)
             FROM
               #{Spree::LineItem.quoted_table_name}
             JOIN
               #{Spree::Variant.quoted_table_name} AS popular_variants
             ON
               popular_variants.id = #{Spree::LineItem.quoted_table_name}.variant_id
             WHERE
               popular_variants.product_id = #{Spree::Product.quoted_table_name}.id
           ), 0) DESC
        }))
          end

          add_search_scope :not_deleted do
            where("#{Spree::Product.quoted_table_name}.deleted_at IS NULL or #{Spree::Product.quoted_table_name}.deleted_at >= ?", Time.current)
          end

          scope :with_master_price, -> do
            joins(:master).where(Spree::Price.where(Spree::Variant.arel_table[:id].eq(Spree::Price.arel_table[:variant_id])).arel.exists)
          end
          # Can't use add_search_scope for this as it needs a default argument
          def self.available(available_on = nil)
            with_master_price
              .where("#{Spree::Product.quoted_table_name}.available_on <= ?", available_on || Time.current)
              .where("#{Spree::Product.quoted_table_name}.discontinue_on IS NULL OR" \
                     "#{Spree::Product.quoted_table_name}.discontinue_on >= ?", Time.current)
          end
          search_scopes << :available

          add_search_scope :taxons_name_eq do |name|
            group("spree_products.id").joins(:taxons).where(Spree::Taxon.arel_table[:name].eq(name))
          end

          def self.with_all_variant_sku_cont(sku)
            variant_table = Spree::Variant.arel_table
            subquery = Spree::Variant.with_discarded.where(
              variant_table[:sku].matches("%#{sku}%").and(
                variant_table[:product_id].eq(arel_table[:id])
              )
            )
            where(subquery.arel.exists)
          end

          def self.with_kept_variant_sku_cont(sku)
            variant_table = Spree::Variant.arel_table
            subquery = Spree::Variant.where(
              variant_table[:sku].matches("%#{sku}%").and(
                variant_table[:product_id].eq(arel_table[:id])
              )
            )
            where(subquery.arel.exists)
          end

          class << self
            private

            def price_table_name
              Spree::Price.quoted_table_name
            end

            # specifically avoid having an order for taxon search (conflicts with main order)
            def prepare_taxon_conditions(taxons)
              ids = taxons.flat_map { |taxon| taxon.self_and_descendants.pluck(:id) }.uniq
              joins(:taxons).where("#{Spree::Taxon.table_name}.id" => ids)
            end

            # Produce an array of keywords for use in scopes.
            # Always return array with at least an empty string to avoid SQL errors
            def prepare_words(words)
              return [''] if words.blank?

              splitted = words.split(/[,\s]/).map(&:strip)
              splitted.any? ? splitted : ['']
            end

            def get_taxons(*ids_or_records_or_names)
              taxons = Spree::Taxon.table_name
              ids_or_records_or_names.flatten.map { |taxon|
                case taxon
                when Integer then Spree::Taxon.find_by(id: taxon)
                when ActiveRecord::Base then taxon
                when String
                  Spree::Taxon.find_by(name: taxon) ||
                    Spree::Taxon.where("#{taxons}.permalink LIKE ? OR #{taxons}.permalink = ?", "%/#{taxon}/", "#{taxon}/").first
                end
              }.compact.flatten.uniq
            end
          end
        end
      end

      ::Spree::Product.prepend self
    end
  end
end
