;; 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.loggers.mattermost
  "A mattermost integration for error reporting."
  (:require
   [app.common.data :as d]
   [app.common.exceptions :as ex]
   [app.common.logging :as l]
   [app.common.pprint :as pp]
   [app.common.uri :as u]
   [app.config :as cf]
   [app.http.client :as http]
   [app.loggers.audit :as audit]
   [app.rpc.rlimit :as-alias rlimit]
   [app.util.json :as json]
   [integrant.core :as ig]
   [promesa.exec :as px]
   [promesa.exec.csp :as sp]))

(defonce enabled (atom true))

(defn- send-mattermost-notification!
  [cfg {:keys [id] :as report}]
  (let [type (get report :type)
        text (str "#" type " | " (d/escape-markdown (get report :hint)) "\n"
                  (when id
                    (str (u/join (cf/get :public-uri) "/dbg/error/" id) " "))

                  (when-let [pid (:profile-id report)]
                    (if (uuid? pid)
                      (str "(pid: #uuid-" pid ")")
                      (str "(pid: #ip-" pid ")")))
                  "\n"
                  "- host: #" (:host report) "\n"
                  "- tenant: #" (:tenant report) "\n"
                  "- origin: #" (:origin report) "\n"
                  (when-let [href (get report :href)]
                    (str "- href: `" (d/escape-markdown href) "`\n"))
                  (when-let [version (get report :frontend-version)]
                    (str "- frontend-version: `" version "`\n"))
                  (when-let [version (get report :backend-version)]
                    (str "- backend-version: `" version "`\n"))
                  "\n"
                  (when-let [info (:info report)]
                    (str "```\n" info "```"))
                  (when-let [trace (:trace report)]
                    (str "```\n"
                         "Trace:\n"
                         trace
                         "```")))

        resp (http/req cfg
                       {:uri (cf/get :error-report-webhook)
                        :method :post
                        :headers {"content-type" "application/json"}
                        :body (json/encode-str {:text text})}
                       {:sync? true})]

    (when (not= 200 (:status resp))
      (l/warn :hint "error on sending data"
              :response (pr-str resp)))))

(defn- log-record->report
  [{:keys [::l/context ::l/id ::l/cause ::l/message] :as record}]
  (assert (l/valid-record? record) "expectd valid log record")

  (let [public-uri (cf/get :public-uri)]
    {:id               id
     :type             "exception"
     :origin           "logging"
     :hint             (or (some-> cause ex-message) @message)
     :tenant           (cf/get :tenant)
     :host             (cf/get :host)
     :backend-version  (:full cf/version)
     :frontend-version (:frontend/version context)
     :profile-id       (:request/profile-id context)
     :href             (-> public-uri
                           (assoc :path (:request/path context))
                           (str))
     :trace            (ex/format-throwable cause :detail? false :header? false)}))

(defn- audit-event->report
  [{:keys [context props id] :as event}]
  {:id               id
   :type             "exception"
   :origin           "audit-log"
   :hint             (get props :hint)
   :tenant           (cf/get :tenant)
   :host             (cf/get :host)
   :backend-version  (:full cf/version)
   :frontend-version (:version context)
   :profile-id       (:profile-id event)
   :href             (get props :href)})

(defn- rlimit-event->report
  [event]
  {:id               (::rlimit/id event)
   :type             "notification"
   :origin           "rlimit"
   :hint             (str "rlimit reject of "
                          (::rlimit/method event)
                          " for "
                          (::rlimit/uid event))
   :tenant           (cf/get :tenant)
   :host             (cf/get :host)
   :backend-version  (:full cf/version)
   :profile-id       (::rlimit/profile-id event)
   :info             (with-out-str
                       (println "Rejected by:")
                       (println "------------")
                       (println "Method:        " (::rlimit/method event))
                       (println "Limit Name:    " (::rlimit/name event))
                       (println "Limit Strategy:" (::rlimit/strategy event))
                       (println)
                       (println "Results & Config:")
                       (println "-----------------")
                       (doseq [result (::rlimit/results event)]
                         (pp/pprint (into (sorted-map) result))))})

(defn- handle-event
  [cfg event event->report]
  (try
    (let [report (event->report event)]
      (send-mattermost-notification! cfg report))
    (catch Throwable cause
      (l/warn :hint "unhandled error" :cause cause))))

(defmethod ig/assert-key ::reporter
  [_ params]
  (assert (http/client? (::http/client params)) "expect valid http client"))

(defmethod ig/init-key ::reporter
  [_ cfg]
  (when-let [uri (cf/get :error-report-webhook)]
    (let [input  (sp/chan :buf (sp/sliding-buffer 256))
          thread (px/thread
                   {:name "penpot/reporter/mattermost"}
                   (l/info :hint "initializing error reporter" :uri uri)

                   (try
                     (loop []
                       (when-let [item (sp/take! input)]
                         (when @enabled
                           (cond
                             (::l/id item)
                             (handle-event cfg item log-record->report)

                             (::rlimit/id item)
                             (handle-event cfg item rlimit-event->report)

                             (-> item meta ::audit/event)
                             (handle-event cfg item audit-event->report)

                             :else
                             (l/warn :hint "received unexpected item" :item item)))

                         (recur)))
                     (catch InterruptedException _
                       (l/debug :hint "reporter interrupted"))
                     (catch Throwable cause
                       (l/error :hint "unexpected error" :cause cause))
                     (finally
                       (l/info :hint "reporter terminated"))))]

      (add-watch l/log-record ::reporter
                 (fn [_ _ _ record]
                   (when (= :error (::l/level record))
                     (sp/put! input record))))

      {::input input
       ::thread thread})))

(defmethod ig/halt-key! ::reporter
  [_ {:keys [::input ::thread]}]
  (remove-watch l/log-record ::reporter)
  (some-> input sp/close!)
  (some-> thread px/interrupt!))

(defn emit
  "Emit an event/report into the mattermost reporter"
  [cfg event]
  (when-let [{:keys [::input]} (get cfg ::reporter)]
    (sp/put! input event)))
