   (:require [clojure.core.match :refer [match]]
             [clojure.tools.logging :as log]
             [clojure.string :as s]
-            [clojure.walk :as walk]
-            [korma.core :refer :all, :exclude [update]]
-            [korma.sql.utils :as utils]
+            (korma [core :as k]
+                   [db :as kdb])
+            (korma.sql [fns :as kfns]
+                       [utils :as utils])
             [metabase.config :as config]
             [metabase.driver :as driver]
             [metabase.driver.query-processor :as qp]

  (if (= filter-type :inside)
    ;; INSIDE filter subclause
    (let [{:keys [lat lon]} filter]
      (list 'and {(formatted (:field lat)) ['< (formatted (:max lat))]}
                 {(formatted (:field lat)) ['> (formatted (:min lat))]}
                 {(formatted (:field lon)) ['< (formatted (:max lon))]}
                 {(formatted (:field lon)) ['> (formatted (:min lon))]}))

    ;; all other filter subclauses
    (let [field (formatted (:field filter))


(defn- filter-clause->predicate [{:keys [compound-type subclauses], :as clause}]
  (case compound-type
    :and (apply kfns/pred-and (map filter-clause->predicate subclauses))
    :or  (apply kfns/pred-or  (map filter-clause->predicate subclauses))
    nil  (filter-subclause->predicate clause)))

(defn- apply-filter [korma-query {clause :filter}]
  (k/where korma-query (filter-clause->predicate clause)))

(defn- apply-join-tables [korma-query {join-tables :join-tables, {source-table-name :name} :source-table}]
  (loop [korma-query korma-query, [{:keys [table-name pk-field source-field]} & more] join-tables]
    (let [korma-query (k/join korma-query table-name
                              (= (keyword (format "%s.%s" source-table-name (:field-name source-field)))
                                 (keyword (format "%s.%s" table-name        (:field-name pk-field)))))]
      (if (seq more)
        (recur korma-query more)
        korma-query))))

(defn- apply-limit [korma-query {value :limit}]
  (k/limit korma-query value))

(defn- apply-order-by [korma-query {subclauses :order-by}]
  (loop [korma-query korma-query, [{:keys [field direction]} & more] subclauses]
    (let [korma-query (k/order korma-query (formatted field) (case direction
                                                               :ascending  :ASC
                                                               :descending :DESC))]
      (if (seq more)
        (recur korma-query more)
        korma-query))))

(defn- apply-page [korma-query {{:keys [items page]} :page}]
  (-> korma-query
      (k/limit items)
      (k/offset (* items (dec page)))))


;;
(defn- log-korma-form
  [korma-form]
  (when-not qp/*disable-qp-logging*
    (log/debug
     (u/format-color 'blue "\nSQL: 😈\n%s\n" (-> (k/as-sql korma-form)
                                                (s/replace #"\sFROM" "\nFROM")           ; add newlines to the SQL to make it more readable
                                                (s/replace #"\sLEFT JOIN" "\nLEFT JOIN")
                                                (s/replace #"\sWHERE" "\nWHERE")
                                                (s/replace #"\sGROUP BY" "\nGROUP BY")
                                                (s/replace #"\sORDER BY" "\nORDER BY")
                                                (s/replace #"\sLIMIT" "\nLIMIT"))))))

(defn process-structured
  "Convert QUERY into a korma `select` form, execute it, and annotate the results."
  [{{:keys [source-table] :as query} :query, driver :driver, database :database, :as outer-query}]
  (binding [*query* outer-query]
    (try
      (let [entity      (korma-entity database source-table)
            timezone    (driver/report-timezone)
            korma-query (cond-> (k/select* entity)
                          (:aggregation query) (apply-aggregation query)
                          (:breakout    query) (apply-breakout    query)
                          (:fields      query) (apply-fields      query)
                          (:filter      query) (apply-filter      query)
                          (:join-tables query) (apply-join-tables query)
                          (:limit       query) (apply-limit       query)
                          (:order-by    query) (apply-order-by    query)
                          (:page        query) (apply-page        query))]

        ;; log query
        (when (config/config-bool :mb-db-logging)
          (log-korma-form korma-query))

        ;; execute query
        (kdb/with-db (:db entity)
          (if (and (seq timezone)
                   (contains? (:features driver) :set-timezone))
            (kdb/transaction
              (k/exec-raw ((:timezone->set-timezone-sql driver) timezone))
              (k/exec korma-query))
            (k/exec korma-query))))

      (catch java.sql.SQLException e
        (let [^String message (or (->> (.getMessage e) ; error message comes back like "Error message ... [status-code]" sometimes
                                       (re-find  #"(?s)(^.*)\s+\[[\d-]+\]$") ; status code isn't useful and makes unit tests hard to write so strip it off
                                       second) ; (?s) = Pattern.DOTALL - tell regex `.` to match newline characters as well
                                  (.getMessage e))]
          (throw (Exception. message)))))))

(defn process-and-run
  "Process and run a query and return results."
  [{:keys [type] :as query}]
  (case (keyword type)
    :native (native/process-and-run query)
    :query  (process-structured query)))
