(ns auto-ap.jobs.compare-sales-summaries
  "Compares sales summaries between two points in the same database.

  Datomic keeps every past value, so a recompute can be audited against exactly what was there
  before by reading `(d/as-of db t)` for some earlier `t` — no snapshot or scratch copy needed.

  The question this exists to answer is narrower than \"did the totals improve\": it is *which
  days changed, and were any of them already balanced*. A day that was balanced before and still
  balances after can still have had its line amounts move, and that is a real change to the
  books even though no red turns green. Counting only balanced/unbalanced transitions would hide
  it entirely."
  (:require
   [auto-ap.datomic :refer [conn]]
   [auto-ap.datomic.sales-summaries :as d-ss]
   [clj-time.coerce :as c]
   [datomic.api :as dc]))

(def item-read
  [:sales-summary-item/category
   :sales-summary-item/manual?
   :ledger-mapped/amount
   {:ledger-mapped/ledger-side [:db/ident]}
   {:ledger-mapped/account [:account/numeric-code]}])

(defn- cents
  "Amounts are doubles carrying float noise, so compare them at the cent — the unit the books are
  actually kept in. Without this, 182.87000000000003 and 182.87 read as a change."
  [x]
  (Math/round (* 100.0 (double (or x 0.0)))))

(defn- line
  "One item reduced to what a reader would call \"the number\": category, side, amount, account."
  [item]
  {:category (:sales-summary-item/category item)
   :side (get-in item [:ledger-mapped/ledger-side :db/ident])
   :cents (cents (:ledger-mapped/amount item))
   :account (get-in item [:ledger-mapped/account :account/numeric-code])})

(defn summaries-in
  "`{[client-code date] {:lines … :imbalance … :balanced?}}` for every summary in `[start end)`.

  Keyed by client code and date rather than entity id so the two sides line up even if an entity
  were recreated between the points being compared."
  [db start end]
  (->> (dc/q {:find [(list 'pull '?s [:sales-summary/date
                                      {:sales-summary/client [:client/code]}
                                      {:sales-summary/items item-read}])]
              :in '[$ ?start ?end]
              :where '[[?s :sales-summary/date ?d]
                       [(>= ?d ?start)]
                       [(< ?d ?end)]]}
             db (c/to-date start) (c/to-date end))
       (map first)
       (reduce (fn [acc s]
                 (let [items (map d-ss/<-pulled-item (:sales-summary/items s))]
                   (assoc acc
                          [(get-in s [:sales-summary/client :client/code]) (:sales-summary/date s)]
                          {:lines (frequencies (map line (:sales-summary/items s)))
                           :imbalance (d-ss/imbalance items)
                           :balanced? (d-ss/balanced? items)})))
               {})))

(defn- classify
  "How one client-day differs. `:numbers-changed` is the interesting one — the lines themselves
  moved, whether or not the day's balance status did."
  [before after]
  (cond
    (nil? before) :added
    (nil? after) :removed
    (= (:lines before) (:lines after)) :identical
    :else :numbers-changed))

(defn compare-window
  "Compares every summary in `[start end)` between two database values.

  Returns per-day rows plus the tallies worth reporting, including the one that is easy to miss:
  days that were **already balanced** and whose numbers moved anyway."
  [before-db after-db start end]
  (let [before (summaries-in before-db start end)
        after (summaries-in after-db start end)
        rows (for [k (distinct (concat (keys before) (keys after)))
                   :let [b (get before k) a (get after k)]]
               {:client (first k)
                :date (second k)
                :change (classify b a)
                :was-balanced? (:balanced? b)
                :now-balanced? (:balanced? a)
                :before-imbalance (:imbalance b)
                :after-imbalance (:imbalance a)
                :lines-before (:lines b)
                :lines-after (:lines a)})
        rows (vec rows)
        changed (filter #(= :numbers-changed (:change %)) rows)]
    {:rows rows
     :tally {:compared (count rows)
             :identical (count (filter #(= :identical (:change %)) rows))
             :numbers-changed (count changed)
             :added (count (filter #(= :added (:change %)) rows))
             :removed (count (filter #(= :removed (:change %)) rows))}
     :balance-transitions
     {:unbalanced->balanced (count (filter #(and (false? (:was-balanced? %)) (true? (:now-balanced? %))) rows))
      :balanced->unbalanced (count (filter #(and (true? (:was-balanced? %)) (false? (:now-balanced? %))) rows))
      :stayed-balanced (count (filter #(and (true? (:was-balanced? %)) (true? (:now-balanced? %))) rows))
      :stayed-unbalanced (count (filter #(and (false? (:was-balanced? %)) (false? (:now-balanced? %))) rows))}
     :previously-balanced-and-changed
     (->> changed (filter :was-balanced?) vec)}))

(defn line-diff
  "Which categories actually moved on one row, as `{category [before-cents after-cents]}`. For
  reading a handful of rows by hand once the tallies point at them."
  [row]
  (let [by-cat (fn [lines] (reduce (fn [m [l n]] (assoc m (:category l) (* n (:cents l)))) {} lines))
        b (by-cat (:lines-before row))
        a (by-cat (:lines-after row))]
    (->> (distinct (concat (keys b) (keys a)))
         (keep (fn [cat]
                 (let [x (get b cat 0) y (get a cat 0)]
                   (when (not= x y) [cat [(/ x 100.0) (/ y 100.0)]]))))
         (into {}))))

(defn compare-against
  "Convenience: compare the current database against its own past value at basis `t`."
  [t start end]
  (let [db (dc/db conn)]
    (compare-window (dc/as-of db t) db start end)))

(comment
  ;; the restore point, i.e. production's own summaries before any of this work
  (def result (compare-against 209608347
                               (clj-time.core/date-time 2026 7 15)
                               (clj-time.core/date-time 2026 8 14)))
  (:tally result)
  (:balance-transitions result)
  (count (:previously-balanced-and-changed result))
  (map line-diff (take 3 (:previously-balanced-and-changed result))))
