# frozen_string_literal: true

module Spree
  module Stock
    # A simple implementation of Stock Coordination
    #
    # The algorithm for allocating inventory is naive:
    #   * For each available Stock Location
    #     * Allocate as much on hand inventory as possible from this location
    #     * Remove the amount allocated from the amount desired
    #   * Repeat but for backordered inventory
    #   * Combine allocated and on hand inventory into a single shipment per-location
    #
    # Allocation logic can be changed using a custom class (as
    # configured in Spree::Config::stock_allocator_class )
    #
    # After allocation, splitters are run on each Package (as configured in
    # Spree::Config.environment.stock_splitters)
    #
    # Finally, shipping rates are calculated using the class configured as
    # Spree::Config.stock.estimator_class.
    class SimpleCoordinator
      attr_reader :order

      # @api private
      attr_reader :inventory_units, :splitters, :stock_locations,
        :filtered_stock_locations, :inventory_units_by_variant, :desired,
        :availability, :allocator, :packages

      def initialize(order, inventory_units = nil)
        @order = order
        @inventory_units =
          inventory_units || Spree::Config.stock.inventory_unit_builder_class.new(order).units
        @splitters = Spree::Config.environment.stock_splitters

        @filtered_stock_locations = Spree::Config.stock.location_filter_class.new(load_stock_locations, order).filter
        sorted_stock_locations = Spree::Config.stock.location_sorter_class.new(filtered_stock_locations).sort
        @stock_locations = sorted_stock_locations

        @inventory_units_by_variant = @inventory_units.group_by(&:variant)
        @desired = Spree::StockQuantities.new(inventory_units_by_variant.transform_values(&:count))
        @availability = Spree::Stock::Availability.new(
          variants: desired.variants,
          stock_locations:
        )

        @allocator = Spree::Config.stock.allocator_class.new(availability)
      end

      def shipments
        @shipments ||= begin
          @packages = build_packages
          shipments = build_shipments

          # Make sure we don't add the proposed shipments to the order
          order.shipments = order.shipments - shipments

          shipments
        end
      end

      private

      def load_stock_locations
        Spree::StockLocation.all
      end

      def build_shipments
        # Turn the Stock::Packages into a Shipment with rates
        packages.map do |package|
          shipment = package.shipment = package.to_shipment
          shipment.shipping_rates = Spree::Config.stock.estimator_class.new.shipping_rates(package)
          shipment
        end
      end

      def build_packages
        # Allocate any available on hand inventory and remaining desired inventory from backorders
        on_hand_packages, backordered_packages, leftover = allocator.allocate_inventory(desired)

        raise Spree::Order::InsufficientStock.new(items: leftover.quantities) unless leftover.empty?

        packages = stock_locations.map do |stock_location|
          # Combine on_hand and backorders into a single package per-location
          on_hand = on_hand_packages[stock_location.id] || Spree::StockQuantities.new
          backordered = backordered_packages[stock_location.id] || Spree::StockQuantities.new

          # Skip this location it has no inventory
          next if on_hand.empty? && backordered.empty?

          # Turn our raw quantities into a Stock::Package
          package = Spree::Stock::Package.new(stock_location)
          package.add_multiple(get_units(on_hand), :on_hand)
          package.add_multiple(get_units(backordered), :backordered)

          package
        end.compact

        # Split the packages
        split_packages(packages)
      end

      def split_packages(initial_packages)
        initial_packages.flat_map do |initial_package|
          stock_location = initial_package.stock_location
          Spree::Stock::SplitterChain.new(stock_location, @splitters).split([initial_package])
        end
      end

      def get_units(quantities)
        # Change our raw quantities back into inventory units
        quantities.flat_map do |variant, quantity|
          inventory_units_by_variant[variant].shift(quantity)
        end
      end
    end
  end
end
