(ns auto-ap.datomic.sales-summaries
  (:require
   [iol-ion.query :refer [dollars=]]))

(defn- ledger-side
  "The ledger side of an item as a keyword, whether it arrived as a plain keyword (a
  transaction map, or a pull using `:xform iol-ion.query/ident`) or as the `{:db/ident ...}`
  map a plain `pull` returns. Resolving both shapes here matters: items whose side does not
  compare equal are counted on neither side, which would leave a summary looking balanced at
  zero and therefore silently accepted."
  [item]
  (let [side (:ledger-mapped/ledger-side item)]
    (if (map? side) (:db/ident side) side)))

(defn- side-total [side items]
  (->> items
       (filter #(= side (ledger-side %)))
       (map #(:ledger-mapped/amount % 0.0))
       (reduce + 0.0)))

(defn total-debits [items]
  (side-total :ledger-side/debit items))

(defn total-credits [items]
  (side-total :ledger-side/credit items))

(defn fully-mapped? [items]
  (every? :ledger-mapped/account items))

(defn fully-sided?
  "Every item says which side of the ledger it belongs on. Guards `accepted?` against
  reading a collection of sideless items as balanced at zero."
  [items]
  (every? #(#{:ledger-side/debit :ledger-side/credit} (ledger-side %)) items))

(defn balanced? [items]
  (dollars= (total-debits items) (total-credits items)))

(defn imbalance
  "Signed debits minus credits. `balanced?` answers yes or no; this says by how much and in
  which direction, so a day that does not balance can be logged and queried rather than only
  rendered red. Positive means the tender side exceeds what revenue accounts for."
  [items]
  (- (total-debits items) (total-credits items)))

(defn accepted?
  "True once a summary is finished: every line is mapped to an account and debits equal
  credits. This is the same condition the sales summaries grid renders as \"Balanced\", and
  the condition the scheduled refresh treats as \"leave this alone\"."
  [items]
  (boolean (and (seq items)
                (fully-mapped? items)
                (fully-sided? items)
                (balanced? items))))

(defn <-pulled-item
  "Flattens the ref values on a pulled sales summary item back to the scalars a transaction
  expects. `accepted?` reads either shape, so this is only needed on the write path."
  [item]
  (cond-> item
    (map? (:ledger-mapped/ledger-side item)) (update :ledger-mapped/ledger-side :db/ident)
    (map? (:ledger-mapped/account item))     (update :ledger-mapped/account :db/id)))
