(ns iol-ion.tx
  (:require [datomic.client.api :as dc])
  (:import [java.util UUID]))

(defn random-tempid []
  (str (UUID/randomUUID)))

(defn by
  ([f xs]
   (by f identity xs))
  ([f fv xs]
   (reduce
     #(assoc %1 (f %2) (fv %2))
     {}
     xs)))

(defn pull-many [db read ids ]
  (->> (dc/q '[:find (pull ?e r)
               :in $ [?e ...] r]
             db
             ids
             read)
       (map first)))

(declare upsert-entity)

(defn reset-rels [db e a vs]
  (assert (every? :db/id vs) (format "In order to reset attribute %s, every value must have :db/id" a))
  (let [ids         (when-not (string? e)
                      (->> (dc/q '[:find  ?z
                                   :in    $ ?e ?a
                                   :where [?e ?a ?z]]
                                 db e a)
                           (map first)))
        new-id-set  (set (map :db/id vs))
        retract-ids (filter (complement new-id-set) ids)
        {is-component? :db/isComponent} (dc/pull db [:db/isComponent] a)
        new-rels      (filter (complement (set ids)) (map :db/id vs))]
    (-> []
        (into (map (fn [i] (if is-component?
                                [:db/retractEntity i]
                                [:db/retract e a i ])) retract-ids))
        (into (map (fn [i] [:db/add e a i]) new-rels))
        (into (mapcat (fn [i] (upsert-entity db i)) vs)))))

(defn reset-scalars [db e a vs]
  
  (let [extant   (when-not (string? e)
                   (->> (dc/q '[:find  ?z
                                :in    $ ?e ?a
                                :where [?e ?a ?z]]
                              db e a)
                        (map first)))
        retracts (filter (complement (set vs)) extant)
        new      (filter (complement (set extant)) vs)]
    (-> []
        (into (map (fn [i] [:db/retract e a i ]) retracts))
        (into (map (fn [i] [:db/add e a i]) new)))))


;; TODO unit test this
(defn upsert-entity [db entity]
  (assert (or (:db/id entity)
              (:db/ident entity))
          (str "Cannot upsert without :db/id or :db/ident, " entity))
  (let [e             (or (:db/id entity) (:db/ident entity))
        is-new?       (string? e)
        extant-entity (when-not is-new?
                        (dc/pull db (keys entity) (or (:db/id entity) (:db/ident entity))))
        ident->value-type (by :db/ident (comp :db/ident
                                              :db/valueType)
                              (pull-many
                                db
                                [:db/valueType :db/ident]
                                (keys entity)))

        ident->cardinality (by :db/ident (comp :db/ident
                                               :db/cardinality)
                               (pull-many
                                 db
                                 [:db/cardinality :db/ident]
                                 (keys entity)))
        ops           (->> entity
                           (reduce
                             (fn [ops [a v]]
                               (cond
                                 (= :db/id a)
                                 ops

                                 (= :db/ident a)
                                 ops

                                 (or (= v (a extant-entity))
                                     (= v (:db/ident (a extant-entity) :nope))
                                     (= v (:db/id (a extant-entity)) :nope))
                                 ops
                                 
                                 (and (nil? v)
                                      (not (nil? (a extant-entity))))
                                 (if (= :db.cardinality/many (ident->cardinality a))
                                   (into ops (map (fn [v]
                                                    [:db/retract e a (cond-> v
                                                                       (:db/id v) :db/id)])
                                                  (a extant-entity)))

                                   (conj ops [:db/retract e a (cond-> (a extant-entity)
                                                                (:db/id (a extant-entity)) :db/id)]))

                                 (nil? v)
                                 ops

                                 ;; reset relationships if it's refs, and not a lookup (i.e., seq of maps, or empty seq)

                                 (and (sequential? v) (= :db.type/tuple (ident->value-type a)))
                                 (conj ops [:db/add e a v])

                                 (and (sequential? v) (= :db.type/ref (ident->value-type a)) (every? map? v))
                                 (into ops (reset-rels db e a v))


                                 (= :db.cardinality/many (ident->cardinality a))
                                 (into ops (reset-scalars db e a v))

                                 (and (sequential? v) (not= :db.type/ref (ident->value-type a)))
                                 (into ops (reset-scalars db e a v))

                                 (and (map? v)
                                      (= :db.type/ref (ident->value-type a)))
                                 (let [id (or (:db/id v) (random-tempid))]
                                   (-> ops
                                       (conj [:db/add e a id])
                                       (into (upsert-entity db (assoc v :db/id id)))))

                                 :else
                                 (conj ops [:db/add e a v])
                                 ))
                             []))]
    ops))

