(ns auto-ap.handler
  (:require [amazonica.core :refer [defcredential]]
            [auto-ap.client-routes :as client-routes]
            [auto-ap.datomic :refer [conn pull-many]]
            [auto-ap.graphql.utils :refer [extract-client-ids limited-clients]]
            [auto-ap.logging :as alog]
            [auto-ap.routes.auth :as auth]
            [auto-ap.routes.exports :as exports]
            [auto-ap.routes.ezcater :as ezcater]
            [auto-ap.routes.graphql :as graphql]
            [auto-ap.routes.health :as health]
            [auto-ap.routes.invoices :as invoices]
            [auto-ap.routes.queries :as queries]
            [auto-ap.routes.yodlee2 :as yodlee2]
            [auto-ap.session-version :as session-version]
            [auto-ap.ssr-routes :as ssr-routes]
            [auto-ap.ssr.core :as ssr]
            [auto-ap.ssr.utils :refer [entity-id main-transformer]]
            [bidi.bidi :as bidi]
            [bidi.ring :refer [->ResourcesMaybe make-handler]]
            [buddy.auth.backends.session :refer [session-backend]]
            [buddy.auth.backends.token :refer [jws-backend]]
            [buddy.auth.middleware :refer [wrap-authentication
                                           wrap-authorization]]
            [cemerick.url :as url]
            [cheshire.core :as cheshire]
            [clj-time.coerce :as coerce]
            [clj-time.core :as time]
            [clojure.data.json :as json]
            [clojure.set :as set]
            [clojure.string :as str]
            [com.brunobonacci.mulog :as mu]
            [config.core :refer [env]]
            [datomic.api :as dc]
            [hiccup2.core :as hiccup]
            [malli.core :as mc]
            [ring.middleware.edn :refer [wrap-edn-params]]
            [ring.middleware.multipart-params :as mp]
            [ring.middleware.params :refer [wrap-params]]
            [ring.middleware.session :refer [wrap-session]]
            [ring.middleware.session.cookie :refer [cookie-store]]
            [ring.util.response :as response]))

(when (:aws-access-key-id env)
  (defcredential (:aws-access-key-id env) (:aws-secret-access-key env) (:aws-region env)))

