;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL

(ns app.setup
  "Initial data setup of instance."
  (:require
   [app.common.data :as d]
   [app.common.logging :as l]
   [app.common.schema :as sm]
   [app.common.uuid :as uuid]
   [app.config :as cf]
   [app.db :as db]
   [app.loggers.audit :as audit]
   [app.main :as-alias main]
   [app.setup.keys :as keys]
   [app.setup.templates]
   [buddy.core.codecs :as bc]
   [buddy.core.nonce :as bn]
   [cuerdas.core :as str]
   [integrant.core :as ig]))

(defn- generate-random-key
  []
  (-> (bn/random-bytes 64)
      (bc/bytes->b64-str true)))

(defn- get-all-props
  [conn]
  (->> (db/query conn :server-prop {:preload true})
       (filter #(not= "secret-key" (:id %)))
       (map (fn [row]
              [(keyword (:id row))
               (db/decode-transit-pgobject (:content row))]))
       (into {})))

(defn- handle-instance-id
  [instance-id conn]
  (or instance-id
      (let [instance-id (uuid/random)]
        (try
          (db/insert! conn :server-prop
                      {:id "instance-id"
                       :preload true
                       :content (db/tjson instance-id)})
          (catch Throwable cause
            (l/warn :hint "unable to persist instance-id"
                    :instance-id instance-id
                    :cause cause)))
        instance-id)))

(def sql:add-prop
  "INSERT INTO server_prop (id, content, preload)
   VALUES (?, ?, ?)
       ON CONFLICT (id)
       DO UPDATE SET content=?, preload=?")

(defn get-prop
  ([system prop] (get-prop system prop nil))
  ([system prop default]
   (let [prop (d/name prop)]
     (db/run! system (fn [{:keys [::db/conn]}]
                       (or (db/get* conn :server-prop {:id prop})
                           default))))))

(defn set-prop!
  [system prop value]
  (let [value (db/tjson value)
        prop  (d/name prop)]
    (db/run! system (fn [{:keys [::db/conn]}]
                      (db/exec-one! conn [sql:add-prop prop value false value false])))))

(defmethod ig/assert-key ::props
  [_ params]
  (assert (db/pool? (::db/pool params)) "expected valid database pool"))

(defmethod ig/init-key ::props
  [_ {:keys [::key] :as cfg}]
  (audit/submit cfg {:type "trigger"
                     :name "instance-start"
                     :props {:version (:full cf/version)
                             :flags (mapv name cf/flags)
                             :public-uri (str (cf/get :public-uri))}})

  (db/tx-run! cfg (fn [{:keys [::db/conn]}]
                    (db/xact-lock! conn 0)
                    (when-not key
                      (l/wrn :hint (str "using autogenerated secret-key, it will change "
                                        "on each restart and will invalidate "
                                        "all sessions on each restart, it is highly "
                                        "recommended setting up the "
                                        "PENPOT_SECRET_KEY environment variable")))
                    (let [secret (or key (generate-random-key))]
                      (-> (get-all-props conn)
                          (assoc :secret-key secret)
                          (assoc :tokens-key (keys/derive secret :salt "tokens"))
                          (update :instance-id handle-instance-id conn))))))

(defmethod ig/init-key ::shared-keys
  [_ {:keys [::props] :as cfg}]
  (let [secret (get props :secret-key)]
    (reduce (fn [keys id]
              (let [key (or (get cfg id)
                            (-> (keys/derive secret :salt (name id))
                                (bc/bytes->b64-str true)))]
                (if (or (str/empty? key)
                        (str/blank? key))
                  (do
                    (l/wrn :id (name id) :hint "key is disabled because empty string found")
                    keys)
                  (do
                    (l/inf :id (name id) :hint "key initialized" :key (d/obfuscate-string key))
                    (assoc keys id key)))))
            {}
            [:exporter
             :admin-console
             :nexus
             :media-processor])))

(sm/register! ::props [:map-of :keyword ::sm/any])
(sm/register! ::shared-keys [:map-of :keyword ::sm/text])

