(ns metabase.config
  (:require [clojure.java.io :as io]
            [clojure.string :as str]
            [environ.core :as environ]
            [metabase.plugins.classloader :as classloader])
  (:import clojure.lang.Keyword
           java.util.UUID))

(def ^Boolean is-windows?
  "Are we running on a Windows machine?"
  (str/includes? (str/lower-case (System/getProperty "os.name")) "win"))

(def ^:private app-defaults
  "Global application defaults"
  {:mb-run-mode            "prod"
   ;; DB Settings
   :mb-db-type             "h2"
   :mb-db-file             "metabase.db"
   :mb-db-automigrate      "true"
   :mb-db-logging          "true"
   ;; Jetty Settings. Full list of options is available here: https://github.com/ring-clojure/ring/blob/master/ring-jetty-adapter/src/ring/adapter/jetty.clj
   :mb-jetty-port          "3000"
   :mb-jetty-join          "true"
   ;; other application settings
   :mb-password-complexity "normal"
   :mb-version-info-url    "http://static.metabase.com/version-info.json"
   :mb-ns-trace            ""                                             ; comma-separated namespaces to trace
   :max-session-age        "20160"                                        ; session length in minutes (14 days)
   :mb-colorize-logs       (str (not is-windows?))                        ; since PowerShell and cmd.exe don't support ANSI color escape codes or emoji,
   :mb-emoji-in-logs       (str (not is-windows?))                        ; disable them by default when running on Windows. Otherwise they're enabled
   :mb-qp-cache-backend    "db"})

;; separate map for EE stuff so merge conflicts aren't annoying.
(def ^:private ee-app-defaults
  {:embed-max-session-age      "1440"   ; how long a FULL APP EMBED session is valid for. One day, by default
   :mb-session-cookie-samesite "lax"})

(alter-var-root #'app-defaults merge ee-app-defaults)

(defn config-str
  "Retrieve value for a single configuration key.  Accepts either a keyword or a string.

   We resolve properties from these places:

   1.  environment variables (ex: MB_DB_TYPE -> :mb-db-type)
   2.  jvm options (ex: -Dmb.db.type -> :mb-db-type)
   3.  hard coded `app-defaults`"
  [k]
  (let [k       (keyword k)
        env-val (k environ/env)]
    (or (when-not (str/blank? env-val) env-val)
        (k app-defaults))))


;; These are convenience functions for accessing config values that ensures a specific return type
;;
;; TODO - These names are bad. They should be something like `int`, `boolean`, and `keyword`, respectively. See
;; https://github.com/metabase/metabase/wiki/Metabase-Clojure-Style-Guide#dont-repeat-namespace-alias-in-function-names
;; for discussion
(defn ^Integer config-int  "Fetch a configuration key and parse it as an integer." [k] (some-> k config-str Integer/parseInt))
(defn ^Boolean config-bool "Fetch a configuration key and parse it as a boolean."  [k] (some-> k config-str Boolean/parseBoolean))
(defn ^Keyword config-kw   "Fetch a configuration key and parse it as a keyword."  [k] (some-> k config-str keyword))

(def ^Boolean is-dev?  "Are we running in `dev` mode (i.e. in a REPL or via `lein ring server`)?" (= :dev  (config-kw :mb-run-mode)))
(def ^Boolean is-prod? "Are we running in `prod` mode (i.e. from a JAR)?"                         (= :prod (config-kw :mb-run-mode)))
(def ^Boolean is-test? "Are we running in `test` mode (i.e. via `lein test`)?"                    (= :test (config-kw :mb-run-mode)))

;;; Version stuff

(defn- version-info-from-properties-file []
  (when-let [props-file (io/resource "version.properties")]
    (with-open [reader (io/reader props-file)]
      (let [props (java.util.Properties.)]
        (.load props reader)
        (into {} (for [[k v] props]
                   [(keyword k) v]))))))

;; TODO - Can we make this `^:const`, so we don't have to read the file at launch when running from the uberjar?
(def mb-version-info
  "Information about the current version of Metabase. Comes from `version.properties` which is generated by the build
  script.

     mb-version-info -> {:tag: \"v0.11.1\", :hash: \"afdf863\", :branch: \"about_metabase\", :date: \"2015-10-05\"}"
  (or (version-info-from-properties-file)
      ;; if version info is not defined for whatever reason
      {}))

(def ^String mb-version-string
  "A formatted version string representing the currently running application.
   Looks something like `v0.25.0-snapshot (1de6f3f nested-queries-icon)`."
  (let [{:keys [tag hash branch]} mb-version-info]
    (format "%s (%s %s)" tag hash branch)))

(def ^String mb-app-id-string
  "A formatted version string including the word 'Metabase' appropriate for passing along
   with database connections so admins can identify them as Metabase ones.
   Looks something like `Metabase v0.25.0.RC1`."
  (str "Metabase " (mb-version-info :tag)))

(defonce ^{:doc "This UUID is randomly-generated upon launch and used to identify this specific Metabase instance during
                this specifc run. Restarting the server will change this UUID, and each server in a horizontal cluster
                will have its own ID, making this different from the `site-uuid` Setting."}
  local-process-uuid
  (str (UUID/randomUUID)))

(defn- mb-session-cookie-samesite*
  []
  (let [same-site (str/lower-case (config-str :mb-session-cookie-samesite))]
    (when-not (#{"none", "lax", "strict"} same-site)
      (throw (ex-info "Invalid value for MB_COOKIE_SAMESITE" {:mb-session-cookie-samesite same-site})))
    (keyword same-site)))

(def ^Keyword mb-session-cookie-samesite
  "Value for session cookie's `SameSite` directive. Must be one of \"none\", \"lax\", or \"strict\" (case insensitive)."
  (mb-session-cookie-samesite*))


;; This only affects dev:
;;
;; If for some wacky reason the test namespaces are getting loaded (e.g. when running via
;; `lein ring` or `lein ring sever`, DO NOT RUN THE EXPECTATIONS TESTS AT SHUTDOWN! THIS WILL NUKE YOUR APPLICATION DB
(try
  (classloader/require 'expectations)
  ((resolve 'expectations/disable-run-on-shutdown))
  ;; This will fail if the test dependencies aren't present (e.g. in a JAR situation) which is totally fine
  (catch Throwable _))
