# frozen_string_literal: true

require 'open_food_network/property_merge'

# PRODUCTS
# Products represent an entity for sale in a store.
# Products can have variations, called variants
# Products properties include description, meta_keywork, etc. that do not change by variant.
#
# VARIANTS
# Every product has at least one variant (standard variant), which stores price and  availability,
# shipping category, sku, size and weight, etc.
# All variants can access the product name, description, and meta_keyword directly (via reverse
# delegation).
# Inventory units are tied to Variant.
# All variants have option values and may have inventory units.
# Sum of on_hand each variant's inventory level determine "on_hand" level for the product.
#
module Spree
  class Product < ApplicationRecord
    include LogDestroyPerformer
    include ProductSortByStocks

    self.belongs_to_required_by_default = false
    # These columns have been moved to variant. Currently this is only for documentation purposes,
    # because they are declared as attr_accessor below, declaring them as ignored columns has no
    # effect
    self.ignored_columns += [
      :supplier_id, :primary_taxon_id, :variant_unit, :variant_unit_scale, :variant_unit_name
    ]

    acts_as_paranoid

    searchable_attributes :meta_keywords, :sku, :on_hand, :backorderable_priority,
                          :backorderable_name
    searchable_associations :properties, :variants
    searchable_scopes :active, :with_properties

    has_one :image, class_name: "Spree::Image", as: :viewable, dependent: :destroy
    has_one :semantic_link, as: :subject, dependent: :delete

    has_many :product_properties, dependent: :destroy
    has_many :properties, through: :product_properties
    has_many :variants, -> { order("spree_variants.id ASC") }, class_name: 'Spree::Variant',
                                                               inverse_of: :product,
                                                               dependent: :destroy

    has_many :prices, -> { order('spree_variants.id, currency') }, through: :variants

    has_many :stock_items, through: :variants
    has_many :variant_images, -> { order(:position) }, source: :images,
                                                       through: :variants

    validates_lengths_from_database
    validates :name, presence: true
    validate :validate_image
    validates :price, numericality: { greater_than_or_equal_to: 0, if: ->{ new_record? } }

    # These validators are used to make sure the standard variant created via
    # `ensure_standard_variant` will be valid. The are only used when creating a new product
    with_options on: :create_and_create_standard_variant do
      validates :supplier_id, presence: true
      validates :primary_taxon_id, presence: true
      validates :variant_unit, presence: true
      validates :unit_value, presence: true, if: ->(product) {
        %w(weight volume).include?(product.variant_unit)
      }
      validates :unit_value, numericality: { greater_than: 0 }, allow_blank: true
      validates :unit_description, presence: true, if: ->(product) {
        product.variant_unit.present? && product.unit_value.nil?
      }
      validates :variant_unit_scale, presence: true, if: ->(product) {
        %w(weight volume).include?(product.variant_unit)
      }
      validates :variant_unit_name, presence: true, if: ->(product) {
        product.variant_unit == 'items'
      }
    end

    accepts_nested_attributes_for :image
    accepts_nested_attributes_for :product_properties,
                                  allow_destroy: true,
                                  reject_if: ->(pp) { pp[:property_name].blank? }

    # Transient attributes used temporarily when creating a new product,
    # these values are persisted on the product's variant
    attr_accessor :price, :display_as, :unit_value, :unit_description, :variant_unit,
                  :variant_unit_name, :variant_unit_scale, :tax_category_id, :shipping_category_id,
                  :primary_taxon_id, :supplier_id

    after_create :ensure_standard_variant
    around_destroy :destruction
    after_touch :touch_supplier

    # -- Scopes
    scope :with_properties, ->(*property_ids) {
      left_outer_joins(:product_properties).
        where(inherits_properties: true).
        where(spree_product_properties: { property_id: property_ids })
    }

    scope :with_order_cycles_outer, lambda {
      joins("
        LEFT OUTER JOIN spree_variants AS o_spree_variants
          ON (o_spree_variants.product_id = spree_products.id)").
        joins("
          LEFT OUTER JOIN exchange_variants AS o_exchange_variants
            ON (o_exchange_variants.variant_id = o_spree_variants.id)").
        joins("
          LEFT OUTER JOIN exchanges AS o_exchanges
            ON (o_exchanges.id = o_exchange_variants.exchange_id)").
        joins("
          LEFT OUTER JOIN order_cycles AS o_order_cycles
            ON (o_order_cycles.id = o_exchanges.order_cycle_id)")
    }

    scope :imported_on, lambda { |import_date|
      import_date = Time.zone.parse import_date if import_date.is_a? String
      import_date = import_date.to_date
      joins(:variants).merge(Spree::Variant.
        where(import_date: import_date.all_day))
    }

    scope :with_order_cycles_inner, -> { joins(variants: { exchanges: :order_cycle }) }

    scope :visible_for, lambda { |enterprise|
      joins('
        LEFT OUTER JOIN spree_variants AS o_spree_variants
          ON (o_spree_variants.product_id = spree_products.id)').
        joins('
          LEFT OUTER JOIN inventory_items AS o_inventory_items
            ON (o_spree_variants.id = o_inventory_items.variant_id)').
        where('o_inventory_items.enterprise_id = (?) AND visible = (?)', enterprise, true).
        distinct
    }

    scope :in_supplier, lambda { |supplier|
      distinct.joins(:variants).where(spree_variants: { supplier: })
    }

    # Products distributed via the given distributor through an OC
    scope :in_distributor, lambda { |distributor|
      distributor = distributor.respond_to?(:id) ? distributor.id : distributor.to_i

      with_order_cycles_outer.
        where('(o_exchanges.incoming = ? AND o_exchanges.receiver_id = ?)', false, distributor).
        select('distinct spree_products.*')
    }

    scope :in_distributors, lambda { |distributors|
      with_order_cycles_outer.
        where('(o_exchanges.incoming = ? AND o_exchanges.receiver_id IN (?))', false, distributors).
        distinct
    }

    # Products distributed by the given order cycle
    scope :in_order_cycle, lambda { |order_cycle|
      with_order_cycles_inner.
        merge(Exchange.outgoing).
        where(order_cycles: { id: order_cycle })
    }

    scope :in_an_active_order_cycle, lambda {
      with_order_cycles_inner.
        merge(OrderCycle.active).
        merge(Exchange.outgoing).
        where.not(order_cycles: { id: nil })
    }

    scope :by_producer, -> { joins(variants: :supplier).order('enterprises.name') }
    scope :by_name, -> { order('spree_products.name') }

    scope :managed_by, lambda { |user|
      if user.admin?
        where(nil)
      else
        in_supplier(user.enterprises)
      end
    }

    scope :active, lambda { where(spree_products: { deleted_at: nil }) }

    def self.group_by_products_id
      group(column_names.map { |col_name| "#{table_name}.#{col_name}" })
    end

    # for adding products which are closely related to existing ones
    def duplicate
      duplicator = Spree::Core::ProductDuplicator.new(self)
      duplicator.duplicate
    end

    def self.like_any(fields, values)
      where fields.map { |field|
        values.map { |value|
          arel_table[field].matches("%#{value}%")
        }.inject(:or)
      }.inject(:or)
    end

    def property(property_name)
      return nil unless prop = properties.find_by(name: property_name)

      product_properties.find_by(property: prop).try(:value)
    end

    def set_property(property_name, property_value)
      ActiveRecord::Base.transaction do
        property = Property.where(name: property_name).first_or_create!(presentation: property_name)
        product_property = ProductProperty.where(product: self,
                                                 property:).first_or_initialize
        product_property.value = property_value
        product_property.save!
      end
    end

    def properties_including_inherited
      # Product properties override producer properties
      ps = product_properties.all

      if inherits_properties
        # NOTE: Set the supplier as the first variant supplier. If variants have different supplier,
        # result might not be correct
        supplier = variants.first.supplier
        ps = OpenFoodNetwork::PropertyMerge.merge(ps, supplier&.producer_properties || [])
      end

      ps.
        sort_by(&:position).
        map { |pp| { id: pp.property.id, name: pp.property.presentation, value: pp.value } }
    end

    def in_distributor?(distributor)
      self.class.in_distributor(distributor).include? self
    end

    def in_order_cycle?(order_cycle)
      self.class.in_order_cycle(order_cycle).include? self
    end

    def variants_distributed_by(order_cycle, distributor)
      order_cycle.variants_distributed_by(distributor).where(product_id: self)
    end

    # Get the most recent import_date of a product's variants
    def import_date
      variants.map(&:import_date).compact.max
    end

    def destruction
      transaction do
        ExchangeVariant.
          where(exchange_variants: { variant_id: variants.with_deleted.
            select(:id) }).destroy_all

        yield
      end
    end

    # rubocop:disable Metrics/AbcSize
    def ensure_standard_variant
      return unless variants.empty?

      variant = Spree::Variant.new
      variant.product = self
      variant.price = price
      variant.display_as = display_as
      variant.unit_value = unit_value
      variant.unit_description = unit_description
      variant.variant_unit = variant_unit
      variant.variant_unit_name = variant_unit_name
      variant.variant_unit_scale = variant_unit_scale
      variant.tax_category_id = tax_category_id
      variant.shipping_category_id = shipping_category_id
      variant.primary_taxon_id = primary_taxon_id
      variant.supplier_id = supplier_id
      variants << variant
    end
    # rubocop:enable Metrics/AbcSize

    # Remove any unsupported HTML.
    def description=(html)
      super(HtmlSanitizer.sanitize(html))
    end

    private

    def touch_supplier
      return if variants.empty?

      # Assume the product supplier is the supplier of the first variant
      # Will breack if product has mutiple variants with different supplier
      first_variant = variants.first

      # The variant is invalid if no supplier is present, but this method can be triggered when
      # importing product. In this scenario the variant has not been updated with the supplier yet
      # hence the check.
      first_variant.supplier.touch if first_variant.supplier.present?
    end

    def validate_image
      return if image.blank? || !image.changed? || image.valid?

      errors.add(:base, I18n.t('spree.admin.products.image_not_processable'))
    end
  end
end
