# frozen_string_literal: true

require 'open_food_network/scope_variant_to_hub'

# Previously Spree::OrderPopulator. Modified to work with max_quantity and variant overrides.

class CartService
  attr_accessor :order
  attr_reader :errors

  def initialize(order)
    @order = order
    @errors = ActiveModel::Errors.new(self)
  end

  def populate(from_hash)
    @distributor, @order_cycle = distributor_and_order_cycle

    variants_data = read_variants_hash(from_hash)

    @order.with_lock do
      attempt_cart_add_variants variants_data
      overwrite_variants variants_data
    end
    valid?
  end

  def valid?
    errors.empty?
  end

  private

  def attempt_cart_add_variants(variants_data)
    loaded_variants = indexed_variants(variants_data)

    variants_data.each do |variant_data|
      loaded_variant = loaded_variants[variant_data[:variant_id]]

      if loaded_variant.deleted? || !variant_data[:quantity].positive?
        cart_remove(loaded_variant)
        next
      end

      next unless varies_from_cart(variant_data, loaded_variant)

      attempt_cart_add(loaded_variant, variant_data[:quantity], variant_data[:max_quantity])
    end
  end

  def indexed_variants(variants_data)
    @indexed_variants ||= begin
      variant_ids_in_data = variants_data.pluck(:variant_id)

      Spree::Variant.with_deleted.where(id: variant_ids_in_data).
        includes(:default_price, :stock_items, :product).
        index_by(&:id)
    end
  end

  def attempt_cart_add(variant, quantity, max_quantity = nil)
    scoper.scope(variant)

    return unless valid_variant?(variant)

    cart_add(variant, quantity, max_quantity)
  end

  def cart_add(variant, quantity, max_quantity)
    attributes = final_quantities(variant, quantity, max_quantity)

    if attributes[:quantity].positive?
      @order.contents.update_or_create(variant, attributes)
    else
      cart_remove(variant)
    end
  end

  def cart_remove(variant)
    order.contents.remove(variant)
  rescue ActiveRecord::RecordNotFound
    # Nothing to remove; no line items for this variant were found.
  end

  def final_quantities(variant, quantity, max_quantity)
    # If not enough stock is available, add as much as we can to the cart
    on_hand = variant.on_hand
    on_hand = [quantity, max_quantity].compact.max if variant.on_demand
    final_quantity = [quantity, on_hand].min
    final_max_quantity = max_quantity # max_quantity is not capped

    { quantity: final_quantity, max_quantity: final_max_quantity }
  end

  def overwrite_variants(variants)
    variants_removed(variants).each do |variant_id|
      variant = Spree::Variant.with_deleted.find(variant_id)
      cart_remove(variant)
    end
  end

  def scoper
    @scoper ||= OpenFoodNetwork::ScopeVariantToHub.new(@distributor)
  end

  def read_variants_hash(data)
    variants_array = []
    (data[:variants] || []).each do |variant_id, quantity|
      if quantity.is_a?(ActionController::Parameters)
        variants_array.push({
                              variant_id: variant_id.to_i,
                              quantity: quantity[:quantity].to_i,
                              max_quantity: quantity[:max_quantity].to_i
                            })
      else
        variants_array.push({
                              variant_id: variant_id.to_i,
                              quantity: quantity.to_i
                            })
      end
    end
    variants_array
  end

  def distributor_and_order_cycle
    [@order.distributor, @order.order_cycle]
  end

  # Returns true if the saved cart differs from what's in the posted data, otherwise false
  def varies_from_cart(variant_data, loaded_variant)
    li = line_item_for_variant loaded_variant

    li_added = li.nil? && (variant_data[:quantity].to_i > 0 || variant_data[:max_quantity].to_i > 0)
    li_quantity_changed = li.present? && li.quantity != variant_data[:quantity].to_i
    li_max_quantity_changed = li.present? &&
                              li.max_quantity.to_i != variant_data[:max_quantity].to_i

    li_added || li_quantity_changed || li_max_quantity_changed
  end

  def variants_removed(variants_data)
    variant_ids_given = variants_data.map { |data| data[:variant_id].to_i }

    (variant_ids_in_cart - variant_ids_given).uniq
  end

  def valid_variant?(variant)
    check_order_cycle_provided && check_variant_available_under_distribution(variant)
  end

  def check_order_cycle_provided
    order_cycle_provided = @order_cycle.present?
    errors.add(:base, I18n.t(:spree_order_cycle_error)) unless order_cycle_provided
    order_cycle_provided
  end

  def check_variant_available_under_distribution(variant)
    return true if OrderCycles::DistributedVariantsService.new(@order_cycle, @distributor)
      .available_variants.include? variant

    errors.add(:base, I18n.t(:spree_order_populator_availability_error))
    false
  end

  def line_item_for_variant(variant)
    order.find_line_item_by_variant variant
  end

  def variant_ids_in_cart
    @order.line_items.pluck :variant_id
  end
end
