;; 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.common
  "A general purpose events."
  (:require
   [app.common.data :as d]
   [app.common.data.macros :as dm]
   [app.common.schema :as sm]
   [app.common.time :as ct]
   [app.common.types.organization :as cto]
   [app.common.types.team :as ctt]
   [app.config :as cf]
   [app.main.data.helpers :as dsh]
   [app.main.data.modal :as modal]
   [app.main.data.notifications :as ntf]
   [app.main.data.persistence :as-alias dps]
   [app.main.repo :as rp]
   [app.main.router :as rt]
   [app.main.store :as st]
   [app.util.dom :as-alias dom]
   [app.util.i18n :refer [tr]]
   [beicon.v2.core :as rx]
   [potok.v2.core :as ptk]))

(declare go-to-dashboard-recent)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHARE LINK
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn share-link-created
  [link]
  (ptk/reify ::share-link-created
    ptk/UpdateEvent
    (update [_ state]
      (update state :share-links (fnil conj []) link))))

(defn create-share-link
  [params]
  (ptk/reify ::create-share-link
    ptk/WatchEvent
    (watch [_ _ _]
      (->> (rp/cmd! :create-share-link params)
           (rx/map share-link-created)))))

(defn delete-share-link
  [{:keys [id] :as link}]
  (ptk/reify ::delete-share-link
    ptk/UpdateEvent
    (update [_ state]
      (update state :share-links
              (fn [links]
                (filterv #(not= id (:id %)) links))))

    ptk/WatchEvent
    (watch [_ _ _]
      (->> (rp/cmd! :delete-share-link {:id id})
           (rx/ignore)))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; NOTIFICATIONS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn force-reload!
  []
  (.reload js/location))

(defn hide-notifications!
  []
  (st/emit! (ntf/hide)))

(defn handle-notification
  [{:keys [message code] :as params}]
  (ptk/reify ::show-notification
    ptk/WatchEvent
    (watch [_ _ _]
      (case code
        :upgrade-version
        (rx/of (ntf/dialog
                :content (tr "notifications.by-code.upgrade-version")
                :accept {:label (tr "labels.refresh")
                         :callback force-reload!}
                :tag :notification))

        :maintenance
        (rx/of (ntf/dialog
                :content (tr "notifications.by-code.maintenance")
                :accept {:label (tr "labels.accept")
                         :callback hide-notifications!}
                :tag :notification))

        (rx/of (ntf/dialog
                :content message
                :accept {:label (tr "labels.close")
                         :callback hide-notifications!}
                :tag :notification))))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHARED LIBRARY
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn show-shared-dialog
  [file-id add-shared]
  (ptk/reify ::show-shared-dialog
    ptk/WatchEvent
    (watch [_ _ _]
      (->> (rp/cmd! :get-file-summary {:id file-id})
           (rx/map (fn [summary]
                     (let [count (+ (-> summary :components :count)
                                    (-> summary :graphics :count)
                                    (-> summary :colors :count)
                                    (-> summary :typographies :count))]
                       (modal/show
                        {:type :confirm
                         :title (tr "modals.add-shared-confirm.message" (:name summary))
                         :message (if (zero? count) (tr "modals.add-shared-confirm-empty.hint") (tr "modals.add-shared-confirm.hint"))
                         :cancel-label (if (zero? count) (tr "labels.cancel") :omit)
                         :accept-label (tr "modals.add-shared-confirm.accept")
                         :accept-style :primary
                         :on-accept add-shared}))))))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Exportations
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def ^:private schema:export-files
  [:sequential {:title "Files"}
   [:map {:title "FileParam"}
    [:id ::sm/uuid]
    [:name :string]
    [:project-id ::sm/uuid]
    [:is-shared ::sm/boolean]]])

(def check-export-files!
  (sm/check-fn schema:export-files))

(def valid-export-formats
  #{:binfile-v1 :binfile-v3 :legacy-zip})

(defn export-files
  [files format]
  (dm/assert!
   "expected valid files param"
   (check-export-files! files))

  (dm/assert!
   "expected valid format"
   (contains? valid-export-formats format))

  (ptk/reify ::export-files
    ptk/WatchEvent
    (watch [_ state _]
      (let [features (get state :features)
            team-id  (get state :current-team-id)]
        (->> (rx/from files)
             (rx/mapcat
              (fn [file]
                (->> (rp/cmd! :has-file-libraries {:file-id (:id file)})
                     (rx/map #(assoc file :has-libraries %)))))
             (rx/reduce conj [])
             (rx/map (fn [files]
                       (modal/show
                        {:type :export
                         :features features
                         :team-id team-id
                         :files files
                         :format format}))))))))

;;;;;;;;;;;;;;;;;;;;;;
;; Team Request
;;;;;;;;;;;;;;;;;;;;;;

(defn create-team-access-request
  [params]
  (ptk/reify ::create-team-access-request
    ptk/WatchEvent
    (watch [_ _ _]
      (let [{:keys [on-success on-error]
             :or {on-success identity
                  on-error rx/throw}} (meta params)]
        (->> (rp/cmd! :create-team-access-request params)
             (rx/tap on-success)
             (rx/catch on-error))))))

(defn- get-change-role-msg
  [role]
  (case role
    :viewer (tr "dashboard.permissions-change.viewer")
    :editor (tr "dashboard.permissions-change.editor")
    :admin  (tr "dashboard.permissions-change.admin")
    :owner  (tr "dashboard.permissions-change.owner")))

(defn change-team-role
  [{:keys [team-id role]}]
  (dm/assert! (uuid? team-id))
  (dm/assert! (contains? ctt/valid-roles role))

  (ptk/reify ::change-team-role
    ptk/WatchEvent
    (watch [_ state _]
      (let [current-team-id (:current-team-id state)]
        (when (= team-id current-team-id)
          (rx/of (ntf/info (get-change-role-msg role))))))

    ptk/UpdateEvent
    (update [_ state]
      (let [permissions (get ctt/permissions-for-role role)]
        (-> state
            (update :permissions merge permissions)
            (update-in [:team :permissions] merge permissions)
            (d/update-in-when [:teams team-id :permissions] merge permissions))))))

(defn team-membership-change
  [{:keys [team-id team-name change]}]
  (dm/assert! (uuid? team-id))
  (ptk/reify ::team-membership-change
    ptk/UpdateEvent
    (update [_ state]
      ;; FIXME: Remove on 2.5
      (assoc state :current-team-id (dm/get-in state [:profile :default-team-id])))

    ptk/WatchEvent
    (watch [_ state _]
      (when (= :removed change)
        (let [message (tr "dashboard.removed-from-team" team-name)
              team-id (-> state :profile :default-team-id)]
          (rx/concat
           (rx/of (go-to-dashboard-recent :team-id team-id))
           (->> (rx/of (ntf/info message))
                ;; Delay so the navigation can finish
                (rx/delay 250))))))))

(defn- check-team-sso
  [team-id]
  (let [url (rt/get-current-href)]
    (->> (rp/cmd! :check-nitrate-sso {:team-id team-id :url url})
         (rx/mapcat (fn [{:keys [authorized redirect-uri]}]
                      (if authorized
                        (rx/empty)
                        (if redirect-uri
                          (rx/of (rt/nav-raw :uri (str redirect-uri)))
                          (rx/empty))))))))

(defn handle-change-team-organization
  "Handle :team-organization-change websocket messages on dashboard and workspace.
  Updates local team organization data and redirects to SSO when required."
  [{:keys [team notification]}]
  (ptk/reify ::handle-change-team-organization
    ptk/WatchEvent
    (watch [_ state _]
      (let [current-team-id (:current-team-id state)
            organization    (:organization team)
            current-team?   (= (:id team) current-team-id)]
        (when (and (contains? cf/flags :admin-console)
                   current-team?)
          (rx/concat
           (when notification
             (rx/of (ntf/show {:content (tr notification (:name organization))
                               :type :toast
                               :level :info
                               :timeout nil})))
           (when (:id organization)
             (check-team-sso current-team-id))))))
    ptk/UpdateEvent
    (update [_ state]
      (if (contains? cf/flags :admin-console)
        (let [team-id      (:id team)
              team-name    (:name team)
              organization (:organization team)]
          (d/update-in-when state [:teams team-id]
                            (fn [team]
                              (cond-> (cto/apply-organization team organization)
                                team-name (assoc :name team-name)))))
        state))))

(defn handle-organization-change-sso
  "Handle :organization-change-sso websocket messages on dashboard and workspace.
  Redirects to the organization SSO login when the current team belongs to that organization."
  [{:keys [organization-id]}]
  (ptk/reify ::handle-organization-change-sso
    ptk/WatchEvent
    (watch [_ state _]
      (when (contains? cf/flags :admin-console)
        (let [team-id (:current-team-id state)
              team    (dm/get-in state [:teams team-id])
              team-organization-id (dm/get-in team [:organization :id])]
          (when (= organization-id team-organization-id)
            (check-team-sso team-id)))))))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PROGRESS EVENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def noop-fn
  (constantly nil))

(def ^:private schema:progress-params
  [:map {:title "Progress"}
   [:key {:optional true} ::sm/text]
   [:index {:optional true} ::sm/int]
   [:total ::sm/int]
   [:hints
    [:map-of :keyword fn?]]
   [:slow-progress-threshold {:optional true} ::sm/int]])

(def ^:private check-progress-params
  (sm/check-fn schema:progress-params))

(defn initialize-progress
  [& {:keys [key index total hints slow-progress-threshold] :as params}]

  (assert (check-progress-params params))

  (ptk/reify ::initialize-progress
    ptk/UpdateEvent
    (update [_ state]
      (update state :progress
              (fn [_]
                (let [hint ((:normal hints noop-fn) params)]
                  {:threshold (or slow-progress-threshold 5000)
                   :key key
                   :last-update (ct/now)
                   :healthy true
                   :visible true
                   :hints hints
                   :progress (d/nilv index 0)
                   :total total
                   :hint hint}))))))

(defn update-progress
  [{:keys [index total] :as params}]

  (assert (check-progress-params params))

  (ptk/reify ::update-progress
    ptk/UpdateEvent
    (update [_ state]
      (update state :progress
              (fn [state]
                (let [last-update (get state :last-update)
                      hints       (get state :hints)
                      threshold   (get state :slow-progress-threshold)

                      time-diff   (ct/diff-ms last-update (ct/now))
                      healthy?    (< time-diff threshold)

                      hint        (if healthy?
                                    ((:normal hints noop-fn) params)
                                    ((:slow hints noop-fn) params))]

                  (-> state
                      (assoc :progress index)
                      (assoc :total total)
                      (assoc :last-update (ct/now))
                      (assoc :healthy healthy?)
                      (assoc :hint hint))))))))

(defn toggle-progress-visibility
  []
  (ptk/reify ::toggle-progress-visibility
    ptk/UpdateEvent
    (update [_ state]
      (update state :progress
              (fn [state]
                (update state :visible not))))))

(defn clear-progress
  []
  (ptk/reify ::clear-progress
    ptk/UpdateEvent
    (update [_ state]
      (dissoc state :progress))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; NAVEGATION EVENTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn go-to-feedback
  []
  (ptk/reify ::go-to-feedback
    ptk/WatchEvent
    (watch [_ _ _]
      (rx/of (rt/nav :settings-feedback {}
                     ::rt/new-window true
                     ::rt/window-name "penpot-feedback")))))

(defn go-to-dashboard-files
  [& {:keys [project-id team-id] :as options}]
  (ptk/reify ::go-to-dashboard-files
    ptk/WatchEvent
    (watch [_ state _]
      (let [profile    (get state :profile)
            team-id    (or team-id (:current-team-id state))
            project-id (if (= project-id :default)
                         (:default-project-id profile)
                         project-id)

            params     {:team-id team-id
                        :project-id project-id}]
        (rx/of (rt/nav :dashboard-files params options))))))

(defn go-to-dashboard-search
  [& {:keys [term] :as options}]
  (ptk/reify ::go-to-dashboard-search
    ptk/WatchEvent
    (watch [_ state stream]
      (let [team-id (:current-team-id state)]
        (rx/merge
         (->> (rx/of (rt/nav :dashboard-search
                             {:team-id team-id
                              :search-term term})
                     (modal/hide))
              (rx/observe-on :async))

         (->> stream
              (rx/filter (ptk/type? ::rt/navigated))
              (rx/take 1)
              (rx/map (fn [_]
                        (ptk/event ::dom/focus-element
                                   {:name "search-input"})))))))))

(defn go-to-dashboard-libraries
  [& {:keys [team-id] :as options}]
  (ptk/reify ::go-to-dashboard-libraries
    ptk/WatchEvent
    (watch [_ state _]
      (let [team-id (or team-id (:current-team-id state))]
        (rx/of (rt/nav :dashboard-libraries {:team-id team-id}))))))


(defn go-to-dashboard-fonts
  [& {:keys [team-id] :as options}]
  (ptk/reify ::go-to-dashboard-fonts
    ptk/WatchEvent
    (watch [_ state _]
      (let [team-id (or team-id (:current-team-id state))]
        (rx/of (rt/nav :dashboard-fonts {:team-id team-id}))))))

(defn go-to-dashboard-recent
  [& {:keys [team-id] :as options}]
  (ptk/reify ::go-to-dashboard-recent
    ptk/WatchEvent
    (watch [_ state _]
      (let [profile (get state :profile)
            team-id (cond
                      (= :default team-id)
                      (:default-team-id profile)

                      (uuid? team-id)
                      team-id

                      :else
                      (:current-team-id state))
            params  {:team-id team-id}]
        (rx/of (modal/hide)
               (rt/nav :dashboard-recent params options))))))

(defn go-to-dashboard-members
  [& {:as options}]
  (ptk/reify ::go-to-dashboard-members
    ptk/WatchEvent
    (watch [_ state _]
      (let [team-id (:current-team-id state)]
        (rx/of (rt/nav :dashboard-members {:team-id team-id}))))))

(defn go-to-dashboard-invitations
  [& {:as options}]
  (ptk/reify ::go-to-dashboard-invitations
    ptk/WatchEvent
    (watch [_ state _]
      (let [team-id (:current-team-id state)]
        (rx/of (rt/nav :dashboard-invitations {:team-id team-id}))))))

(defn go-to-dashboard-webhooks
  [& {:as options}]
  (ptk/reify ::go-to-dashboard-webhooks
    ptk/WatchEvent
    (watch [_ state _]
      (let [team-id (:current-team-id state)]
        (rx/of (rt/nav :dashboard-webhooks {:team-id team-id}))))))

(defn go-to-dashboard-settings
  [& {:as options}]
  (ptk/reify ::go-to-dashboard-settings
    ptk/WatchEvent
    (watch [_ state _]
      (let [team-id (:current-team-id state)]
        (rx/of (rt/nav :dashboard-settings {:team-id team-id}))))))

(defn go-to-workspace
  [& {:keys [team-id file-id page-id layout] :as options}]
  (ptk/reify ::go-to-workspace
    ptk/WatchEvent
    (watch [_ state _]
      (let [team-id (or team-id (:current-team-id state))
            file-id (or file-id (:current-file-id state))
            page-id (or page-id (:current-page-id state)
                        (-> (dsh/lookup-file-data state file-id)
                            (get :pages)
                            (first)))

            params  (-> (rt/get-params state)
                        (assoc :team-id team-id)
                        (assoc :file-id file-id)
                        (assoc :page-id page-id)
                        (update :layout  #(or layout %))
                        (d/without-nils))]
        (rx/of (rt/nav :workspace params options))))))

(defn go-to-viewer
  [& {:keys [file-id page-id section frame-id index] :as options}]
  (ptk/reify ::go-to-viewer
    ptk/WatchEvent
    (watch [_ state _]
      (let [page-id (or page-id (:current-page-id state))
            file-id (or file-id (:current-file-id state))
            section (or section :interactions)
            selected (get-in state [:workspace-local :selected])
            objects  (dsh/lookup-page-objects state file-id page-id)
            frame-id (or frame-id
                         (reduce
                          (fn [_ id]
                            (let [obj (get objects id)]
                              (when (and obj
                                         (= :frame (:type obj)))
                                (reduced (:id obj)))))
                          nil
                          selected))
            params  {:file-id file-id
                     :page-id page-id
                     :section section
                     :frame-id frame-id
                     :index index}
            params  (d/without-nils params)
            name    (dm/str "viewer-" file-id)
            options (merge {::rt/new-window true
                            ::rt/window-name name}
                           options)]
        (rx/of ::dps/force-persist
               (rt/nav :viewer params options))))))

(defn go-to-dashboard-deleted
  [& {:keys [team-id] :as options}]
  (ptk/reify ::go-to-dashboard-deleted
    ptk/WatchEvent
    (watch [_ state _]
      (let [profile (get state :profile)
            team-id (cond
                      (= :default team-id)
                      (:default-team-id profile)

                      (uuid? team-id)
                      team-id

                      :else
                      (:current-team-id state))
            params  {:team-id team-id}]
        (rx/of (modal/hide)
               (rt/nav :dashboard-deleted params options))))))
