# frozen_string_literal: true

module Orders
  class WorkflowService
    attr_reader :order

    def initialize(order)
      @order = order
    end

    def complete
      advance_to_state("complete", advance_order_options)
    end

    def complete!
      advance_order!(advance_order_options)
    end

    def next(options = {})
      result = advance_order_one_step

      after_transition_hook(options)

      result
    end

    def advance_to_payment
      return unless order.before_payment_state?

      advance_to_state("payment", advance_order_options)
    end

    def advance_checkout(options = {})
      advance_to = order.before_payment_state? ? "payment" : "confirmation"

      advance_to_state(advance_to, advance_order_options.merge(options))
    end

    private

    def advance_order_options
      shipping_method_id = order.shipping_method.id if order.shipping_method.present?
      { "shipping_method_id" => shipping_method_id }
    end

    def advance_to_state(target_state, options = {})
      until order.state == target_state
        break unless order.next

        after_transition_hook(options)
      end

      order.state == target_state
    end

    def advance_order!(options)
      until order.completed?
        order.next!
        after_transition_hook(options)
      end
    end

    def advance_order_one_step
      tries ||= 3
      order.next
    rescue ActiveRecord::StaleObjectError
      retry unless (tries -= 1).zero?
      false
    end

    def after_transition_hook(options)
      if order.state == "delivery"
        order.select_shipping_method(options["shipping_method_id"])
      end

      persist_all_payments if order.state == "payment"
    end

    # When a payment fails, the order state machine stays in 'payment'
    # and rollbacks all transactions
    #   This rollback also reverts the payment state from 'failed', 'void' or 'invalid' to 'pending'
    #   Despite the rollback, the in-memory payment still has the correct state, so we persist it
    def persist_all_payments
      order.payments.each do |payment|
        in_memory_payment_state = payment.state
        if different_from_db_payment_state?(in_memory_payment_state, payment.id)
          payment.reload.update(state: in_memory_payment_state)
        end
      end
    end

    # Verifies if the in-memory payment state is different from the one stored in the database
    #   This is be done without reloading the payment so that in-memory data is not changed
    def different_from_db_payment_state?(in_memory_payment_state, payment_id)
      # Re-load payment from the DB (unless it was cleared by clear_invalid_payments)
      db_payment = Spree::Payment.find_by(id: payment_id)

      db_payment.present? && in_memory_payment_state != db_payment.state
    end
  end
end
