;; 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.projects
  (:require
   [app.common.data :as d]
   [app.common.data.macros :as dm]
   [app.common.exceptions :as ex]
   [app.common.schema :as sm]
   [app.common.time :as ct]
   [app.common.uuid :as uuid]
   [app.db :as db]
   [app.db.sql :as-alias sql]
   [app.features.logical-deletion :as ldel]
   [app.loggers.audit :as-alias audit]
   [app.loggers.webhooks :as webhooks]
   [app.rpc :as-alias rpc]
   [app.rpc.commands.teams :as teams]
   [app.rpc.doc :as-alias doc]
   [app.rpc.helpers :as rph]
   [app.rpc.permissions :as perms]
   [app.rpc.quotes :as quotes]
   [app.util.services :as sv]
   [app.worker :as wrk]))

;; --- Check Project Permissions

(def ^:private sql:project-permissions
  "select tpr.is_owner,
          tpr.is_admin,
          tpr.can_edit
     from team_profile_rel as tpr
    inner join project as p on (p.team_id = tpr.team_id)
    where p.id = ?
      and tpr.profile_id = ?
   union all
   select ppr.is_owner,
          ppr.is_admin,
          ppr.can_edit
     from project_profile_rel as ppr
    where ppr.project_id = ?
      and ppr.profile_id = ?")

(defn- get-permissions
  [conn profile-id project-id]
  (let [rows     (db/exec! conn [sql:project-permissions
                                 project-id profile-id
                                 project-id profile-id])
        is-owner (boolean (some :is-owner rows))
        is-admin (boolean (some :is-admin rows))
        can-edit (boolean (some :can-edit rows))]
    (when (seq rows)
      {:is-owner is-owner
       :is-admin (or is-owner is-admin)
       :can-edit (or is-owner is-admin can-edit)
       :can-read true})))

(defn- get-read-permissions
  [cfg profile-id project-id]
  (or (get-permissions cfg profile-id project-id)
      (perms/get-organization-owner-permissions cfg profile-id :project-id project-id)))

(def has-edit-permissions?
  (perms/make-edition-predicate-fn get-permissions))

(def has-read-permissions?
  (perms/make-read-predicate-fn get-read-permissions))

(def check-edition-permissions!
  (perms/make-check-fn has-edit-permissions?))

(def check-read-permissions!
  (perms/make-check-fn has-read-permissions?))

;; --- QUERY: Get projects

(def ^:private sql:projects
  "SELECT p.*,
          coalesce(tpp.is_pinned, false) as is_pinned,
          (SELECT count(*) FROM file AS f
            WHERE f.project_id = p.id
              AND f.deleted_at is null) AS count,
          (SELECT count(*) FROM file AS f
            WHERE f.project_id = p.id) AS total_count
     FROM project AS p
    INNER JOIN team AS t ON (t.id = p.team_id)
     LEFT JOIN team_project_profile_rel AS tpp
            ON (tpp.project_id = p.id AND
                tpp.team_id = p.team_id AND
                tpp.profile_id = ?)
    WHERE p.team_id = ?
      AND t.deleted_at is null
    ORDER BY p.modified_at DESC")

(defn get-projects
  [conn profile-id team-id]
  (db/exec! conn [sql:projects profile-id team-id]))

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

(sv/defmethod ::get-projects
  {::doc/added "1.18"
   ::doc/changes [["2.12" "This endpoint now return deleted but recoverable projects"]]
   ::sm/params schema:get-projects}
  [cfg {:keys [::rpc/profile-id team-id]}]
  (teams/check-read-permissions! cfg profile-id team-id)
  (get-projects cfg profile-id team-id))

;; --- QUERY: Get all projects

(declare get-all-projects)

(def ^:private schema:get-all-projects
  [:map {:title "get-all-projects"}])

(sv/defmethod ::get-all-projects
  {::doc/added "1.18"
   ::sm/params schema:get-all-projects}
  [{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
  (dm/with-open [conn (db/open pool)]
    (get-all-projects conn profile-id)))

(def sql:all-projects
  "select p1.*, t.name as team_name, t.is_default as is_default_team
     from project as p1
    inner join team as t on (t.id = p1.team_id)
    where t.id in (select team_id
                     from team_profile_rel as tpr
                    where tpr.profile_id = ?
                      and (tpr.can_edit = true or
                           tpr.is_owner = true or
                           tpr.is_admin = true))
      and t.deleted_at is null
      and p1.deleted_at is null
   union
   select p2.*, t.name as team_name, t.is_default as is_default_team
     from project as p2
    inner join team as t on (t.id = p2.team_id)
    where p2.id in (select project_id
                     from project_profile_rel as ppr
                    where ppr.profile_id = ?
                      and (ppr.can_edit = true or
                           ppr.is_owner = true or
                           ppr.is_admin = true))
      and t.deleted_at is null
      and p2.deleted_at is null
    order by team_name, name;")

(defn get-all-projects
  [conn profile-id]
  (db/exec! conn [sql:all-projects profile-id profile-id]))


;; --- QUERY: Get project

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

(sv/defmethod ::get-project
  {::doc/added "1.18"
   ::rpc/id-type :project
   ::sm/params schema:get-project}
  [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
  (dm/with-open [conn (db/open pool)]
    (let [project (db/get-by-id conn :project id)]
      (check-read-permissions! cfg profile-id id)
      project)))



;; --- MUTATION: Create Project

(defn- create-project
  [{:keys [::db/conn] :as cfg} {:keys [::rpc/request-at profile-id team-id] :as params}]
  (assert (ct/inst? request-at) "expect request-at assigned")
  (let [params    (-> params
                      (assoc :created-at request-at)
                      (assoc :modified-at request-at))
        project   (teams/create-project conn params)
        timestamp (::rpc/request-at params)]
    (teams/create-project-role conn profile-id (:id project) :owner)
    (db/insert! conn :team-project-profile-rel
                {:id (uuid/next)
                 :project-id (:id project)
                 :profile-id profile-id
                 :created-at timestamp
                 :modified-at timestamp
                 :team-id team-id
                 :is-pinned false})
    (assoc project :is-pinned false)))

(def ^:private schema:create-project
  [:map {:title "create-project"}
   [:team-id ::sm/uuid]
   [:name [:string {:max 250 :min 1}]]
   [:id {:optional true} ::sm/uuid]])

(sv/defmethod ::create-project
  {::doc/added "1.18"
   ::webhooks/event? true
   ::sm/params schema:create-project}
  [cfg {:keys [::rpc/profile-id team-id] :as params}]

  (teams/check-edition-permissions! cfg profile-id team-id)
  (quotes/check! cfg {::quotes/id ::quotes/projects-per-team
                      ::quotes/profile-id profile-id
                      ::quotes/team-id team-id})

  (let [params (assoc params :profile-id profile-id)]
    (db/tx-run! cfg create-project params)))

;; --- MUTATION: Toggle Project Pin

(def ^:private
  sql:update-project-pin
  "insert into team_project_profile_rel (team_id, project_id, profile_id, is_pinned)
   values (?, ?, ?, ?)
       on conflict (team_id, project_id, profile_id)
       do update set is_pinned=?")

(def ^:private schema:update-project-pin
  [:map {:title "update-project-pin"}
   [:team-id ::sm/uuid]
   [:is-pinned ::sm/boolean]
   [:id ::sm/uuid]])

(sv/defmethod ::update-project-pin
  {::doc/added "1.18"
   ::rpc/id-type :project
   ::sm/params schema:update-project-pin
   ::webhooks/batch-timeout (ct/duration "5s")
   ::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
   ::webhooks/event? true
   ::db/transaction true}
  [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id team-id is-pinned] :as params}]
  (check-read-permissions! cfg profile-id id)
  (db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned])
  nil)

;; --- MUTATION: Rename Project

(declare rename-project)

(def ^:private schema:rename-project
  [:map {:title "rename-project"}
   [:name [:string {:max 250 :min 1}]]
   [:id ::sm/uuid]])

(sv/defmethod ::rename-project
  {::doc/added "1.18"
   ::rpc/id-type :project
   ::sm/params schema:rename-project
   ::webhooks/event? true
   ::db/transaction true}
  [{:keys [::db/conn]} {:keys [::rpc/profile-id id name] :as params}]
  (check-edition-permissions! conn profile-id id)
  (let [project (db/get-by-id conn :project id ::sql/for-update true)
        name    (d/normalize-string name)]
    (db/update! conn :project
                {:name name}
                {:id id})
    (rph/with-meta (rph/wrap)
      {::audit/props {:team-id (:team-id project)
                      :prev-name (:name project)}})))

;; --- MUTATION: Delete Project

(defn- delete-project
  [conn team project-id]
  (let [delay   (ldel/get-deletion-delay team)
        project (db/update! conn :project
                            {:deleted-at (ct/in-future delay)}
                            {:id project-id}
                            {::db/return-keys true})]

    (when (:is-default project)
      (ex/raise :type :validation
                :code :non-deletable-project
                :hint "impossible to delete default project"))

    (wrk/submit! {::db/conn conn
                  ::wrk/task :delete-object
                  ::wrk/params {:object :project
                                :deleted-at (:deleted-at project)
                                :id project-id}})

    project))

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

(sv/defmethod ::delete-project
  {::doc/added "1.18"
   ::rpc/id-type :project
   ::sm/params schema:delete-project
   ::webhooks/event? true
   ::db/transaction true}
  [{:keys [::db/conn]} {:keys [::rpc/profile-id id] :as params}]
  (check-edition-permissions! conn profile-id id)
  (let [team    (teams/get-team conn
                                :profile-id profile-id
                                :project-id id)
        project (delete-project conn team id)]
    (rph/with-meta (rph/wrap)
      {::audit/props {:team-id (:team-id project)
                      :name (:name project)
                      :created-at (:created-at project)
                      :modified-at (:modified-at project)}})))
