(ns auto-ap.ledger
  (:require
   [auto-ap.datomic
    :refer [audit-transact
            audit-transact-batch
            conn
            pull-id
            pull-ref
            remove-nils]]
   [auto-ap.utils :refer [by dollars-0? dollars=]]
   [clj-time.coerce :as c]
   [clj-time.core :as t]
   [clojure.tools.logging :as log]
   [com.brunobonacci.mulog :as mu]
   [com.unbounce.dogstatsd.core :as statsd]
   [datomic.client.api :as dc]
   [iol-ion.tx :refer [upsert-ledger]]))

(defn datums->impacted-entity [db [e changes]]
  (let [entity (dc/pull db '[{:invoice/_expense-accounts [:db/id] :transaction/_accounts [:db/id]}] e)
        namespaces (->> changes
                        (map #(:db/ident (dc/pull db '[:db/ident] (:a %))))
                        (map namespace)
                        set)]
    (cond (namespaces "invoice" )   [[:invoice e]]
          (namespaces "invoice-expense-account" ) [[:invoice (:db/id (:invoice/_expense-accounts entity))]]
          (namespaces "transaction-account" ) [[:transaction (:db/id (:transaction/_accounts entity))]]
          (namespaces "transaction" )   [[:transaction e]]
          #_#_(namespaces "expected-deposit" )   [[:expected-deposit e]]
          :else nil)))

(defmulti entity-change->ledger (fn [_ [type]]
                                  type))

(defmethod entity-change->ledger :invoice
  [db [_ id]]
  (when id 
    (let [entity (dc/pull db ['* {:invoice/vendor '[*]
                                 :invoice/payment '[*]
                                 :invoice/status '[:db/ident]
                                 :invoice/import-status '[:db/ident]}] id)
          credit-invoice? (< (:invoice/total entity 0.0) 0.0)]
      (when-not (or
                 (not (:invoice/total entity))
                 (= true (:invoice/exclude-from-ledger entity))
                 (= :import-status/pending (:db/ident (:invoice/import-status entity)))
                 (= :invoice-status/voided (:db/ident (:invoice/status entity)))
                 (dollars-0? (:invoice/total entity)))
        
        (remove-nils 
         {:journal-entry/source "invoice"
          :journal-entry/client (:db/id (:invoice/client entity))
          :journal-entry/date (:invoice/date entity)
          :journal-entry/original-entity (:db/id entity)
          :journal-entry/vendor (:db/id (:invoice/vendor entity))
          :journal-entry/amount (Math/abs (:invoice/total entity))

          :journal-entry/line-items (into [(cond-> {:db/id (str (:db/id entity) "-" 0)
                                                    :journal-entry-line/account :account/accounts-payable
                                                    :journal-entry-line/location "A"
                                                    }
                                             credit-invoice? (assoc :journal-entry-line/debit (Math/abs (:invoice/total entity)))
                                             (not credit-invoice?) (assoc :journal-entry-line/credit (Math/abs (:invoice/total entity))))]
                                          (map-indexed (fn [i ea]
                                                 (cond-> 
                                                     {:db/id (str (:db/id entity) "-" (inc i))
                                                      :journal-entry-line/account (:db/id (:invoice-expense-account/account ea))
                                                      :journal-entry-line/location (or (:invoice-expense-account/location ea) "HQ")
                                                      }
                                                   credit-invoice? (assoc :journal-entry-line/credit (Math/abs (:invoice-expense-account/amount ea)))
                                                   (not credit-invoice?) (assoc :journal-entry-line/debit (Math/abs (:invoice-expense-account/amount ea)))))
                                               (:invoice/expense-accounts entity)))
          :journal-entry/cleared (and (< (:invoice/outstanding-balance entity) 0.01)
                                      (every? #(= :payment-status/cleared (:payment/status %)) (:invoice/payments entity))
                                      )})))))

(defmethod entity-change->ledger :transaction
  [db [_ id]]
  (when id 
    (let [entity (dc/pull db ['* {:transaction/vendor '[*]
                                 :transaction/client '[*]
                                 :transaction/approval-status '[*]
                                 :transaction/bank-account '[* {:bank-account/type [:db/ident]}]
                                 :transaction/accounts '[*
                                                         {:transaction-account/account [*]}] }] id)
          decreasing? (< (or (:transaction/amount entity) 0.0) 0.0)
          credit-from-bank? decreasing?
          debit-from-bank? (not decreasing?)]
      (when (and (not (= :transaction-approval-status/excluded (:db/ident (:transaction/approval-status entity))))
                 (not (= :transaction-approval-status/suppressed (:db/ident (:transaction/approval-status entity))))
                 (:transaction/amount entity)
                 (not (dollars-0? (:transaction/amount entity))))
        (remove-nils 
          {:journal-entry/source                "transaction"
           :journal-entry/client                (:db/id (:transaction/client entity))
           :journal-entry/date                  (:transaction/date entity)
           :journal-entry/original-entity       (:db/id entity)
           :journal-entry/alternate-description (:transaction/description-original entity)
           :journal-entry/vendor                (:db/id (:transaction/vendor entity))
           :journal-entry/amount                (Math/abs (:transaction/amount entity))
           :journal-entry/cleared-against       (:transaction/cleared-against entity)

           :journal-entry/line-items (into [(remove-nils {:journal-entry-line/account  (:db/id (:transaction/bank-account entity))
                                                          :db/id (str (:db/id entity) "-" 0)
                                                          :journal-entry-line/location "A"
                                                          :journal-entry-line/credit   (when credit-from-bank?
                                                                                       (Math/abs (:transaction/amount entity)))
                                                          :journal-entry-line/debit    (when debit-from-bank?
                                                                                      (Math/abs (:transaction/amount entity)))})
                                            ]
                                           (map-indexed
                                             (fn [i a]
                                               (remove-nils{
                                                            :db/id (str (:db/id entity) "-" (inc i))
                                                            :journal-entry-line/account  (:db/id (:transaction-account/account a))
                                                            :journal-entry-line/location (:transaction-account/location a)
                                                            :journal-entry-line/debit    (when credit-from-bank?
                                                                                        (Math/abs (:transaction-account/amount a)))
                                                            :journal-entry-line/credit   (when debit-from-bank?
                                                                                         (Math/abs (:transaction-account/amount a)))}))
                                             (if (seq (:transaction/accounts entity))
                                               (:transaction/accounts entity)
                                               [{:transaction-account/amount (:transaction/amount entity)}])))
           
           :journal-entry/cleared true})))))

#_(defmethod entity-change->ledger :expected-deposit
  [db [type id]]
  (let [{:expected-deposit/keys [total client date]} (d/pull db '[:expected-deposit/total :expected-deposit/client :expected-deposit/date] id)]
    #:journal-entry
    {:source "expected-deposit"
     :original-entity id

     :client client
     :date date
     :amount total
     :vendor :vendor/ccp-square
     :line-items [#:journal-entry-line
                  {:credit total
                   :location "A"
                   :account :account/receipts-split}
                  #:journal-entry-line
                  {:debit total
                   :location "A"
                   :account :account/ccp}]}))

(defmethod entity-change->ledger :invoice-expense-account
  [_ _]
  nil
  )

(defmethod entity-change->ledger nil
  [_ _]
  nil)

(defn reconcile-ledger
  ([] (reconcile-ledger (-> (t/now)
                            (t/plus (t/months -6))
                            (c/to-date))))
  ([start-date]
   (let [txes-missing-ledger-entries (->> (dc/q {:query {:find  ['?t ]
                                                            :in    ['$ '?sd]
                                                            :where [
                                                                    '[?t :transaction/date ?d]
                                                                    '[(>= ?d ?sd)]
                                                                    '(not [_ :journal-entry/original-entity ?t])
                                                                    '(not [?t :transaction/amount 0.0])
                                                                    '(not [?t :transaction/approval-status :transaction-approval-status/excluded])
                                                                    '(not [?t :transaction/approval-status :transaction-approval-status/suppressed])
                                                                    ]}
                                                    :args  [(dc/db conn) start-date]})
                                          (map first)
                                          (mapv #(entity-change->ledger (dc/db conn) [:transaction %])))


         invoices-missing-ledger-entries (->> (dc/q {:query {:find  ['?t ]
                                                                :in    ['$ '?sd]
                                                                :where ['[?t :invoice/date ?d]
                                                                        '[(>= ?d ?sd)]
                                                                        '(not [_ :journal-entry/original-entity ?t])
                                                                        '[?t :invoice/total ?amt]
                                                                        '[(not= 0.0 ?amt)]
                                                                        '(not [?t :invoice/status :invoice-status/voided])
                                                                        '(not [?t :invoice/import-status :import-status/pending])
                                                                        '(not [?t :invoice/exclude-from-ledger true])
                                                                        ]}
                                                        :args  [(dc/db conn) start-date]})
                                              (map first)
                                              (mapv #(entity-change->ledger (dc/db conn) [:invoice %])))
         repairs                         (vec (concat txes-missing-ledger-entries invoices-missing-ledger-entries))]
     (when (seq repairs)
       (log/info (take 3 repairs))
       (log/warn "repairing " (count txes-missing-ledger-entries) " missing transactions, " (count invoices-missing-ledger-entries) " missing invoices that were missing ledger entries")
       (dc/transact conn {:tx-data repairs})))))


(defn touch-transaction [e]
  (when-let [change (entity-change->ledger (dc/db conn)
                                           [:transaction e])]
    (dc/transact conn {:tx-data [{:db/id  "datomic.tx"
                                  :db/doc "touching transaction to update ledger"}
                                 `(upsert-ledger ~change)]})))

(defn touch-invoice [e]
  (when-let [change (entity-change->ledger (dc/db conn)
                                           [:invoice e])]
    (dc/transact conn [{:db/id  "datomic.tx"
                        :db/doc "touching invoice to update ledger"}
                       `(upsert-ledger ~change)])))

(defn lazy-tx-range
  ([start end xf] (lazy-tx-range start end xf 0))
  ([start end xf o]
   (let [next-results (dc/tx-range conn {:start  start
                                         :end    end
                                         :offset o
                                         :limit  200})]
     (lazy-seq
       (if (seq next-results)
         (concat (sequence  (comp (mapcat :data)
                                xf) next-results) (lazy-tx-range start
                                                          end
                                                          xf
                                                          (+ (count next-results)
                                                             o)))
         next-results)))))



(defn recently-changed-entities [start end]
  (set (map (fn [d]
              (:e d))
            (mapcat :data (dc/tx-range conn {:start start
                                             :end   end})))))

(defn entities-since-last-ledger-entry []
  (count (dc/tx-range conn {:start (c/to-date (t/plus (t/now) (t/days -5)))
                            :end (c/to-date (t/now))})))

(defn mismatched-transactions
  ([]
   (mismatched-transactions (c/to-date (t/minus (t/now) (t/days 7)))
                            (c/to-date (t/minus (t/now) (t/hours 1)))) )
  ([changed-between-start changed-between-end]
   (let [entities-to-consider (recently-changed-entities
                                   changed-between-start
                                   changed-between-end)
         _ (log/info "checking" (count entities-to-consider) "transactions looking for mismatches between" changed-between-start changed-between-end)
         jel-accounts         (reduce
                                (fn [acc [e lia]]
                                  (update acc e (fnil conj #{} ) lia))
                                {}
                                (dc/q '[:find  ?e ?lia
                                       :in    $ [?e ...]
                                       :where
                                       [?je :journal-entry/original-entity ?e]
                                        [?e :transaction/date]
                                       [?je :journal-entry/line-items ?li]
                                       [?li :journal-entry-line/account ?lia]
                                       [?lia :account/name]]
                                     (dc/db conn)
                                     entities-to-consider))
         transaction-accounts (reduce
                                (fn [acc [e lia]]
                                  (update acc e (fnil conj #{} ) lia))
                                {}
                                (dc/q '[:find  ?e ?lia
                                       :in    $ [?e ...]
                                       :where
                                       [?e :transaction/date ?d]
                                       [?e :transaction/accounts ?li]
                                       (not [?e :transaction/approval-status :transaction-approval-status/excluded])
                                       (not [?e :transaction/approval-status :transaction-approval-status/suppressed])
                                       [?li :transaction-account/account ?lia]
                                       [?lia :account/name]
                                       [?e :transaction/amount ?amt]
                                       [(not= ?amt 0.0)]]
                                     (dc/db conn)
                                     entities-to-consider))]
     (->> transaction-accounts 
          (filter 
            (fn [[e accounts]] (not= accounts (get jel-accounts e))))))))

(defn unbalanced-transactions 
  ([] (unbalanced-transactions (c/to-date (t/minus (t/now) (t/days 7)))
                               (c/to-date (t/minus (t/now) (t/hours 1)))))
  ([changed-between-start changed-between-end]
   (let [entities-to-consider (recently-changed-entities changed-between-start changed-between-end)]
     (log/info "checking" (count entities-to-consider) "transaction journal entries looking for mismatches between" changed-between-start changed-between-end)
     (->> (dc/q '[:find  ?je ?a (sum ?debit) (sum ?credit)
                 :with  ?jel
                 :in    $ [?je ...]
                 :where [?je :journal-entry/amount ?a]
                 [?je :journal-entry/line-items ?jel]
                 [(get-else $ ?jel :journal-entry-line/debit 0.0) ?debit]
                 [(get-else $ ?jel :journal-entry-line/credit 0.0) ?credit]
                 ]
               (dc/db conn)
               entities-to-consider)
          (filter (fn [[_ a d c]]
                    (or (not (dollars= a d))
                        (not (dollars= a c)))))
          (map first)
          (map (fn [je]
                 (pull-ref (dc/db conn) :journal-entry/original-entity je)))))))



(defn unbalanced-invoices
  ([] (unbalanced-invoices (c/to-date (t/minus (t/now) (t/days 7)))
                           (c/to-date (t/minus (t/now) (t/hours 1)))))
  ([changed-between-start changed-between-end]
   (let [entities-to-consider (recently-changed-entities
                                changed-between-start
                                changed-between-end)]
     (log/info "checking" (count entities-to-consider) "invoice journal entries looking for mismatches between" changed-between-start changed-between-end)
     (->> (dc/q '[:find  ?je ?a (sum ?debit) (sum ?credit)
                  :with  ?jel
                  :in    $ [?je ...]
                  :where [?je :journal-entry/amount ?a]
                  [?je :journal-entry/original-entity ?i]
                  [?i :invoice/date]
                  [?je :journal-entry/line-items ?jel]
                  [(get-else $ ?jel :journal-entry-line/debit 0.0) ?debit]
                  [(get-else $ ?jel :journal-entry-line/credit 0.0) ?credit]]
                (dc/db conn)
                entities-to-consider)
          (filter (fn [[_ a d c]]
                    (or (not (dollars= a d))
                        (not (dollars= a c)))))
          (map first)
          (map (fn [je]
                 (pull-ref (dc/db conn) :journal-entry/original-entity je)))))))

(defn mismatched-invoices
  ([]
   (mismatched-invoices (c/to-date (t/minus (t/now) (t/days 7)))
                        (c/to-date (t/minus (t/now) (t/hours 1)))) )

  ([changed-between-start changed-between-end]
   (let [entities-to-consider (recently-changed-entities changed-between-start changed-between-end)
         _                    (log/info (count entities-to-consider) "invoices have changed between" changed-between-start "and" changed-between-end)
         jel-accounts         (reduce
                                (fn [acc [e lia]]
                                  (update acc e (fnil conj #{} ) lia))
                                {}
                                (dc/q '[:find  ?e ?lia
                                       :in    $ [?e ...]
                                       :where 
                                       [?je :journal-entry/original-entity ?e]
                                        [?e :invoice/date]
                                       [?je :journal-entry/line-items ?li]
                                       [?li :journal-entry-line/account ?lia]
                                       (not [?lia :account/numeric-code 21000])
                                       [?lia :account/name]]
                                     (dc/db conn)
                                     entities-to-consider))
         invoice-accounts     (reduce
                                (fn [acc [e lia]]
                                  (update acc e (fnil conj #{} ) lia))
                                {}
                                (dc/q '[:find  ?e ?lia
                                       :in    $ [?e ...]
                                       :where
                                       [?e :invoice/expense-accounts ?li]
                                       (not [?e :invoice/total 0.0])
                                       [?li :invoice-expense-account/account ?lia]
                                       [?lia :account/name]
                                       (not [?lia :account/numeric-code 21000])
                                       (not [?e :invoice/status :invoice-status/voided])
                                       (not [?e :invoice/exclude-from-ledger true])
                                       [?e :invoice/import-status :import-status/imported]]
                                     (dc/db conn)
                                     entities-to-consider))
         ]
     (filter 
       (fn [[e accounts]]
         (not= accounts (get jel-accounts e)))
       invoice-accounts))))

(defn touch-broken-ledger []
  (statsd/event {:title    "Reconciling Ledger"
                 :text     "This process looks for unbalance ledger entries, or missing ledger entries"
                 :priority :low}
                nil)
  (log/info "Attempting to fix transactions that are in the ledger but are wrong")
  (let [mismatched-ts (mismatched-transactions)]
    (if (seq mismatched-ts)
      (do 
        (log/warn (count mismatched-ts) " transactions exist but don't match ledger " (pr-str (take 10 mismatched-ts) ))
        (doseq [[m] mismatched-ts]
          (touch-transaction m))
        (statsd/gauge "data.mismatched_transactions" (count (mismatched-transactions))))
      (statsd/gauge "data.mismatched_transactions" 0.0)))
  (log/info "Attempting to fix transactions that are in the ledger but debits/credits don't add up")
  (let [unbalanced-ts (unbalanced-transactions)]
    (if (seq unbalanced-ts)
      (do 
        (log/warn (count unbalanced-ts) " transactions exist but don't have matching debits/credits (" (pr-str (take 10 unbalanced-ts) ) ")")
        (doseq [m unbalanced-ts]
          (touch-transaction m))
        (statsd/gauge "data.unbalanced_transactions" (count (unbalanced-transactions))))
      (statsd/gauge "data.unbalanced_transactions" 0.0)))
  (log/info "Finished fixing transactions that are in the ledger but are wrong")
  (let [mismatched-is (mismatched-invoices)]
    (if (seq mismatched-is)
      (do 
        (log/warn (count mismatched-is) " invoice exist but don't match ledger ")
        (doseq [[m] mismatched-is]
          (touch-invoice m))
        (statsd/gauge "data.mismatched_invoices" (count (mismatched-invoices))))
      (statsd/gauge "data.mismatched_invoices" 0.0)))
  (log/info "Attempting to fix transactions that are in the ledger but debits/credits don't add up")
  (let [unbalanced-is (unbalanced-invoices)]
    (if (seq unbalanced-is)
      (do 
        (log/warn (count unbalanced-is) " invoices exist but don't have matching debits/credits ")
        (doseq [m unbalanced-is]
          (touch-invoice m))
        (statsd/gauge "data.unbalanced_invoices" (count (unbalanced-invoices))))
      (statsd/gauge "data.unbalanced_invoices" 0.0)))

  (log/info "Finish fixing invoices that are in the ledger but are wrong")
  (statsd/event {:title    "Finished Reconciling Ledger"
                 :text     "This process looks for unbalance ledger entries, or missing ledger entries"
                 :priority :low}
                nil))

(defn transact-with-ledger [transaction id]
  (let [db (dc/db conn)
        tx                (audit-transact transaction id)
        affected-entities (->> (:tx-data tx)
                               (map (fn [x]
                                      {:e     (:e x)
                                       :a     (:a x)
                                       :v     (:v x)
                                       :added (:added x)}))
                               (group-by :e)
                               (mapcat #(datums->impacted-entity db %))
                               (set))
        ledger-txs (->> affected-entities
                   (map #(entity-change->ledger (:db-after tx) %))
                   (filter seq)
                   (map (fn [l]
                          `(upsert-ledger ~l))))]
    (when (seq ledger-txs)
      (audit-transact ledger-txs id))
    tx))

(defn transact-batch-with-ledger [txes id]
  (let [batch-id (.toString (java.util.UUID/randomUUID))]
    (reduce
      (fn [full-tx batch]
        (let [batch     (conj (vec batch) {:db/id       "datomic.tx"
                                           :audit/batch batch-id})
              _         (log/info "transacting batch " batch-id " " (count batch))
              tx-result (transact-with-ledger batch id)]

          (cond-> full-tx
            (:tx-data full-tx)         (update :tx-data #(into % (:tx-data tx-result)))
            (not (:tx-data full-tx))   (assoc :tx-data (vec (:tx-data tx-result)))
            (not (:db-before full-tx)) (assoc :db-before (:db-before tx-result))
            true                       (assoc :db-after (:db-after tx-result))
            true                       (update :tempids merge (:tempids tx-result)))))

      {}
      (partition-all 50 txes))))

(defn build-account-lookup [client-id]
  (let [accounts (by :db/id (map first (dc/q {:query {:find ['(pull ?e [:db/id :account/name
                                                                           :account/numeric-code
                                                                           {:account/type [:db/ident]
                                                                            :account/client-overrides [:account-client-override/client :account-client-override/name]}
                                                                           ])]
                                                         :in ['$]
                                                         :where ['[?e :account/name]]}
                                                 :args [(dc/db conn )]})))

        bank-accounts (by :db/id (map first (dc/q {:query {:find ['(pull ?e [:db/id :bank-account/name :bank-account/numeric-code {:bank-account/type [:db/ident]}])]
                                                              :in ['$]
                                                              :where ['[?e :bank-account/name]]}
                                                      :args [(dc/db conn)]})))  
        overrides-by-client (->> accounts
                                 vals
                                 (mapcat (fn [a]
                                           (map (fn [o]
                                                  [[(:db/id a) (:db/id (:account-client-override/client o))]
                                                   (:account-client-override/name o)])
                                                (:account/client-overrides a))
                                           ) )
                                 (into {} ))]
    (fn [a]
      {:name (or (:bank-account/name (bank-accounts a))
                 (overrides-by-client [a client-id])
                 (:account/name (accounts a)))
       :account_type (or (:db/ident (:account/type (accounts a)))
                         ({:bank-account-type/check :account-type/asset
                           :bank-account-type/cash :account-type/asset
                           :bank-account-type/credit :account-type/liability}
                          (:db/ident (:bank-account/type (bank-accounts a)))))
       :numeric_code (or (:account/numeric-code (accounts a))
                         (:bank-account/numeric-code (bank-accounts a)))
       :client_id client-id})))

(defn reset-client+account+location+date
  ([] (reset-client+account+location+date (map first (dc/q  '[:find ?c :where [?c :client/code]] (dc/db conn)))))
  ([clients]
    (doseq [client clients
            :let [_ (mu/log ::reseting-index-for :client client)]
            batch
            (->> (dc/qseq '[:find (pull ?je [:journal-entry/date :journal-entry/client {:journal-entry/line-items [:journal-entry-line/account :journal-entry-line/location :db/id]}])
                            :in $ ?c
                            :where [?je :journal-entry/client ?c]]
                          (dc/db conn)
                          client
                          
                          )
                 (map first)
                 (mapcat (fn [je]
                           (map (fn [jel]
                                  {:db/id (:db/id jel)
                                   :journal-entry-line/client+account+location+date
                                   [(-> je :journal-entry/client :db/id)
                                    (-> jel :journal-entry-line/account :db/id)

                                    (-> jel :journal-entry-line/location)

                                    (-> je :journal-entry/date)]})
                                (:journal-entry/line-items je))))
                 (partition-all 500)
                 )]
      (mu/log ::batch-completed)
      (dc/transact conn {:tx-data batch}))))

(defn find-mismatch-index []
  (reduce + 0 
    (for [c    (map first (dc/q  '[:find ?c :where [?c :client/code]] (dc/db conn)))
          :let [_ (println "searching for" c)
                a (->> (dc/index-pull (dc/db conn)
                                      {:index    :avet
                                       :selector [:db/id :journal-entry-line/location :journal-entry-line/account :journal-entry-line/client+account+location+date {:journal-entry/_line-items [:journal-entry/date :journal-entry/client]}]
                                       :start    [:journal-entry-line/client+account+location+date [c]]})
                       (take-while (fn [result]
                                     (= c (first (:journal-entry-line/client+account+location+date result)))
                                     ))
                       (filter (fn [{index :journal-entry-line/client+account+location+date :as result}]
                                 (not= index
                                       [(-> result :journal-entry/_line-items :journal-entry/client :db/id)
                                        (-> result :journal-entry-line/account :db/id)
                                        (-> result :journal-entry-line/location)
                                        (-> result :journal-entry/_line-items :journal-entry/date)]))))]]
      (do (println (count a))
          (count a)))))



(defn accounts-needing-rebuild [ db client]
  (let [client (pull-id db client)]
    (->> (dc/qseq '[:find ?c ?a ?l (min ?d) 
                    :in $ ?c
                    :where 
                    [?jel :journal-entry-line/dirty true]
                    [?jel :journal-entry-line/account ?a]
                    [?jel :journal-entry-line/location ?l]
                    [?je :journal-entry/line-items ?jel]
                    [?je :journal-entry/client ?c]
                    [?je :journal-entry/date ?d]]
                  db
                  client)
         (map (fn [[client account location starting-at ]]
                {:client      client
                 :account     account
                 :starting-at starting-at
                 :location    location})))))

(defn find-running-balance-start [{:keys [client account location starting-at]} db ]
  (let [client  (pull-id db client)
        account (pull-id db account)]
    (or 
      (->> (dc/index-pull db
                          {:index    :avet
                           :selector [:db/id :journal-entry-line/running-balance :journal-entry-line/client+account+location+date]
                           :start    [:journal-entry-line/client+account+location+date [client account location starting-at]]
                           :reverse true
                           :limit   500})
           (take-while (fn [result]
                         (= [client
                             account
                             location]
                            (take 3 (:journal-entry-line/client+account+location+date result)))))
           (take 5)
           (drop-while (fn [{[_ _ _ date] :journal-entry-line/client+account+location+date}]
                         (>= (compare date starting-at) 0)))
           first
           :journal-entry-line/running-balance)
      0.0)))

(defn get-dirty-entries [{:keys [client account location starting-at]} db ]
  (let [client  (pull-id db client)
        account (pull-id db account)]
    (into []
          (comp
            (map (fn [i]
                   (dc/index-pull db
                                  {:index    :avet
                                   :selector [:db/id :journal-entry-line/debit :journal-entry-line/credit :journal-entry-line/client+account+location+date]
                                   :start    [:journal-entry-line/client+account+location+date
                                              [client account location starting-at]]
                                   :offset   (* i 1000)
                                   :limit    1000}
                                  )))
            (take-while identity)
            (mapcat identity)
            (take-while (fn [{[result-client result-account result-location] :journal-entry-line/client+account+location+date}]
                          #_(println
                            [[ client result-client]
                             [ account result-account]
                             [ location result-location]])
                          (and 
                            (= client result-client)
                            (= account result-account)
                            (= location result-location))))
            (map (fn [result]
                   [(:db/id result) (:journal-entry-line/debit result 0.0) (:journal-entry-line/credit result 0.0) ])))
          (range 100))))

(defn compute-running-balance [account-needing-refresh]
  (mu/trace  ::compute
             [:dirty-count (count (:dirty-entries account-needing-refresh))]
             (second 
               (reduce
                 (fn [[running-balance rows] [id debit credit] ]
                   (let [new-running-balance (+ running-balance
                                                (if (#{:account-type/asset
                                                       :account-type/dividend
                                                       :account-type/expense} (:account-type account-needing-refresh))
                                                  (- debit credit)
                                                  (- credit debit)))]
                     [new-running-balance
                      (conj rows 
                            {:db/id                              id
                             :journal-entry-line/running-balance new-running-balance
                             :journal-entry-line/dirty           false})]))

                 [(:build-from account-needing-refresh) []]
                 (:dirty-entries account-needing-refresh)))))


(defn refresh-running-balance-cache
  ([] (refresh-running-balance-cache (shuffle (map first
                                                   (dc/q '[:find (pull ?c [:client/code :db/id])
                                                           :where [?c :client/code]]
                                                         (dc/db conn))))))
  ([clients]
   (doseq [c clients]
     (mu/trace ::building-running-balance
       [:client c]
       (mu/with-context {:client c}
         (let [db                       (dc/db conn)
               accounts-needing-rebuild (accounts-needing-rebuild db (:db/id c))]
           (when (seq accounts-needing-rebuild)
             (mu/log ::found-accounts-needing-rebuild
                     :accounts (count accounts-needing-rebuild))
             (audit-transact-batch
               (->>  accounts-needing-rebuild
                     (mapcat (fn [account-needing-rebuild]
                               (mu/with-context {:account account-needing-rebuild}
                                 (-> account-needing-rebuild
                                     (assoc :build-from (find-running-balance-start account-needing-rebuild db))
                                     (assoc :dirty-entries (get-dirty-entries account-needing-rebuild db))
                                     (assoc :account-type (:account_type ((build-account-lookup (:client account-needing-rebuild)) (:account account-needing-rebuild))))
                                     (compute-running-balance))))))
               {:user/name "running-balance-cache"}))))))))


;; TODO only enable once IOL is set up in clod
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
#_(mount/defstate running-balance-cache-worker
  :start (scheduler/every (* 15 60 (+ 500 (rand-int 500))) (heartbeat refresh-running-balance-cache "running-balance-cache"))
  :stop (scheduler/stop running-balance-cache-worker))
