(ns metabase.sso.settings
  (:require
   [clojure.string :as str]
   [metabase.models.setting :as setting :refer [defsetting]]
   [metabase.models.setting.multi-setting :refer [define-multi-setting define-multi-setting-impl]]
   [metabase.premium-features.core :as premium-features]
   [metabase.util :as u]
   [metabase.util.i18n :refer [deferred-tru tru]]
   [metabase.util.json :as json])
  (:import
   (com.unboundid.ldap.sdk DN)))
(set! *warn-on-reflection* true)
(defsetting ldap-host
  (deferred-tru "Server hostname.")
  :encryption :when-encryption-key-set
  :audit :getter)
(defsetting ldap-port
  (deferred-tru "Server port, usually 389 or 636 if SSL is used.")
  :encryption :when-encryption-key-set
  :type       :integer
  :default    389
  :audit      :getter)
(defsetting ldap-security
  (deferred-tru "Use SSL, TLS or plain text.")
  :type    :keyword
  :default :none
  :audit   :raw-value
  :setter  (fn [new-value]
             (when (some? new-value)
               (assert (#{:none :ssl :starttls} (keyword new-value))))
             (setting/set-value-of-type! :keyword :ldap-security new-value)))
(defsetting ldap-bind-dn
  (deferred-tru "The Distinguished Name to bind as (if any), this user will be used to lookup information about other users.")
  :encryption :when-encryption-key-set
  :audit :getter)
(defsetting ldap-password
  (deferred-tru "The password to bind with for the lookup user.")
  :encryption :when-encryption-key-set
  :sensitive? true
  :audit     :getter)
(defsetting ldap-user-base
  (deferred-tru "Search base for users. (Will be searched recursively)")
  :encryption :no
  :audit      :getter)
(defsetting ldap-user-filter
  (deferred-tru "User lookup filter. The placeholder '''{login}''' will be replaced by the user supplied login.")
  :default    "(&(objectClass=inetOrgPerson)(|(uid={login})(mail={login})))"
  :encryption :no
  :audit      :getter)
(defsetting ldap-attribute-email
  (deferred-tru "Attribute to use for the user''s email. (usually ''mail'', ''email'' or ''userPrincipalName'')")
  :default    "mail"
  :encryption :no
  :getter     (fn [] (u/lower-case-en (setting/get-value-of-type :string :ldap-attribute-email)))
  :audit      :getter)
(defsetting ldap-attribute-firstname
  (deferred-tru "Attribute to use for the user''s first name. (usually ''givenName'')")
  :default    "givenName"
  :getter     (fn [] (u/lower-case-en (setting/get-value-of-type :string :ldap-attribute-firstname)))
  :encryption :no
  :audit      :getter)
(defsetting ldap-attribute-lastname
  (deferred-tru "Attribute to use for the user''s last name. (usually ''sn'')")
  :encryption :no
  :default    "sn"
  :getter     (fn [] (u/lower-case-en (setting/get-value-of-type :string :ldap-attribute-lastname)))
  :audit      :getter)
(defsetting ldap-group-sync
  (deferred-tru "Enable group membership synchronization with LDAP.")
  :type    :boolean
  :default false
  :audit   :getter)
(defsetting ldap-group-base
  (deferred-tru "Search base for groups. Not required for LDAP directories that provide a ''memberOf'' overlay, such as Active Directory. (Will be searched recursively)")
  :audit      :getter
  :encryption :no)
(defsetting ldap-group-mappings
  ;; Should be in the form: {"cn=Some Group,dc=...": [1, 2, 3]} where keys are LDAP group DNs and values are lists of
  ;; MB groups IDs
  (deferred-tru "JSON containing LDAP to Metabase group mappings.")
  :encryption :no
  :type       :json
  :cache?     false
  :default    {}
  :audit      :getter
  :getter     (fn []
                (json/decode (setting/get-value-of-type :string :ldap-group-mappings) #(DN. (str %))))
  :setter     (fn [new-value]
                (cond
                  (string? new-value)
                  (recur (json/decode new-value))
                  (map? new-value)
                  (do
                    (doseq [k (keys new-value)]
                      (when-not (instance? DN k) ; handle DN-encoded keys like we get from the `:getter`
                        (when-not (DN/isValidDN (u/qualified-name k))
                          (throw (IllegalArgumentException. (tru "{0} is not a valid DN." (u/qualified-name k)))))))
                    (setting/set-value-of-type! :json :ldap-group-mappings new-value)))))
(defsetting ldap-configured?
  (deferred-tru "Have the mandatory LDAP settings (host and user search base) been validated and saved?")
  :type       :boolean
  :visibility :public
  :setter     :none
  :getter     (fn [] (boolean (and (ldap-host)
                                   (ldap-user-base))))
  :doc        false)
(defsetting ldap-enabled
  (deferred-tru "Is LDAP currently enabled?")
  :type       :boolean
  :visibility :public
  :setter     (fn [new-value]
                (let [new-value (boolean new-value)]
                  (when new-value
                    ;; Test the LDAP settings before enabling
                    (let [result ((requiring-resolve 'metabase.sso.ldap/test-current-ldap-details))]
                      (when-not (= :SUCCESS (:status result))
                        (throw (ex-info (tru "Unable to connect to LDAP server with current settings")
                                        ((requiring-resolve 'metabase.sso.ldap/humanize-error-messages) result))))))
                  (setting/set-value-of-type! :boolean :ldap-enabled new-value)))
  :default    false
  :audit      :getter)

Google Auth

(defsetting google-auth-client-id
  (deferred-tru "Client ID for Google Sign-In.")
  :encryption :when-encryption-key-set
  :visibility :public
  :audit      :getter
  :setter     (fn [client-id]
                (if (seq client-id)
                  (let [trimmed-client-id (str/trim client-id)]
                    (when-not (str/ends-with? trimmed-client-id ".apps.googleusercontent.com")
                      (throw (ex-info (tru "Invalid Google Sign-In Client ID: must end with \".apps.googleusercontent.com\)
                                      {:status-code 400})))
                    (setting/set-value-of-type! :string :google-auth-client-id trimmed-client-id))
                  (do
                    (setting/set-value-of-type! :string :google-auth-client-id nil)
                    (setting/set-value-of-type! :boolean :google-auth-enabled false)))))
(defsetting google-auth-configured
  (deferred-tru "Is Google Sign-In configured?")
  :type   :boolean
  :setter :none
  :getter (fn [] (boolean (google-auth-client-id))))
(defsetting google-auth-enabled
  (deferred-tru "Is Google Sign-in currently enabled?")
  :visibility :public
  :type       :boolean
  :audit      :getter
  :getter     (fn []
                (if-some [value (setting/get-value-of-type :boolean :google-auth-enabled)]
                  value
                  (boolean (google-auth-client-id))))
  :setter     (fn [new-value]
                (if-let [new-value (boolean new-value)]
                  (if-not (google-auth-client-id)
                    (throw (ex-info (tru "Google Sign-In is not configured. Please set the Client ID first.")
                                    {:status-code 400}))
                    (setting/set-value-of-type! :boolean :google-auth-enabled new-value))
                  (setting/set-value-of-type! :boolean :google-auth-enabled new-value))))
(define-multi-setting google-auth-auto-create-accounts-domain
  (deferred-tru "When set, allow users to sign up on their own if their Google account email address is from this domain.")
  (fn [] (if (premium-features/enable-sso-google?) :ee :oss))
  :encryption :when-encryption-key-set)
(define-multi-setting-impl google-auth-auto-create-accounts-domain :oss
  :getter (fn [] (setting/get-value-of-type :string :google-auth-auto-create-accounts-domain))
  :setter (fn [domain]
            (when (and domain (str/includes? domain ","))
                ;; Multiple comma-separated domains requires the `:sso-google` premium feature flag
              (throw (ex-info (tru "Invalid domain") {:status-code 400})))
            (setting/set-value-of-type! :string :google-auth-auto-create-accounts-domain domain)))

Common

(define-multi-setting send-new-sso-user-admin-email?
  (deferred-tru "Should new email notifications be sent to admins, for all new SSO users?")
  (fn [] (if (premium-features/enable-any-sso?)
           :ee
           :oss))
  :type :boolean)
(define-multi-setting-impl send-new-sso-user-admin-email? :oss
  :getter (constantly true)
  :setter :none)