# frozen_string_literal: true

module Orders
  class HandleFeesService # rubocop:disable Metrics/ClassLength
    attr_reader :order

    delegate :distributor, :order_cycle, to: :order

    FeeValue = Struct.new(:fee, :role, keyword_init: true)

    def initialize(order)
      @order = order
    end

    def recreate_all_fees!
      # `with_lock` acquires an exclusive row lock on order so no other
      # requests can update it until the transaction is commited.
      # See https://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/locking/pessimistic.rb#L69
      # and https://www.postgresql.org/docs/current/static/sql-select.html#SQL-FOR-UPDATE-SHARE
      order.with_lock do
        EnterpriseFee.clear_order_adjustments order

        # To prevent issue with fee being removed when a product is not linked to the order cycle
        # anymore, we now create or update line item fees.
        # Previously fees were deleted and recreated, like we still do for order fees.
        create_or_update_line_item_fees!
        create_order_fees!
      end

      tax_enterprise_fees! unless order.before_payment_state?
      order.update_order!
    end

    def create_or_update_line_item_fees!
      order.line_items.includes(:variant).each do |line_item|
        # No fee associated with the line item so we just create them
        if line_item.enterprise_fee_adjustments.blank?
          create_line_item_fees!(line_item)
          next
        end
        create_or_update_line_item_fee!(line_item)

        # delete any fees removed from the Order Cycle
        delete_removed_fees!(line_item)
      end
    end

    def create_order_fees!
      return unless order_cycle

      calculator.create_order_adjustments_for order
    end

    def tax_enterprise_fees!
      Spree::TaxRate.adjust(order, order.all_adjustments.enterprise_fee)
    end

    def update_line_item_fees!(line_item)
      line_item.adjustments.enterprise_fee.each do |fee|
        fee.update_adjustment!(line_item, force: true)
      end
    end

    def update_order_fees!
      order.adjustments.enterprise_fee.where(adjustable_type: 'Spree::Order').each do |fee|
        fee.update_adjustment!(order, force: true)
      end
    end

    private

    def create_line_item_fees!(line_item)
      return unless provided_by_order_cycle? line_item

      calculator.create_line_item_adjustments_for(line_item)
    end

    def create_or_update_line_item_fee!(line_item)
      applicators = calculator.per_item_enterprise_fee_applicators_for(line_item.variant)

      applicators.each do |fee_applicator|
        fee_adjustment = line_item.adjustments.by_originator_and_enterprise_role(
          fee_applicator.enterprise_fee, fee_applicator.role
        )

        if fee_adjustment
          fee_adjustment.update_adjustment!(line_item, force: true)
        elsif provided_by_order_cycle? line_item
          fee_applicator.create_line_item_adjustment(line_item)
        end
      end

      # Update any fees not already processed
      fees_to_update = order_cycle_fees.map(&:fee) - applicators.map(&:enterprise_fee)
      update_fee_adjustments!(line_item, fees_to_update)
    end

    def update_fee_adjustments!(line_item, fees_to_update)
      fees_to_update.each do |fee|
        fee_adjustment = line_item.adjustments.find_by(originator: fee)

        fee_adjustment&.update_adjustment!(line_item, force: true)
      end
    end

    def delete_removed_fees!(line_item)
      removed_fees = line_item.enterprise_fee_adjustments.where.not(
        originator: order_cycle_fees.map(&:fee)
      )

      # The same fee can be used in the incoming and outgoing exchange, (supplier and distributor
      # fees), so we need an extra check to see if a fee linked to both exchanges has been removed
      order_cycle_fees.each do |order_cycle_fee|
        # Check if there is any fee adjustment with a role other than the one in the order cycle fee
        fee = line_item.enterprise_fee_adjustments.by_originator_and_not_enterprise_role(
          order_cycle_fee.fee, order_cycle_fee.role
        )

        # Check if the fee matches a fee linked to the order cycle
        if fee.nil? || order_cycle_fees_include_fee?(fee)
          next
        end

        # If not linked to the order cycle we add it to the list of fee to be removed
        removed_fees = removed_fees.to_a.push(fee)
      end

      removed_fees.each(&:destroy)
    end

    def order_cycle_fees
      return @order_cycle_fees if defined? @order_cycle_fees
      return [] unless order_cycle && distributor

      @order_cycle_fees = begin
        fees = []

        order_cycle.exchanges.supplying_to(distributor).each do |exchange|
          exchange.enterprise_fees.per_item.each do |enterprise_fee|
            fee_value = FeeValue.new(fee: enterprise_fee, role: exchange.role)
            fees << fee_value
          end
        end

        order_cycle.coordinator_fees.per_item.each do |enterprise_fee|
          fees << FeeValue.new(fee: enterprise_fee, role: "coordinator")
        end

        fees
      end
    end

    def order_cycle_fees_include_fee?(fee)
      matching = order_cycle_fees.select do |order_cycle_fee|
        order_cycle_fee.fee == fee.originator &&
          order_cycle_fee.role == fee.metadata.enterprise_role
      end
      matching.present?
    end

    def calculator
      @calculator ||= OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle)
    end

    def provided_by_order_cycle?(line_item)
      @order_cycle_variant_ids ||= order_cycle&.variants&.map(&:id) || []
      @order_cycle_variant_ids.include? line_item.variant_id
    end
  end
end
