;; 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.rpc.doc
  "API autogenerated documentation."
  (:require
   [app.common.data :as d]
   [app.common.exceptions :as ex]
   [app.common.json :as json]
   [app.common.pprint :as pp]
   [app.common.schema :as sm]
   [app.common.schema.desc-js-like :as smdj]
   [app.common.schema.desc-native :as smdn]
   [app.common.schema.openapi :as oapi]
   [app.common.schema.registry :as sr]
   [app.common.uri :as u]
   [app.config :as cf]
   [app.http.sse :as-alias sse]
   [app.loggers.webhooks :as-alias webhooks]
   [app.rpc :as-alias rpc]
   [app.util.services :as sv]
   [app.util.template :as tmpl]
   [clojure.java.io :as io]
   [clojure.spec.alpha :as s]
   [cuerdas.core :as str]
   [pretty-spec.core :as ps]
   [yetti.response :as-alias yres]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; DOC (human readable)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn- context
  [{:keys [methods entrypoint label openapi]}]
  (letfn [(fmt-spec [mdata]
            (when-let [spec (ex/ignoring (s/spec (::sv/spec mdata)))]
              (with-out-str
                (ps/pprint (s/form spec)
                           {:ns-aliases {"clojure.spec.alpha" "s"
                                         "clojure.core.specs.alpha" "score"
                                         "clojure.core" nil}}))))

          (fmt-schema [type mdata key]
            (when-let [schema (get mdata key)]
              (if (= type :js)
                (smdj/describe (sm/schema schema) {::smdj/max-level 4})
                (-> (smdn/describe (sm/schema schema))
                    (pp/pprint-str {:level 5 :width 70})))))

          (get-context [mdata]
            {:name (::sv/name mdata)
             :module (or (some-> (::module mdata) d/name)
                         (-> (:ns mdata) (str/split ".") last))
             :auth (::rpc/auth mdata true)
             :sse (::sse/stream? mdata false)
             :webhook (::webhooks/event? mdata false)
             :docs (::sv/docstring mdata)
             :deprecated (::deprecated mdata)
             :added (::added mdata)
             :changes (some->> (::changes mdata) (partition-all 2) (map vec))
             :spec (fmt-spec mdata)
             :entrypoint (-> entrypoint
                             (u/ensure-path-slash)
                             (u/join (::sv/name mdata))
                             (str))
             :params-schema-js   (fmt-schema :js mdata ::sm/params)
             :result-schema-js   (fmt-schema :js mdata ::sm/result)
             :webhook-schema-js  (fmt-schema :js mdata ::sm/webhook)
             :params-schema-clj  (fmt-schema :clj mdata ::sm/params)
             :result-schema-clj  (fmt-schema :clj mdata ::sm/result)
             :webhook-schema-clj (fmt-schema :clj mdata ::sm/webhook)})]

    {:version (:main cf/version)
     :label label
     :entrypoint (str entrypoint)
     :openapi (str openapi)
     :methods
     (->> methods
          (map val)
          (map first)
          (remove ::skip)
          (map get-context)
          (sort-by (juxt :module :name)))}))

