(ns metabase.api.query-metadata
  (:require
   [metabase.api.field :as api.field]
   [metabase.api.table :as api.table]
   [metabase.legacy-mbql.normalize :as mbql.normalize]
   [metabase.lib.schema.id :as lib.schema.id]
   [metabase.lib.util :as lib.util]
   [metabase.models.interface :as mi]
   [metabase.util :as u]
   [metabase.util.malli :as mu]
   [toucan2.core :as t2]))
(defn- get-databases
  [ids]
  (when (seq ids)
    (into [] (filter mi/can-read?)
          (t2/select :model/Database :id [:in ids]))))
(defn- field-ids->table-ids
  [field-ids]
  (if (seq field-ids)
    (t2/select-fn-set :table_id :model/Field :id [:in field-ids])
    #{}))
(defn- split-tables-and-legacy-card-refs [source-ids]
  (-> (reduce (fn [m src]
                (if-let [card-id (lib.util/legacy-string-table-id->card-id src)]
                  (update m :cards conj! card-id)
                  (update m :tables conj! src)))
              {:cards  (transient #{})
               :tables (transient #{})}
              source-ids)
      (update-vals persistent!)))
(defn- query->template-tag-field-ids [query]
  (when-let [template-tags (some-> query :native :template-tags vals seq)]
    (for [{tag-type :type, [dim-tag id _opts] :dimension} template-tags
          :when (and (= tag-type :dimension)
                     (= dim-tag :field)
                     (integer? id))]
      id)))

Fetch dependent metadata for ad-hoc queries.

(defn- batch-fetch-query-metadata*
  [queries]
  (let [source-ids                (into #{} (mapcat #(lib.util/collect-source-tables (:query %)))
                                        queries)
        {source-table-ids :tables
         source-card-ids  :cards} (split-tables-and-legacy-card-refs source-ids)
        source-tables             (concat (api.table/batch-fetch-table-query-metadatas source-table-ids)
                                          (api.table/batch-fetch-card-query-metadatas source-card-ids))
        fk-target-field-ids       (into #{} (comp (mapcat :fields)
                                                  (keep :fk_target_field_id))
                                        source-tables)
        fk-target-table-ids       (into #{} (remove source-table-ids)
                                        (field-ids->table-ids fk-target-field-ids))
        fk-target-tables          (api.table/batch-fetch-table-query-metadatas fk-target-table-ids)
        tables                    (concat source-tables fk-target-tables)
        template-tag-field-ids    (into #{} (mapcat query->template-tag-field-ids) queries)
        query-database-ids        (into #{} (keep :database) queries)
        database-ids              (into query-database-ids
                                        (keep :db_id)
                                        tables)]
    {;; TODO: This is naive and issues multiple queries currently. That's probably okay for most dashboards,
     ;; since they tend to query only a handful of databases at most.
     :databases (sort-by :id (get-databases database-ids))
     ;; apparently some of these tables come back with normal integer IDs and some come back with string IDs... not sure
     ;; what the hecc is going on but I guess maybe some of them in 2024 are still using that old `card__<id>` hack or
     ;; something. Idk. Anyways let's just sort the ones with numeric IDs first and the ones with string IDs last. We're
     ;; sorting these in a sane order because lots of tests expect them to be sorted by numeric ID and break if you sort
     ;; by something like `(str (:id %))`. -- Cam
     :tables    (sort-by (fn [{:keys [id]}]
                           (if (integer? id)
                             [id ""]
                             [Integer/MAX_VALUE (str id)]))
                         tables)
     :fields    (sort-by :id (api.field/get-fields template-tag-field-ids))}))

Fetch dependent metadata for ad-hoc queries.

(defn batch-fetch-query-metadata
  [queries]
  (batch-fetch-query-metadata* (map mbql.normalize/normalize queries)))

Fetch dependent metadata for cards.

Models and native queries need their definitions walked as well as their own, card-level metadata.

(defn batch-fetch-card-metadata
  [cards]
  (let [queries (into (vec (keep :dataset_query cards)) ; All the queries on all the cards
                      ;; Plus the card-level metadata of each model and native query
                      (comp (filter (fn [card] (or (= :model (:type card))
                                                   (= :native (-> card :dataset_query :type)))))
                            (map (fn [card] {:query {:source-table (str "card__" (u/the-id card))}})))
                      cards)]
    (batch-fetch-query-metadata queries)))
(defn- click-behavior->link-details
  [{:keys [linkType type targetId] :as _click-behavior}]
  (when (= type "link")
    (when-let [link-type (case linkType
                           "question"  :card
                           "dashboard" :dashboard
                           nil)]
      {:type link-type
       :id   targetId})))
(mu/defn- get-cards :- [:maybe [:sequential [:map [:id ::lib.schema.id/card]]]]
  [ids :- [:maybe [:or
                   [:sequential ::lib.schema.id/card]
                   [:set ::lib.schema.id/card]]]]
  (when (seq ids)
    (let [cards (into [] (filter mi/can-read?)
                      (t2/select :model/Card :id [:in ids]))]
      (t2/hydrate cards :can_write))))
(defn- dashcard->click-behaviors [dashcard]
  (let [viz-settings        (:visualization_settings dashcard)
        top-click-behavior  (:click_behavior viz-settings)
        col-click-behaviors (keep (comp :click_behavior val)
                                  (:column_settings viz-settings))]
    (conj col-click-behaviors top-click-behavior)))
(defn- batch-fetch-linked-dashboards
  [dashboard-ids]
  (when (seq dashboard-ids)
    (let [dashboards (->> (t2/select :model/Dashboard :id [:in dashboard-ids])
                          (filter mi/can-read?))]
      (t2/hydrate dashboards
                  :can_write
                  :param_fields))))
(defn- batch-fetch-dashboard-links
  [dashcards]
  (let [links      (group-by :type
                             (into #{} (comp (mapcat dashcard->click-behaviors)
                                             (keep click-behavior->link-details))
                                   dashcards))
        link-cards (get-cards (into #{} (map :id) (:card links)))
        dashboards (->> (:dashboard links)
                        (into #{} (map :id))
                        batch-fetch-linked-dashboards)]
    {:cards      (sort-by :id link-cards)
     :dashboards (sort-by :id dashboards)}))

Fetch dependent metadata for dashboards.

(defn batch-fetch-dashboard-metadata
  [dashboards]
  (let [dashcards (mapcat :dashcards dashboards)
        cards     (for [{:keys [card series]} dashcards
                        :let   [all (conj series card)]
                        card all]
                    card)
        card-ids  (into #{} (map :id) cards)
        links     (batch-fetch-dashboard-links dashcards)]
    (merge
     (->> (remove (comp card-ids :id) (:cards links))
          (concat cards)
          batch-fetch-card-metadata)
     {:cards      (or (:cards links)      [])
      :dashboards (or (:dashboards links) [])})))