;; 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.ui.dashboard.team
  (:require-macros [app.main.style :as stl])
  (:require
   [app.common.data :as d]
   [app.common.data.macros :as dm]
   [app.common.schema :as sm]
   [app.config :as cfg]
   [app.main.data.common :as dcm]
   [app.main.data.event :as ev]
   [app.main.data.modal :as modal]
   [app.main.data.nitrate :as dnt]
   [app.main.data.notifications :as ntf]
   [app.main.data.team :as dtm]
   [app.main.refs :as refs]
   [app.main.repo :as rp]
   [app.main.router :as rt]
   [app.main.store :as st]
   [app.main.ui.alert]
   [app.main.ui.components.dropdown :refer [dropdown]]
   [app.main.ui.components.file-uploader :refer [file-uploader]]
   [app.main.ui.components.forms :as fm]
   [app.main.ui.components.organization-avatar :refer [organization-avatar*]]
   [app.main.ui.dashboard.change-owner]
   [app.main.ui.dashboard.subscription :refer [members-cta*
                                               show-subscription-members-banner?
                                               team*]]
   [app.main.ui.dashboard.team-form]
   [app.main.ui.ds.buttons.button :refer [button*]]
   [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
   [app.main.ui.ds.controls.combobox :refer [combobox*]]
   [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
   [app.main.ui.ds.foundations.typography :as t]
   [app.main.ui.ds.foundations.typography.heading :refer [heading*]]
   [app.main.ui.ds.foundations.typography.text :refer [text*]]
   [app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
   [app.main.ui.ds.tooltip.tooltip :refer [tooltip*]]
   [app.main.ui.forms :as fc]
   [app.main.ui.icons :as deprecated-icon]
   [app.main.ui.notifications.badge :refer [badge-notification]]
   [app.main.ui.notifications.context-notification :refer [context-notification]]
   [app.util.dom :as dom]
   [app.util.forms :as uforms]
   [app.util.i18n :as i18n :refer [tr]]
   [app.util.timers :as tm]
   [beicon.v2.core :as rx]
   [cuerdas.core :as str]
   [rumext.v2 :as mf]))

(def ^:private arrow-icon
  (deprecated-icon/icon-xref :arrow (stl/css :arrow-icon)))

(def ^:private menu-icon
  (deprecated-icon/icon-xref :menu (stl/css :menu-icon)))

(def ^:private organization-menu-icon
  (deprecated-icon/icon-xref :menu (stl/css :organization-menu-icon)))

(def ^:private warning-icon
  (deprecated-icon/icon-xref :msg-warning (stl/css :warning-icon)))

(def ^:private success-icon
  (deprecated-icon/icon-xref :msg-success (stl/css :success-icon)))

(def ^:private image-icon
  (deprecated-icon/icon-xref :img (stl/css :image-icon)))

(def ^:private user-icon
  (deprecated-icon/icon-xref :user (stl/css :user-icon)))

(def ^:private document-icon
  (deprecated-icon/icon-xref :document (stl/css :document-icon)))

(def ^:private group-icon
  (deprecated-icon/icon-xref :group (stl/css :group-icon)))

(mf/defc header*
  {::mf/wrap [mf/memo]}
  [{:keys [section team profile]}]
  (let [on-nav-members       (mf/use-fn #(st/emit! (dcm/go-to-dashboard-members)))
        on-nav-settings      (mf/use-fn #(st/emit! (dcm/go-to-dashboard-settings)))
        on-nav-invitations   (mf/use-fn #(st/emit! (dcm/go-to-dashboard-invitations)))
        on-nav-webhooks      (mf/use-fn #(st/emit! (dcm/go-to-dashboard-webhooks)))

        route                (mf/deref refs/route)
        invite-email         (-> route :query-params :invite-email)
        team-id              (:id team)

        members-section?     (= section :dashboard-team-members)
        settings-section?    (= section :dashboard-team-settings)
        invitations-section? (= section :dashboard-team-invitations)
        webhooks-section?    (= section :dashboard-team-webhooks)
        permissions          (:permissions team)
        can-invite?          (dnt/can-send-invitations?
                              {:organization (:organization team)
                               :profile-id (:id profile)
                               :team-permissions permissions})
        invitations          (:invitations team)

        on-invite-member
        (mf/use-fn
         (mf/deps team-id invite-email)
         (fn []
           (st/emit! (dtm/check-and-invite-members {:team-id team-id
                                                    :origin :team
                                                    :invite-email invite-email}))))]

    ;; Depend on `team-id` (stable) rather than `team` (a map whose
    ;; reference changes on every teams/members fetch) and clear
    ;; `invite-email` from the URL once consumed, so this can't
    ;; keep re-triggering `check-and-invite-members` in a loop.
    (mf/with-effect [team-id invite-email]
      (when (and team-id invite-email)
        (on-invite-member)
        (st/emit! (rt/nav (get-in route [:data :name]) {:team-id team-id} {::rt/replace true}))))

    [:header {:class (stl/css :dashboard-header :team) :data-testid "dashboard-header"}
     [:div {:class (stl/css :dashboard-title)}
      [:h1 (cond
             members-section? (tr "labels.members")
             settings-section? (tr "labels.settings")
             invitations-section? (tr "labels.invitations")
             webhooks-section? (tr "labels.webhooks")
             :else nil)]]
     [:nav {:class (stl/css :dashboard-header-menu)}
      [:ul {:class (stl/css :dashboard-header-options)}
       [:li {:class (when members-section? (stl/css :active))}
        [:a {:on-click on-nav-members} (tr "labels.members")]]
       [:li {:class (when invitations-section? (stl/css :active))}
        [:a {:on-click on-nav-invitations} (tr "labels.invitations")]]
       (when (contains? cfg/flags :webhooks)
         [:li {:class (when webhooks-section? (stl/css :active))}
          [:a {:on-click on-nav-webhooks} (tr "labels.webhooks")]])
       [:li {:class (when settings-section? (stl/css :active))}
        [:a {:on-click on-nav-settings} (tr "labels.settings")]]]]
     [:div {:class (stl/css :dashboard-buttons)}
      (when (and (or invitations-section? members-section?) (not-empty invitations))
        (let [organization          (:organization team)
              owners-only-invites?  (and (contains? cfg/flags :admin-console)
                                         organization
                                         (= (get-in organization [:permissions :send-invitations]) "owners"))
              title-text            (if owners-only-invites?
                                      (tr "dashboard.invite-profile-disabled.owners-only" (:name organization))
                                      (tr "dashboard.invite-profile-disabled"))
              invite-button         (mf/html
                                     [:> button* {:class (stl/css :invite-button)
                                                  :variant "secondary"
                                                  :on-click on-invite-member
                                                  :disabled (not can-invite?)
                                                  :data-testid "invite-member"}
                                      (tr "dashboard.invite-profile")])]
          (if can-invite?
            invite-button
            [:> tooltip* {:content title-text
                          :id "invite-member-disabled-tooltip"
                          :tab-index 0}
             invite-button])))]]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INVITATIONS MODAL
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn get-available-roles
  [permissions]
  (->> [{:id "viewer" :value "viewer" :label (tr "labels.viewer")}
        {:id "editor" :value "editor" :label (tr "labels.editor")}
        (when (:is-admin permissions)
          {:id "admin" :value "admin" :label (tr "labels.admin")})]
       (filterv identity)))

(def ^:private schema:invite-member-form
  [:map {:title "InviteMemberForm"}
   [:role :keyword]
   [:emails [::sm/set {:min 1} ::sm/email]]
   [:team-id ::sm/uuid]])


(defn- do-invite-members!
  [params origin]
  (st/emit! (-> (dtm/create-invitations params)
                (with-meta {::ev/origin origin}))
            (dtm/fetch-invitations)
            (dtm/fetch-members)))

(mf/defc invite-members-modal
  {::mf/register modal/components
   ::mf/register-as :invite-members
   ::mf/props :obj}
  [{:keys [team origin invite-email]}]
  (let [teams       (mf/deref refs/teams)

        perms       (get team :permissions)
        team-id     (get team :id)

        members     (get-in teams [team-id :members])

        roles       (mf/with-memo [perms]
                      (get-available-roles perms))

        initial     (mf/with-memo [team-id invite-email]
                      (if invite-email
                        {:role "editor" :team-id team-id :emails #{invite-email}}
                        {:role "editor" :team-id team-id}))

        form        (fm/use-form :schema schema:invite-member-form
                                 :initial initial)
        error-text  (mf/use-state  "")

        current-data-emails (into #{} (dm/get-in @form [:clean-data :emails]))
        current-members-emails (into #{} (map :email) members)

        on-success
        (fn [_form {:keys [total]}]
          (when (pos? total)
            (st/emit! (ntf/success (tr "notifications.invitation-email-sent"))))

          (st/emit! (modal/hide)
                    (dtm/fetch-members)
                    (dtm/fetch-invitations)))

        on-error
        (fn [_form cause]
          (let [{:keys [type code] :as error} (ex-data cause)]
            (cond
              (and (= :validation type)
                   (= :profile-is-muted code))
              (st/emit! (ntf/error (tr "errors.profile-is-muted"))
                        (modal/hide))

              (and (= :validation type)
                   (= :max-invitations-by-request code))
              (swap! error-text (tr "errors.maximum-invitations-by-request-reached" (:threshold error)))

              (and (= :restriction type)
                   (= :max-quote-reached code))
              (swap! error-text (tr "errors.max-quota-reached" (:target error)))

              (or (= :member-is-muted code)
                  (= :email-has-permanent-bounces code)
                  (= :email-has-complaints code))
              (swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error)))

              (and (= :restriction type)
                   (= :email-domain-is-not-allowed code))
              (st/emit! (ntf/error (tr "errors.email-domain-not-allowed"))
                        (modal/hide))
              (and (= :validation type)
                   (= :insufficient-permissions code))
              (st/emit! (modal/show :no-permission-modal {:type :invite-members}))

              :else
              (st/emit! (ntf/error (tr "errors.generic"))
                        (modal/hide)))))

        on-submit
        (fn [form]
          (let [params (:clean-data @form)
                mdata  {:on-success (partial on-success form)
                        :on-error   (partial on-error form)}]
            (st/emit! (dtm/check-and-submit-invite-members (with-meta params mdata) origin do-invite-members!))))]

    (mf/with-effect [team-id]
      (st/emit! (dtm/fetch-members team-id)))

    [:div {:class (stl/css-case :modal-team-container true
                                :modal-team-container-workspace (= origin :workspace)
                                :hero (= origin :hero))}
     [:> fc/form* {:form form
                   :class (stl/css :form-wrapper)
                   :on-submit on-submit}
      [:> heading* {:level 2
                    :typography t/headline-medium
                    :class (stl/css :color-light)}
       (tr "modals.invite-team-member.title")]

      (when (= :workspace origin)
        [:> text* {:as "p"
                   :typography t/body-large
                   :class (stl/css :color-light)}
         (tr "modals.invite-team-member.text")])

      (when-not (= "" @error-text)
        [:> context-notification* {:level :error}
         @error-text])

      (when (some current-data-emails current-members-emails)
        [:> context-notification* {:level :warning}
         (tr "modals.invite-member.repeated-invitation")])

      [:div {:class (stl/css :form-group)}
       [:> text* {:as "label"
                  :typography t/body-medium
                  :class (stl/css :color-light)}
        (tr "onboarding.choice.team-up.roles")]
       [:> fc/form-select* {:name :role
                            :default-selected "editor"
                            :options roles}]
       [:> fc/form-multi-input* {:type "email"
                                 :name :emails
                                 :auto-focus? true
                                 :trim true
                                 :valid-item-fn sm/parse-email
                                 :caution-item-fn current-members-emails
                                 :placeholder (tr "modals.invite-member.emails")}]]

      [:div {:class (stl/css :form-buttons)}
       [:> fc/form-submit*
        {:disabled (and (boolean (some current-data-emails current-members-emails))
                        (empty? (remove current-members-emails current-data-emails)))}
        (tr "modals.invite-member-confirm.accept")]]]]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INVITE RESTRICTED MEMBERS MODAL
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(mf/defc invite-restricted-members-modal
  {::mf/register modal/components
   ::mf/register-as :invite-restricted-members}
  [{:keys [on-accept blocked-emails]}]
  (let [expanded* (mf/use-state false)
        expanded? (deref expanded*)
        on-toggle (mf/use-fn #(swap! expanded* not))]
    [:div {:class (stl/css :modal-overlay)}
     [:div {:class (stl/css :modal-restricted-container :modal-container)}
      [:div {:class (stl/css :modal-restricted-header)}
       [:h2 {:class (stl/css :modal-restricted-title)}
        (tr "modals.invite-restricted-members.title")]
       [:button {:class (stl/css :modal-close-btn)
                 :on-click modal/hide!} deprecated-icon/close]]

      [:div {:class (stl/css :modal-restricted-content)}
       [:p (tr "modals.invite-restricted-members.description")]
       [:& context-notification {:content (tr "modals.invite-restricted-members.warning")
                                 :class (stl/css :restricted-warning)
                                 :level :warning}]
       [:div {:class (stl/css :restricted-emails-section)}
        [:button {:class (stl/css :restricted-emails-toggle)
                  :type "button"
                  :aria-expanded expanded?
                  :on-click on-toggle}
         [:span {:class (stl/css :restricted-email-summary)}
          (tr "modals.invite-restricted-members.blocked-addresses")]
         [:> icon* {:icon-id i/arrow
                    :size "s"
                    :class (stl/css-case :restricted-emails-arrow true
                                         :expanded expanded?)}]]
        (when expanded?
          [:ul {:class (stl/css :restricted-email-list)}
           (for [email blocked-emails]
             [:li {:key email} email])])]]

      [:div {:class (stl/css :modal-footer)}
       [:div {:class (stl/css :action-buttons :modal-invitation-action-buttons)}
        [:> button*
         {:class (stl/css :cancel-button)
          :variant "secondary"
          :type "button"
          :on-click modal/hide!}
         (tr "modals.invite-restricted-members.cancel")]
        [:> button*
         {:class (stl/css :accept-btn)
          :variant "primary"
          :type "button"
          :on-click (fn []
                      (modal/hide!)
                      (on-accept))}
         (tr "modals.invite-restricted-members.send")]]]]]))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MEMBERS SECTION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(mf/defc member-info*
  [{:keys [member profile]}]
  (let [is-you? (= (:id profile) (:id member))]
    [:*
     [:img  {:class (stl/css :member-image)
             :src (cfg/resolve-profile-photo-url member)}]
     [:div {:class (stl/css :member-info)}
      [:div {:class (stl/css :member-name)} (:name member)
       (when is-you?
         [:span {:class (stl/css :you)} (tr "labels.you")])]
      [:div {:class (stl/css :member-email)} (:email member)]]]))

(mf/defc rol-info*
  [{:keys [member team on-set-admin on-set-editor on-set-owner on-set-viewer profile]}]
  (let [member-is-owner  (:is-owner member)
        member-is-admin  (and (:is-admin member) (not member-is-owner))
        member-is-editor (and (:can-edit member) (and (not member-is-admin) (not member-is-owner)))
        member-is-viewer (and (not member-is-editor) (not member-is-admin) (not member-is-owner))
        show?            (mf/use-state false)

        permissions      (:permissions team)
        is-owner         (:is-owner permissions)
        is-admin         (:is-admin permissions)

        is-you           (= (:id profile) (:id member))

        can-change-rol   (or is-owner is-admin)
        not-superior     (or (and (not member-is-owner) is-admin)
                             (and can-change-rol (or member-is-admin member-is-editor member-is-viewer)))

        role             (cond
                           member-is-owner  "labels.owner"
                           member-is-admin  "labels.admin"
                           member-is-editor "labels.editor"
                           :else            "labels.viewer")

        on-show          (mf/use-fn #(reset! show? true))
        on-hide          (mf/use-fn #(reset! show? false))]
    [:*
     (if (and can-change-rol not-superior (not (and is-you is-owner)))
       [:div {:class (stl/css :rol-selector :has-priv)
              :role "combobox"
              :aria-labelledby "role-label-id"
              :on-click on-show}
        [:span {:class (stl/css :rol-label)
                :id "role-label-id"} (tr role)]
        arrow-icon]
       [:div {:class (stl/css :rol-selector)}
        [:span {:class (stl/css :rol-label)} (tr role)]])

     [:& dropdown {:show @show? :on-close on-hide :dropdown-id (str "member-role-" (:id member))}
      [:ul {:class (stl/css :roles-dropdown)
            :role "listbox"}
       [:li {:on-click on-set-viewer
             :class (stl/css :rol-dropdown-item)}
        (tr "labels.viewer")]
       [:li {:on-click on-set-editor
             :class (stl/css :rol-dropdown-item)}
        (tr "labels.editor")]
       [:li {:on-click on-set-admin
             :class (stl/css :rol-dropdown-item)}
        (tr "labels.admin")]
       (when is-owner
         [:li {:on-click (partial on-set-owner member)
               :class (stl/css :rol-dropdown-item)}
          (tr "labels.owner")])]]]))

(mf/defc member-actions*
  [{:keys [member team on-delete on-leave profile]}]
  (let [is-owner?   (:is-owner member)
        owner?      (dm/get-in team [:permissions :is-owner])
        admin?      (dm/get-in team [:permissions :is-admin])
        show?       (mf/use-state false)
        is-you?     (= (:id profile) (:id member))
        can-delete? (or owner? admin?)

        on-show     (mf/use-fn #(reset! show? true))
        on-hide     (mf/use-fn #(reset! show? false))]


    (when (or is-you? (and can-delete? (not (and is-owner? (not owner?)))))
      [:*
       [:button {:class (stl/css :menu-btn)
                 :on-click on-show}
        menu-icon]

       [:& dropdown {:show @show? :on-close on-hide :dropdown-id (str "member-actions-" (:id member))}
        [:ul {:class (stl/css :actions-dropdown)}
         (when is-you?
           [:li {:on-click on-leave
                 :class (stl/css :action-dropdown-item)
                 :key "is-you-option"}
            (tr "dashboard.leave-team")])
         (when (and can-delete? (not is-you?) (not (and is-owner? (not owner?))))
           [:li {:on-click on-delete
                 :class (stl/css :action-dropdown-item)
                 :key "is-not-you-option"} (tr "labels.remove-member")])]]])))

(defn- set-role! [member-id role]
  (let [params {:member-id member-id :role role}]
    (st/emit! (dtm/update-member-role params))))

(mf/defc team-member*
  {::mf/wrap [mf/memo]}
  [{:keys [team member total-members profile]}]

  (let [member-id     (:id member)
        on-set-admin  (mf/use-fn (mf/deps member-id) (partial set-role! member-id :admin))
        on-set-editor (mf/use-fn (mf/deps member-id) (partial set-role! member-id :editor))
        on-set-viewer (mf/use-fn (mf/deps member-id) (partial set-role! member-id :viewer))
        owner?        (dm/get-in team [:permissions :is-owner])

        on-set-owner
        (mf/use-fn
         (mf/deps member)
         (fn [member _event]
           (let [params {:type :confirm
                         :title (tr "modals.promote-owner-confirm.title")
                         :message (tr "modals.promote-owner-confirm.message" (:name member))
                         :scd-message (tr "modals.promote-owner-confirm.hint")
                         :accept-label (tr "modals.promote-owner-confirm.accept")
                         :on-accept (partial set-role! member-id :owner)
                         :accept-style :primary}]
             (st/emit! (modal/show params)))))

        on-success
        (mf/use-fn #(rx/of (dcm/go-to-dashboard-recent :team-id :default)))

        on-error
        (mf/use-fn
         (fn [{:keys [code] :as error}]
           (condp = code
             :no-enough-members-for-leave
             (rx/of (ntf/error (tr "errors.team-leave.insufficient-members")))

             :member-does-not-exist
             (rx/of (ntf/error (tr "errors.team-leave.member-does-not-exists")))

             :owner-cant-leave-team
             (rx/of (ntf/error (tr "errors.team-leave.owner-cant-leave")))

             (rx/throw error))))

        on-delete-accepted
        (mf/use-fn
         (mf/deps team on-success on-error)
         (fn []
           (st/emit! (dtm/delete-team (with-meta team {:on-success on-success
                                                       :on-error on-error})))))

        on-leave-accepted
        (mf/use-fn
         (mf/deps on-success on-error)
         (fn [member-id]
           (let [params (cond-> {} (uuid? member-id) (assoc :reassign-to member-id))]
             (st/emit! (dtm/leave-current-team (with-meta params
                                                 {:on-success on-success
                                                  :on-error on-error}))))))

        on-leave-and-close
        (mf/use-fn
         (mf/deps on-delete-accepted)
         (fn []
           (st/emit! (modal/show
                      {:type :confirm
                       :title (tr "modals.leave-confirm.title")
                       :message  (tr "modals.leave-and-close-confirm.message" (:name team))
                       :scd-message (tr "modals.leave-and-close-confirm.hint")
                       :accept-label (tr "modals.leave-confirm.accept")
                       :on-accept on-delete-accepted}))))

        on-change-owner-and-leave
        (mf/use-fn
         (mf/deps profile team on-leave-accepted)
         (fn []
           (st/emit! (dtm/fetch-members)
                     (modal/show
                      {:type :leave-and-reassign
                       :profile profile
                       :team team
                       :accept on-leave-accepted}))))

        on-leave
        (mf/use-fn
         (mf/deps on-leave-accepted)
         (fn []
           (st/emit! (modal/show
                      {:type :confirm
                       :title (tr "modals.leave-confirm.title")
                       :message  (tr "modals.leave-confirm.message")
                       :accept-label (tr "modals.leave-confirm.accept")
                       :on-accept on-leave-accepted}))))

        on-delete
        (mf/use-fn
         (mf/deps member-id)
         (fn []
           (let [on-accept #(st/emit! (dtm/delete-member {:member-id member-id}))
                 params    {:type :confirm
                            :title (tr "modals.delete-team-member-confirm.title")
                            :message  (tr "modals.delete-team-member-confirm.message")
                            :accept-label (tr "modals.delete-team-member-confirm.accept")
                            :on-accept on-accept}]
             (st/emit! (modal/show params)))))

        on-leave'
        (cond (= 1 total-members) on-leave-and-close
              (= true owner?)       on-change-owner-and-leave
              :else                 on-leave)]

    [:div {:class (stl/css :table-row)}
     [:div {:class (stl/css :table-field :field-name)}
      [:> member-info* {:member member :profile profile}]]

     [:div {:class (stl/css :table-field :field-roles)}
      [:> rol-info*  {:member member
                      :team team
                      :on-set-admin on-set-admin
                      :on-set-editor on-set-editor
                      :on-set-viewer on-set-viewer
                      :on-set-owner on-set-owner
                      :profile profile}]]

     [:div {:class (stl/css :table-field :field-actions)}
      [:> member-actions* {:member member
                           :profile profile
                           :team team
                           :on-delete on-delete
                           :on-leave on-leave'}]]]))

(mf/defc team-members*
  {::mf/private true}
  [{:keys [team profile]}]
  (let [members (get team :members)

        total-members
        (count members)

        owner
        (mf/with-memo [members]
          (d/seek :is-owner members))

        members
        (mf/with-memo [team]
          (->> (:members team)
               (sort-by :created-at)
               (remove :is-owner)
               (vec)))]

    [:div {:class (stl/css :dashboard-table :team-members)}
     [:div {:class (stl/css :table-header)}
      [:div {:class (stl/css :table-field :title-field-name)} (tr "labels.member")]
      [:div {:class (stl/css :table-field :title-field-role)} (tr "labels.role")]]

     [:div {:class (stl/css :table-rows)}
      [:> team-member*
       {:member owner
        :team team
        :profile profile
        :total-members total-members}]

      (for [item members]
        [:> team-member*
         {:member item
          :team team
          :profile profile
          :key (dm/str (:id item))
          :total-members total-members}])]]))

(mf/defc team-members-page*
  [{:keys [team profile]}]
  (mf/with-effect [(:id team)]
    (dom/set-html-title
     (tr "title.team-members"
         (if (:is-default team)
           (tr "dashboard.your-penpot")
           (:name team)))))

  (mf/with-effect [(:id team)]
    (st/emit! (dtm/fetch-members)))

  [:*
   [:> header* {:section :dashboard-team-members
                :team team
                :profile profile}]
   [:section {:class (stl/css :dashboard-container :dashboard-team-members)}

    [:> team-members*
     {:profile profile
      :team team}]

    (when (and (contains? cfg/flags :subscriptions)
               (show-subscription-members-banner? team profile))
      [:> members-cta* {:team team}])]])

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INVITATIONS SECTION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(mf/defc invitation-role-selector*
  [{:keys [can-invite role status on-change]}]
  (let [show?   (mf/use-state false)
        label   (cond
                  (= role :owner)  (tr "labels.owner")
                  (= role :admin)  (tr "labels.admin")
                  (= role :editor) (tr "labels.editor")
                  :else            (tr "labels.viewer"))

        on-hide (mf/use-fn #(reset! show? false))
        on-show (mf/use-fn #(reset! show? true))

        on-change'
        (mf/use-fn
         (mf/deps on-change)
         (fn [event]
           (let [role (-> (dom/get-current-target event)
                          (dom/get-data "role")
                          (keyword))]
             (on-change role event))))]

    [:*
     (if (and can-invite (= status :pending))
       [:div {:class (stl/css :rol-selector :has-priv)
              :on-click on-show}
        [:span {:class (stl/css :rol-label)} label]
        arrow-icon]
       [:div {:class (stl/css :rol-selector)}
        [:span {:class (stl/css :rol-label)} label]])

     [:& dropdown {:show @show? :on-close on-hide :dropdown-id "invitation-role-selector"}
      [:ul {:class (stl/css :roles-dropdown)}
       [:li {:data-role "admin"
             :class (stl/css :rol-dropdown-item)
             :on-click on-change'}
        (tr "labels.admin")]
       [:li {:data-role "editor"
             :class (stl/css :rol-dropdown-item)
             :on-click on-change'}
        (tr "labels.editor")]
       [:li {:data-role "viewer"
             :class (stl/css :rol-dropdown-item)
             :on-click on-change'}
        (tr "labels.viewer")]]]]))

(mf/defc invitation-actions*
  {::mf/private true}
  [{:keys [invitation team-id]}]
  (let [email    (:email invitation)
        copied*  (mf/use-state false)
        copied?  (deref copied*)

        on-error
        (mf/use-fn
         (mf/deps email)
         (fn [cause]
           (let [{:keys [type code] :as error} (ex-data cause)]
             (cond
               (and (= :validation type)
                    (= :profile-is-muted code))
               (rx/of (ntf/error (tr "errors.profile-is-muted")))

               (and (= :validation type)
                    (= :member-is-muted code))
               (rx/of (ntf/error (tr "errors.member-is-muted")))

               (and (= :restriction type)
                    (or (= :email-has-permanent-bounces code)
                        (= :email-has-complaints code)))
               (rx/of (ntf/error (tr "errors.email-has-permanent-bounces" email)))

               :else
               (rx/throw cause)))))

        on-copy-success
        (mf/use-fn
         (fn []
           (reset! copied* true)
           (tm/schedule 1000 #(reset! copied* false))
           (st/emit! (ntf/success (tr "notifications.invitation-link-copied"))
                     (modal/hide))))

        on-copy
        (mf/use-fn
         (mf/deps email team-id)
         (fn []
           (let [params (with-meta {:email email :team-id team-id}
                          {:on-success on-copy-success
                           :on-error on-error})]
             (st/emit!
              (-> (dtm/copy-invitation-link params)
                  (with-meta {::ev/origin :team}))))))]

    [:> icon-button* {:variant "ghost"
                      :aria-label (tr "labels.copy-invitation-link")
                      :on-click on-copy
                      :icon (if copied? "tick" "clipboard")}]))

(mf/defc invitation-row*
  {::mf/wrap [mf/memo]
   ::mf/private true}
  [{:keys [invitation can-invite team-id selected on-select-change]}]

  (let [expired? (:expired invitation)
        email    (:email invitation)
        role     (:role invitation)
        status   (if expired? :expired :pending)
        type     (if expired? :warning :default)

        badge-content
        (if (= status :expired)
          (tr "labels.expired-invitation")
          (tr "labels.pending-invitation"))

        is-selected? (fn [email]
                       (contains? @selected email))

        on-change
        (mf/use-fn
         (mf/deps can-invite on-select-change)
         (fn [event]
           (when can-invite
             (let [email (-> (dom/get-current-target event)
                             (dom/get-data "attr"))]
               (on-select-change email)))))

        on-change-role
        (mf/use-fn
         (mf/deps email team-id)
         (fn [role _event]
           (let [params {:email email :team-id team-id :role role}
                 mdata  {:on-success #(st/emit! (dtm/fetch-invitations))}]
             (st/emit! (dtm/update-invitation-role (with-meta params mdata))))))]

    [:div {:class (stl/css :table-row :table-row-invitations)}
     [:div {:class (stl/css :table-field :field-email)}
      (if can-invite
        [:div {:class (stl/css :input-wrapper)}
         [:label
          [:span {:class (stl/css-case :input-checkbox true
                                       :global/checked (is-selected? email))}
           deprecated-icon/status-tick]

          [:input {:type "checkbox"
                   :id (dm/str "email-" email)
                   :data-attr email
                   :value email
                   :checked (is-selected? email)
                   :on-change on-change}]
          email]]
        [:div email])]

     [:div {:class (stl/css :table-field :field-roles)}
      [:> invitation-role-selector*
       {:can-invite can-invite
        :role role
        :status status
        :on-change on-change-role}]]

     [:div {:class (stl/css :table-field :field-status)}
      [:& badge-notification {:type type :content badge-content}]]

     [:div {:class (stl/css :table-field :field-actions)}
      (when ^boolean can-invite
        [:> invitation-actions*
         {:invitation invitation
          :team-id team-id}])]]))

(mf/defc empty-invitation-table*
  {::mf/private true}
  [{:keys [can-invite team]}]
  (let
   [route                (mf/deref refs/route)
    invite-email         (-> route :query-params :invite-email)
    on-invite-member     (mf/use-fn
                          (mf/deps team invite-email)
                          (fn []
                            (st/emit! (dtm/check-and-invite-members {:team-id (:id team)
                                                                     :origin :team
                                                                     :invite-email invite-email}))))]
    [:div {:class (stl/css :empty-invitations)}
     [:div (tr "labels.no-invitations")]
     (if ^boolean can-invite
       [:*
        [:div (tr "labels.no-invitations-gather-people")]
        [:> button* {:variant "primary"
                     :class (stl/css :btn-empty-invitations)
                     :on-click on-invite-member
                     :data-testid "invite-member"}
         (tr "dashboard.invite-profile")]]
       [:div {:class (stl/css :no-permission-text)} (tr "dashboard.invitations.no-permission")])]))

(mf/defc invitation-modal
  {::mf/register modal/components
   ::mf/register-as :invitation-modal}
  [{:keys [selected delete on-confirm]}]
  [:div {:class (stl/css :modal-overlay)}
   [:div {:class (stl/css :modal-invitation-container :modal-container)}
    [:div {:class (stl/css :modal-invitation-header)}
     [:h2 {:class (stl/css :modal-invitation-title)}
      (if delete
        (tr "dashboard.invitation-modal.title.delete-invitations")
        (tr "dashboard.invitation-modal.title.resend-invitations"))]

     [:button {:class (stl/css :modal-close-btn)
               :on-click modal/hide!} deprecated-icon/close]]

    [:div {:class (stl/css :modal-invitation-content)}
     [:p
      (if delete
        (tr "dashboard.invitation-modal.delete")
        (tr "dashboard.invitation-modal.resend"))]
     [:div {:class (stl/css :invitation-list)}
      (for [{:keys [email role]} selected]
        [:p {:key email}
         (str "- " email " (" (tr (str "labels." (name role))) ")")])]]

    [:div {:class (stl/css :modal-footer)}
     [:div {:class (stl/css :action-buttons :modal-invitation-action-buttons)}
      (when-not delete
        [:> button*
         {:class (stl/css :cancel-button)
          :variant "secondary"
          :type "button"
          :on-click modal/hide!}
         (tr "labels.cancel")])
      [:> button*
       {:class (stl/css :accept-btn)
        :variant "primary"
        :type "button"
        :on-click on-confirm}
       (if delete
         (tr "labels.continue")
         (tr "labels.resend"))]]]]])


(def schema:organization-form [:map {:title "SelectOrgForm"}
                               [:selected-id ::sm/uuid]])

(mf/defc render-organization-combobox-avatar*
  [{:keys [avatar]}]
  [:> organization-avatar* {:organization (:organization avatar)
                            :size (:size avatar)}])

(mf/defc select-organization-modal
  {::mf/register modal/components
   ::mf/register-as :select-organization-modal}
  [{:keys [organizations organizations-allowed current-organization-id on-confirm title-key text-key choose-key placeholder-key accept-key cancel-key info-message-key team-id]}]
  (let [valid-organizations (mf/with-memo [organizations]
                              (remove #(= (:id %) current-organization-id) organizations))
        options (mf/with-memo [valid-organizations organizations-allowed]
                  (mapv (fn [organization]
                          (let [organization-id (:id organization)
                                ;; organizations-allowed is a map of organization-id and a boolean indicating if it is allowed
                                enabled? (or (nil? organizations-allowed)
                                             (true? (get organizations-allowed organization-id)))]
                            (cond-> {:id (str organization-id)
                                     :label (:name organization)
                                     :disabled (not enabled?)
                                     :dimmed (not enabled?)
                                     :avatar {:render-fn render-organization-combobox-avatar*
                                              :organization organization
                                              :size "xl"}}
                              (not enabled?)
                              (assoc :title (tr "dashboard.team-organization.disabled-organization-tooltip")))))
                        valid-organizations))

        form (fm/use-form :schema schema:organization-form :initial {})

        warning-info* (mf/use-state nil)
        warning-info (deref warning-info*)
        selected-organization (mf/with-memo [warning-info valid-organizations]
                                (when warning-info
                                  (d/seek #(= (:id %) (:organization-id warning-info)) valid-organizations)))

        on-change
        (mf/use-fn
         (mf/deps form team-id)
         (fn [id]
           (uforms/on-input-change form :selected-id id)
           ;; Check for external invitations when selection changes
           (when (and team-id id)
             (let [organization-id (d/parse-uuid id)]
               (->> (rp/cmd! :check-team-external-invitations
                             {:team-id team-id
                              :organization-id organization-id})
                    (rx/subs!
                     (fn [result]
                       (reset! warning-info* (assoc result :organization-id organization-id)))
                     (fn [_]
                       (reset! warning-info* nil))))))))

        on-confirm'
        (mf/use-fn
         (mf/deps on-confirm form)
         (fn []
           (on-confirm (dm/get-in @form [:clean-data :selected-id]))))]
    [:div {:class (stl/css :modal-overlay)}
     [:div {:class (stl/css :modal-select-organization-container :modal-container)}
      [:div {:class (stl/css :modal-select-organization-header)}
       [:h2 {:class (stl/css :modal-select-organization-title)}
        (tr title-key)]

       [:button {:class (stl/css :modal-close-btn)
                 :on-click modal/hide!} deprecated-icon/close]]

      (when text-key
        [:div {:class (stl/css :modal-content :modal-select-organization-text)} (tr text-key)])

      [:div {:class (stl/css :modal-select-organization-body)}
       (when info-message-key
         [:div {:class (stl/css :modal-select-organization-info)}
          (tr info-message-key)])
       [:div {:class (stl/css :modal-select-organization-content)}
        (tr choose-key)]
       [:> combobox* {:id "selected-id"
                      :class (stl/css :team-member)
                      :options options
                      :select-only true
                      :default-selected (or (some-> (get-in @form [:data :selected-id]) str) "")
                      :placeholder (tr placeholder-key)
                      :on-change on-change}]

       ;; Warning for external invitations
       (when (and warning-info
                  (:has-external-invitations warning-info)
                  (not (:allows-anybody warning-info))
                  selected-organization)
         [:div {:class (stl/css :modal-select-organization-warning)}
          [:& context-notification
           {:content (tr "dashboard.select-organization-modal.external-invitations-will-be-canceled")
            :class (stl/css :external-invitations-warning)
            :level :warning}]
          [:div {:class (stl/css :modal-select-organization-content)}
           (tr "dashboard.select-organization-modal.external-invitations-warning" (:name selected-organization))]])]

      [:div {:class (stl/css :modal-footer)}
       [:div {:class (stl/css :action-buttons :modal-invitation-action-buttons)}

        [:> button*
         {:class (stl/css :cancel-button)
          :variant "secondary"
          :type "button"
          :on-click modal/hide!}
         (tr cancel-key)]
        [:> button*
         {:class (stl/css :accept-btn)
          :variant "primary"
          :type "button"
          :disabled (not (:valid @form))
          :on-click on-confirm'}
         (tr accept-key)]]]]]))

(mf/defc invitation-section*
  {::mf/private true}
  [{:keys [team profile]}]
  (let [permissions (get team :permissions)
        invitations (mf/use-state (get team :invitations))

        team-id     (get team :id)

        can-invite? (dnt/can-send-invitations?
                     {:organization (:organization team)
                      :profile-id (:id profile)
                      :team-permissions permissions})

        selected    (mf/use-state #{})

        ;; Sort state: {:field :status/:role, :direction :asc/:desc}
        sort-state  (mf/use-state {:field nil :direction :asc})

        selected-invitations (mf/with-memo [selected invitations]
                               (filterv #(contains? @selected (:email %)) @invitations))

        on-select-change
        (mf/use-fn
         (mf/deps can-invite? selected)
         (fn [email]
           (when can-invite?
             (if (contains? @selected email)
               (swap! selected disj email)
               (swap! selected conj email)))))

        on-confirm-delete
        (mf/use-fn
         (mf/deps selected team-id)
         (fn []
           (doseq [email @selected]
             (let [params {:email email :team-id team-id}
                   mdata  {:on-success #(st/emit! (ntf/success (tr "notifications.invitation-deleted"))
                                                  (dtm/fetch-invitations)
                                                  (modal/hide))}]
               (st/emit! (dtm/delete-invitation (with-meta params mdata)))))
           (reset! selected #{})))

        on-delete
        (mf/use-fn
         (mf/deps selected-invitations team-id)
         (fn []
           (st/emit! (modal/show :invitation-modal {:selected selected-invitations :delete true :on-confirm on-confirm-delete}))))

        on-error
        (fn [form]
          (let [{:keys [type code] :as error} (ex-data form)]
            (cond
              (and (= :validation type)
                   (= :profile-is-muted code))
              (st/emit! (ntf/error (tr "errors.profile-is-muted"))
                        (modal/hide))

              (and (= :validation type)
                   (= :max-invitations-by-request code))
              (st/emit! (ntf/error (tr "errors.maximum-invitations-by-request-reached" (:threshold error))))

              (and (= :restriction type)
                   (= :max-quote-reached code))
              (st/emit! (ntf/error (tr "errors.max-quote-reached" (:target error))))

              (or (= :member-is-muted code)
                  (= :email-has-permanent-bounces code)
                  (= :email-has-complaints code))
              (st/emit! (ntf/error (tr "errors.email-spam-or-permanent-bounces" (:email error))))

              :else
              (st/emit! (ntf/error (tr "errors.generic"))
                        (modal/hide)))))

        on-resend-success
        (mf/use-fn
         (fn []
           (st/emit! (ntf/success (tr "notifications.invitation-email-sent"))
                     (modal/hide)
                     (dtm/fetch-invitations))
           (reset! selected #{})))

        on-confirm-resend
        (mf/use-fn
         (mf/deps selected-invitations team-id on-resend-success)
         (fn []
           (modal/hide!)
           (let [params (with-meta {:invitations selected-invitations
                                    :team-id team-id
                                    :resend? true}
                          {:on-success on-resend-success
                           :on-error on-error})]

             (st/emit!
              (-> (dtm/create-invitations params)
                  (with-meta {::ev/origin :team}))))))

        on-resend
        (mf/use-fn
         (mf/deps team-id selected-invitations)
         (fn []
           (st/emit! (modal/show :invitation-modal {:selected selected-invitations :on-confirm on-confirm-resend}))))

        on-order-by-status
        (mf/use-fn
         (mf/deps sort-state)
         (fn []
           (let [current-field (:field @sort-state)
                 current-direction (:direction @sort-state)
                 new-direction (if (= current-field :status)
                                 (if (= current-direction :asc) :desc :asc)
                                 :asc)]
             (swap! sort-state assoc :field :status :direction new-direction)
             (swap! invitations #(let [sorted (sort-by (juxt :expired :email) %)]
                                   (if (= new-direction :desc)
                                     (reverse sorted)
                                     sorted))))))

        on-order-by-role
        (mf/use-fn
         (mf/deps sort-state)
         (fn []
           (let [current-field (:field @sort-state)
                 current-direction (:direction @sort-state)
                 new-direction (if (= current-field :role)
                                 (if (= current-direction :asc) :desc :asc)
                                 :asc)]
             (swap! sort-state assoc :field :role :direction new-direction)
             (swap! invitations #(let [sorted (sort-by (juxt :role :email) %)]
                                   (if (= new-direction :desc)
                                     (reverse sorted)
                                     sorted))))))]

    (mf/with-effect [team]
      (reset! invitations (get team :invitations))
      (reset! sort-state {:field nil :direction :asc}))

    [:div {:class (stl/css :invitations)}
     (when (and (not can-invite?)
                (seq @invitations))
       [:div {:class (stl/css :empty-invitations)}
        [:div {:class (stl/css :no-permission-text)}
         (tr "dashboard.invitations.no-permission")]])
     (when (and can-invite?
                (> (count @selected) 0))
       [:*
        [:div {:class (stl/css :invitations-actions)}
         [:div
          (tr "team.invitations-selected" (i18n/c (count @selected)))]
         [:div
          [:> button* {:variant "secondary"
                       :type "button"
                       :on-click on-resend}
           (tr "labels.resend-invitation")]]
         [:> icon-button* {:on-click on-delete
                           :variant "destructive"
                           :aria-label (tr "labels.delete-invitation")
                           :icon "delete"}]]])
     [:div {:class (stl/css :table-header)}
      [:div {:class (stl/css :title-field-name)} (tr "labels.invitations")]
      [:div {:class (stl/css :title-field-role)} (tr "labels.role")
       [:> icon-button* {:variant "action"
                         :class (stl/css-case :sort-active (= (:field @sort-state) :role)
                                              :sort-inactive (not= (:field @sort-state) :role))
                         :aria-label (tr "dashboard.order-invitations-by-role")
                         :icon (if (= (:field @sort-state) :role)
                                 (if (= (:direction @sort-state) :asc) "arrow-down" "arrow-up")
                                 "arrow-down")
                         :on-click on-order-by-role}]]
      [:div {:class (stl/css :title-field-status)} (tr "labels.status")
       [:> icon-button* {:variant "action"
                         :class (stl/css-case :sort-active (= (:field @sort-state) :status)
                                              :sort-inactive (not= (:field @sort-state) :status))
                         :aria-label (tr "dashboard.order-invitations-by-status")
                         :icon (if (= (:field @sort-state) :status)
                                 (if (= (:direction @sort-state) :asc) "arrow-down" "arrow-up")
                                 "arrow-down")
                         :on-click on-order-by-status}]]]
     (if (empty? @invitations)
       [:> empty-invitation-table* {:can-invite can-invite? :team team}]
       [:div {:class (stl/css :table-rows)}
        (for [invitation @invitations]
          [:> invitation-row*
           {:key (:email invitation)
            :invitation invitation
            :can-invite can-invite?
            :team-id team-id
            :selected selected
            :on-select-change on-select-change}])])]))

(mf/defc team-invitations-page*
  [{:keys [team profile]}]

  (mf/with-effect [team]
    (dom/set-html-title
     (tr "title.team-invitations"
         (if (:is-default team)
           (tr "dashboard.your-penpot")
           (:name team)))))

  (mf/with-effect [(:id team)]
    (st/emit! (dtm/fetch-invitations)))

  [:*
   [:> header* {:section :dashboard-team-invitations
                :team team
                :profile profile}]
   [:section {:class (stl/css :dashboard-team-invitations)}

    [:> invitation-section* {:team team :profile profile}]

    (when (and (contains? cfg/flags :subscriptions)
               (show-subscription-members-banner? team profile))
      [:> members-cta* {:team team}])]])

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; WEBHOOKS SECTION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def ^:private schema:webhook-form
  [:map {:title "WebhookForm"}
   [:uri [::sm/uri {:max 4069 :prefix #"^http[s]?://"
                    :error/code "errors.webhooks.invalid-uri"}]]
   [:mtype ::sm/text]])

(def valid-webhook-mtypes
  [{:label "application/json" :value "application/json"}
   {:label "application/transit+json" :value "application/transit+json"}])

(defn- extract-status
  [error-code]
  (-> error-code (str/split #":") second str/trim))

(defn- translate-error-hint
  [hint]
  (cond
    (= hint "invalid-uri")
    (tr "errors.webhooks.invalid-uri")

    (= hint "ssl-validation-error")
    (tr "errors.webhooks.ssl-validation")

    (= hint "timeout")
    (tr "errors.webhooks.timeout")

    (= hint "connection-error")
    (tr "errors.webhooks.connection")

    (str/starts-with? hint "unexpected-status")
    (tr "errors.webhooks.unexpected-status" (extract-status hint))

    (str/starts-with? hint "blocked-request")
    (tr "errors.webhooks.connection")

    :else
    (tr "errors.webhooks.unexpected")))

(mf/defc webhook-modal
  {::mf/register modal/components
   ::mf/register-as :webhook}
  [{:keys [webhook]}]

  (let [initial (mf/with-memo []
                  (or (some-> webhook (update :uri str))
                      {:is-active false :mtype "application/json"}))
        form    (fm/use-form :schema schema:webhook-form
                             :initial initial)
        on-success
        (mf/use-fn
         (fn [_]
           (let [message (tr "dashboard.webhooks.create.success")]
             (rx/of (ntf/success message)
                    (modal/hide)))))

        on-error
        (mf/use-fn
         (fn [form cause]
           (let [{:keys [type code hint] :as error} (ex-data cause)]
             (if (and (= type :validation)
                      (= code :webhook-validation))
               (let [message (translate-error-hint hint)]
                 (swap! form assoc-in [:extra-errors :uri] {:message message})
                 (rx/empty))
               (rx/throw cause)))))

        on-create-submit
        (mf/use-fn
         (fn [form]
           (let [cdata  (:clean-data @form)
                 mdata  {:on-success (partial on-success form)
                         :on-error   (partial on-error form)}
                 params {:uri        (:uri cdata)
                         :mtype      (:mtype cdata)
                         :is-active  (:is-active cdata)}]
             (st/emit! (dtm/create-webhook
                        (with-meta params mdata))))))

        on-update-submit
        (mf/use-fn
         (fn [form]
           (let [params (:clean-data @form)
                 mdata  {:on-success (partial on-success form)
                         :on-error   (partial on-error form)}]
             (st/emit! (dtm/update-webhook
                        (with-meta params mdata))))))

        on-submit
        (mf/use-fn
         (fn [form]
           (let [data (:clean-data @form)]
             (if (:id data)
               (on-update-submit form)
               (on-create-submit form)))))]

    [:div {:class (stl/css :modal-overlay)}
     [:div {:class (stl/css :modal-container)}
      [:& fm/form {:form form :on-submit on-submit}
       [:div {:class (stl/css :modal-header)}
        [:h2 {:class (stl/css :modal-title)}
         (if webhook
           (tr "modals.edit-webhook.title")
           (tr "modals.create-webhook.title"))]

        [:button {:class (stl/css :modal-close-btn)
                  :on-click modal/hide!} deprecated-icon/close]]

       [:div {:class (stl/css :modal-content)}
        [:div {:class (stl/css :fields-row)}
         [:& fm/input {:type "text"
                       :auto-focus? true
                       :form form
                       :name :uri
                       :label (tr "modals.create-webhook.url.label")
                       :placeholder (tr "modals.create-webhook.url.placeholder")}]]
        [:div  {:class (stl/css :fields-row)}
         [:div {:class (stl/css :select-title)} (tr "dashboard.webhooks.content-type")]
         [:& fm/select {:options valid-webhook-mtypes
                        :default "application/json"
                        :name :mtype}]]
        [:div  {:class (stl/css :fields-row)}
         [:& fm/input {:type "checkbox"
                       :class (stl/css :custom-input-checkbox)
                       :form form
                       :name :is-active
                       :label (tr "dashboard.webhooks.active")}]
         [:div {:class (stl/css :hint)} (tr "dashboard.webhooks.active.explain")]]]

       [:div {:class (stl/css :modal-footer)}
        [:div {:class (stl/css :action-buttons)}
         [:input {:class (stl/css :cancel-button)
                  :type "button"
                  :value (tr "labels.cancel")
                  :on-click modal/hide!}]
         [:> fm/submit-button*
          {:label (if webhook
                    (tr "modals.edit-webhook.submit-label")
                    (tr "modals.create-webhook.submit-label"))}]]]]]]))

(mf/defc webhooks-hero*
  []
  [:div {:class (stl/css :webhooks-hero-container)}
   [:h2 {:class (stl/css :hero-title)}
    (tr "labels.webhooks")]
   [:> i18n/tr-html* {:class (stl/css :hero-desc)
                      :content (tr "dashboard.webhooks.description")}]
   [:button {:class (stl/css :hero-btn)
             :on-click #(st/emit! (modal/show :webhook {}))}
    (tr "dashboard.webhooks.create")]])

(mf/defc webhook-actions*
  {::mf/private true}
  [{:keys [on-edit on-delete can-edit]}]
  (let [show?   (mf/use-state false)
        on-show (mf/use-fn #(reset! show? true))
        on-hide (mf/use-fn #(reset! show? false))]
    (if can-edit
      [:*
       [:button {:class (stl/css :menu-btn)
                 :on-click on-show}
        menu-icon]
       [:& dropdown {:show @show? :on-close on-hide :dropdown-id "webhook-actions"}
        [:ul {:class (stl/css :webhook-actions-dropdown)}
         [:li {:on-click on-edit
               :class (stl/css :webhook-dropdown-item)} (tr "labels.edit")]
         [:li {:on-click on-delete
               :class (stl/css :webhook-dropdown-item)} (tr "labels.delete")]]]]

      [:span {:title (tr "dashboard.webhooks.cant-edit")
              :class (stl/css :menu-disabled)}
       [:> icon* {:icon-id i/menu}]])))

(mf/defc webhook-item*
  {::mf/wrap [mf/memo]
   ::mf/private true}
  [{:keys [webhook permissions]}]
  (let [error-code (:error-code webhook)
        id         (:id webhook)
        creator-id (:profile-id webhook)
        profile    (mf/deref refs/profile)
        user-id    (:id profile)
        can-edit   (or (:can-edit permissions)
                       (= creator-id user-id))
        on-edit
        (mf/use-fn
         (mf/deps webhook)
         (fn []
           (st/emit! (modal/show :webhook {:webhook webhook}))))

        on-delete-accepted
        (mf/use-fn
         (mf/deps id)
         (fn []
           (let [params {:id id}
                 mdata  {:on-success #(st/emit! (dtm/fetch-webhooks))}]
             (st/emit! (dtm/delete-webhook (with-meta params mdata))))))

        on-delete
        (mf/use-fn
         (mf/deps on-delete-accepted)
         (fn []
           (let [params {:type :confirm
                         :title (tr "modals.delete-webhook.title")
                         :message (tr "modals.delete-webhook.message")
                         :accept-label (tr "modals.delete-webhook.accept")
                         :on-accept on-delete-accepted}]
             (st/emit! (modal/show params)))))

        last-delivery-text
        (if (nil? error-code)
          (tr "webhooks.last-delivery.success")
          (dm/str (tr "errors.webhooks.last-delivery")
                  (cond
                    (= error-code "ssl-validation-error")
                    (dm/str " " (tr "errors.webhooks.ssl-validation"))

                    (str/starts-with? error-code "unexpected-status")
                    (dm/str " " (tr "errors.webhooks.unexpected-status" (extract-status error-code)))

                    :else
                    (dm/str " " (tr "errors.webhooks.unexpected")))))]

    [:div {:class (stl/css :table-row :webhook-row)}
     [:div {:class (stl/css :table-field :last-delivery)
            :title last-delivery-text}
      (if (nil? error-code)
        success-icon
        warning-icon)]
     [:div {:class (stl/css :table-field :uri)}
      [:div (dm/str (:uri webhook))]]
     [:div {:class (stl/css :table-field :active)}
      [:div (if (:is-active webhook)
              (tr "labels.active")
              (tr "labels.inactive"))]]
     [:div {:class (stl/css :table-field :actions)}
      [:> webhook-actions*
       {:on-edit on-edit
        :on-delete on-delete
        :can-edit can-edit}]]]))

(mf/defc webhooks-list*
  {::mf/private true}
  [{:keys [webhooks permissions]}]
  [:div {:class (stl/css :table-rows :webhook-table)}
   (for [webhook webhooks]
     [:> webhook-item*
      {:webhook webhook
       :key (dm/str (:id webhook))
       :permissions permissions}])])

(mf/defc webhooks-page*
  [{:keys [team]}]
  (let [webhooks (:webhooks team)]

    (mf/with-effect [team]
      (dom/set-html-title
       (tr "title.team-webhooks"
           (if (:is-default team)
             (tr "dashboard.your-penpot")
             (:name team)))))

    (mf/with-effect []
      (st/emit! (dtm/fetch-webhooks)))

    [:*
     [:> header* {:team team :section :dashboard-team-webhooks}]
     [:section {:class (stl/css :dashboard-container :dashboard-team-webhooks)}
      [:*
       [:> webhooks-hero* {}]
       (if (empty? webhooks)
         [:div {:class (stl/css :webhooks-empty)}
          [:div (tr "dashboard.webhooks.empty.no-webhooks")]
          [:div (tr "dashboard.webhooks.empty.add-one")]]
         [:> webhooks-list*
          {:webhooks webhooks
           :permissions (:permissions team)}])]]]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SETTINGS SECTION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(mf/defc team-settings-page*
  [{:keys [team]}]
  (let [nitrate?    (contains? cfg/flags :admin-console)
        finput      (mf/use-ref)

        members     (get team :members)
        stats       (get team :stats)

        owner       (d/seek :is-owner members)

        permissions (:permissions team)
        can-edit    (or (:is-owner permissions)
                        (:is-admin permissions))

        profile       (mf/deref refs/profile)
        profile-id    (:id profile)

        all-organizations (mf/deref refs/teams)
        all-organizations (mf/with-memo [all-organizations]
                            (->> (vals all-organizations)
                                 (filter :is-default)
                                 (filter :organization)
                                 (map dtm/team->organization)))

        ;; Filter to organizations where user is allowed to create/add teams
        organizations (mf/with-memo [all-organizations profile-id]
                        (->> all-organizations
                             (filter (fn [organization]
                                       (let [perm      (get-in organization [:permissions :create-teams])
                                             is-owner? (= profile-id (:owner-id organization))]
                                         (or (= perm "any") is-owner?))))))

        ;; Keep parity with UX requirement: hide only when user belongs to one organization.
        can-change-organization? (mf/with-memo [all-organizations]
                                   (> (count all-organizations) 1))

        can-add-to-organization? (mf/with-memo [organizations all-organizations permissions]
                                   (and (:is-owner permissions)
                                        (pos? (count all-organizations))
                                        (not (:is-default team))))

        show-organization-options-menu*
        (mf/use-state false)

        show-organization-options-menu?
        (deref show-organization-options-menu*)

        on-show-options-click
        (mf/use-fn
         (fn [event]
           (dom/stop-propagation event)
           (swap! show-organization-options-menu* not)))

        close-organization-options-menu
        (mf/use-fn #(reset! show-organization-options-menu* false))

        on-image-click
        (mf/use-fn #(dom/click (mf/ref-val finput)))

        on-file-selected
        (fn [file]
          (st/emit! (dtm/update-team-photo file)))

        on-remove-team-from-organization
        (mf/use-fn
         (mf/deps team)
         (fn []
           (st/emit! (dnt/show-remove-team-from-organization-modal {:team-id (:id team)}))))

        on-add-team-to-organization
        (mf/use-fn
         (mf/deps team)
         (fn []
           (st/emit! (dnt/show-add-team-to-organization-modal {:team-id (:id team)}))))

        on-change-team-organization
        (mf/use-fn
         (mf/deps team)
         (fn []
           (st/emit! (dnt/show-change-team-organization-modal {:team-id (:id team)}))))]

    (mf/with-effect [team]
      (dom/set-html-title (tr "title.team-settings"
                              (if (:is-default team)
                                (tr "dashboard.your-penpot")
                                (:name team)))))

    (mf/with-effect []
      (st/emit! (dtm/fetch-members)
                (dtm/fetch-stats)))

    [:*
     [:> header* {:section :dashboard-team-settings :team team}]
     [:section {:class (stl/css :dashboard-team-settings)}
      [:div {:class (stl/css :settings-container)}
       [:div {:class (stl/css :block :info-block)}
        [:div {:class (stl/css :team-icon)}
         (when can-edit
           [:button {:class (stl/css :update-overlay)
                     :on-click on-image-click}
            image-icon])
         [:img {:class (stl/css :team-image)
                :src (cfg/resolve-team-photo-url team)}]
         (when can-edit
           [:& file-uploader {:accept "image/jpeg,image/png"
                              :multi false
                              :ref finput
                              :on-selected on-file-selected}])]
        [:div {:class (stl/css :block-label)}
         (tr "dashboard.team-info")]
        [:div {:class (stl/css :block-text)}
         (:name team)]]

       (when nitrate?
         [:div {:class (stl/css :block)}
          [:div {:class (stl/css :block-label)}
           (tr "dashboard.team-organization")]
          (let [organization (:organization team)]
            (if organization
              [:div {:class (stl/css :block-content)}
               [:div {:class (stl/css :organization-block-content)}
                [:> organization-avatar* {:organization (dtm/team->organization team) :size "xxxl"}]
                [:span {:class (stl/css :block-text)}
                 (:name organization)]

                (when (and (:is-owner permissions) (not (:is-default team)))
                  [:*
                   [:> button* {:variant "ghost"
                                :type "button"
                                :class (stl/css-case :organization-options-btn (not show-organization-options-menu?) :organization-options-btn-open show-organization-options-menu?)
                                :on-click on-show-options-click}
                    organization-menu-icon

                    [:& dropdown {:show show-organization-options-menu? :on-close close-organization-options-menu :dropdown-id "organization-options"}
                     [:ul {:class (stl/css :organization-dropdown)
                           :role "listbox"}
                      (when can-change-organization?
                        [:li {:on-click on-change-team-organization
                              :class (stl/css :organization-dropdown-item)}
                         (tr "dashboard.team-organization.change")])
                      [:li {:on-click on-remove-team-from-organization
                            :class (stl/css :organization-dropdown-item)}
                       (tr "dashboard.team-organization.remove")]]]]])]]
              [:*
               [:div {:class (stl/css :block-content)}
                [:span {:class (stl/css :block-text)}
                 (tr "dashboard.team-organization.none")]]
               (when can-add-to-organization?
                 [:div {:class (stl/css :block-content)}
                  [:span {:class (stl/css :block-text)}
                   [:a {:on-click on-add-team-to-organization} (tr "dashboard.team-organization.add")]]])]))])

       [:div {:class (stl/css :block)}
        [:div {:class (stl/css :block-label)}
         (tr "dashboard.team-members")]

        [:div {:class (stl/css :block-content)}
         [:img {:class (stl/css :owner-icon)
                :src (cfg/resolve-profile-photo-url owner)}]
         [:span {:class (stl/css :block-text)}
          (str (:name owner) " ("  (tr "labels.owner") ")")]]

        [:div {:class (stl/css :block-content)}
         user-icon
         [:span {:class (stl/css :block-text)}
          (tr "dashboard.num-of-members" (count members))]]]

       [:div {:class (stl/css :block)}
        [:div {:class (stl/css :block-label)}
         (tr "dashboard.team-projects")]

        [:div {:class (stl/css :block-content)}
         group-icon
         [:span {:class (stl/css :block-text)}
          (tr "labels.num-of-projects" (i18n/c (dec (:projects stats))))]]

        [:div {:class (stl/css :block-content)}
         document-icon
         [:span {:class (stl/css :block-text)}
          (tr "labels.num-of-files" (i18n/c (:files stats)))]]]

       (when (contains? cfg/flags :subscriptions)
         [:> team* {:is-owner (:is-owner permissions) :team team}])]]]))
