(ns metabase.lib.column-group
  (:require
   [medley.core :as m]
   [metabase.lib.card :as lib.card]
   [metabase.lib.join :as lib.join]
   [metabase.lib.join.util :as lib.join.util]
   [metabase.lib.metadata :as lib.metadata]
   [metabase.lib.metadata.calculation :as lib.metadata.calculation]
   [metabase.lib.schema.common :as lib.schema.common]
   [metabase.lib.schema.id :as lib.schema.id]
   [metabase.lib.schema.metadata :as lib.schema.metadata]
   [metabase.lib.util :as lib.util]
   [metabase.util.i18n :as i18n]
   [metabase.util.malli :as mu]))
(def ^:private GroupType
  [:enum
   ;; the `:group-type/main` group includes all the columns from the source Table/Card/previous stage as well as ones
   ;; added in this stage.
   :group-type/main
   ;; the other two group types are for various types of joins.
   :group-type/join.explicit
   :group-type/join.implicit])
(def ^:private group-type-ordering
  {:group-type/main          1
   :group-type/join.explicit 2
   :group-type/join.implicit 3})

Schema for the metadata returned by [[group-columns]], and accepted by [[columns-group-columns]].

(def ^:private ColumnGroup
  [:and
   [:map
    [:lib/type    [:= :metadata/column-group]]
    [::group-type GroupType]
    [::columns    [:sequential [:ref ::lib.schema.metadata/column]]]]
   [:multi
    {:dispatch ::group-type}
    [:group-type/main
     any?]
    ;; if we're in the process of BUILDING a join and using this in combination
    ;; with [[metabase.lib.join/join-condition-rhs-columns]], the alias won't be present yet, so group things by the
    ;; joinable -- either the Card we're joining, or the Table we're joining. See #32493
    [:group-type/join.explicit
     [:and
      [:map
       [:join-alias {:optional true} [:ref ::lib.schema.common/non-blank-string]]
       [:table-id   {:optional true} [:ref ::lib.schema.id/table]]
       [:card-id    {:optional true} [:ref ::lib.schema.id/card]]]
      [:fn
       {:error/message ":group-type/join.explicit should only have at most one of :join-alias, :table-id, or :card-id"}
       (fn [m]
         (>= (count (keys (select-keys m [:join-alias :table-id :card-id]))) 1))]]]
    [:group-type/join.implicit
     [:map
      [:fk-field-id [:ref ::lib.schema.id/field]]]]]])
(defmethod lib.metadata.calculation/metadata-method :metadata/column-group
  [_query _stage-number column-group]
  column-group)
