(ns frontend.handler.plugin
  "System-component-like ns that provides all high level plugin functionality"
  (:require [promesa.core :as p]
            [rum.core :as rum]
            [frontend.util :as util]
            [clojure.walk :as walk]
            [logseq.graph-parser.mldoc :as gp-mldoc]
            [frontend.handler.notification :as notification]
            [frontend.handler.common.plugin :as plugin-common-handler]
            [frontend.modules.shortcut.utils :as shortcut-utils]
            [frontend.storage :as storage]
            [camel-snake-kebab.core :as csk]
            [frontend.state :as state]
            [medley.core :as medley]
            [frontend.fs :as fs]
            [electron.ipc :as ipc]
            [cljs-bean.core :as bean]
            [clojure.string :as string]
            [lambdaisland.glogi :as log]
            [frontend.components.svg :as svg]
            [logseq.shui.ui :as shui]
            [frontend.context.i18n :refer [t]]
            [frontend.config :as config]
            [frontend.format :as format]))

(defn- normalize-keyword-for-json
  [input]
  (when input
    (let [f (fn [[k v]] (if (keyword? k) [(csk/->camelCase (name k)) v] [k v]))]
      (walk/postwalk
        (fn [x]
          (cond
            (map? x) (into {} (map f x))
            (uuid? x) (str x)
            :else x)) input))))

(defn invoke-exported-api
  [type & args]
  (try
    (apply js-invoke (aget js/window.logseq "api") type args)
    (catch :default e (js/console.error e))))

(defn markdown-to-html
  [s]
  (try
    (if (string? s)
      (js/window.marked.parse s) s)
    (catch js/Error e
      (js/console.error e) s)))

;; state handlers
(defonce central-endpoint "https://raw.githubusercontent.com/logseq/marketplace/master/")
(defonce plugins-url (str central-endpoint "plugins.json"))
(defonce stats-url (str central-endpoint "stats.json"))
(declare select-a-plugin-theme)

