/api/timeline endpoints.

(ns metabase.timeline.api.timeline
  (:require
   [metabase.api.common :as api]
   [metabase.api.macros :as api.macros]
   [metabase.models.collection :as collection]
   [metabase.models.collection.root :as collection.root]
   [metabase.timeline.models.timeline :as timeline]
   [metabase.timeline.models.timeline-event :as timeline-event]
   [metabase.util :as u]
   [metabase.util.date-2 :as u.date]
   [metabase.util.malli.registry :as mr]
   [metabase.util.malli.schema :as ms]
   [toucan2.core :as t2]))
(set! *warn-on-reflection* true)
(mr/def ::include
  [:enum :events])
(mr/def ::Timeline
  [:map
   [:id pos-int?]])
(api.macros/defendpoint :post "/" :- ::Timeline
  "Create a new [[Timeline]]."
  [_route-params
   _query-params
   {:keys [icon], collection-id :collection_id, :as body} :- [:map
                                                              [:name          ms/NonBlankString]
                                                              [:default       {:optional true} [:maybe :boolean]]
                                                              [:description   {:optional true} [:maybe :string]]
                                                              [:icon          {:optional true} [:maybe timeline-event/Icon]]
                                                              [:collection_id {:optional true} [:maybe ms/PositiveInt]]
                                                              [:archived      {:optional true} [:maybe :boolean]]]]
  (collection/check-write-perms-for-collection collection-id)
  (let [tl (merge
            body
            {:creator_id api/*current-user-id*}
            (when-not icon
              {:icon timeline-event/default-icon}))]
    (first (t2/insert-returning-instances! :model/Timeline tl))))
(api.macros/defendpoint :get "/" :- [:sequential ::Timeline]
  "Fetch a list of `Timeline`s. Can include `archived=true` to return archived timelines."
  [_route-params
   {:keys [include], archived? :archived} :- [:map
                                              [:include  {:optional true} ::include]
                                              [:archived {:default false} ms/BooleanValue]]]
  (let [timelines (->> (t2/select :model/Timeline
                                  {:where    [:and
                                              [:= :archived archived?]
                                              (collection/visible-collection-filter-clause)]
                                   :order-by [[:%lower.name :asc]]})
                       (map collection.root/hydrate-root-collection))]
    (cond->> (t2/hydrate timelines :creator [:collection :can_write])
      (= include :events)
      (map #(timeline-event/include-events-singular % {:events/all? archived?})))))
(api.macros/defendpoint :get "/:id" :- ::Timeline
  "Fetch the `Timeline` with `id`. Include `include=events` to unarchived events included on the timeline. Add
  `archived=true` to return all events on the timeline, both archived and unarchived."
  [{:keys [id]}                         :- [:map
                                            [:id ms/PositiveInt]]
   {:keys [include archived start end]} :- [:map
                                            [:include  {:optional true}  ::include]
                                            [:archived {:default :false} ms/BooleanValue]
                                            [:start    {:optional true}  ms/TemporalString]
                                            [:end      {:optional true}  ms/TemporalString]]]
  (let [archived? archived
        timeline  (api/read-check (t2/select-one :model/Timeline :id id))]
    (cond-> (t2/hydrate timeline :creator [:collection :can_write])
      ;; `collection_id` `nil` means we need to assoc 'root' collection
      ;; because hydrate `:collection` needs a proper `:id` to work.
      (nil? (:collection_id timeline))
      collection.root/hydrate-root-collection

      (= include :events)
      (timeline-event/include-events-singular {:events/all?  archived?
                                               :events/start (when start (u.date/parse start))
                                               :events/end   (when end (u.date/parse end))}))))
(api.macros/defendpoint :put "/:id"
  "Update the [[Timeline]] with `id`. Returns the timeline without events. Archiving a timeline will archive all of the
  events in that timeline."
  [{:keys [id]} :- [:map
                    [:id ms/PositiveInt]]
   _query-params
   {:keys [archived] :as timeline-updates} :- [:map
                                               [:name          {:optional true} [:maybe ms/NonBlankString]]
                                               [:default       {:optional true} [:maybe :boolean]]
                                               [:description   {:optional true} [:maybe :string]]
                                               [:icon          {:optional true} [:maybe timeline-event/Icon]]
                                               [:collection_id {:optional true} [:maybe ms/PositiveInt]]
                                               [:archived      {:optional true} [:maybe :boolean]]]]
  (let [existing (api/write-check :model/Timeline id)
        current-archived (:archived (t2/select-one :model/Timeline :id id))]
    (collection/check-allowed-to-change-collection existing timeline-updates)
    (t2/update! :model/Timeline id
                (u/select-keys-when timeline-updates
                                    :present #{:description :icon :collection_id :default :archived}
                                    :non-nil #{:name}))
    (when (and (some? archived) (not= current-archived archived))
      (t2/update! :model/TimelineEvent {:timeline_id id} {:archived archived}))
    (t2/hydrate (t2/select-one :model/Timeline :id id) :creator [:collection :can_write])))
(api.macros/defendpoint :delete "/:id"
  "Delete a [[Timeline]]. Will cascade delete its events as well."
  [{:keys [id]} :- [:map
                    [:id ms/PositiveInt]]]
  (api/write-check :model/Timeline id)
  (t2/delete! :model/Timeline :id id)
  api/generic-204-no-content)
(api.macros/defendpoint :get "/collection/root"
  "Fetch the root Collection's timelines."
  [_route-params
   {:keys [include archived]} :- [:map
                                  [:include  {:optional true} [:maybe [:= "events"]]]
                                  [:archived {:default false} [:maybe :boolean]]]]
  (api/read-check collection/root-collection)
  (timeline/timelines-for-collection nil {:timeline/events?   (= include "events")
                                          :timeline/archived? archived}))
(api.macros/defendpoint :get "/collection/:id"
  "Fetch a specific Collection's timelines."
  [{:keys [id]} :- [:map
                    [:id ms/PositiveInt]]
   {:keys [include archived]} :- [:map
                                  [:include  {:optional true} [:maybe [:= "events"]]]
                                  [:archived {:default false} [:maybe :boolean]]]]
  (api/read-check (t2/select-one :model/Collection :id id))
  (timeline/timelines-for-collection id {:timeline/events?   (= include "events")
                                         :timeline/archived? archived}))