(ns frontend.worker.db.migrate
  "Handles SQLite and datascript migrations for DB graphs"
  (:require [clojure.string :as string]
            [clojure.walk :as walk]
            [datascript.core :as d]
            [datascript.impl.entity :as de]
            [frontend.worker-common.util :as worker-util]
            [frontend.worker.db.rename-db-ident :as rename-db-ident]
            [logseq.common.config :as common-config]
            [logseq.common.util :as common-util]
            [logseq.db :as ldb]
            [logseq.db.frontend.class :as db-class]
            [logseq.db.frontend.property :as db-property]
            [logseq.db.frontend.schema :as db-schema]
            [logseq.db.sqlite.create-graph :as sqlite-create-graph]
            [logseq.db.sqlite.util :as sqlite-util]))

;; Frontend migrations
;; ===================

(defn- rename-properties-fix
  [db props-to-rename]
  (let [;; update property title/name
        ;; don't update :db/ident since it's addressed by `:rename-db-idents`
        property-tx (map
                     (fn [[old new]]
                       (merge {:db/id (:db/id (d/entity db old))}
                              (when-let [new-title (get-in db-property/built-in-properties [new :title])]
                                {:block/title new-title
                                 :block/name (common-util/page-name-sanity-lc new-title)})))
                     props-to-rename)
        titles-tx (->> (d/datoms db :avet :block/title)
                       (keep (fn [d]
                               (let [title (:v d)]
                                 (if (string? title)
                                   (when-let [props (seq (filter (fn [[old _new]] (string/includes? (:v d) (str old))) props-to-rename))]
                                     (let [title' (reduce (fn [title [old new]]
                                                            (string/replace title (str old) (str new))) title props)]
                                       [:db/add (:e d) :block/title title']))
                                   [:db/retract (:e d) :block/title])))))
        sorting-tx (->> (d/datoms db :avet :logseq.property.table/sorting)
                        (keep (fn [d]
                                (when (coll? (:v d))
                                  (when-let [props (seq (filter (fn [[old _new]]
                                                                  (some (fn [item] (= old (:id item))) (:v d))) props-to-rename))]
                                    (let [value (reduce
                                                 (fn [sorting [old new]]
                                                   (mapv
                                                    (fn [item]
                                                      (if (= old (:id item))
                                                        (assoc item :id new)
                                                        item))
                                                    sorting))
                                                 (:v d)
                                                 props)]
                                      [:db/add (:e d) :logseq.property.table/sorting value]))))))
        sized-columns-tx (->> (d/datoms db :avet :logseq.property.table/sized-columns)
                              (keep (fn [d]
                                      (when (map? (:v d))
                                        (when-let [props (seq (filter (fn [[old _new]] (get (:v d) old)) props-to-rename))]
                                          (let [value (reduce
                                                       (fn [sizes [old new]]
                                                         (if-let [size (get sizes old)]
                                                           (-> sizes
                                                               (dissoc old)
                                                               (assoc new size))
                                                           sizes))
                                                       (:v d)
                                                       props)]
                                            [:db/add (:e d) :logseq.property.table/sized-columns value]))))))
        hidden-columns-tx (mapcat
                           (fn [[old new]]
                             (->> (d/datoms db :avet :logseq.property.table/hidden-columns old)
                                  (mapcat (fn [d]
                                            [[:db/retract (:e d) :logseq.property.table/hidden-columns old]
                                             [:db/add (:e d) :logseq.property.table/hidden-columns new]]))))
                           props-to-rename)
        ordered-columns-tx (->> (d/datoms db :avet :logseq.property.table/ordered-columns)
                                (keep (fn [d]
                                        (when (coll? (:v d))
                                          (when-let [props (seq (filter (fn [[old _new]] ((set (:v d)) old)) props-to-rename))]
                                            (let [value (reduce
                                                         (fn [col [old new]]
                                                           (mapv (fn [v] (if (= old v) new v)) col))
                                                         (:v d)
                                                         props)]
                                              [:db/add (:e d) :logseq.property.table/ordered-columns value]))))))
        filters-tx (->> (d/datoms db :avet :logseq.property.table/filters)
                        (keep (fn [d]
                                (let [filters (:filters (:v d))]
                                  (when (coll? filters)
                                    (when-let [props (seq (filter (fn [[old _new]]
                                                                    (some (fn [item] (and (vector? item)
                                                                                          (= old (first item)))) filters)) props-to-rename))]
                                      (let [value (update (:v d) :filters
                                                          (fn [col]
                                                            (reduce
                                                             (fn [col [old new]]
                                                               (mapv (fn [item]
                                                                       (if (and (vector? item) (= old (first item)))
                                                                         (vec (cons new (rest item)))
                                                                         item))
                                                                     col))
                                                             col
                                                             props)))]
                                        [:db/add (:e d) :logseq.property.table/filters value])))))))]
    (concat property-tx
            titles-tx
            sorting-tx
            sized-columns-tx
            hidden-columns-tx
            ordered-columns-tx
            filters-tx)))

(defn- rename-properties
  [props-to-rename {:keys [fix]}]
  {:rename-db-idents (fn [_db]
                       (mapv
                        (fn [[old-ident new-ident]]
                          {:db-ident-or-block-uuid old-ident
                           :new-db-ident new-ident})
                        props-to-rename))
   :fix (fn [db]
          (let [common-fix (rename-properties-fix db
                                                  {:logseq.property.asset/external-src
                                                   :logseq.property.asset/external-url})
                additional-fix (when (fn? fix)
                                 (fix db))]
            (concat common-fix additional-fix)))})

(defn- add-quick-add-page
  [_db]
  (let [page (-> (-> (sqlite-util/build-new-page common-config/quick-add-page-name)
                     sqlite-create-graph/mark-block-as-built-in))]
    [page]))

(defn- add-missing-page-name
  [db]
  (let [pages (d/datoms db :avet :block/name "")]
    (keep
     (fn [d]
       (let [page (d/entity db (:e d))]
         (when-not (string/blank? (:block/title page))
           {:db/id (:db/id page)
            :block/name (common-util/page-name-sanity-lc (:block/title page))})))
     pages)))

(defn remove-block-path-refs
  [db]
  (when (d/entity db :block/path-refs)
    (let [remove-datoms (->> (d/datoms db :avet :block/path-refs)
                             (map :e)
                             (distinct)
                             (mapv (fn [id]
                                     [:db/retract id :block/path-refs])))]
      (conj remove-datoms [:db/retractEntity :block/path-refs]))))

(defn- remove-position-property-from-url-properties
  [db]
  (->> (d/datoms db :avet :logseq.property/type :url)
       (keep (fn [d]
               (let [e (d/entity db (:e d))]
                 (when (:logseq.property/ui-position e)
                   [:db/retract (:e d) :logseq.property/ui-position]))))))

(defn- deprecated-ensure-graph-uuid
  [_db])

(def schema-version->updates
  "A vec of tuples defining datascript migrations. Each tuple consists of the
   schema version integer and a migration map. A migration map can have keys of :properties, :classes
   :rename-db-idents and :fix."
  [["65.7" {:fix add-quick-add-page}]
   ["65.8" {:fix add-missing-page-name}]
   ["65.9" {:properties [:logseq.property.embedding/hnsw-label-updated-at]}]
   ["65.10" {:properties [:block/journal-day :logseq.property.view/sort-groups-by-property :logseq.property.view/sort-groups-desc?]}]
   ["65.11" {:fix remove-block-path-refs}]
   ["65.12" {:fix remove-position-property-from-url-properties}]
   ["65.13" {:properties [:logseq.property.asset/width
                          :logseq.property.asset/height]}]
   ["65.14" {:properties [:logseq.property.asset/external-src]}]
   ["65.15" (rename-properties {:logseq.property.asset/external-src
                                :logseq.property.asset/external-url}
                               {})]
   ["65.16" {:properties [:logseq.property.asset/external-file-name]}]
   ["65.17" {:properties [:logseq.property.publish/published-url]}]
   ["65.18" {:fix deprecated-ensure-graph-uuid}]
   ["65.19" {:properties [:logseq.property/choice-classes :logseq.property/choice-exclusions]}]])

(let [[major minor] (last (sort (map (comp (juxt :major :minor) db-schema/parse-schema-version first)
                                     schema-version->updates)))]
  (when major
    (let [max-schema-version {:major major :minor minor}
          compare-result (db-schema/compare-schema-version db-schema/version max-schema-version)]
      (assert (>= 0 compare-result) [db-schema/version max-schema-version])
      (when (neg? compare-result)
        (js/console.warn (str "Current db schema-version is " db-schema/version ", max available schema-version is " max-schema-version))))))

(defn ensure-built-in-data-exists!
  "Return tx-data"
  [conn]
  (let [*uuids (atom {})
        initial-data (sqlite-create-graph/build-db-initial-data "")
        data (->> initial-data
                  (keep (fn [data]
                          (cond
                            ;; Already created db-idents like :logseq.kv/graph-initial-schema-version should not be overwritten
                            (= "logseq.kv" (some-> (:db/ident data) namespace))
                            nil

                            (= (:block/title data) "Contents")
                            nil

                            (:file/path data)
                            (if-let [block (d/entity @conn [:file/path (:file/path data)])]
                              (let [existing-data (assoc (into {} block) :db/id (:db/id block))]
                                (merge data existing-data))
                              data)

                            (= [:block/uuid :logseq.property/built-in?] (keys data))
                            data

                            (:block/uuid data)
                            (if-let [block (d/entity @conn [:block/uuid (:block/uuid data)])]
                              (do
                                (swap! *uuids assoc (:block/uuid data) (:block/uuid block))
                                (let [existing-data (assoc (into {} block) :db/id (:db/id block))]
                                  (reduce
                                   (fn [data [k existing-value]]
                                     (update data k
                                             (fn [v]
                                               (cond
                                                 (= k :logseq.property/built-in?)
                                                 true
                                                 (= k :logseq.property/type)
                                                 v
                                                 (coll? v)
                                                 v

                                                 :else
                                                 (let [existing-value (if (and (coll? existing-value) (not (map? existing-value)))
                                                                        (remove nil? existing-value)
                                                                        existing-value)]
                                                   (cond
                                                     (contains? #{:block/title :block/name} k)
                                                     v
                                                     (some? existing-value)
                                                     existing-value
                                                     :else
                                                     v))))))
                                   data
                                   existing-data)))
                              data)

                            :else
                            data)))
                  common-util/fast-remove-nils)
        ;; using existing page's uuid
        data' (->>
               (walk/prewalk
                (fn [f]
                  (cond
                    (and (de/entity? f) (:block/uuid f))
                    (or (:db/ident f) [:block/uuid (:block/uuid f)])
                    (and (vector? f) (= :block/uuid (first f)) (@*uuids (second f)))
                    [:block/uuid (@*uuids (second f))]
                    :else
                    f))
                data)
               (map (fn [m] (dissoc m :db/id))))

        r (d/transact! conn data' {:fix-db? true
                                   :db-migrate? true})]
    (assoc r :migrate-updates
           ;; fake it as a normal :fix type migration
           {:fix (constantly :ensure-built-in-data-exists!)})))

