(ns metabase.xrays.automagic-dashboards.util
  (:require
   [buddy.core.codecs :as codecs]
   [clojure.string :as str]
   [medley.core :as m]
   [metabase.analyze.core :as analyze]
   [metabase.legacy-mbql.predicates :as mbql.preds]
   [metabase.legacy-mbql.schema :as mbql.s]
   [metabase.legacy-mbql.util :as mbql.u]
   [metabase.lib.util.match :as lib.util.match]
   [metabase.models.interface :as mi]
   [metabase.util :as u]
   [metabase.util.json :as json]
   [metabase.util.log :as log]
   [metabase.util.malli :as mu]
   [metabase.util.malli.schema :as ms]
   [ring.util.codec :as codec]
   [toucan2.core :as t2]))

isa? on a field, checking semantictype and then basetype

(defn field-isa?
  [{:keys [base_type semantic_type]} t]
  (or (isa? (keyword semantic_type) t)
      (isa? (keyword base_type) t)))

Workaround for our leaky type system which conflates types with properties.

(defn key-col?
  [{:keys [base_type semantic_type name]}]
  (and (isa? base_type :type/Number)
       (or (#{:type/PK :type/FK} semantic_type)
           (let [name (u/lower-case-en name)]
             (or (= name "id")
                 (str/starts-with? name "id_")
                 (str/ends-with? name "_id"))))))

filter tables by tablespec, which is just an entity type (eg. :entity/GenericTable)

(defn filter-tables
  [tablespec tables]
  (filter #(-> % :entity_type (isa? tablespec)) tables))

Is metric a saved metric?

(def ^{:arglists '([metric]) :doc } saved-metric?
  (partial mbql.u/is-clause? :metric))

Is this a custom expression?

(def ^{:arglists '([metric]) :doc } custom-expression?
  (partial mbql.u/is-clause? :aggregation-options))

Is this an adhoc metric?

(def ^{:arglists '([metric]) :doc } adhoc-metric?
  (complement (some-fn saved-metric? custom-expression?)))

Encode given object as base-64 encoded JSON.

(def ^{:arglists '([x]) :doc "Base64 encode"} encode-base64-json
  (comp codec/base64-encode codecs/str->bytes json/encode))
(mu/defn field-reference->id :- [:maybe [:or ms/NonBlankString ms/PositiveInt]]
  "Extract field ID from a given field reference form."
  [clause]
  (lib.util.match/match-one clause [:field id _] id))
(mu/defn collect-field-references :- [:maybe [:sequential mbql.s/field]]
  "Collect all `:field` references from a given form."
  [form]
  (lib.util.match/match form :field &match))
(mu/defn ->field :- [:maybe (ms/InstanceOf :model/Field)]
  "Return `Field` instance for a given ID or name in the context of root."
  [{{result-metadata :result_metadata} :source, :as root}
   field-id-or-name-or-clause :- [:or ms/PositiveInt ms/NonBlankString [:fn mbql.preds/Field?]]]
  (let [id-or-name (if (sequential? field-id-or-name-or-clause)
                     (field-reference->id field-id-or-name-or-clause)
                     field-id-or-name-or-clause)]
    (or
     ;; Handle integer Field IDs.
     (when (integer? id-or-name)
       (t2/select-one :model/Field :id id-or-name))
     ;; handle field string names. Only if we have result metadata. (Not sure why)
     (when (string? id-or-name)
       (when-not result-metadata
         (log/warn "Warning: Automagic analysis context is missing result metadata. Unable to resolve Fields by name."))
       (when-let [field (m/find-first #(= (:name %) id-or-name)
                                      result-metadata)]
         (as-> field field
           (update field :base_type keyword)
           (update field :semantic_type keyword)
           (mi/instance :model/Field field)
           (analyze/run-classifiers field {}))))
     ;; otherwise this isn't returning something, and that's probably an error. Log it.
     (log/warnf "Cannot resolve Field %s in automagic analysis context\n%s" field-id-or-name-or-clause (u/pprint-to-str root)))))