(ns iol-ion.tx.upsert-entity
  (:require [datomic.api :as dc]
            ;; [clj-time.core :as time]
            ;; [clj-time.coerce :as coerce]
            )
  (:import [java.util UUID]))


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

(defn -by
  [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)))

;; TODO add DATOMIC_EXT_CLASSPATH ala https://docs.datomic.com/pro/reference/database-functions.html#transaction-functions 
;; (defn transform-common [v]
;;     (cond
;;       (nil? v)
;;       v

;;       (satisfies? clj-time.core/DateTimeProtocol v)
;;       (clj-time.coerce/to-date v)

;;       :else
;;       v))


(defn upsert-entity [db entity]
  (when-not (or (:db/id entity)
                (:db/ident entity))
    (datomic.api/cancel {:cognitect.anomalies/category :cognitect.anomalies/incorrect
                         :cognitect.anomalies/message
                         (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]} :db/ident]
                                (keys entity)))

        ident->cardinality (-by :db/ident (comp :db/ident
                                                :db/cardinality)
                                (-pull-many
                                 db
                                 [{:db/cardinality [:db/ident]} :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)) (not (= :db.cardinality/many (ident->cardinality 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 e a v]])

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

                                (and (sequential? v) (not= :db.type/ref (ident->value-type a)))
                                (into ops [[:reset-scalars 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 (assoc v :db/id id)]])))

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