(defn- upgrade-version!
  "Return tx-data"
  [conn version {:keys [properties classes rename-db-idents fix] :as migrate-updates}]
  (let [version (db-schema/parse-schema-version version)
        db @conn
        new-properties (->> (select-keys db-property/built-in-properties properties)
                            ;; property already exists, this should never happen
                            (remove (fn [[k _]]
                                      (when (d/entity db k)
                                        (assert (str "DB migration: property already exists " k)))))
                            (into {})
                            sqlite-create-graph/build-properties
                            (map (fn [b] (assoc b :logseq.property/built-in? true))))
        classes' (->> (concat [:logseq.class/Property :logseq.class/Tag :logseq.class/Page :logseq.class/Journal :logseq.class/Whiteboard] classes)
                      distinct)
        new-classes (->> (select-keys db-class/built-in-classes classes')
                         ;; class already exists, this should never happen
                         (remove (fn [[k _]] (d/entity db k)))
                         (into {})
                         (#(sqlite-create-graph/build-initial-classes* % (zipmap properties properties)))
                         (map (fn [b] (assoc b :logseq.property/built-in? true))))
        new-class-idents (keep (fn [class]
                                 (when-let [db-ident (:db/ident class)]
                                   {:db/ident db-ident})) new-classes)
        [rename-db-idents-tx-data rename-db-idents-coll]
        (when rename-db-idents
          (rename-db-ident/rename-db-idents-migration-tx-data db rename-db-idents))
        fixes (when (fn? fix)
                (fix db))
        tx-data (concat new-class-idents new-properties new-classes rename-db-idents-tx-data fixes)
        tx-data' (concat
                  [(sqlite-util/kv :logseq.kv/schema-version version)]
                  tx-data)
        r (ldb/transact! conn tx-data' {:db-migrate? true
                                        :skip-validate-db? true})
        migrate-updates (cond-> migrate-updates
                          rename-db-idents (assoc :rename-db-idents rename-db-idents-coll))]
    (println "DB schema migrated to" version)
    (assoc r :migrate-updates migrate-updates)))

