(ns metabase.query-processor.execute
  (:require
   [metabase.lib.schema.id :as lib.schema.id]
   [metabase.query-processor.middleware.cache :as cache]
   [metabase.query-processor.middleware.enterprise :as qp.middleware.enterprise]
   [metabase.query-processor.middleware.permissions :as qp.perms]
   [metabase.query-processor.middleware.update-used-cards :as update-used-cards]
   [metabase.query-processor.pipeline :as qp.pipeline]
   [metabase.query-processor.schema :as qp.schema]
   [metabase.query-processor.setup :as qp.setup]
   [metabase.query-processor.util :as qp.util]
   [metabase.util :as u]
   [metabase.util.log :as log]
   [metabase.util.malli :as mu]))
(set! *warn-on-reflection* true)
(defn- add-native-form-to-result-metadata [qp]
  (fn [query rff]
    (letfn [(rff* [metadata]
              {:pre [(map? metadata)]}
              (rff (cond-> metadata
                     (not (:native_form metadata))
                     (assoc :native_form ((some-fn :qp/compiled-inline :qp/compiled :native) query)))))]
      (qp query rff*))))
(defn- add-preprocessed-query-to-result-metadata-for-userland-query [qp]
  (fn [query rff]
    (letfn [(rff* [metadata]
              {:pre [(map? metadata)]}
              (rff (cond-> metadata
                     ;; process-userland-query needs the preprocessed-query to find field usages
                     ;; it'll then be removed from the result
                     (qp.util/userland-query? query)
                     (assoc :preprocessed_query query))))]
      (qp query rff*))))

Middleware that happens after compilation, AROUND query execution itself. Has the form

(f qp) -> qp

e.g.

(f (f query rff)) -> (f query rff)

(def ^:private middleware
  [#'update-used-cards/update-used-cards!
   #'add-native-form-to-result-metadata
   #'add-preprocessed-query-to-result-metadata-for-userland-query
   #'cache/maybe-return-cached-results
   #'qp.perms/check-query-permissions
   #'qp.middleware.enterprise/check-download-permissions-middleware
   #'qp.middleware.enterprise/maybe-apply-column-level-perms-check-middleware])
(def ^:private execute* nil)
(defn- run [query rff]
  ;; if the query has a `:qp/compiled` key (i.e., this query was compiled from MBQL), rename it to `:native`, so the
  ;; driver implementations only need to look for one key. Can't really do this any sooner because it will break schema
  ;; checks in the middleware
  (let [query (cond-> query
                (not (:native query)) (assoc :native (:qp/compiled query)))]
    (qp.pipeline/*run* query rff)))
(defn- rebuild-execute-fn! []
  (alter-var-root #'execute* (constantly
                              (reduce
                               (fn [qp middleware-fn]
                                 (u/prog1 (middleware-fn qp)
                                   (assert (ifn? <>) (format "%s did not return a valid function" middleware-fn))))
                               run
                               middleware))))
(rebuild-execute-fn!)
(doseq [varr middleware]
  (add-watch varr ::reload (fn [_key _ref _old-state _new-state]
                             (log/infof "%s changed, rebuilding %s" varr `execute*)
                             (rebuild-execute-fn!))))
(def ^:private CompiledQuery
  [:and
   [:map
    [:database ::lib.schema.id/database]]
   [:fn
    {:error/message "Query must be compiled -- should have either :native or :qp/compiled."}
    (some-fn :native :qp/compiled)]])

TODO -- consider whether this should return an IReduceInit that we can reduce as a separate step.

(mu/defn execute :- some?
  "Execute a compiled query, then reduce the results."
  [compiled-query :- CompiledQuery
   rff            :- ::qp.schema/rff]
  (qp.setup/with-qp-setup [compiled-query compiled-query]
    (execute* compiled-query rff)))