;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL

(ns app.rpc.commands.teams-invitations
  (:require
   [app.binfile.common :as bfc]
   [app.common.data :as d]
   [app.common.data.macros :as dm]
   [app.common.exceptions :as ex]
   [app.common.features :as cfeat]
   [app.common.logging :as l]
   [app.common.schema :as sm]
   [app.common.time :as ct]
   [app.common.types.organization :as cto]
   [app.common.types.team :as types.team]
   [app.common.uuid :as uuid]
   [app.config :as cf]
   [app.db :as db]
   [app.email :as eml]
   [app.email.blacklist :as email.blacklist]
   [app.loggers.audit :as audit]
   [app.main :as-alias main]
   [app.nitrate :as nitrate]
   [app.rpc :as-alias rpc]
   [app.rpc.commands.profile :as profile]
   [app.rpc.commands.teams :as teams]
   [app.rpc.doc :as-alias doc]
   [app.rpc.helpers :as rph]
   [app.rpc.quotes :as quotes]
   [app.setup :as-alias setup]
   [app.tokens :as tokens]
   [app.util.services :as sv]
   [cuerdas.core :as str]))

;; --- Mutation: Create Team Invitation

(def sql:upsert-team-invitation
  "insert into team_invitation(id, team_id, org_id, email_to, created_by, role, valid_until)
   values (?, ?, null, ?, ?, ?, ?)
       on conflict(team_id, email_to) do
          update set role = ?, valid_until = ?, updated_at = now()
   returning *")

(def sql:upsert-organization-invitation
  "insert into team_invitation(id, team_id, org_id, email_to, created_by, role, valid_until)
    values (?, null, ?, ?, ?, ?, ?)
        on conflict(org_id, email_to) where team_id is null do
           update set role = ?, valid_until = ?, updated_at = now()
    returning *")

(def ^:private sql:check-recent-invitation
  "SELECT 1 FROM team_invitation
   WHERE team_id = ? AND email_to = ?
     AND updated_at > now() - interval '5 minutes'
   LIMIT 1")

(def ^:private sql:check-recent-org-invitation
  "SELECT 1 FROM team_invitation
   WHERE org_id = ? AND email_to = ?
     AND updated_at > now() - interval '5 minutes'
   LIMIT 1")

(defn- recently-invited?
  [{:keys [::db/conn]} team-id org-id email]
  (let [query (if org-id
                [sql:check-recent-org-invitation org-id email]
                [sql:check-recent-invitation team-id email])]
    (some? (db/exec-one! conn query))))

(defn- create-invitation-token
  [cfg {:keys [profile-id valid-until organization-id organization-name team-id member-id member-email role]}]
  (tokens/generate cfg
                   {:iss :team-invitation
                    :exp valid-until
                    :profile-id profile-id
                    :role role
                    :team-id team-id
                    :organization-id organization-id
                    :organization-name organization-name
                    :member-email member-email
                    :member-id member-id}))

(defn- create-profile-identity-token
  [cfg profile-id]
  (assert (uuid? profile-id) "expected valid uuid for profile-id")
  (tokens/generate cfg
                   {:iss :profile-identity
                    :profile-id profile-id
                    :exp (ct/in-future {:days 30})}))

(def ^:private schema:create-invitation
  [:map {:title "params:create-invitation"}
   [::rpc/profile-id ::sm/uuid]
   [:team
    [:map
     [:id ::sm/uuid]
     [:name :string]]]
   [:profile
    [:map
     [:id ::sm/uuid]
     [:fullname :string]]]
   [:role types.team/schema:role]
   [:email ::sm/email]])

(def ^:private schema:create-organization-invitation
  [:map {:title "params:create-organization-invitation"}
   [::rpc/profile-id ::sm/uuid]
   [:organization cto/schema:organization-with-avatar]
   [:profile
    [:map
     [:id ::sm/uuid]
     [:fullname :string]]]
   [:role types.team/schema:role]
   [:email ::sm/email]])

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

(def ^:private check-create-organization-invitation-params
  (sm/check-fn schema:create-organization-invitation))

(defn- allow-invitation-emails?
  [member]
  (let [notifications (dm/get-in member [:props :notifications])]
    (not= :none (:email-invites notifications))))

(defn- assert-email-can-be-invited
  "Asserts that member is an organization member when the organization
  restricts who can be added to teams."
  [member organization-member-ids]
  (when (some? organization-member-ids)
    (let [is-member? (and (some? member) (contains? organization-member-ids (:id member)))]
      (when-not is-member?
        (ex/raise :type :validation
                  :code :email-not-organization-member
                  :hint "The invited email is not a member of the organization")))))

(defn- create-invitation
  [{:keys [::db/conn] :as cfg}
   {:keys [team organization profile role email organization-member-ids all-organization-member-ids] :as params}]

  (assert (db/connection-map? cfg)
          "expected cfg with valid connection")
  (if organization
    (assert (check-create-organization-invitation-params params))
    (assert (check-create-invitation-params params)))

  (let [email  (profile/clean-email email)
        member (profile/get-profile-by-email conn email)]

    (when (and (email.blacklist/enabled? cfg)
               (email.blacklist/contains? cfg email))
      (ex/raise :type :restriction
                :code :email-domain-is-not-allowed
                :hint "email domain is in the blacklist"))

    ;; When nitrate is active and the team belongs to an organization, check that
    ;; the email is already an organization member unless the organization explicitly allows adding anybody.
    (when (and (contains? cf/flags :admin-console)
               (:organization team))
      (assert-email-can-be-invited member organization-member-ids))


    ;; When we have email verification disabled and invitation user is
    ;; already present in the database, we proceed to add it to the
    ;; team as-is, without email roundtrip.

    ;; TODO: if member does not exists and email verification is
    ;; disabled, we should proceed to create the profile (?)
    (if (and (not (contains? cf/flags :email-verification))
             (some? member))
      (let [params (merge {:team-id (:id team)
                           :profile-id (:id member)}
                          (get types.team/permissions-for-role role))]

        (if organization
          ;; Insert the invited member to the organization
          (when (contains? cf/flags :admin-console)
            (teams/initialize-user-in-organization cfg (:id member) (:id organization) email))
          ;; Insert the invited member to the team
          (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}))

        ;; If profile is not yet verified, mark it as verified because
        ;; accepting an invitation link serves as verification.
        (when-not (:is-active member)
          (db/update! conn :profile
                      {:is-active true}
                      {:id (:id member)}))

        nil)

      (do
        (some->> member (teams/check-profile-muted conn))
        (teams/check-email-bounce conn email true)
        (teams/check-email-spam conn email true)

        (let [id              (uuid/next)
              expire          (if organization
                                (ct/in-future "876000h")  ;; Organization invitations doesn't expire
                                (ct/in-future "168h")) ;; 7 days
              recent?         (recently-invited? cfg (:id team) (:id organization) email)
              invitation      (db/exec-one! conn (if organization
                                                   [sql:upsert-organization-invitation id
                                                    (:id organization)
                                                    (str/lower email)
                                                    (:id profile)
                                                    (name role) expire
                                                    (name role) expire]
                                                   [sql:upsert-team-invitation id
                                                    (:id team)
                                                    (str/lower email)
                                                    (:id profile)
                                                    (name role) expire
                                                    (name role) expire]))
              updated?        (not= id (:id invitation))
              profile-id      (:id profile)
              team-organization-id (get-in team [:organization :id])
              tprops          {:profile-id profile-id
                               :invitation-id (:id invitation)
                               :valid-until expire
                               :team-id (:id team)
                               :organization-id (:id organization)
                               :organization-name (:name organization)
                               :member-email (:email-to invitation)
                               :member-id (:id member)
                               :role role}
              audit-props
              (cond-> {:invitation-id (:id invitation)
                       :valid-until expire
                       :team-id (:id team)
                       :organization-id (:id organization)
                       :organization-name (:name organization)
                       :member-email (:email-to invitation)
                       :member-id (:id member)
                       :role role}
                organization
                (assoc :user-who-send-invitation (str profile-id))

                (not organization)
                (assoc :team-belongs-to-organization (boolean team-organization-id)
                       :adds-invitee-to-organization (boolean team-organization-id)
                       :invitee-already-organization-member
                       (boolean
                        (and team-organization-id
                             member
                             (contains? all-organization-member-ids (:id member))))))
              itoken          (create-invitation-token cfg tprops)
              ptoken          (create-profile-identity-token cfg profile-id)]

          (when (contains? cf/flags :log-invitation-tokens)
            (l/info :hint "invitation token" :token itoken))

          (let [props  (audit/clean-props audit-props)
                evname (cond
                         (and updated? organization) "update-organization-invitation"
                         updated? "update-team-invitation"
                         organization "create-organization-invitation"
                         :else "create-team-invitation")
                event (-> (audit/event-from-rpc-params params)
                          (assoc :name evname)
                          (assoc :props props))]
            (audit/submit cfg event))

          (when (and (allow-invitation-emails? member)
                     (not recent?))
            (if organization
              (when (contains? cf/flags :admin-console)
                (eml/send! {::eml/conn conn
                            ::eml/factory eml/invite-to-organization
                            :public-uri (cf/get :public-uri)
                            :to email
                            :invited-by (:fullname profile)
                            :user-name (:fullname member)
                            :organization organization
                            :token itoken
                            :extra-data ptoken}))
              (eml/send! {::eml/conn conn
                          ::eml/factory eml/invite-to-team
                          :public-uri (cf/get :public-uri)
                          :to email
                          :invited-by (:fullname profile)
                          :team (:name team)
                          :organization (:organization team)
                          :token itoken
                          :extra-data ptoken})))

          itoken)))))

(defn create-organization-invitation
  [cfg {:keys [::rpc/profile-id] :as params}]
  (let [profile  (db/get-by-id cfg :profile profile-id)]
    (create-invitation cfg
                       (assoc params
                              :profile profile
                              :role :editor))))

(defn- add-member-to-team
  [{:keys [::db/conn] :as cfg} profile team role member]
  (assert (db/connection-map? cfg)
          "expected cfg with valid connection")

  (let [team-id (:id team)
        params  (merge
                 {:team-id team-id
                  :profile-id (:id member)}
                 (get types.team/permissions-for-role role))]

    ;; Do not allow blocked users to join teams.
    (when (:is-blocked member)
      (ex/raise :type :restriction
                :code :profile-blocked))

    (quotes/check!
     {::db/conn conn
      ::quotes/id ::quotes/profiles-per-team
      ::quotes/profile-id (:id member)
      ::quotes/team-id team-id})

    ;; Insert the member to the team
    (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true})

    ;; Delete any request
    (db/delete! conn :team-access-request
                {:team-id team-id :requester-id (:id member)})

    ;; Delete any invitation
    (db/delete! conn :team-invitation
                {:team-id team-id :email-to (:email member)})

    (eml/send! {::eml/conn conn
                ::eml/factory eml/join-team
                :public-uri (cf/get :public-uri)
                :to (:email member)
                :invited-by (:fullname profile)
                :team (:name team)
                :team-id (:id team)})))

(def ^:private sql:valid-access-request-profiles
  "SELECT p.id, p.email, p.is_blocked
     FROM team_access_request AS tr
     JOIN profile AS p ON (tr.requester_id = p.id)
    WHERE tr.team_id = ?
      AND tr.auto_join_until > now()
      AND (p.deleted_at IS NULL OR
           p.deleted_at > now())")

(defn- get-valid-access-request-profiles
  [conn team-id]
  (db/exec! conn [sql:valid-access-request-profiles team-id]))

(def ^:private xf:map-email (map :email))

(defn- create-team-invitations
  "Unified function to handle both create and resend team invitations.
  Accepts either:
  - emails (set) + role (single role for all emails)
  - invitations (vector of {:email :role} maps)"
  [{:keys [::db/conn] :as cfg} {:keys [profile team role emails invitations] :as params}]
  (let [;; Enrich team with organization info once for all invitations when nitrate is active
        team             (if (contains? cf/flags :admin-console)
                           (nitrate/add-organization-info-to-team cfg team {})
                           team)
        organization              (:organization team)
        organization-id           (:id organization)
        restricted?      (and organization-id (not (cto/allowed? :add-anybody-to-team {:organization-perms organization})))
        all-organization-member-ids
        (when organization-id
          (into #{} (nitrate/call cfg :get-organization-members {:organization-id organization-id})))
        organization-member-ids (when restricted? all-organization-member-ids)
        params           (assoc params
                                :team team
                                :organization-member-ids organization-member-ids
                                :all-organization-member-ids all-organization-member-ids)

        ;; Normalize input to a consistent format: [{:email :role}]
        invitation-data  (cond
                           ;; Case 1: emails + single role (create invitations style)
                           (and emails role)
                           (map (fn [email] {:email email :role role}) emails)

                           ;; Case 2: invitations with individual roles (resend invitations style)
                           (some? invitations)
                           invitations

                           :else
                           (throw (ex-info "Invalid parameters: must provide either emails+role or invitations" {})))

        invitation-emails (into #{} (map :email) invitation-data)

        join-requests     (->> (get-valid-access-request-profiles conn (:id team))
                               (d/index-by :email))

        team-members      (into #{} xf:map-email
                                (teams/get-team-members conn (:id team)))

        invitations       (into #{}
                                (comp
                                 ;; We don't re-send invitations to
                                 ;; already existing members
                                 (remove #(contains? team-members (:email %)))
                                 ;; We don't send invitations to
                                 ;; join-requested members
                                 (remove #(contains? join-requests (:email %)))
                                 (map (fn [{:keys [email role]}]
                                        (create-invitation cfg
                                                           (-> params
                                                               (assoc :email email)
                                                               (assoc :role role)))))
                                 (remove nil?))
                                invitation-data)]

    ;; For requested invitations, do not send invitation emails, add
    ;; the user directly to the team
    (->> join-requests
         (filter #(contains? invitation-emails (key %)))
         (map (fn [[email member]]
                (let [role (:role (first (filter #(= (:email %) email) invitation-data)))]
                  (add-member-to-team cfg profile team role member))))
         (doall))

    invitations))

(def ^:private schema:create-team-invitations
  [:and
   [:map {:title "create-team-invitations"}
    [:team-id ::sm/uuid]
    ;; Support both formats:
    ;; 1. emails (set) + role (single role for all)
    ;; 2. invitations (vector of {:email :role} maps)
    [:emails {:optional true} [::sm/set ::sm/email]]
    [:role {:optional true} types.team/schema:role]
    [:invitations {:optional true} [:vector [:map
                                             [:email ::sm/email]
                                             [:role types.team/schema:role]]]]]

   ;; Ensure exactly one format is provided
   [:fn (fn [params]
          (let [has-emails-role (and (contains? params :emails)
                                     (contains? params :role))
                has-invitations (contains? params :invitations)]
            (and (or has-emails-role has-invitations)
                 (not (and has-emails-role has-invitations)))))]])

(def ^:private max-invitations-by-request-threshold
  "The number of invitations can be sent in a single rpc request"
  25)

(sv/defmethod ::create-team-invitations
  "A rpc call that allows to send single or multiple invitations to join the team.

  Supports two parameter formats:
  1. emails (set) + role (single role for all emails)
  2. invitations (vector of {:email :role} maps for individual roles)"
  {::doc/added "1.17"
   ::doc/module :teams
   ::sm/params schema:create-team-invitations}
  [cfg {:keys [::rpc/profile-id team-id role emails] :as params}]
  (let [perms    (teams/get-permissions cfg profile-id team-id)
        profile  (db/get-by-id cfg :profile profile-id)
        team     (db/get-by-id cfg :team team-id)
        team-with-org (when (contains? cf/flags :admin-console)
                        (nitrate/add-organization-info-to-team cfg team {}))
        organization (:organization team-with-org)
        ;; Determine which format is being used
        using-emails-format? (and emails role)
        ;; Handle both parameter formats
        emails   (if using-emails-format?
                   (into #{} (map profile/clean-email) emails)
                   #{})
        ;; Calculate total invitation count for both formats
        invitation-count (if using-emails-format?
                           (count emails)
                           (count (:invitations params)))]

    (when-not (:is-admin perms)
      (ex/raise :type :validation
                :code :insufficient-permissions))

    (when (and (contains? cf/flags :admin-console)
               organization
               (not (cto/allowed? :send-invitations
                                  {:organization-perms {:owner-id    (:owner-id organization)
                                                        :permissions (:permissions organization)}
                                   :profile-id profile-id
                                   :team-perms perms})))
      (ex/raise :type :validation
                :code :insufficient-permissions
                :hint "Organization policy does not allow you to send invitations"))

    ;; Don't allow promote to owner to admin users.
    (when (and (not (:is-owner perms))
               (or (= role :owner)
                   (some #(= :owner (:role %)) (:invitations params))))
      (ex/raise :type :validation
                :code :cant-promote-to-owner))

    (when (> invitation-count max-invitations-by-request-threshold)
      (ex/raise :type :validation
                :code :max-invitations-by-request
                :hint "the maximum of invitation on single request is reached"
                :threshold max-invitations-by-request-threshold))

    (-> cfg
        (assoc ::quotes/profile-id profile-id)
        (assoc ::quotes/team-id team-id)
        (assoc ::quotes/incr invitation-count)
        (quotes/check! {::quotes/id ::quotes/invitations-per-team}
                       {::quotes/id ::quotes/profiles-per-team}))

    ;; Check if the current profile is allowed to send emails
    (teams/check-profile-muted cfg profile)

    (let [team        (db/get-by-id cfg :team team-id)
          ;; NOTE: Is important pass RPC method params down to the
          ;; `create-team-invitations` because it uses the implicit
          ;; RPC properties from params for fill necessary data on
          ;; emiting an entry to the audit-log
          invitations (db/tx-run! cfg create-team-invitations
                                  (-> params
                                      (assoc :profile profile)
                                      (assoc :team team)
                                      ;; Pass parameters in the correct format for the unified function
                                      (cond-> using-emails-format?
                                        ;; If using emails+role format, ensure both are present
                                        (assoc :emails emails :role role)
                                        ;; If using invitations format, the :invitations key is already in params
                                        (not using-emails-format?) identity)))]

      (with-meta {:total (count invitations)
                  :invitations invitations}
        {::audit/props {:invitations (count invitations)}}))))

;; --- Mutation: Create Team & Invite Members

(def ^:private schema:create-team-with-invitations
  [:map {:title "create-team-with-invitations"}
   [:name [:string {:max 250}]]
   [:features {:optional true} ::cfeat/features]
   [:id {:optional true} ::sm/uuid]
   [:emails [::sm/set ::sm/email]]
   [:role types.team/schema:role]])

(sv/defmethod ::create-team-with-invitations
  {::doc/added "1.17"
   ::doc/module :teams
   ::sm/params schema:create-team-with-invitations
   ::db/transaction true}
  [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id emails role name] :as params}]
  (let [features (-> (cfeat/get-enabled-features cf/flags)
                     (cfeat/check-client-features! (:features params)))

        params   (-> params
                     (assoc :profile-id profile-id)
                     (assoc :features features))

        team     (teams/create-team cfg params)
        emails   (into #{} (map profile/clean-email) emails)]

    (-> cfg
        (assoc ::quotes/profile-id profile-id)
        (assoc ::quotes/team-id (:id team))
        (assoc ::quotes/incr (count emails))
        (quotes/check! {::quotes/id ::quotes/teams-per-profile}
                       {::quotes/id ::quotes/invitations-per-team}
                       {::quotes/id ::quotes/profiles-per-team}))

    (when (> (count emails) max-invitations-by-request-threshold)
      (ex/raise :type :validation
                :code :max-invitations-by-request
                :hint "the maximum of invitation on single request is reached"
                :threshold max-invitations-by-request-threshold))

    (let [props {:name name :features features}
          event (-> (audit/event-from-rpc-params params)
                    (assoc :name "create-team")
                    (assoc :props props))]
      (audit/submit cfg event))

    ;; Create invitations for all provided emails.
    (let [profile     (db/get-by-id conn :profile profile-id)
          params      (-> params
                          (assoc :team team)
                          (assoc :profile profile)
                          (assoc :role role))
          invitations (->> emails
                           (map (fn [email] (assoc params :email email)))
                           (map (partial create-invitation cfg)))]

      (vary-meta team assoc ::audit/props {:invitations (count invitations)}))))

;; --- Query: get-team-invitation-token

(def ^:private schema:get-team-invitation-token
  [:map {:title "get-team-invitation-token"}
   [:team-id ::sm/uuid]
   [:email ::sm/email]])

(sv/defmethod ::get-team-invitation-token
  {::doc/added "1.17"
   ::doc/module :teams
   ::sm/params schema:get-team-invitation-token}
  [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}]
  (teams/check-read-permissions! cfg profile-id team-id)
  (let [email (profile/clean-email email)
        invit (-> (db/get pool :team-invitation
                          {:team-id team-id
                           :email-to email})
                  (update :role keyword))

        member (profile/get-profile-by-email pool (:email-to invit))
        token  (create-invitation-token cfg {:team-id (:team-id invit)
                                             :profile-id profile-id
                                             :valid-until (:valid-until invit)
                                             :role (:role invit)
                                             :member-id (:id member)
                                             :member-email (or (:email member)
                                                               (profile/clean-email (:email-to invit)))})]
    {:token token}))

;; --- Mutation: Update invitation role

(def ^:private schema:update-team-invitation-role
  [:map {:title "update-team-invitation-role"}
   [:team-id ::sm/uuid]
   [:email ::sm/email]
   [:role types.team/schema:role]])

(sv/defmethod ::update-team-invitation-role
  {::doc/added "1.17"
   ::doc/module :teams
   ::sm/params schema:update-team-invitation-role
   ::db/transaction true}
  [{:keys [::db/conn]} {:keys [::rpc/profile-id team-id email role] :as params}]
  (let [perms (teams/get-permissions conn profile-id team-id)]

    (when-not (:is-admin perms)
      (ex/raise :type :validation
                :code :insufficient-permissions))

    ;; Don't allow promote to owner to admin users.
    (when (and (not (:is-owner perms)) (= role :owner))
      (ex/raise :type :validation
                :code :cant-promote-to-owner))

    (db/update! conn :team-invitation
                {:role (name role) :updated-at (ct/now)}
                {:team-id team-id :email-to (profile/clean-email email)})

    nil))

;; --- Mutation: Delete invitation

(def ^:private schema:delete-team-invition
  [:map {:title "delete-team-invitation"}
   [:team-id ::sm/uuid]
   [:email ::sm/email]])

(sv/defmethod ::delete-team-invitation
  {::doc/added "1.17"
   ::sm/params schema:delete-team-invition
   ::db/transaction true}
  [{:keys [::db/conn]} {:keys [::rpc/profile-id team-id email] :as params}]
  (let [perms (teams/get-permissions conn profile-id team-id)]

    (when-not (:is-admin perms)
      (ex/raise :type :validation
                :code :insufficient-permissions))

    (let [invitation (db/delete! conn :team-invitation
                                 {:team-id team-id
                                  :email-to (profile/clean-email email)}
                                 {::db/return-keys true})]
      (rph/wrap nil {::audit/props {:invitation-id (:id invitation)}}))))


;; --- Mutation: Request Team Invitation

(def ^:private sql:get-team-owner
  "SELECT p.*
     FROM profile AS p
     JOIN team_profile_rel AS tpr ON (tpr.profile_id = p.id)
    WHERE tpr.team_id = ?
      AND tpr.is_owner IS TRUE")

(defn- get-team-owner
  "Return a complete profile of the team owner"
  [conn team-id]
  (->> (db/exec! conn [sql:get-team-owner team-id])
       (remove db/is-row-deleted?)
       (map profile/decode-row)
       (first)))

(defn- check-existing-team-access-request
  "Checks if an existing team access request is still valid"
  [{:keys [::db/conn]} team-id profile-id]
  (when-let [request (db/get* conn :team-access-request
                              {:team-id team-id
                               :requester-id profile-id})]
    (when (ct/is-after? (:valid-until request) (ct/now))
      (ex/raise :type :validation
                :code :request-already-sent
                :hint "you have already made a request to join this team less than 24 hours ago"))))

(def ^:private sql:upsert-team-access-request
  "INSERT INTO team_access_request (id, team_id, requester_id, valid_until, auto_join_until)
   VALUES (?, ?, ?, ?, ?)
       ON CONFLICT (team_id, requester_id)
       DO UPDATE SET valid_until = ?, auto_join_until = ?, updated_at = now()
   RETURNING *")

(defn- upsert-team-access-request
  "Create or update team access request for provided team and profile-id"
  [{:keys [::db/conn] :as cfg} team-id requester-id]
  (check-existing-team-access-request cfg team-id requester-id)
  (let [valid-until     (ct/in-future {:hours 24})
        auto-join-until (ct/in-future {:days 7})
        request-id      (uuid/next)]
    (db/exec-one! conn [sql:upsert-team-access-request
                        request-id team-id requester-id
                        valid-until auto-join-until
                        valid-until auto-join-until])))

(defn- get-file-for-team-access-request
  "A specific method for obtain a file with name and page-id used for
  team request access procediment"
  [cfg file-id]
  (let [file (bfc/get-file cfg file-id :migrate? false)]
    (-> file
        (dissoc :data)
        (dissoc :deleted-at)
        (assoc :page-id (-> file :data :pages first)))))

(def ^:private schema:create-team-access-request
  [:and
   [:map {:title "create-team-access-request"}
    [:file-id {:optional true} ::sm/uuid]
    [:team-id {:optional true} ::sm/uuid]
    [:is-viewer {:optional true} ::sm/boolean]]

   [:fn (fn [params]
          (or (contains? params :file-id)
              (contains? params :team-id)))]])

(sv/defmethod ::create-team-access-request
  "A rpc call that allow to request for an invitations to join the team."
  {::doc/added "2.2.0"
   ::doc/module :teams
   ::sm/params schema:create-team-access-request
   ::db/transaction true}
  [{:keys [::db/conn] :as cfg}
   {:keys [::rpc/profile-id file-id team-id is-viewer] :as params}]

  (let [requester  (profile/get-profile conn profile-id)
        team       (if team-id
                     (->> (db/get-by-id conn :team team-id)
                          (teams/decode-row))
                     (teams/get-team-for-file conn file-id))

        team-id    (:id team)

        team-owner (get-team-owner conn team-id)

        file       (when (some? file-id)
                     (get-file-for-team-access-request cfg file-id))]

    (-> cfg
        (assoc ::quotes/profile-id profile-id)
        (assoc ::quotes/team-id team-id)
        (quotes/check! {::quotes/id ::quotes/team-access-requests-per-team}
                       {::quotes/id ::quotes/team-access-requests-per-requester}))

    (teams/check-profile-muted conn requester)
    (teams/check-email-bounce conn (:email team-owner) false)
    (teams/check-email-spam conn (:email team-owner) true)

    (let [request (upsert-team-access-request cfg team-id profile-id)
          factory (cond
                    (and (some? file) (:is-default team) is-viewer)
                    eml/request-file-access-yourpenpot-view

                    (and (some? file) (:is-default team))
                    eml/request-file-access-yourpenpot

                    (some? file)
                    eml/request-file-access

                    :else
                    eml/request-team-access)]

      (eml/send! {::eml/conn conn
                  ::eml/factory factory
                  :public-uri (cf/get :public-uri)
                  :to (:email team-owner)
                  :requested-by (:fullname requester)
                  :requested-by-email (:email requester)
                  :team-name (:name team)
                  :team-id team-id
                  :file-name (:name file)
                  :file-id file-id
                  :page-id (:page-id file)})

      (with-meta {:request request}
        {::audit/props {:request 1}}))))