(defn migrate
  "Migrate 'frontend' datascript schema and data. To add a new migration,
  add an entry to schema-version->updates and bump db-schema/version"
  [conn & {:keys [target-version] :or {target-version db-schema/version}}]
  (let [db @conn
        version-in-db (db-schema/parse-schema-version (or (:kv/value (d/entity db :logseq.kv/schema-version)) 0))
        compare-result (db-schema/compare-schema-version target-version version-in-db)]
    (cond
      (zero? compare-result)
      nil

      (neg? compare-result) ; outdated client, db version could be synced from server
      (worker-util/post-message :notification ["Your app is using an outdated version that is incompatible with your current graph. Please update your app before editing this graph." :error false])

      (pos? compare-result)
      (try
        (let [updates (keep (fn [[v updates]]
                              (let [v* (db-schema/parse-schema-version v)]
                                (when (and (neg? (db-schema/compare-schema-version version-in-db v*))
                                           (not (pos? (db-schema/compare-schema-version v* target-version))))
                                  [v updates])))
                            schema-version->updates)
              result-ks [:tx-data :db-before :db-after :migrate-updates]
              *upgrade-result-coll (atom [])]
          (println "DB schema migrated from" version-in-db)
          (doseq [[v m] updates]
            (let [r (upgrade-version! conn v m)]
              (swap! *upgrade-result-coll conj (select-keys r result-ks))))
          (swap! *upgrade-result-coll conj
                 (select-keys (ensure-built-in-data-exists! conn) result-ks))
          {:from-version version-in-db
           :to-version target-version
           :upgrade-result-coll @*upgrade-result-coll})
        (catch :default e
          (prn :error (str "DB migration failed to migrate to " target-version " from " version-in-db ":"))
          (js/console.error e)
          (throw e))))))
