(ns metabase.revisions.impl.dashboard
  (:require
   [clojure.data :refer [diff]]
   [clojure.set :as set]
   [clojure.string :as str]
   [metabase.models.dashboard :as dashboard]
   [metabase.models.dashboard-card :as dashboard-card]
   [metabase.models.dashboard-tab :as dashboard-tab]
   [metabase.revisions.models.revision :as revision]
   [metabase.util :as u]
   [metabase.util.i18n :refer [deferred-tru deferred-trun]]
   [toucan2.core :as t2]
   [toucan2.realize :as t2.realize]))
(def ^:private excluded-columns-for-dashboard-revision
  #{:id :created_at :updated_at :creator_id :points_of_interest :caveats :show_in_getting_started :entity_id
    ;; not sure what position is for, from the column remark:
    ;; > The position this Dashboard should appear in the Dashboards list,
    ;;   lower-numbered positions appearing before higher numbered ones.
    ;; TODO: querying on stats we don't have any dashboard that has a position, maybe we could just drop it?
    :public_uuid :made_public_by_id
    :position :initially_published_at :view_count
    :last_viewed_at})
(def ^:private excluded-columns-for-dashcard-revision
  [:entity_id :created_at :updated_at :collection_authority_level])
(def ^:private excluded-columns-for-dashboard-tab-revision
  [:created_at :updated_at :entity_id])
