# frozen_string_literal: true

module Spree
  # Spree::OrderMerger is responsible for taking two orders and merging them
  # together by adding the line items from additional orders to the order
  # that the OrderMerger is initialized with.
  #
  # Orders that are merged should be incomplete orders which should belong to
  # the same user. They should also be in the same currency.
  class OrderMerger
    # @!attribute order
    #   @api public
    #   @return [Spree::Order] The order which items wll be merged into.
    attr_accessor :order

    # Create the OrderMerger
    #
    # @api public
    # @param [Spree::Order] order The order which line items will be merged
    # into.
    def initialize(order)
      @order = order
    end

    # Merge a second order in to the order the OrderMerger was initialized with
    #
    # The line items from `other_order` will be merged in to the `order` for
    # this OrderMerger object. If the line items are for the same variant, it
    # will add the quantity of the incoming line item to the existing line item.
    # Otherwise, it will assign the line item to the new order.
    #
    # After the orders have been merged the `other_order` will be destroyed.
    #
    # @example
    #   initial_order = Spree::Order.find(1)
    #   order_to_merge = Spree::Order.find(2)
    #   merger = Spree::OrderMerger.new(initial_order)
    #   merger.merge!(order_to_merge)
    #   # order_to_merge is destroyed, initial order now contains the line items
    #   # of order_to_merge
    #
    # @api public
    # @param [Spree::Order] other_order An order which will be merged in to the
    # order the OrderMerger was initialized with.
    # @param [Spree::User] user Associate the order the user specified. If not
    # specified, the order user association will not be changed.
    # @return [void]
    def merge!(other_order, user = nil)
      if other_order.currency == order.currency
        other_order.line_items.each do |other_order_line_item|
          current_line_item = find_matching_line_item(other_order_line_item)
          handle_merge(current_line_item, other_order_line_item)
        end
      end

      set_user(user)
      if order.valid?
        persist_merge

        # So that the destroy doesn't take out line items which may have been re-assigned
        other_order.line_items.reload
        other_order.destroy
      end
    end

    private

    # Retreive a matching line item from the existing order
    #
    # It will compare line items based on variants, and all line item
    # comparison hooks on the order.
    #
    # @api private
    # @param [Spree::LineItem] other_order_line_item The line item from
    # `other_order` we are attempting to merge in.
    # @return [Spree::LineItem] A matching line item from the order. nil if none
    # exist.
    def find_matching_line_item(other_order_line_item)
      order.line_items.detect do |my_li|
        my_li.variant == other_order_line_item.variant &&
          order.line_item_comparison_hooks.all? do |hook|
            order.send(hook, my_li, other_order_line_item.serializable_hash)
          end
      end
    end

    # Associate the user with the order
    #
    # @api private
    # @param [Spree::User] user The user to associate with the order. If nil
    # the order user association will remain the same. If the order is already
    # associated with a user, it will not be changed.
    # @return [void]
    def set_user(user)
      order.associate_user!(user) if !order.user && user
    end

    # Merge the `other_order_line_item` into `current_line_item`
    #
    # If `current_line_item` is nil, the `other_order_line_item` will be
    # re-assigned to the `order`.
    #
    # If the merged line item can not be saved, an error will be added to
    # `order`.
    #
    # @api private
    # @param [Spree::LineItem] current_line_item The line item to be merged
    # into. If nil, the `other_order_line_item` will be re-assigned.
    # @param [Spree::LineItem] other_order_line_item The line item to merge in.
    # @return [void]
    def handle_merge(current_line_item, other_order_line_item)
      if current_line_item
        current_line_item.quantity += other_order_line_item.quantity
        handle_error(current_line_item) unless current_line_item.save
      else
        new_line_item = order.line_items.build(other_order_line_item.attributes.except("id"))
        handle_error(new_line_item) unless new_line_item.save
      end
    end

    # Handle an error from saving the `line_item`
    #
    # This adds errors from the line item to the `errors[:base]` of the order.
    #
    # @api private
    # @param [Spree::LineItem] line_item The line item which could not be saved
    # @return [void]
    def handle_error(line_item)
      order.errors.add(:base, line_item.errors.full_messages)
    end

    # Save the order totals after merge
    #
    # It triggers the order recalculator to ensure that item counts and totals are
    # up to date.
    #
    # @api private
    # @return [void]
    def persist_merge
      order.recalculate
    end
  end
end
