class Spree::ProductSet < ModelSet
  def initialize(attributes = {})
    super(Spree::Product, [], attributes, proc { |attrs| attrs[:product_id].blank? })
  end

  def save
    @collection_hash.each_value.all? do |product_attributes|
      update_attributes(product_attributes)
    end
  end

  def collection_attributes=(attributes)
    @collection = Spree::Product
      .where(id: attributes.each_value.map { |product| product[:id] })
    @collection_hash = attributes
  end

  private

  # A separate method of updating products was required due to an issue with
  # the way Rails' assign_attributes and updates_attributes behave when
  # delegated attributes of a nested object are updated via the parent object
  # (ie. price of variants). Updating such attributes by themselves did not
  # work using:
  #
  #   product.update_attributes(variants_attributes: [{ id: y, price: xx.x }])
  #
  # and so an explicit call to update attributes on each individual variant was
  # required. ie:
  #
  #   variant.update_attributes( { price: xx.x } )
  #
  def update_attributes(attributes)
    split_taxon_ids!(attributes)

    product = find_model(@collection, attributes[:id])
    if product.nil?
      @klass.new(attributes).save unless @reject_if.andand.call(attributes)
    else
      update_product(product, attributes)
    end
  end

  def split_taxon_ids!(attributes)
    attributes[:taxon_ids] = attributes[:taxon_ids].split(',') if attributes[:taxon_ids].present?
  end

  def update_product(product, attributes)
    original_supplier = product.supplier_id
    return false unless update_product_only_attributes(product, attributes)

    ExchangeVariantDeleter.new.delete(product) if original_supplier != product.supplier_id

    update_product_variants(product, attributes) &&
      update_product_master(product, attributes)
  end

  def update_product_only_attributes(product, attributes)
    variant_related_attrs = [:id, :variants_attributes, :master_attributes]
    product_related_attrs = attributes.except(*variant_related_attrs)
    return true if product_related_attrs.blank?

    product.assign_attributes(product_related_attrs)

    validate_presence_of_unit_value_in_product(product)

    product.errors.empty? && product.save
  end

  def validate_presence_of_unit_value_in_product(product)
    product.variants.each do |variant|
      validate_presence_of_unit_value_in_variant(product, variant)
    end
  end

  def validate_presence_of_unit_value_in_variant(product, variant)
    return unless %w(weight volume).include?(product.variant_unit)
    return if variant.unit_value.present?

    product.errors.add(:unit_value, "can't be blank")
  end

  def update_product_variants(product, attributes)
    return true unless attributes[:variants_attributes]

    update_variants_attributes(product, attributes[:variants_attributes])
  end

  def update_product_master(product, attributes)
    return true unless attributes[:master_attributes]

    create_or_update_variant(product, attributes[:master_attributes])
  end

  def update_variants_attributes(product, variants_attributes)
    variants_attributes.each do |attributes|
      create_or_update_variant(product, attributes)
    end
  end

  def create_or_update_variant(product, variant_attributes)
    variant = find_model(product.variants_including_master, variant_attributes[:id])
    if variant.present?
      variant.update_attributes(variant_attributes.except(:id))
    else
      create_variant(product, variant_attributes)
    end
  end

  def create_variant(product, variant_attributes)
    on_hand = variant_attributes.delete(:on_hand)
    on_demand = variant_attributes.delete(:on_demand)

    variant = product.variants.create(variant_attributes)

    begin
      variant.on_demand = on_demand if on_demand.present?
      variant.on_hand = on_hand.to_i if on_hand.present?
    rescue StandardError => e
      notify_bugsnag(e, product, variant, variant_attributes)
      raise e
    end
  end

  def notify_bugsnag(error, product, variant, variant_attributes)
    Bugsnag.notify(error) do |report|
      report.add_tab(:product, product.attributes)
      report.add_tab(:product_error, product.errors.first) unless product.valid?
      report.add_tab(:variant_attributes, variant_attributes)
      report.add_tab(:variant, variant.attributes)
      report.add_tab(:variant_error, variant.errors.first) unless variant.valid?
    end
  end

  def find_model(collection, model_id)
    collection.find do |model|
      model.id.to_s == model_id.to_s && model.persisted?
    end
  end
end