(defmethod revision/serialize-instance :model/Dashboard
  [_model _id dashboard]
  (let [dashcards (or (:dashcards dashboard)
                      (:dashcards (t2/hydrate dashboard :dashcards)))
        dashcards (when (seq dashcards)
                    (if (contains? (first dashcards) :series)
                      dashcards
                      (t2/hydrate dashcards :series)))
        tabs  (or (:tabs dashboard)
                  (:tabs (t2/hydrate dashboard :tabs)))]
    (-> (apply dissoc dashboard excluded-columns-for-dashboard-revision)
        (assoc :cards (vec (for [dashboard-card dashcards]
                             (-> (apply dissoc dashboard-card excluded-columns-for-dashcard-revision)
                                 (assoc :series (mapv :id (:series dashboard-card)))))))
        (assoc :tabs (map #(apply dissoc % excluded-columns-for-dashboard-tab-revision) tabs)))))
(defn- revert-dashcards
  [dashboard-id serialized-cards]
  (let [current-cards    (t2/select-fn-vec #(apply dissoc (t2.realize/realize %) excluded-columns-for-dashcard-revision)
                                           :model/DashboardCard
                                           :dashboard_id dashboard-id)
        id->current-card (zipmap (map :id current-cards) current-cards)
        {:keys [to-create to-update to-delete]} (u/row-diff current-cards serialized-cards)]
    (when (seq to-delete)
      (dashboard-card/delete-dashboard-cards! (map :id to-delete)))
    (when (seq to-create)
      (dashboard-card/create-dashboard-cards! (map #(assoc % :dashboard_id dashboard-id) to-create)))
    (when (seq to-update)
      (doseq [update-card to-update]
        (dashboard-card/update-dashboard-card! update-card (id->current-card (:id update-card)))))))

Given a list of dashcards, remove any dashcard that references cards that are archived, do not exist, or now belong to other dashboards .

(defn- remove-invalid-dashcards
  [dashboard-id dashcards]
  (let [card-ids          (set (keep :card_id dashcards))
        active-card-ids   (when-let [card-ids (seq card-ids)]
                            (t2/select-pks-set :model/Card
                                               {:where [:and
                                                        [:in :id card-ids]
                                                        ;; skip when archived
                                                        [:= :archived false]
                                                        ;; belong to this dashboard, or are not Dashboard Questions
                                                        [:or
                                                         [:= :dashboard_id dashboard-id]
                                                         [:= :dashboard_id nil]]]}))
        inactive-card-ids (set/difference card-ids active-card-ids)]
    (remove #(contains? inactive-card-ids (:card_id %)) dashcards)))
(defmethod revision/revert-to-revision! :model/Dashboard
  [model dashboard-id user-id serialized-dashboard]
  ;; Update the dashboard description / name / permissions
  ((get-method revision/revert-to-revision! :default) model dashboard-id user-id (dissoc serialized-dashboard :cards :tabs))
  ;; Now update the tabs and cards as needed
  (let [serialized-dashcards      (:cards serialized-dashboard)
        current-tabs              (t2/select-fn-vec #(dissoc (t2.realize/realize %) :created_at :updated_at :entity_id :dashboard_id)
                                                    :model/DashboardTab :dashboard_id dashboard-id)
        {:keys [old->new-tab-id]} (dashboard-tab/do-update-tabs! dashboard-id current-tabs (:tabs serialized-dashboard))
        _                         (dashboard/archive-or-unarchive-internal-dashboard-questions! dashboard-id serialized-dashcards)
        serialized-dashcards      (cond->> serialized-dashcards
                                    true
                                    (remove-invalid-dashcards dashboard-id)
                                    ;; in case reverting result in new tabs being created,
                                    ;; we need to remap the tab-id
                                    (seq old->new-tab-id)
                                    (map (fn [card]
                                           (if-let [new-tab-id (get old->new-tab-id (:dashboard_tab_id card))]
                                             (assoc card :dashboard_tab_id new-tab-id)
                                             card))))]
    (revert-dashcards dashboard-id serialized-dashcards))
  serialized-dashboard)
(defmethod revision/diff-strings :model/Dashboard
  [_model prev-dashboard dashboard]
  (let [[removals changes]  (diff prev-dashboard dashboard)
        check-series-change (fn [idx card-changes]
                              (when (and (:series card-changes)
                                         (get-in prev-dashboard [:cards idx :card_id]))
                                (let [num-seriesā‚ (count (get-in prev-dashboard [:cards idx :series]))
                                      num-seriesā‚‚ (count (get-in dashboard [:cards idx :series]))]
                                  (cond
                                    (< num-seriesā‚ num-seriesā‚‚)
                                    (deferred-tru "added some series to card {0}" (get-in prev-dashboard [:cards idx :card_id]))

                                    (> num-seriesā‚ num-seriesā‚‚)
                                    (deferred-tru "removed some series from card {0}" (get-in prev-dashboard [:cards idx :card_id]))

                                    :else
                                    (deferred-tru "modified the series on card {0}" (get-in prev-dashboard [:cards idx :card_id]))))))]
    (-> [(when-let [default-description (u/build-sentence ((get-method revision/diff-strings :default) :model/Dashboard prev-dashboard dashboard))]
           (cond-> default-description
             (str/ends-with? default-description ".") (subs 0 (dec (count default-description)))))
         (when (:cache_ttl changes)
           (cond
             (nil? (:cache_ttl prev-dashboard)) (deferred-tru "added a cache ttl")
             (nil? (:cache_ttl dashboard)) (deferred-tru "removed the cache ttl")
             :else (deferred-tru "changed the cache ttl from \"{0}\" to \"{1}\""
                                 (:cache_ttl prev-dashboard) (:cache_ttl dashboard))))
         (when (or (:cards changes) (:cards removals))
           (let [prev-card-ids  (set (map :id (:cards prev-dashboard)))
                 num-prev-cards (count prev-card-ids)
                 new-card-ids   (set (map :id (:cards dashboard)))
                 num-new-cards  (count new-card-ids)
                 num-cards-diff (abs (- num-prev-cards num-new-cards))
                 keys-changes   (set (flatten (concat (map keys (:cards changes))
                                                      (map keys (:cards removals)))))]
             (cond
               (and
                (set/subset? prev-card-ids new-card-ids)
                (< num-prev-cards num-new-cards))                     (deferred-trun "added a card" "added {0} cards" num-cards-diff)
               (and
                (set/subset? new-card-ids prev-card-ids)
                (> num-prev-cards num-new-cards))                     (deferred-trun "removed a card" "removed {0} cards" num-cards-diff)
               (set/subset? keys-changes #{:row :col :size_x :size_y}) (deferred-tru "rearranged the cards")
               :else                                                   (deferred-tru "modified the cards"))))

         (when (or (:tabs changes) (:tabs removals))
           (let [prev-tabs     (:tabs prev-dashboard)
                 new-tabs      (:tabs dashboard)
                 prev-tab-ids  (set (map :id prev-tabs))
                 num-prev-tabs (count prev-tab-ids)
                 new-tab-ids   (set (map :id new-tabs))
                 num-new-tabs  (count new-tab-ids)
                 num-tabs-diff (abs (- num-prev-tabs num-new-tabs))]
             (cond
               (and
                (set/subset? prev-tab-ids new-tab-ids)
                (< num-prev-tabs num-new-tabs))              (deferred-trun "added a tab" "added {0} tabs" num-tabs-diff)

               (and
                (set/subset? new-tab-ids prev-tab-ids)
                (> num-prev-tabs num-new-tabs))              (deferred-trun "removed a tab" "removed {0} tabs" num-tabs-diff)

               (= (set (map #(dissoc % :position) prev-tabs))
                  (set (map #(dissoc % :position) new-tabs))) (deferred-tru "rearranged the tabs")

               :else                                          (deferred-tru "modified the tabs"))))
         (let [f (comp boolean :auto_apply_filters)]
           (when (not= (f prev-dashboard) (f dashboard))
             (deferred-tru "set auto apply filters to {0}" (str (f dashboard)))))]
        (concat (map-indexed check-series-change (:cards changes)))
        (->> (filter identity)))))