(defmulti ^:private display-info-for-group-method
  {:arglists '([query stage-number column-group])}
  (fn [_query _stage-number column-group]
    (::group-type column-group)))
(defmethod display-info-for-group-method :group-type/main
  [query stage-number _column-group]
  (merge
   (let [stage (lib.util/query-stage query stage-number)]
     (or
      (when-let [table (some->> (:source-table stage) (lib.metadata/table query))]
        (lib.metadata.calculation/display-info query stage-number table))
      (when-let [card (some->> (:source-card stage) (lib.metadata/card query))]
        (lib.metadata.calculation/display-info query stage-number card))
      ;; multi-stage queries (#30108)
      (when (next (:stages query))
        {:display-name (i18n/tru "Summaries")})
      ;; if this is a native query or something else that doesn't have a source Table or source Card then use the
      ;; stage display name.
      {:display-name (lib.metadata.calculation/display-name query stage-number stage)}))
   {:is-main-group          true
    :is-from-join           false
    :is-implicitly-joinable false}))
(defmethod display-info-for-group-method :group-type/join.explicit
  [query stage-number {:keys [join-alias table-id card-id], :as _column-group}]
  (merge
   (or
    (when join-alias
      (when-let [join (lib.join/resolve-join query stage-number join-alias)]
        (lib.metadata.calculation/display-info query stage-number join)))
    (when table-id
      (when-let [table (lib.metadata/table query table-id)]
        (lib.metadata.calculation/display-info query stage-number table)))
    (when card-id
      (if-let [card (lib.metadata/card query card-id)]
        (lib.metadata.calculation/display-info query stage-number card)
        {:display-name (lib.card/fallback-display-name card-id)})))
   {:is-main-group          false
    :is-from-join           true
    :is-implicitly-joinable false}))
(defmethod display-info-for-group-method :group-type/join.implicit
  [query stage-number {:keys [fk-field-id], :as _column-group}]
  (merge
   (when-let [;; TODO: This is clumsy and expensive; there is likely a neater way to find the full FK column.
              ;; Note that using `lib.metadata/field` is out - we need to respect metadata overrides etc. in models, and
              ;; `lib.metadata/field` uses the field's original status.
              fk-column (->> (lib.util/query-stage query stage-number)
                             (lib.metadata.calculation/visible-columns query stage-number)
                             (m/find-first #(and (= (:id %) fk-field-id)
                                                 (:fk-target-field-id %))))]
     (let [fk-info (lib.metadata.calculation/display-info query stage-number fk-column)]
       ;; Implicitly joined column pickers don't use the target table's name, they use the FK field's name with
       ;; "ID" dropped instead.
       ;; This is very intentional: one table might have several FKs to one foreign table, each with different
       ;; meaning (eg. ORDERS.customer_id vs. ORDERS.supplier_id both linking to a PEOPLE table).
       ;; See #30109 for more details.
       (update fk-info :display-name lib.util/strip-id)))
   {:is-main-group          false
    :is-from-join           false
    :is-implicitly-joinable true}))
(defmethod lib.metadata.calculation/display-info-method :metadata/column-group
  [query stage-number column-group]
  (display-info-for-group-method query stage-number column-group))
(defmulti ^:private column-group-info-method
  {:arglists '([column-metadata])}
  :lib/source)
(defmethod column-group-info-method :source/implicitly-joinable
  [column-metadata]
  {::group-type :group-type/join.implicit,
   :fk-field-id (:fk-field-id column-metadata)
   :fk-join-alias (:fk-join-alias column-metadata)})
(defmethod column-group-info-method :source/joins
  [{:keys [table-id], :lib/keys [card-id], :as column-metadata}]
  (merge
   {::group-type :group-type/join.explicit}
   ;; if we're in the process of BUILDING a join and using this in combination
   ;; with [[metabase.lib.join/join-condition-rhs-columns]], the alias won't be present yet, so group things by the
   ;; joinable -- either the Card we're joining, or the Table we're joining. Prefer `:lib/card-id` because when we
   ;; join a Card the Fields might have `:table-id` but we want the entire Card to appear as one group. See #32493
   (or
    (when-let [join-alias (lib.join.util/current-join-alias column-metadata)]
      {:join-alias join-alias})
    (when card-id
      {:card-id card-id})
    (when table-id
      {:table-id table-id}))))
(defmethod column-group-info-method :default
  [_column-metadata]
  {::group-type :group-type/main})
(mu/defn- column-group-info :- [:map [::group-type GroupType]]
  "The value we should use to `group-by` inside [[group-columns]]."
  [column-metadata :- ::lib.schema.metadata/column]
  (column-group-info-method column-metadata))
(defn- column-group-ordering
  [fk-field-names {::keys [group-type] :as column-group}]
  (into [(group-type-ordering group-type)]
        (case group-type
          :group-type/main          ["main"] ; There's only ever one main group, so no need to sort them further.
          :group-type/join.explicit [(:join-alias column-group)]
          :group-type/join.implicit [(or (:fk-join-alias column-group) )
                                     (fk-field-names (:fk-field-id column-group) )])))
(mu/defn group-columns :- [:sequential ColumnGroup]
  "Given a group of columns returned by a function like [[metabase.lib.order-by/orderable-columns]], group the columns
  by Table or equivalent (e.g. Saved Question) so that they're in an appropriate shape for showing in the Query
  Builder. e.g a sequence of columns like
    [venues.id
     venues.name
     venues.category-id
     ;; implicitly joinable
     categories.id
     categories.name]
  would get grouped into groups like
    [{::columns [venues.id
                 venues.name
                 venues.category-id]}
     {::columns [categories.id
                 categories.name]}]
  Groups have the type `:metadata/column-group` and can be passed directly
  to [[metabase.lib.metadata.calculation/display-info]].
  Ordered to put own columns first, then explicit joins alphabetically by join alias, then implicit joins alphabetically
  by FK join alias + FK field name (which is used as the table name). So if the same FK is available multiple times,
  they are ordered: own first, then alphabetically by the join alias for that FK."
  [column-metadatas :- [:sequential ::lib.schema.metadata/column]]
  (let [fk-field-names (into {} (comp (filter :id)
                                      (map (juxt :id :name)))
                             column-metadatas)]
    (->> (group-by column-group-info column-metadatas)
         (map (fn [[group-info columns]]
                (assoc group-info
                       :lib/type :metadata/column-group
                       ::columns columns)))
         (sort-by (partial column-group-ordering fk-field-names))
         vec)))
(mu/defn columns-group-columns :- [:sequential ::lib.schema.metadata/column]
  "Get the columns associated with a column group"
  [column-group :- ColumnGroup]
  (::columns column-group))
(defmethod lib.metadata.calculation/display-name-method :metadata/column-group
  [query stage-number column-group _display-name-style]
  (:display-name (lib.metadata.calculation/display-info query stage-number column-group)))