class AllocationJob < ApplicationJob
  include Audited::ActiveJob
  include ApplicationHelper
  queue_as :default

  def initialize(family, subcategory, options={})
    @family = family
    @subcategory = subcategory
    @student = options[:student]
    super()
  end

  def perform
    transfer_increases = transaction_details(:increase).where(student: nil).to_a
    decreases = transaction_details(:decrease).to_a
    allocate_funds(transfer_increases, decreases, :transfer_fund)

    increases_by_student = transaction_details(:increase).group_by(&:student_id)
    decreases_by_student = transaction_details(:decrease).group_by(&:student_id)

    increases_by_student.each do |student_id, increases|
      allocate_funds(increases, decreases_by_student[student_id])
    end

    remaining_increases = transaction_details(:increase).to_a
    family_decreases = decreases_by_student[nil]
    allocate_funds(remaining_increases, family_decreases, :family_fund)

    # cache family outstanding/unallocated balances
    @family.cache_balances
    @family.all_students.each(&:cache_balances)
  end

  private
    def transaction_details(type)
      @family.accounting_transaction_details
        .includes("#{type}_allocations")
        .by_student_id_or_nil(@student&.id)
        .with_subcategory(@subcategory.id)
        .with_type(type.to_sym)
        .without_void(true)
        .opened
        .order('accounting_transactions.posted_on DESC')
    end

    def allocate_funds(increases, decreases, type=nil)
      increase = increases&.pop
      decrease = decreases&.pop

      return increase if increase.nil? || decrease.nil?

      increase_balance = increase.balance
      decrease_balance = decrease.balance

      1000.times do
        while increase_balance.zero?
          increase.update!(closed: true)
          increase = increases.pop
          break if increase.nil?

          increase_balance = increase.balance
        end

        while decrease_balance.zero?
          decrease.update!(closed: true)
          decrease = decreases.pop
          break if decrease.nil?

          decrease_balance = decrease.balance
        end

        return increase if increase.nil? || decrease.nil?

        @objects_to_save = []
        amount = [decrease_balance, increase_balance].min

        case type
        when :family_fund
          decrease = new_transaction(decreases, decrease, increase.student, amount)
          decrease_balance = amount
        when :transfer_fund
          increase = new_transaction(increases, increase, decrease.student, amount)
          increase_balance = amount
        end

        join = Accounting::Allocation.new
        join.increase = increase
        join.decrease = decrease
        join.amount = amount

        transactional_save(@objects_to_save << join)

        increase_balance -= amount
        decrease_balance -= amount
      end
    end

    def new_transaction(transactions, object, student, amount)
      object.amount -= amount
      @objects_to_save << object
      transactions << object unless object.amount.zero?

      transaction = object.accounting_transaction.transaction_details.build
      transaction.student = student
      transaction.subcategory = @subcategory
      transaction.amount = amount
      transaction
    end
end
