module Skr

    # A transaction is a record of a business event that has financial consequences.
    # It consists of an at least one credit and at least one debit
    # Transactions can be nested, with each level compacting all the entries that were made on it
    #
    #     require 'skr/core'
    #     customer = Customer.find_by_code "MONEYBAGS"
    #     GlTransaction.record( source: invoice, description: "Invoice Example" ) do | transaction |
    #       transaction.location = Location.default # <- could also specify in record's options
    #       Sku.where( code: ['HAT','STRING'] ).each do | sku |
    #           transaction.add_posting( amount: sku.default_price,
    #                                     debit:  sku.gl_asset_account,
    #                                    credit: customer.gl_receivables_account )
    #       end
    #     end
    #
    class GlTransaction < Skr::Model

        is_immutable

        # A Transaction must refer to another record,
        # such as Invoice, Inventory Adjustment, or a Manual Posting.
        belongs_to :source, :polymorphic=>true

        # Each transaction belongs to an accounting period
        belongs_to :period, :class_name=>'Skr::GlPeriod', export: true

        has_many :postings, class_name: 'Skr::GlPosting',
                 inverse_of: :gl_transaction, export: { writable: true }

        has_many :credits, ->{ where({ is_debit: false }) }, class_name: 'Skr::GlPosting',
                 extend: Concerns::GlTran::Postings,
                 inverse_of: :gl_transaction, export: { writable: true }

        # Must equal credits, checked by the {#ensure_postings_correct} validation
        has_many :debits, ->{  where({ is_debit: true }) }, class_name: 'Skr::GlPosting',
                 extend: Concerns::GlTran::Postings,
                 inverse_of: :gl_transaction, export: { writable: true }

        before_validation :set_defaults
        validate  :ensure_postings_correct
        validates :source, :period,  :set=>true
        validates :description,      :presence=>true

        scope :with_details, lambda { | acct = nil |
            query = compose_query_using_detail_view(view: 'skr_gl_transaction_details',
                                                    join_to: :gl_transaction_id)
            acct.blank? ? query : query.joins(:postings).merge( GlPosting.matching(acct) )
        }, export: true

        # Add a debit/credit pair to the transaction with amount
        # @param amount [BigDecimal] the amount to apply to each posting
        # @param debit [GlAccount]
        # @param credit [GlAccount]
        def add_posting( amount: nil, debit: nil, credit: nil )
            Lanes.logger.debug "GlTransaction add_posting #{debit} : #{credit}"
            self.credits.build( location: @location, is_debit: false,
                                account: credit, amount: amount )
            self.debits.build(  location: @location, is_debit: true,
                                account: debit,  amount: amount * -1 )
        end

        # Passes the location onto the postings.
        # @param location [Location]
        def location=(location)
            @location = location
            each_posting do | posting |
                posting.location = location
            end
        end

        # @yield [GlPosting] each posting associated with the Transaction
        def each_posting
            self.credits.each{ |posting| yield posting }
            self.debits.each{  |posting| yield posting }
        end

        # @return [GlTransaction] the current transaction that's in progress
        def self.current
            glt = Thread.current[:gl_transaction]
            glt ? glt.last : nil
        end

        # Start a new nested GlTransaction
        # When a transaction is created, it can have
        # @return [GlTransaction] new transaction
        # @yield  [GlTransaction] new transaction
        def self.record( attributes={} )
            Thread.current[:gl_transaction] ||= []
            glt = GlTransaction.new( attributes )
            Thread.current[:gl_transaction].push( glt )
            Lanes.logger.debug "B4 GlTransaction"
            results = yield glt
            Thread.current[:gl_transaction].pop
            if results
                if results.is_a?(Hash) && results.has_key?(:attributes)
                    glt.assign_attributes( results[:attributes] )
                end
                glt._save_recorded
                Lanes.logger.debug "AF GlTransaction new=#{glt.new_record?} #{glt.errors.full_messages}"
            end
            return glt
        end

        # @param owner [Skr::Model]
        # @param amount [BigDecimal]
        # @param debit  [GlAccount]
        # @param credit  [GlAccount]
        # @param options  [Hash] options to pass to the [GlTransaction] if one is created
        def self.push_or_save( owner: nil, amount: nil, debit:nil, credit:nil, options:{} )
            if (glt = self.current) # we push
                glt.add_posting( amount: amount, debit: debit, credit: credit )
            else
                options.merge!(source: owner, location: options[:location] || owner.location)
                if owner.respond_to?(:attributes_for_gl_transaction)
                    options.reverse_merge!( owner.attributes_for_gl_transaction )
                end
                glt = GlTransaction.new( options )
                glt.add_posting( amount: amount, debit: debit, credit: credit )
                glt.save!
            end
            glt
        end

        # @private
        def _save_recorded
            compact( 'debits'  )
            compact( 'credits' )
            self.save if self.credits.any? || self.debits.any?
            self
        end

        private

        def compact( assoc_name )
            accounts = self.send( assoc_name ).to_a
            self.send( assoc_name + "=", [] )
            account_numbers = accounts.group_by{ |posting| posting.account_number }
            account_numbers.each do | number, matching |
                amount = matching.sum(&:amount)
                self.send( assoc_name ).build({
                    account_number: number,
                    is_debit: ( assoc_name == "debits" ),
                    amount: amount,
                })
            end
        end

        def ensure_postings_correct
            if debits.total !=  ( -1 * credits.total )
                self.errors.add(:credits, "must equal debits")
                self.errors.add(:debits, "must equal credits")
                return false
            end
            true
        end

        def set_defaults
            self.period ||= GlPeriod.current
        end

    end
end