(ns ldap-nginx-plumber.main
  (:require [clojure.spec.alpha :as spec]
            [clojure.string :as str]
            [org.httpkit.server :as http]
            [clj-ldap.client :as ldap]
            [babashka.cli :as cli]
            [clojure.pprint :refer [pprint]])
  (:import [java.util Base64]
           [java.nio.charset StandardCharsets])
  (:gen-class))

(defn- port? [x]
  (and (nat-int? x)
       (<= 0 x 65535)))

(def cli-spec
  {:spec
   {:port {:coerce :int
           :desc "Port to listen on"
           :alias :p
           :validate port?
           :require true}
    :base-dn {:coerce :string
              :desc "Base DN for LDAP searches"
              :require true}
    :ldap-host {:coerce :string
                :require true}
    :ldap-port {:coerce :int
                :validate port?
                :default 389}
    :bind-dn {:coerce :string
              :require true}
    :bind-password {:coerce :string
                    :require true}}})

(def ^:dynamic *opts*)

(defonce ldap-connection-pool
  (atom nil))

(defn- base64->utf8 [base64]
  (try (-> (.decode (Base64/getDecoder) base64)
           (String. StandardCharsets/UTF_8))
       (catch java.lang.IllegalArgumentException _))
  nil)

(defn- response [status & {:as more}]
  (apply merge
         {:status status
          :headers {"Content-Type" "text/plain"}}
         more))

(defn- consultant-app [req]
  (printf "\n%s received request:\n" (.toString (java.util.Date.)))
  (pprint req)
  (try
    (if-let [[_ user pass] (some->> (get-in [req :headers] "Authorization")
                                    str/lower-case
                                    (re-matches #"basic ([a-zA-Z0-9+/=]+)")
                                    second
                                    base64->utf8
                                    (re-matches #"([^:]+):(.*)"))]
      (response 200 :body "yay!")
      (response
       401
       :headers {"WWW-Authenticate" "Basic realm=\"Restricted\""
                 "Cache-Control" "no-cache"}))
    (catch Exception e
      (println "`consultant-app` threw an error:")
      (prn e)
      (response 500))))

(defonce consultant-server (atom nil))

(defn- stop-consultant! []
  (when @consultant-server
    ;; Graceful shutdown: wait 100ms for existing requests to be finished.
    ;; :timeout is optional, when no timeout, stop immediately.
    (http/server-stop! @consultant-server {:timeout 100})
    (reset! consultant-server nil)))

(defn- start-consultant [& {:keys [port] :as opts}]
  (binding [*opts* opts]
    (if @consultant-server
     (throw (ex-info "Refusing to start the server whilst a previous lingers" {}))
     (reset! consultant-server
             (http/run-server #'consultant-app
                              {:port port
                               :legacy-return-value? false})))))

(defn- connect-to-ldap
  [& {:keys [ldap-host ldap-port bind-dn bind-password]}]
  (reset! ldap-connection-pool
          (or @ldap-connection-pool
              (ldap/connect {:host {:address ldap-host
                                    :port ldap-port}
                             :max-connections 8
                             :bind-dn bind-dn
                             :password bind-password}))))

(defn- main* [& opts]
  (and (apply connect-to-ldap opts)
       (apply start-consultant opts)))

(comment
  (let [ask (let [base-dn "dc=identify,dc=deertopia,dc=net"]
            (consultant-app {:port 8080 :ldap-host "192.168.68.79" :ldap-port 3890
                             :base-dn base-dn
                             :bind-dn (str "uid=nginx-bind-user,ou=people," base-dn)
                             :bind-password "secret123"}))]
    (ask {})))

(comment ; Start on :8080
  (let [base-dn "dc=identify,dc=deertopia,dc=net"]
    (main* :port 8080 :ldap-host "192.168.68.79" :ldap-port 3890 :base-dn base-dn
           :bind-dn (str "uid=nginx-bind-user,ou=people," base-dn)
           :bind-password "secret123")))

(comment ; Shutdown
  (stop-consultant!))

(defn -main [& args]
  (main* (cli/parse-opts args cli-spec)))
