class Accounting::Transaction < ApplicationRecord
  audited associated_with: :family, except: :closed

  include Validatable

  attr_accessor :student, :subcategory, :force_delete, :transaction_tags

  ACTIONS = [
    ['Charge', 'charge'],
    ['Credit', 'credit'],
    ['Payment', 'payment'],
    ['Transfer', 'transfer']
  ]

  TYPES = {
    increase: 'Accounting::Increase',
    decrease: 'Accounting::Decrease'
  }

  belongs_to :family, class_name: '::Family', optional: true

  has_one :payment_detail, dependent: :destroy, validate: false, autosave: true
  has_one :billing_processed_transaction, class_name: 'Billing::ProcessedTransaction',
    validate: false, autosave: true, dependent: :destroy
  has_one :cafeteria_transaction, dependent: :destroy
  has_one :student_cafeteria_transaction, through: :cafeteria_transaction
  has_one :invoice_charge, foreign_key: :transaction_id, dependent: :restrict_with_error
  has_one :invoice, through: :invoice_charge

  has_many :transaction_details, validate: false, dependent: :destroy
  has_many :notification_messages, foreign_key: :associated_id, as: :associated,
    dependent: :destroy, inverse_of: :associated
  has_many :attached_tags, class_name: '::AttachedTag', as: :associated, inverse_of: :associated
  has_many :tags, through: :attached_tags, dependent: :destroy

  has_many :subcategories, through: :transaction_details
  has_many :students, through: :transaction_details
  has_many :decrease_allocations, through: :transaction_details
  has_many :increase_allocations, through: :transaction_details

  has_associated_audits

  delegate :school, to: :family

  before_save :set_posted_on
  before_save :attach_tags
  before_save :destroy_allocations, if: :void

  after_save :cache_balances, if: :void
  after_save :sync_cafeteria_transaction

  before_destroy :prevent_destroying_transfer, if: :transfer?

  after_commit :cache_balances, on: :destroy

  validates :memo, presence: true
  validates :amount, numericality: { greater_than: 0 }

  scope :ordered, -> { order(posted_on: :desc, created_at: :desc) }
  scope :with_action, ->(action) { where(action: action) if action.present? }
  scope :with_families, ->(ids) { where(family_id: ids) if ids.present? }
  scope :by_action, ->(action) { where(action: action) if action.present? }
  scope :with_tags, ->(ids) { joins(:tags).where(tags: { id: ids }) if ids.present? }
  scope :without_void, ->(flag) { where(void: false) if flag }

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

    joins(:transaction_details).where(accounting_transaction_details: { student_id: id })
  end

  scope :with_or_without_invoice, ->(flag) do
    if flag
      joins(:invoice)
    elsif flag == false
      left_joins(:invoice).where(accounting_invoices: { id: nil })
    end
  end

  scope :with_date_range, ->(range) do
    where(posted_on: range) if range.present?
  end

  scope :with_category, ->(category) do
    where('accounting_subcategories.id = ?', category) if category.present?
  end

  scope :with_categories, ->(categories) do
    if categories.present?
      joins(:transaction_details).merge(Accounting::TransactionDetail.with_subcategory(categories))
    end
  end

  def self.only_increases
    where(type: 'Accounting::Increase')
  end

  def self.only_decreases
    where(type: 'Accounting::Decrease')
  end

  def self.not_closed
    where(closed: false)
  end

  def self.with_subcategory(id)
    joins(transaction_details: :subcategory)
      .where('accounting_subcategories.id = ?', id)
  end

  def increase?
    TYPES.key(type) == :increase
  end

  def student_name
    if students.one?
      student = students.first
      student ? student.full_name(:reverse) : ''
    elsif students.many?
      array = students.uniq
      array.one? ? array.first.full_name(:reverse) : 'Multiple Students'
    end
  end

  def category_name
    if subcategories.one?
      subcategory = subcategories.first
      subcategory.decorate.title
    elsif subcategories.many?
      array = subcategories.uniq
      array.one? ? array.first.decorate.title : 'Multiple Categories'
    end
  end

  def remove_allocations_and_update(detail)
    return false unless valid?

    begin
      ActiveRecord::Base.transaction do
        destroy_allocations
        detail.save!
        save!
      end
    rescue
      false
    end
  end

  def balance
    transaction_details.reduce(0) { |total, t| total + t.balance(TYPES.key(type)) }
  end

  def own_and_associated_audits
    AuditOverrider.by_associate_type('Accounting::Transaction').by_associate_id(id).not_created
      .or(AuditOverrider.by_auditable_type('Accounting::Transaction').by_auditable_id(id))
      .order(created_at: :desc)
  end

  private
    def set_posted_on
      self.posted_on = Time.current if posted_on.nil?
    end

    def subcategory_required
      return if transaction_details.all? { |d| d.subcategory.present? }

      errors.add(:category, 'must exist')
    end

    def student_required
      transaction_details.each do |transaction|
        next unless transaction.student_id.nil?

        errors.add(:student, 'must exist')
      end
    end

    def create_transaction_associations
      return unless subcategory

      transaction_details.build(
        student: student,
        subcategory: subcategory,
        amount: amount
      )
    end

    # TODO: Credits and Charges are being skipped because this causes problems when editing.
    def check_totals
      return if transaction_details.empty?

      transaction_amounts = transaction_details.reduce(0) { |i, c| i + c.amount }
      if amount == transaction_amounts
        nil
      else
        errors.add(:base, 'Details do not equal total')
      end
    end

    def prevent_destroying_transfer
      return if force_delete

      errors.add(:base, 'Cannot delete transfers')
      throw(:abort)
    end

    def destroy_allocations
      decrease_allocations&.each(&:destroy!)
      increase_allocations&.each(&:destroy!)
    end

    def attach_tags
      self.tags = transaction_tags if transaction_tags
    end

    def sync_cafeteria_transaction
      return unless student_cafeteria_transaction

      variable_amount = action == 'charge' ? amount * -1 : amount
      student_cafeteria_transaction.update(amount: variable_amount, date: posted_on, memo: memo)
    end

    def cache_balances
      family.cache_balances
      family.all_students.each(&:cache_balances)
    end
end