(defn- handler
  [& {:keys [template] :as options}]
  (if (contains? cf/flags :backend-api-doc)
    (let [context  (delay (context options))
          template (or template "app/templates/api-doc.tmpl")]
      (fn [request]
        (let [params  (:query-params request)
              pstyle  (:type params "js")
              context (assoc @context :param-style pstyle)]

          {::yres/status 200
           ::yres/headers {"content-type" "text/html; charset=utf-8"}
           ::yres/body (-> (io/resource template)
                           (tmpl/render context))})))
    (fn [_]
      {::yres/status 404})))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OPENAPI / SWAGGER (v3.1)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn- openapi-context
  [{:keys [methods entrypoint description]}]
  (let [definitions (atom {})
        options {:registry sr/default-registry
                 ::oapi/definitions-path "#/components/schemas/"
                 ::oapi/definitions definitions}

        output-transformer
        (sm/json-transformer)

        gen-response-doc
        (fn [tsx schema]
          (let [schema  (sm/schema schema)
                example (sm/generate schema)
                example (sm/encode schema example output-transformer)
                example (json/encode example :key-fn json/write-camel-key)]

            {:default
             {:description "A default response"
              :content
              {"application/json"
               {:schema tsx
                :example example}}}}))

        gen-params-doc
        (fn [tsx schema]
          (let [example (sm/generate schema)
                example (sm/encode schema example output-transformer)
                example (json/encode example :key-fn json/write-camel-key)]

            {:required true
             :content
             {"application/json"
              {:schema tsx
               :example example}}}))

        gen-method-doc
        (fn [mdata]
          (let [pschema (::sm/params mdata)
                rschema (::sm/result mdata)

                sparams (-> pschema (oapi/transform options) (gen-params-doc pschema))
                sresp   (some-> rschema (oapi/transform options) (gen-response-doc rschema))

                rpost   {:description (::sv/docstring mdata)
                         :deprecated (::deprecated mdata false)
                         :requestBody sparams}

                rpost  (cond-> rpost
                         (some? sresp)
                         (assoc :responses sresp))]

            {:name (-> mdata ::sv/name d/name)
             :module (-> (:ns mdata) (str/split ".") last)
             :repr {:post rpost}}))

        paths
        (binding [oapi/*definitions* definitions]
          (->> methods
               (map (comp first val))
               (filter ::sm/params)
               (map gen-method-doc)
               (sort-by (juxt :module :name))
               (map (fn [doc]
                      [(:name doc) (:repr doc)]))
               (into {})))]

    {:openapi "3.0.0"
     :info {:version (:main cf/version)}
     :servers [{:url (str entrypoint)
                :description (or description "")}]
     :paths paths
     :components {:schemas @definitions}}))

(defn- openapi-json-handler
  [& {:as options}]
  (if (contains? cf/flags :backend-openapi-doc)
    (let [context (delay (openapi-context options))]
      (fn [_]
        {::yres/status 200
         ::yres/headers {"content-type" "application/json; charset=utf-8"}
         ::yres/body (json/encode @context)}))
    (fn [_]
      {::yres/status 404})))

(defn- openapi-handler
  [& {:keys [uri label]}]
  (if (contains? cf/flags :backend-openapi-doc)
    (fn [_]
      (let [swagger-js (slurp (io/resource "app/assets/swagger-ui-4.18.3.js"))
            swagger-cs (slurp (io/resource "app/assets/swagger-ui-4.18.3.css"))
            context    {:uri (str uri)
                        :label label
                        :swagger-js swagger-js
                        :swagger-css swagger-cs}]
        {::yres/status 200
         ::yres/headers {"content-type" "text/html"}
         ::yres/body (-> (io/resource "app/templates/openapi.tmpl")
                         (tmpl/render context))}))
    (fn [_]
      {::yres/status 404})))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ROUTES HELPER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn routes
  [& {:keys [label base-uri description methods]}]
  (let [entrypoint
        (-> base-uri
            (u/ensure-path-slash)
            (u/join "methods"))

        openapi
        (-> base-uri
            (u/ensure-path-slash)
            (u/join "doc/openapi"))

        template
        (case label
          "management" "app/templates/management-api-doc.tmpl"
          "main"       "app/templates/main-api-doc.tmpl")]

    ["/doc"
     ["" {:handler (handler :methods methods
                            :label label
                            :entrypoint entrypoint
                            :openapi openapi
                            :template template)
          :allowed-methods #{:get}}]

     ["/openapi"
      {:handler (openapi-handler
                 :uri (u/join openapi "openapi.json")
                 :label label)
       :allowed-methods #{:get}}]

     ["/openapi.json"
      {:handler (openapi-json-handler {:entrypoint entrypoint
                                       :description description
                                       :methods methods})

       :allowed-methods #{:get}}]]))