(defn load-plugin-preferences
  []
  (-> (invoke-exported-api "load_user_preferences")
    (p/then #(bean/->clj %))
    (p/then #(state/set-state! :plugin/preferences %))
    (p/catch
     #(js/console.error %))))

(defn save-plugin-preferences!
  ([input] (save-plugin-preferences! input true))
  ([input reload-state?]
   (when-let [^js input (and (map? input) (bean/->js input))]
     (p/then
       (js/LSPluginCore.saveUserPreferences input)
       #(when reload-state?
          (load-plugin-preferences))))))

(defn gh-repo-url [repo]
  (str "https://github.com/" repo))

(defn pkg-asset [id asset]
  (if (and asset (string/starts-with? asset "http"))
    asset (when-let [asset (and asset (string/replace asset #"^[./]+" ""))]
            (str central-endpoint "packages/" id "/" asset))))

(defn load-marketplace-plugins
  [refresh?]
  (if (or refresh? (nil? (:plugin/marketplace-pkgs @state/state)))
    (p/create
      (fn [resolve reject]
        (let [on-ok (fn [res]
                      (if-let [res (and res (bean/->clj res))]
                        (let [pkgs (:packages res)]
                          (state/set-state! :plugin/marketplace-pkgs pkgs)
                          (resolve pkgs))
                        (reject nil)))]
          (if (state/http-proxy-enabled-or-val?)
            (-> (ipc/ipc :httpFetchJSON plugins-url)
              (p/then on-ok)
              (p/catch reject))
            (util/fetch plugins-url on-ok reject)))))
    (p/resolved (:plugin/marketplace-pkgs @state/state))))

(defn load-marketplace-stats
  [refresh?]
  (if (or refresh? (nil? (:plugin/marketplace-stats @state/state)))
    (p/create
      (fn [resolve reject]
        (let [on-ok (fn [^js res]
                      (if-let [res (and res (bean/->clj res))]
                        (do
                          (state/set-state!
                            :plugin/marketplace-stats
                            (into {} (map (fn [[k stat]]
                                            [k (assoc stat
                                                 :total_downloads
                                                 (reduce (fn [a b] (+ a (get b 2))) 0 (:releases stat)))])
                                       res)))
                          (resolve nil))
                        (reject nil)))]
          (if (state/http-proxy-enabled-or-val?)
            (-> (ipc/ipc :httpFetchJSON stats-url)
              (p/then on-ok)
              (p/catch reject))
            (util/fetch stats-url on-ok reject)))))
    (p/resolved nil)))

(defn check-or-update-marketplace-plugin!
  [{:keys [id] :as pkg} error-handler]
  (when-not (and (:plugin/installing @state/state)
              (not (plugin-common-handler/installed? id)))
    (state/set-state! :plugin/installing pkg)

    (-> (load-marketplace-plugins false)
      (p/then (fn [mfts]
                (let [mft (some #(when (= (:id %) id) %) mfts)]
                  ;;TODO: (throw (js/Error. [:not-found-in-marketplace id]))
                  (ipc/ipc :updateMarketPlugin (merge (dissoc pkg :logger) mft)))
                true))
      (p/catch (fn [^js e]
                 (state/reset-all-updates-state)
                 (error-handler e)
                 (state/set-state! :plugin/installing nil)
                 (js/console.error e))))))

(defn get-plugin-inst
  [pid]
  (try
    (js/LSPluginCore.ensurePlugin (name pid))
    (catch :default _e
      nil)))

(defn call-plugin-user-model!
  [pid key args]
  (when-let [^js pl (get-plugin-inst pid)]
    (let [^js caller (.-caller pl)]
      (.apply (.-callUserModelAsync caller) caller (bean/->js (list* (name key) args))))))

(defn call-plugin-user-command!
  [pid key args]
  (when-let [commands (and key (seq (get (:plugin/simple-commands @state/state) (keyword pid))))]
    (when-let [matched (medley/find-first #(= (:key (second %)) key) commands)]
      (let [[_ cmd action pid] matched]
        (state/pub-event!
          [:exec-plugin-cmd {:type type :key key :pid pid :cmd (assoc cmd :args args) :action action}])))))

(defn open-updates-downloading
  []
  (when (and (not (:plugin/updates-downloading? @state/state))
          (seq (state/all-available-coming-updates)))
    (->> (:plugin/updates-coming @state/state)
      (map #(if (state/coming-update-new-version? (second %1))
              (update % 1 dissoc :error-code) %1))
      (into {})
      (state/set-state! :plugin/updates-coming))
    (state/set-state! :plugin/updates-downloading? true)))

(defn close-updates-downloading
  []
  (when (:plugin/updates-downloading? @state/state)
    (state/set-state! :plugin/updates-downloading? false)))

(defn has-setting-schema?
  [id]
  (when-let [^js pl (and id (get-plugin-inst (name id)))]
    (boolean (.-settingsSchema pl))))

(defn get-enabled-plugins-if-setting-schema
  []
  (when-let [plugins (seq (state/get-enabled?-installed-plugins false true true true))]
    (filter #(has-setting-schema? (:id %)) plugins)))

(defn setup-install-listener!
  []
  (let [channel (name :lsp-updates)
        listener (fn [_ ^js e]
                   (when-let [{:keys [status payload only-check]} (bean/->clj e)]
                     (case (keyword status)

                       :completed
                       (let [{:keys [id dst name title theme]} payload
                             name (or title name "Untitled")]
                         (if only-check
                           (state/consume-updates-from-coming-plugin! payload false)
                           (if (plugin-common-handler/installed? id)
                             (when-let [^js pl (get-plugin-inst id)] ;; update
                               (p/then
                                 (.reload pl)
                                 #(do
                                    ;;(if theme (select-a-plugin-theme id))
                                    (notification/show!
                                      (t :plugin/update-plugin name (.-version (.-options pl))) :success)
                                    (state/consume-updates-from-coming-plugin! payload true))))

                             (do                            ;; register new
                               (p/then
                                 (js/LSPluginCore.register (bean/->js {:key id :url dst}))
                                 (fn [] (when theme (js/setTimeout #(select-a-plugin-theme id) 300))))
                               (notification/show!
                                 (t :plugin/installed-plugin name) :success)))))

                       :error
                       (let [error-code (keyword (string/replace (:error-code payload) #"^[\s\:\[]+" ""))
                             fake-error? (contains? #{:no-new-version} error-code)
                             [msg type] (case error-code

                                          :no-new-version
                                          [(t :plugin/up-to-date ":)") :success]

                                          [error-code :error])
                             pending? (seq (:plugin/updates-pending @state/state))]

                         (if (and only-check pending?)
                           (state/consume-updates-from-coming-plugin! payload false)

                           (do
                             ;; consume failed download updates
                             (when (and (not only-check) (not pending?))
                               (state/consume-updates-from-coming-plugin! payload true))

                             ;; notify human tips
                             (notification/show!
                               (str
                                 (if (= :error type) "[Error]" "")
                                 (str "<" (:id payload) "> ")
                                 msg) type)))

                         (when-not fake-error?
                           (js/console.error "Update Error:" (:error-code payload))))

                       :default))

                   ;; reset
                   (js/setTimeout #(state/set-state! :plugin/installing nil) 512)
                   true)]

    (js/window.apis.addListener channel listener)))

(defn- normalize-plugin-metadata
  [metadata]
  (cond-> metadata
    (not (string? (:author metadata)))
    (assoc :author (or (get-in metadata [:author :name]) ""))))

(defn register-plugin
  [plugin-metadata]
  (when-let [pid (keyword (:id plugin-metadata))]
    (some->> plugin-metadata
      (normalize-plugin-metadata)
      (swap! state/state update-in [:plugin/installed-plugins] assoc pid))))

(defn host-mounted!
  []
  (and config/lsp-enabled? (js/LSPluginCore.hostMounted)))

(defn register-plugin-slash-command
  [pid [cmd actions]]
  (when-let [pid (keyword pid)]
    (when (contains? (:plugin/installed-plugins @state/state) pid)
      (swap! state/state update-in [:plugin/installed-slash-commands pid]
        (fnil merge {}) (hash-map cmd (mapv #(conj % {:pid pid}) actions)))
      (state/pub-event! [:rebuild-slash-commands-list])
      true)))

(defn unregister-plugin-slash-command
  [pid]
  (swap! state/state medley/dissoc-in [:plugin/installed-slash-commands (keyword pid)])
  (state/pub-event! [:rebuild-slash-commands-list]))

(def keybinding-mode-handler-map
  {:global :shortcut.handler/editor-global
   :non-editing :shortcut.handler/global-non-editing-only
   :editing :shortcut.handler/block-editing-only})

(defn simple-cmd->palette-cmd
  [pid {:keys [key label type desc keybinding] :as cmd} action]
  (let [palette-cmd {:id (keyword (str "plugin." pid "/" key))
                     :desc (or desc label)
                     :shortcut (when-let [shortcut (:binding keybinding)]
                                 (if util/mac?
                                   (or (:mac keybinding) shortcut)
                                   shortcut))
                     :handler-id (let [mode (or (:mode keybinding) :global)]
                                   (get keybinding-mode-handler-map (keyword mode)))
                     :action (fn []
                               (state/pub-event!
                                 [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}]))}]
    palette-cmd))

(defn simple-cmd-keybinding->shortcut-args
  [pid key keybinding]
  (let [id (keyword (str "plugin." pid "/" key))
        binding (:binding keybinding)
        binding (some->> (if (string? binding) [binding] (vec binding))
                  (remove string/blank?)
                  (map shortcut-utils/undecorate-binding))
        binding (if util/mac?
                  (or (:mac keybinding) binding) binding)
        mode (or (:mode keybinding) :global)
        mode (get keybinding-mode-handler-map (keyword mode))]
    [mode id {:binding binding}]))

(defn register-plugin-simple-command
  ;; action => [:action-key :event-key]
  [pid {:keys [type] :as cmd} action]
  (when-let [pid (keyword pid)]
    (when (contains? (:plugin/installed-plugins @state/state) pid)
      (swap! state/state update-in [:plugin/simple-commands pid]
        (fnil conj []) [type cmd action pid])
      true)))

(defn unregister-plugin-simple-command
  [pid]
  (swap! state/state medley/dissoc-in [:plugin/simple-commands (keyword pid)]))

(defn register-plugin-ui-item
  [pid {:keys [key type] :as opts}]
  (when-let [pid (keyword pid)]
    (when (contains? (:plugin/installed-plugins @state/state) pid)
      (let [items (or (get-in @state/state [:plugin/installed-ui-items pid]) [])
            items (filter #(not= key (:key (second %))) items)]
        (swap! state/state assoc-in [:plugin/installed-ui-items pid]
          (conj items [type opts pid])))
      true)))

(defn unregister-plugin-ui-items
  [pid]
  (swap! state/state assoc-in [:plugin/installed-ui-items (keyword pid)] []))

(defn register-plugin-resources
  [pid type {:keys [key] :as opts}]
  (when-let [pid (keyword pid)]
    (when-let [type (and key (keyword type))]
      (let [path [:plugin/installed-resources pid type]]
        ;; TODO: conditions
        ;; (when (contains? #{:error nil} (get-in @state/state (conj path key))))
        (swap! state/state update-in path
          (fnil assoc {}) key (merge opts {:pid pid}))
        true))))

(defn unregister-plugin-resources
  [pid]
  (when-let [pid (keyword pid)]
    (swap! state/state medley/dissoc-in [:plugin/installed-resources pid])
    true))

(defn register-plugin-search-service
  [pid name opts]
  (when-let [pid (and name (keyword pid))]
    (state/install-plugin-service pid :search name opts)))

(defn unregister-plugin-search-services
  [pid]
  (when-let [pid (keyword pid)]
    (state/uninstall-plugin-service pid :search)))

(defn unregister-plugin-themes
  ([pid] (unregister-plugin-themes pid true))
  ([pid effect]
   (js/LSPluginCore.unregisterTheme (name pid) effect)))

(defn get-installed-hooks
  []
  (:plugin/installed-hooks @state/state))

(defn plugin-hook-installed?
  [pid hook]
  (when-let [hooks (and pid (get-installed-hooks))]
    (contains? (get hooks hook) (keyword pid))))

(defn db-block-hook-installed?
  [uuid]
  (when-let [hook (and uuid (str "hook:db:block_" (string/replace (str uuid) "-" "_")))]
    (boolean (seq (get (get-installed-hooks) hook)))))

(defn- create-local-renderer-register
  [type *providers]
  (fn [pid key {subs' :subs :keys [render] :as opts}]
    (when-let [key (and key (keyword key))]
      (register-plugin-resources pid type
        (merge opts {:key key :subs subs' :render render}))
      (swap! *providers conj pid)
      #(swap! *providers disj pid))))

(defn- create-local-renderer-getter
  ([type *providers] (create-local-renderer-getter type *providers false))
  ([type *providers many?]
   (fn [key]
     (when-let [key (and (seq @*providers) key (keyword key))]
       (when-let [rs (->> @*providers
                       (map (fn [pid] (state/get-plugin-resource pid type key)))
                       (remove nil?)
                       (flatten)
                       (seq))]
         (if many? rs (first rs)))))))

(defonce *fenced-code-providers (atom #{}))
(def register-fenced-code-renderer
  ;; [pid key payload]
  (create-local-renderer-register
    :fenced-code-renderers *fenced-code-providers))
(def hook-fenced-code-by-lang
  ;; [key]
  (create-local-renderer-getter
    :fenced-code-renderers *fenced-code-providers))

(def *extensions-enhancer-providers (atom #{}))
(def register-extensions-enhancer
  ;; a plugin can only register one enhancer for a type
  (create-local-renderer-register
    :extensions-enhancers *extensions-enhancer-providers))
(def hook-extensions-enhancers-by-key
  ;; multiple plug-ins can obtain more than one enhancer
  (create-local-renderer-getter
    :extensions-enhancers *extensions-enhancer-providers true))

(def *route-renderer-providers (atom #{}))
(def register-route-renderer
  (create-local-renderer-register
    :route-renderers *route-renderer-providers))
(def get-route-renderers
  (create-local-renderer-getter
    :route-renderers *route-renderer-providers true))

(defonce *daemon-renderer-providers (atom #{}))
(def register-daemon-renderer
  ;; [pid key payload]
  (create-local-renderer-register
    :daemon-renderers *daemon-renderer-providers))
(def get-daemon-renderers
  ;; [key]
  (create-local-renderer-getter
    :daemon-renderers *daemon-renderer-providers true))

(defn select-a-plugin-theme
  [pid]
  (when-let [themes (get (group-by :pid (:plugin/installed-themes @state/state)) pid)]
    (when-let [theme (first themes)]
      (js/LSPluginCore.selectTheme (bean/->js theme)))))

(defn update-plugin-settings-state
  [id settings]
  (state/set-state! [:plugin/installed-plugins id :settings]
    ;; TODO: force settings related ui reactive
    ;; Sometimes toggle to `disable` not working
    ;; But related-option data updated?
    (assoc settings :disabled (boolean (:disabled settings)))))

(defn open-settings-file-in-default-app!
  [id-or-plugin]
  (when-let [plugin (if (coll? id-or-plugin)
                      id-or-plugin (state/get-plugin-by-id id-or-plugin))]
    (when-let [file-path (:usf plugin)]
      (js/apis.openPath file-path))))

(defn open-plugin-settings!
  ([id] (open-plugin-settings! id false))
  ([id nav?]
   (when-let [plugin (and id (state/get-plugin-by-id id))]
     (if (has-setting-schema? id)
       (state/pub-event! [:go/plugins-settings id nav? (or (:name plugin) (:title plugin))])
       (open-settings-file-in-default-app! plugin)))))

(defn open-report-modal!
  ([] (open-report-modal! nil nil))
  ([pid name]
   (shui/dialog-open!
     [:div.p-1
      (when pid
        [:h1.opacity-90.font-bold.pb-1.flex.item-center.gap-1
         [:span.text-red-rx-10.flex.items-center (shui/tabler-icon "alert-triangle-filled" {:size 20})]
         [:span name "  " [:code "#" (str pid)]]])
      [:p
       "If any plugin is unavailable or you think it contains malicious code,
        please email " [:a.hover:underline {:href (str "mailto://support@logseq.com?subject=Report plugin from Logseq Marketplace"
                                                           (when pid (str " (#" pid ")")))} "support@logseq.com"]
       " . Mention the name of the plugin and the URL of its GitHub repository.
       The Logseq team usually responds within a business day."]
      ])))

(defn parse-user-md-content
  [content {:keys [url]}]
  (try
    (when-not (string/blank? content)
      (let [content (if-not (string/blank? url)
                      (string/replace
                        content #"!\[[^\]]*\]\((.*?)\s*(\"(?:.*[^\"])\")?\s*\)"
                        (fn [[matched link]]
                          (if (and link (not (string/starts-with? link "http")))
                            (string/replace matched link (util/node-path.join url link))
                            matched)))
                      content)]
        (format/to-html content :markdown (gp-mldoc/default-config :markdown))))
    (catch :default e
      (log/error :parse-user-md-exception e)
      content)))

(defn open-readme!
  [url item display]
  (let [repo (:repo item)]
    (if (nil? repo)
      ;; local
      (-> (p/let [content (invoke-exported-api "load_plugin_readme" url)
                  content (parse-user-md-content content item)]
            (and (string/blank? (string/trim content)) (throw nil))
            (state/set-state! :plugin/active-readme [content item])
            (shui/dialog-open! (fn [_] (display)) {:label "plugin-readme"}))
        (p/catch #(do (js/console.warn %)
                      (notification/show! "No README content." :warning))))
      ;; market
      (shui/dialog-open! (fn [_] (display repo nil)) {:label "plugin-readme"}))))

(defn load-unpacked-plugin
  []
  (when (util/electron?)
    (p/let [path (ipc/ipc "openDialog")]
      (when-not (:plugin/selected-unpacked-pkg @state/state)
        (state/set-state! :plugin/selected-unpacked-pkg path)))))

(defn reset-unpacked-state
  []
  (state/set-state! :plugin/selected-unpacked-pkg nil))

(defn hook-plugin
  [tag type payload plugin-id]
  (when config/lsp-enabled?
    (try
      (js-invoke js/LSPluginCore
        (str "hook" (string/capitalize (name tag)))
        (name type)
        (if (coll? payload)
          (bean/->js (normalize-keyword-for-json payload))
          payload)
        (if (keyword? plugin-id) (name plugin-id) plugin-id))
      (catch :default e
        (log/error :invoke-hook-exception e)))))

(defn hook-plugin-app
  ([type payload] (hook-plugin-app type payload nil))
  ([type payload plugin-id] (hook-plugin :app type payload plugin-id)))

(defn hook-plugin-editor
  ([type payload] (hook-plugin-editor type payload nil))
  ([type payload plugin-id] (hook-plugin :editor type payload plugin-id)))

(defn hook-plugin-db
  ([type payload] (hook-plugin-db type payload nil))
  ([type payload plugin-id] (hook-plugin :db type payload plugin-id)))

(defn hook-plugin-block-changes
  [{:keys [blocks tx-data tx-meta]}]
  (let [tx-data' (group-by first tx-data)
        blocks' (filter #(when-let [uuid (:block/uuid %)]
                           (db-block-hook-installed? uuid)) blocks)]
    (doseq [b blocks']
      (let [type (str "block:" (:block/uuid b))]
        (hook-plugin-db type {:block b :tx-data (get tx-data' (:db/id b)) :tx-meta tx-meta})))))

(defn hook-plugin-block-slot
  [block payload]
  (when-let [type (and block (str "slot:" (:block/uuid block)))]
    (hook-plugin-editor type (merge payload block) nil)))

(defn get-ls-dotdir-root
  []
  (ipc/ipc "getLogseqDotDirRoot"))

(defn make-fn-to-load-dotdir-json
  [dirname default]
  (fn [key]
    (when-let [key (and key (name key))]
      (p/let [repo ""
              path (get-ls-dotdir-root)
              exist? (fs/file-exists? path dirname)
              _ (when-not exist? (fs/mkdir! (util/node-path.join path dirname)))
              path (util/node-path.join path dirname (str key ".json"))
              _ (fs/create-if-not-exists repo nil path (or default "{}"))
              json (fs/read-file nil path)]
        [path (js/JSON.parse json)]))))

(defn make-fn-to-save-dotdir-json
  [dirname]
  (fn [key content]
    (when-let [key (and key (name key))]
      (p/let [repo ""
              path (get-ls-dotdir-root)
              path (util/node-path.join path dirname (str key ".json"))]
        (fs/write-file! repo nil path content {:skip-compare? true})))))

(defn make-fn-to-unlink-dotdir-json
  [dirname]
  (fn [key]
    (when-let [key (and key (name key))]
      (p/let [repo ""
              path (get-ls-dotdir-root)
              path (util/node-path.join path dirname (str key ".json"))]
        (fs/unlink! repo path nil)))))

(defn show-themes-modal!
  ([] (show-themes-modal! false))
  ([classic?] (state/pub-event! [:modal/show-themes-modal classic?])))

(defn goto-plugins-dashboard!
  []
  (state/pub-event! [:go/plugins]))

(defn goto-plugins-settings!
  []
  (when-let [pl (first (seq (get-enabled-plugins-if-setting-schema)))]
    (state/pub-event! [:go/plugins-settings (:id pl)])))

(defn- get-user-default-plugins
  []
  (p/catch
   (p/let [files ^js (ipc/ipc "getUserDefaultPlugins")
           files (js->clj files)]
     (map #(hash-map :url %) files))
   (fn [e]
     (js/console.error e))))

(defn set-auto-checking!
  [v]
  (let [v (boolean v)]
    (println "Updates: " (if v "start" "finish") " auto-checking...")
    (state/set-state! :plugin/updates-auto-checking? v)))

(defn get-auto-checking?
  []
  (:plugin/updates-auto-checking? @state/state))

(defn get-user-checking?
  []
  (boolean (seq (:plugin/updates-pending @state/state))))

(defn get-updates-downloading?
  []
  (boolean (:plugin/updates-downloading? @state/state)))

(defn cancel-user-checking!
  []
  (when (and (get-user-checking?)
          (not (get-auto-checking?)))
    (state/set-state! :plugin/updates-pending {})))

(defn user-check-enabled-for-updates!
  [theme?]
  (let [user-checking? (get-user-checking?)
        auto-checking? (get-auto-checking?)]
    (when auto-checking?
      (set-auto-checking! false))
    (when (or auto-checking? (not user-checking?))
      ;; TODO: too many requests may be limited by Github api
      (when-let [plugins (seq (take 32 (state/get-enabled?-installed-plugins theme?)))]
        (->> plugins
          (map (fn [v] [(keyword (:id v)) v]))
          (into {})
          (state/set-state! :plugin/updates-pending))
        (state/pub-event! [:plugin/consume-updates])))))

(defn auto-check-enabled-for-updates!
  []
  (when (and (not (get-updates-downloading?))
          (not (get-auto-checking?))
          (not (get-user-checking?)))
    ;; TODO: take some plugins used recently
    (when-let [plugins (seq (take 16 (shuffle (state/get-enabled?-installed-plugins nil))))]
      (->> plugins
        (map (fn [v] [(keyword (:id v)) v]))
        (into {})
        (state/set-state! :plugin/updates-pending))
      (state/pub-event! [:plugin/consume-updates])
      (set-auto-checking! true))))

(defn get-enabled-auto-check-for-updates?
  []
  (not (false? (storage/get :lsp-last-auto-updates))))

(defn set-enabled-auto-check-for-updates
  [v?]
  (storage/set :lsp-last-auto-updates (boolean v?)))

(defn call-plugin
  [^js pl type payload]
  (when pl
    (.call (.-caller pl) (name type) (bean/->js payload))))

(defn request-callback
  [^js pl req-id payload]
  (call-plugin pl :#lsp#request#callback {:requestId req-id :payload payload}))

(defn op-pinned-toolbar-item!
  [key op]
  (let [pinned (state/sub [:plugin/preferences :pinnedToolbarItems])
        pinned (into #{} pinned)]
    (when-let [op-fn (case op
                       :add conj
                       :remove disj)]
      (save-plugin-preferences! {:pinnedToolbarItems (op-fn pinned (name key))}))))

(defn hook-lifecycle-fn!
  [type f & args]
  (when (and type (fn? f))
    (when config/lsp-enabled?
      (hook-plugin-app (str :before-command-invoked type) nil))
    (apply f args)
    (when config/lsp-enabled?
      (hook-plugin-app (str :after-command-invoked type) nil))))

;; components
(rum/defc lsp-indicator < rum/reactive
  []
  (let [text (state/sub :plugin/indicator-text)]
    (when-not (= text "END")
      [:div.flex.align-items.justify-center.h-screen.w-full.preboot-loading
       [:span.flex.items-center.justify-center.w-60.flex-col
        [:small.scale-250.opacity-70.mb-10.animate-pulse (svg/logo)]
        [:small.block.text-sm.relative.opacity-50 {:style {:right "-8px"}} text]]])))

(defn ^:large-vars/cleanup-todo init-plugins!
  [callback]

  (let [el (js/document.createElement "div")]
    (.appendChild js/document.body el)
    (rum/mount
      (lsp-indicator) el))

  (state/set-state! :plugin/indicator-text "LOADING")

  (-> (p/let [root (get-ls-dotdir-root)
              _ (.setupPluginCore js/LSPlugin (bean/->js {:localUserConfigRoot root :dotConfigRoot root}))

              clear-commands! (fn [pid]
                                ;; commands
                                (unregister-plugin-slash-command pid)
                                (invoke-exported-api "unregister_plugin_simple_command" pid)
                                (invoke-exported-api "uninstall_plugin_hook" pid)
                                (unregister-plugin-ui-items pid)
                                (unregister-plugin-resources pid)
                                (unregister-plugin-search-services pid))

              _ (doto js/LSPluginCore
                  (.on "registered"
                    (fn [^js pl]
                      (register-plugin
                        (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))

                  (.on "reloaded"
                    (fn [^js pl]
                      (register-plugin
                        (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))

                  (.on "unregistered" (fn [pid]
                                        (let [pid (keyword pid)]
                                          ;; effects
                                          (unregister-plugin-themes pid)
                                          ;; plugins
                                          (swap! state/state medley/dissoc-in [:plugin/installed-plugins pid])
                                          ;; commands
                                          (clear-commands! pid))))

                  (.on "unlink-plugin" (fn [pid]
                                         (let [pid (keyword pid)]
                                           (ipc/ipc "uninstallMarketPlugin" (name pid)))))

                  (.on "beforereload" (fn [^js pl]
                                        (let [pid (.-id pl)]
                                          (clear-commands! pid)
                                          (unregister-plugin-themes pid false))))

                  (.on "disabled" (fn [pid]
                                    (clear-commands! pid)
                                    (unregister-plugin-themes pid)))

                  (.on "themes-changed" (fn [^js themes]
                                          (swap! state/state assoc :plugin/installed-themes
                                            (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))

                  (.on "theme-selected" (fn [^js theme]
                                          (let [theme (bean/->clj theme)
                                                url (:url theme)
                                                mode (:mode theme)]
                                            (when mode
                                              (state/set-custom-theme! mode theme)
                                              (state/set-theme-mode! mode))
                                            (state/set-state! :plugin/selected-theme url)
                                            (hook-plugin-app :theme-changed theme))))

                  (.on "reset-custom-theme" (fn [^js themes]
                                              (let [themes (bean/->clj themes)
                                                    custom-theme (dissoc themes :mode)
                                                    mode (:mode themes)]
                                                (state/set-custom-theme! {:light (if (nil? (:light custom-theme)) {:mode "light"} (:light custom-theme))
                                                                          :dark (if (nil? (:dark custom-theme)) {:mode "dark"} (:dark custom-theme))})
                                                (state/set-theme-mode! mode))))

                  (.on "settings-changed" (fn [id ^js settings]
                                            (let [id (keyword id)]
                                              (when (and settings
                                                      (contains? (:plugin/installed-plugins @state/state) id))
                                                (update-plugin-settings-state id (bean/->clj settings))))))

                  (.on "ready" (fn [^js perf-table]
                                 (when-let [plugins (and perf-table (.entries perf-table))]
                                   (->> plugins
                                     (keep
                                       (fn [[_k ^js v]]
                                         (when-let [end (and (some-> v (.-o) (.-disabled) (not))
                                                          (.-e v))]
                                           (when (and (number? end)
                                                   ;; valid end time
                                                   (> end 0)
                                                   ;; greater than 6s
                                                   (> (- end (.-s v)) 6000))
                                             v))))
                                     ((fn [perfs]
                                        (doseq [perf perfs]
                                          (state/pub-event! [:plugin/loader-perf-tip (bean/->clj perf)])))))))))

              default-plugins (get-user-default-plugins)

              _ (.register js/LSPluginCore (bean/->js (if (seq default-plugins) default-plugins [])) true)])

    (p/then
      (fn []
        (state/set-state! :plugin/indicator-text "END")
        ;; wait for the plugin register async messages
        (js/setTimeout #(callback) 64)))
    (p/catch
     (fn [^js e]
       (log/error :setup-plugin-system-error e)
       (state/set-state! :plugin/indicator-text (str "Fatal: " e))))))

(defn setup!
  "setup plugin core handler"
  [callback]
  (if (not config/lsp-enabled?)
    (callback)
    (init-plugins! callback)))

(comment
  {:pending (count (:plugin/updates-pending @state/state))
   :auto-checking? (boolean (:plugin/updates-auto-checking? @state/state))
   :coming (count (:plugin/updates-coming @state/state))
   :installing (:plugin/installing @state/state)
   :downloading? (boolean (:plugin/updates-downloading? @state/state))})