(defn min-by [sorter]
  (->> sorter
       sort
       last
       last))

(defn get-line-items-after [db journal-entry]
  (for [jel   (:journal-entry/line-items journal-entry)
        :let  [next-jel (->> (dc/index-pull db {:index    :avet
                                                :selector [:db/id :journal-entry-line/client+account+location+date]
                                                :start    [:journal-entry-line/client+account+location+date
                                                           (:journal-entry-line/client+account+location+date jel)
                                                           (:db/id jel)]
                                                :limit    3
                                                })
                             (filter (fn line-must-match-client-account-location [result]
                                       (and 
                                         (= (take 3 (:journal-entry-line/client+account+location+date result))
                                            (take 3 (:journal-entry-line/client+account+location+date jel)))
                                         (not= (:db/id jel)
                                               (:db/id result)))
                                       ))
                             first
                             :db/id)]
        :when next-jel]
    next-jel))

(def extant-read '[:db/id :journal-entry/date :journal-entry/client {:journal-entry/line-items [:journal-entry-line/account :journal-entry-line/location :db/id :journal-entry-line/client+account+location+date]}])


(defn calc-client+account+location+date [je jel]
  [(or
     (:db/id (:journal-entry/client je))
     (:journal-entry/client je))
   (or (:db/id (:journal-entry-line/account jel))
       (:journal-entry-line/account jel))
   (-> jel :journal-entry-line/location)
   (-> je :journal-entry/date)])

(defn upsert-ledger [db ledger-entry]
  (assert (:journal-entry/date ledger-entry) "Must at least provide date when updating ledger")
  (assert (:journal-entry/client ledger-entry) "Must at least provide client when updating ledger")
  (assert (every? :journal-entry-line/account (:journal-entry/line-items ledger-entry)) "must at least provide account when updating ledger")
  (assert (every? :journal-entry-line/location (:journal-entry/line-items ledger-entry)) "Must at least provide location when updating ledger")
  (let [extant-entry         (or (when-let [original-entity (:journal-entry/original-entity ledger-entry)]
                                   (dc/pull db extant-read [:journal-entry/original-entity original-entity]))
                                 (when-let [external-id (:journal-entry/external-id ledger-entry)]
                                   (dc/pull db extant-read [:journal-entry/external-id external-id])))
        extant-entry-exists? (:db/id extant-entry)]
    
    (cond-> 
        (upsert-entity db (into (-> ledger-entry
                                    (assoc :db/id (or
                                                    (:db/id ledger-entry)
                                                    (:db/id extant-entry)
                                                    (random-tempid)))
                                    (update :journal-entry/line-items
                                            (fn [lis]
                                              (mapv #(-> %
                                                         (assoc  :journal-entry-line/dirty true)
                                                         (assoc :journal-entry-line/client+account+location+date
                                                                (calc-client+account+location+date ledger-entry %)))
                                                    lis))))
                                ))
      extant-entry-exists? (into (map (fn [li]
                                        {:journal-entry-line/dirty true
                                         :db/id                    li})
                                      (get-line-items-after db extant-entry))))))

(defn remove-nils [m]
  (let [result (reduce-kv
                 (fn [m k v]
                   (if (not (nil? v))
                     (assoc m k v)
                     m
                     ))
                 {}
                 m)]
    (if (seq result)
      result
      nil)))

(defn propose-invoice [db invoice]
  (let [existing? (boolean (seq (dc/q '[:find  ?i
                                        :in    $ ?invoice-number ?client ?vendor
                                        :where
                                        [?i :invoice/invoice-number ?invoice-number]
                                        [?i :invoice/client ?client]
                                        [?i :invoice/vendor ?vendor]
                                        (not [?i :invoice/status :invoice-status/voided])]
                                      db
                                      (:invoice/invoice-number invoice)
                                      (:invoice/client invoice)
                                      (:invoice/vendor invoice))))]
    (if existing?
      []
      [(remove-nils invoice)])))