(defn deep-merge [v & vs]
  (letfn [(rec-merge [v1 v2]
            (if (and (map? v1) (map? v2))
              (merge-with deep-merge v1 v2)
              v2))]
    (when (some identity vs)
      (reduce #(rec-merge %1 %2) v vs))))

(def all-routes ["/" (-> (into []
                               (deep-merge ssr-routes/routes
                                           (second client-routes/routes)
                                           graphql/routes
                                           ezcater/routes
                                           health/routes
                                           queries/routes2
                                           yodlee2/routes
                                           auth/routes
                                           invoices/routes
                                           exports/routes2))
                         (conj ["" (->ResourcesMaybe {:prefix "public/"})])
                         (conj [true :not-found])) ;; always go for not found as last resort, have to switch to vec in order for that to work
                 ])

(defn not-found [_]
  {:status  404
   :headers {}
   :body    ""})

(defn render-index [_]
  (response/resource-response "index.html" {:root "public"}))

(def match->handler-lookup
  (-> {:not-found not-found}
      (merge ssr/key->handler)
      (merge graphql/match->handler)
      (merge ezcater/match->handler)
      (merge health/match->handler)
      (merge queries/match->handler)
      (merge yodlee2/match->handler)
      (merge auth/match->handler)
      (merge invoices/match->handler)
      (merge exports/match->handler)
      (merge
       (into {}
             (map

              (fn [k]
                [k  render-index])
              client-routes/all-matches)))))

(def match->handler
  (fn [route]
    (or (get match->handler-lookup route)
        route)))


(def route-handler
  (make-handler all-routes
                match->handler))

(defn wrap-guess-route [handler]
  (fn [{:keys [uri request-method headers] :as request}]
    (let [matched-route (:handler
                         (bidi.bidi/match-route all-routes
                                                uri
                                                :request-method request-method))
          matched-hx-current-url-route (some->> (get headers "hx-current-url")
                                                url/url
                                                :path
                                                (bidi/match-route ssr-routes/only-routes)
                                                :handler)]
      (handler (assoc request
                      :matched-route
                      matched-route
                      :matched-current-page-route
                      matched-hx-current-url-route)))))

(defn test-match-route [method uri]
  (bidi.bidi/match-route all-routes
                         uri
                         :request-method method))


(def auth-backend (jws-backend {:secret (:jwt-secret env) :options {:alg :hs512}}))

(defn wrap-logging [handler]
  (fn [request]
    (mu/with-context (cond-> {:uri       (:uri request)
                              :route  (:handler (bidi.bidi/match-route all-routes
                                                                       (:uri request)
                                                                       :request-method (:request-method request)))

                              :client-selection (:client-selection request)
                              :source    "request"
                              :query (:uri request)
                              :request-method (:request-method request)
                              :user (dissoc (:identity request)
                                            :gz-clients)
                              :user-role (:user/role (:identity request))
                              :user-name (:user/name (:identity request))}
                       (not= "/api/graphql" (:uri request))
                       (assoc :query-params (:query-params request)))
      (mu/trace ::http-request-trace
                {:pairs []
                 :capture (fn [r] {:status (:status r)})}
                (when-not (str/includes? (:uri request) "health-check")
                  (alog/info ::http-request-starting))
                (try
                  (let [response (handler request)]
                    response)
                  (catch Exception e
                    (alog/error ::request-error
                                :status 500
                                :exception e)
                    (throw e)))))))



(defn wrap-idle-session-timeout
  [handler]
  (fn [request]
    (let [session  (:session request {:version session-version/current-session-version})
          end-time (coerce/to-date-time (::idle-timeout session))]
      (if (and end-time (time/before? end-time (time/now)))
        (if (get (:headers request) "hx-request")
          {:session nil
           :status  200
           :headers {"hx-redirect" "/login"}}
          {:session nil
           :status  302
           :headers {"Location" "/login"}})
        (when-let [response (handler request)]
          (let [session (:session response session)]
            (if (nil? session)
              response
              (let [end-time (time/plus (time/now) (time/days 14))]
                (assoc response :session (assoc session ::idle-timeout (coerce/to-date end-time)))))))))))

(defn wrap-hx-current-url-params
  [handler]
  (fn [request]
    (let [query-params (some-> (get-in request [:headers "hx-current-url"]) (url/url) :query)
          request (assoc request :hx-query-params query-params)]
      (handler request))))

(def client-selection-schema
  (mc/schema
   [:orn
    [:global [:enum :all :mine]]
    [:group-name [:map [:group :string]]]
    [:specific [:map [:selected [:vector entity-id]]]]]))

(defn wrap-hydrate-clients
  [handler]
  (fn [request]
    (let [x-clients (-> request :client-selection)
          identity (or (-> request :identity)
                       (-> request :session :identity))
          ideal-ids (set (cond
                           (or (= :all x-clients)
                               (nil? x-clients))
                           (->> (dc/q '[:find ?c
                                        :where [?c :client/code]]
                                      (dc/db conn))
                                (map first))

                           (= :mine x-clients)
                           (map :db/id (:user/clients identity))

                           (:group x-clients)
                           (->>
                            (dc/q '[:find ?c
                                    :in $ ?g
                                    :where [?c :client/groups ?g]]
                                  (dc/db conn)
                                  (str/upper-case (or (:group x-clients) "INVALID")))
                            (map first)
                            set)

                           (seq (:selected x-clients))
                           (->> x-clients
                                :selected
                                (filter #(not (nil? %)))
                                set)))
          limited-clients (some->> (limited-clients identity)
                                   (map :db/id)
                                   set)
          client-ids (if (= "admin" (:user/role identity))
                       ideal-ids
                       (set/intersection ideal-ids
                                         (or limited-clients #{})))
          clients (some->> client-ids
                           seq
                           (pull-many (dc/db conn)
                                      '[:db/id :client/name :client/code :client/locations
                                        :client/matches  :client/feature-flags
                                        {:client/bank-accounts [:db/id
                                                                {:bank-account/type [:db/ident]}
                                                                :bank-account/number
                                                                :bank-account/name
                                                                :bank-account/code]}]))]
      (mu/with-context {:clients (take 10 (map :client/code clients))}
        (handler (assoc request
                        :clients clients
                        :client (when (= 1 (count clients))
                                  (first clients))))))))

(defn wrap-store-client-in-session
  [handler]
  (fn [{:keys [headers identity] :as request}]
    (let [client-selection (try (mc/decode client-selection-schema (some-> (get headers "x-clients") not-empty json/read-str) main-transformer)
                                (catch Exception e
                                  (alog/warn ::cant-access :error e
                                             :identity identity
                                             :x-clients (pr-str (get headers "x-clients")))
                                  nil))

          new-request (if client-selection
                        (assoc-in request [:client-selection] client-selection)
                        (assoc-in request [:client-selection] (get-in request [:session :client-selection] :all)))]
      (cond-> (handler new-request)
        client-selection (update :session
                                 (fn [new-session]
                                   (-> (:session request)
                                       (into new-session)
                                       (assoc :client-selection client-selection))))))))

(defn wrap-gunzip-jwt
  [handler]
  (fn [{:keys [session] :as request}]
    (let [request (if-let [gz-clients (some-> request :identity :gz-clients)]
                    (try
                      (assoc-in request [:identity :user/clients]
                                (auth/gunzip gz-clients))
                      (catch Exception e
                        (alog/error :cant-gunzip-clients
                                    :error e)
                        request))
                    request)]
      (handler request))))

#_(defn wrap-pprint-session
    [handler]
    (fn [request]
      (clojure.pprint/pprint (:session request))
      (handler request)))

(defn wrap-error [handler]
  (fn error-handling-request [request]
    (try
      (handler request)
      (catch Throwable e
        (cond
          (= :notification (:type (ex-data e)))
          {:status 200
           :headers {"hx-trigger" (cheshire/generate-string
                                   {"notification" (str (hiccup/html [:div (.getMessage e)]))})
                     "hx-reswap" "none"}}
          (= :warning (:type (ex-data e)))
          {:status 200
           :headers {"hx-trigger" (cheshire/generate-string
                                   {"notification" (str (hiccup/html [:div (.getMessage e)]))})
                     "hx-reswap" "none"}}  ;; TODO make a warning box so you don't have to reuse the notifaction box, or make it reuse the same box but theme differently
          :else
          {:status 500
           :body (pr-str e)})))))

(defn wrap-trim-clients [handler]
  (fn [request]
    (let [valid-clients (extract-client-ids (:clients request)
                                            (:client request)
                                            (:client-id (:query-params request))
                                            (when (:client-code (:query-params request))
                                              [:client/code (:client-code (:query-params request))]))
          trimmed-clients (->> valid-clients (take 20) set)]
      (handler (assoc request :valid-client-ids valid-clients
                      :valid-trimmed-client-ids trimmed-clients
                      :first-client-id (first valid-clients)
                      :clients-trimmed? (not= (count trimmed-clients) (count valid-clients)))))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defonce app
  (->  route-handler
       (wrap-hx-current-url-params)
       (wrap-guess-route)
       (wrap-logging)
       (wrap-trim-clients)
       (wrap-hydrate-clients)
       (wrap-store-client-in-session)
       (wrap-gunzip-jwt)
       (wrap-authorization auth-backend)
       (wrap-authentication auth-backend
                            (session-backend {:authfn (fn [auth]
                                                        (dissoc auth :exp))}))

       #_(wrap-pprint-session)

       (session-version/wrap-session-version)
       (wrap-idle-session-timeout)
       (wrap-session {:store (cookie-store
                              {:key
                               (byte-array
                                [42, 52, -31, 101, -126, -33, -118, -69, -82, -59, -15, -69, -38, 103, -102, -1])})})

       #_(wrap-reload)
       (wrap-params)
       (mp/wrap-multipart-params)
       (wrap-edn-params)
       (wrap-error)))
