(ns ^:node-only logseq.db.sqlite.db
  "Sqlite fns for db graphs"
  (:require ["better-sqlite3" :as sqlite3]
            [logseq.db.sqlite.common-db :as sqlite-common-db]
            ;; FIXME: datascript.core has to come before datascript.storage or else nbb fails
            #_:clj-kondo/ignore
            [datascript.core :as d]
            [datascript.storage :refer [IStorage]]
            [goog.object :as gobj]
            [clojure.edn :as edn]
            [logseq.db.frontend.schema :as db-schema]
            [logseq.db.sqlite.util :as sqlite-util]))

;; Reference same sqlite default class in cljs + nbb without needing .cljc
(def sqlite (if (find-ns 'nbb.core) (aget sqlite3 "default") sqlite3))

;; sqlite databases
(defonce databases (atom nil))
;; datascript conns
(defonce conns (atom nil))

(defn close!
  []
  (when @databases
    (doseq [[_ database] @databases]
      (.close database))
    (reset! databases nil)))

(def sanitize-db-name sqlite-common-db/sanitize-db-name)

(def get-db-full-path sqlite-common-db/get-db-full-path)

(defn get-conn
  [repo]
  (get @conns (sanitize-db-name repo)))

(defn query
  [db sql]
  (let [stmt (.prepare db sql)]
    (.all ^object stmt)))

(defn upsert-addr-content!
  "Upsert addr+data-seq"
  [db data delete-addrs]
  (let [insert (.prepare db "INSERT INTO kvs (addr, content) values (@addr, @content) on conflict(addr) do update set content = @content")
        delete (.prepare db "DELETE from kvs where addr = ?")
        insert-many (.transaction ^object db
                                  (fn [data]
                                    (doseq [item data]
                                      (.run ^object insert item))
                                    (doseq [addr delete-addrs]
                                      (when addr
                                        (.run ^object delete addr)))))]
    (insert-many data)))

(defn restore-data-from-addr
  [db addr]
  (when-let [content (-> (query db (str "select content from kvs where addr = " addr))
                         first
                         (gobj/get "content"))]
    (try
        (let [data (sqlite-util/transit-read content)]
         (if-let [addresses (:addresses data)]
           (assoc data :addresses (clj->js addresses))
           data))
        (catch :default _e              ; TODO: remove this once db goes to test
          (edn/read-string content)))))

(defn new-sqlite-storage
  "Creates a datascript storage for sqlite. Should be functionally equivalent to db-worker/new-sqlite-storage"
  [db]
  (reify IStorage
    (-store [_ addr+data-seq delete-addrs]
      (let [data (->>
                  (map
                   (fn [[addr data]]
                     #js {:addr addr
                          :content (sqlite-util/transit-write data)})
                   addr+data-seq)
                  (to-array))]
        (upsert-addr-content! db data delete-addrs)))
    (-restore [_ addr]
      (restore-data-from-addr db addr))))

(defn open-db!
  "For a given database name, opens a sqlite db connection for it, creates
  needed sqlite tables if not created and returns a datascript connection that's
  connected to the sqlite db"
  [graphs-dir db-name]
  (let [[db-sanitized-name db-full-path] (get-db-full-path graphs-dir db-name)
        db (new sqlite db-full-path nil)
        ;; For both desktop and CLI, only file graphs have db-name that indicate their db type
        schema (if (sqlite-util/local-file-based-graph? db-name)
                 db-schema/schema
                 db-schema/schema-for-db-based-graph)]
    (sqlite-common-db/create-kvs-table! db)
    (swap! databases assoc db-sanitized-name db)
    (let [storage (new-sqlite-storage db)
          conn (sqlite-common-db/get-storage-conn storage schema)]
      (swap! conns assoc db-sanitized-name conn)
      conn)))

(defn transact!
  [repo tx-data tx-meta]
  (if-let [conn (get-conn repo)]
    (d/transact! conn tx-data tx-meta)
    (throw (ex-info (str "Failed to transact! No db connection found for " repo) {}))))
