;; 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.main.data.workspace.tokens.import-export
  (:require
   [app.common.json :as json]
   [app.common.logging :as l]
   [app.common.path-names :as cpn]
   [app.common.types.tokens-lib :as ctob]
   [app.config :as cf]
   [app.main.data.notifications :as ntf]
   [app.main.data.style-dictionary :as sd]
   [app.main.data.tokenscript :as ts]
   [app.main.data.workspace.tokens.errors :as wte]
   [app.main.store :as st]
   [app.util.dom :as dom]
   [app.util.i18n :as i18n]
   [beicon.v2.core :as rx]
   [cuerdas.core :as str]))

(defn- extract-error-with-code
  "Return the error if it has an error code generated from Penpot code"
  [err]
  (when (contains? (ex-data err) :error/code)
    (wte/error-ex-info (:error/code (ex-data err)) (ex-message err) err)))

(defn- extract-reference-errors
  "Extracts reference errors from errors produced by StyleDictionary."
  [err]
  (let [[header-1 header-2 & errors] (str/split err "\n")]
    (when (and
           (= header-1 "Error: ")
           (= header-2 "Reference Errors:"))
      errors)))

(defn- extract-name-error
  "Extracts name error out of malli schema error during import."
  [err]
  (let [schema-error (some-> (ex-data err)
                             (get-in [:app.common.schema/explain :errors])
                             (first))
        name-error? (= (:in schema-error) [:name])]
    (when name-error?
      (wte/error-ex-info :error.import/invalid-token-name (:value schema-error) err))))

(defn- group-by-value [m]
  (reduce (fn [acc [k v]]
            (update acc v conj k)) {} m))

(defn- show-unknown-types-warning [unknown-tokens]
  (let [type->tokens (group-by-value unknown-tokens)]
    (l/wrn :hint "unsupported token types found during import"
           :tokens (str/join ", " (map (fn [[path type]] (str path " (" type ")")) unknown-tokens)))
    (ntf/show {:content (i18n/tr "workspace.tokens.unknown-token-type-message")
               :is-html true
               :detail (->> (for [[token-type token-paths] type->tokens]
                              (str (i18n/tr "workspace.tokens.unknown-token-type-section"
                                            (dom/escape-html token-type)
                                            (i18n/tr "labels.warning-count" (i18n/c (count token-paths))))
                                   "<ul>"
                                   (->> token-paths
                                        (sort)
                                        (map #(str "<li>" (dom/escape-html %) "</li>"))
                                        (str/join ""))
                                   "</ul>"))
                            (str/join ""))
               :type :toast
               :level :info})))

(defn- decode-json
  [json-string]
  (try
    (json/decode json-string {:key-fn identity})
    (catch js/Error e
      (throw (wte/error-ex-info :error.import/json-parse-error json-string e)))))

(defn- parse-decoded-json
  [decoded-json file-name]
  (try
    {:tokens-lib (ctob/parse-decoded-json decoded-json file-name)
     :unknown-tokens (ctob/get-tokens-of-unknown-type decoded-json {})}
    (catch js/Error e
      (let [err (or (extract-error-with-code e)
                    (extract-name-error e)
                    (wte/error-ex-info :error.import/invalid-json-data decoded-json e))]
        (throw err)))))

(defn- validate-library
  "Resolve tokens in the library and search for errors. Reference errors are ignored, since
   it can be resolved by the user in the UI. All the other errors are thrown as exceptions."
  [{:keys [tokens-lib unknown-tokens]}]
  (when unknown-tokens
    (st/emit! (show-unknown-types-warning unknown-tokens)))
  (try
    (let [tokens-tree     (ctob/get-all-tokens-map tokens-lib)
          resolved-tokens (if (contains? cf/flags :tokenscript)
                            (rx/of (ts/resolve-tokens tokens-tree))
                            (sd/resolve-tokens-with-verbose-errors tokens-tree))]
      (->> resolved-tokens
           (rx/map (fn [_]
                     tokens-lib))
           (rx/catch (fn [sd-error]
                       (let [reference-errors (extract-reference-errors sd-error)]
                         (if reference-errors
                           (rx/of tokens-lib)
                           (throw (wte/error-ex-info :error.import/style-dictionary-unknown-error sd-error sd-error))))))))
    (catch js/Error e
      (throw (wte/error-ex-info :error.import/style-dictionary-unknown-error "" e)))))

(defn- drop-parent-directory
  [path]
  (->> (cpn/split-path path)
       (rest)
       (str/join "/")))

(defn- remove-path-extension
  [path]
  (-> (str/split path ".")
      (butlast)
      (str/join)))

(defn- file-path->set-name
  [path]
  (-> path
      (drop-parent-directory)
      (remove-path-extension)))

(defn import-file-stream
  [file-path file-text]
  (let [file-name (remove-path-extension file-path)]
    (->> file-text
         (rx/map decode-json)
         (rx/map #(parse-decoded-json % file-name))
         (rx/mapcat validate-library))))

(defn import-directory-stream
  [file-stream]
  (->> file-stream
       (rx/map (fn [[file-path file-text]]
                 (let [set-name (file-path->set-name file-path)]
                   (try
                     {set-name (decode-json file-text)}
                     (catch js/Error e
                       ;; Ignore files with json parse errors
                       {:path file-path :error e})))))
       (rx/reduce (fn [merged-json decoded-json]
                    (if (:error decoded-json)
                      merged-json
                      (conj merged-json decoded-json)))
                  {})
       (rx/map (fn [merged-json]
                 (parse-decoded-json (if (= 1 (count merged-json))
                                       (val (first merged-json))
                                       merged-json)
                                     (ffirst merged-json))))
       (rx/mapcat validate-library)))
