class Accounting::TransactionDetail < ApplicationRecord
  audited associated_with: :accounting_transaction, only: [:student_id, :subcategory_id]

  INCREASE_ACTIONS = Accounting::Increase.actions
  DECREASE_ACTIONS = Accounting::Decrease.actions

  belongs_to :accounting_transaction, foreign_key: :transaction_id,
    class_name: 'Accounting::Transaction', optional: false

  belongs_to :increase, foreign_key: :transaction_id,
    class_name: 'Accounting::Increase', optional: true
  belongs_to :decrease, foreign_key: :transaction_id,
    class_name: 'Accounting::Decrease', optional: true

  belongs_to :student, optional: true
  belongs_to :subcategory

  has_many :increase_allocations, class_name: 'Accounting::Allocation',
    foreign_key: :increase_id, dependent: :destroy
  has_many :decrease_allocations, class_name: 'Accounting::Allocation',
    foreign_key: :decrease_id, dependent: :destroy

  has_one :student_cafeteria_transaction, through: :accounting_transaction

  after_save :delete_zero_balance
  after_save :open_or_close_transaction, unless: :new_record?

  after_commit :cache_balances, on: :destroy

  scope :ordered, -> { order('accounting_transactions.posted_on DESC') }
  scope :opened, -> { where(closed: false) }

  scope :with_type, ->(type) do
    if type
      type = Accounting::Transaction::TYPES[type]
      joins(:accounting_transaction).where(accounting_transactions: { type: type })
    end
  end

  scope :without_void, ->(flag) { merge(Accounting::Transaction.without_void(flag)) if flag }

  scope :with_subcategory, ->(id, always_query=false) do
    where(accounting_transaction_details: { subcategory_id: id }) if id.present? || always_query
  end

  scope :by_student, ->(student_id) do
    where(student_id: student_id) if student_id.present?
  end

  scope :by_student_id_or_nil, ->(student_id) do
    return if student_id.nil?

    where(<<~SQL, student_id)
      accounting_transaction_details.student_id = ? OR
      accounting_transaction_details.student_id IS NULL
    SQL
  end

  scope :by_family_id, ->(id) do
    return unless id

    joins(:accounting_transaction).where(accounting_transactions: { family_id: id })
  end

  scope :with_date_range, ->(range) do
    return unless range

    joins(:accounting_transaction).where(accounting_transactions: { posted_on: range })
  end

  # action only accepts enum numeric values
  scope :by_type_and_action, ->(type, action) do
    with_type(type).merge(Accounting::Transaction.by_action(action))
  end

  scope :by_actions, ->(actions) do
    return if actions.blank?

    actions = actions.is_a?(Array) ? actions.map(&:to_s) : [actions.to_s]
    # separates actions arguments by type
    increase_actions = (actions & INCREASE_ACTIONS.keys).map { |a| INCREASE_ACTIONS[a] }
    decrease_actions = (actions & DECREASE_ACTIONS.keys).map { |a| DECREASE_ACTIONS[a] }

    if increase_actions.present? && decrease_actions.present?
      by_type_and_action(:increase, increase_actions)
        .or(by_type_and_action(:decrease, decrease_actions))
    elsif increase_actions.present?
      by_type_and_action(:increase, increase_actions)
    else
      by_type_and_action(:decrease, decrease_actions)
    end
  end

  def self.only_increases
    joins(:accounting_transaction)
      .where(accounting_transactions: { type: 'Accounting::Increase' })
  end

  def self.only_decreases
    joins(:accounting_transaction)
      .where(accounting_transactions: { type: 'Accounting::Decrease' })
  end

  def self.open_transactions
    where(accounting_transactions: { closed: false })
  end

  def balance(type=nil)
    allocations(type).reduce(amount) { |total, c| total - c.amount }
  end

  private
    def allocations(type)
      return send("#{type}_allocations") if type

      types = Accounting::Transaction::TYPES.invert
      transaction_type = types[accounting_transaction.type]

      if transaction_type == :increase
        increase_allocations
      else
        decrease_allocations
      end
    end

    def open_or_close_transaction
      transaction = accounting_transaction
      if transaction.transaction_details.all?(&:closed)
        transaction.update_attribute(:closed, true)
      else
        transaction.update_attribute(:closed, false)
      end
    end

    def delete_zero_balance
      destroy! if amount.zero?
    end

    def cache_balances
      return unless student_id?

      student.cache_balances
    end
end
