(ns auto-ap.square.core3
  (:require
   [auto-ap.datomic :refer [conn remove-nils]]
   [auto-ap.logging :as log :refer [capture-context->lc with-context-as]]
   [auto-ap.time :as atime]
   [cemerick.url :as url]
   [clj-http.client :as client]
   [clj-time.coerce :as coerce]
   [clj-time.core :as time]
   [clj-time.format :as f]
   [clj-time.periodic :as periodic]
   [clojure.data.json :as json]
   [clojure.set :as set]
   [clojure.string :as str]
   [com.brunobonacci.mulog :as mu]
   [datomic.api :as dc]
   [manifold.deferred :as de]
   [manifold.executor :as ex]
   [manifold.stream :as s]
   [manifold.time :as mt]
   [clojure.data.csv :as csv]))

(defn client-base-headers
  ([client] (client-base-headers client "2021-08-18"))
  ([client v]
   {"Square-Version" v
    "Authorization"  (str "Bearer " (:client/square-auth-token client))
    "Content-Type"   "application/json"}))


(defn ->square-date [d]
  (f/unparse (f/formatter "YYYY-MM-dd'T'HH:mm:ssZZ") d))


(def manifold-api-stream
  (let [stream (s/stream 100)]
    (->> stream
         (s/throttle 25)
         (s/map (fn [[request attempt response-deferred]]
                  (de/catch
                   (de/chain
                    (de/loop [attempt 0]
                      (-> (de/chain (de/future-with (ex/execute-pool)
                                                    #_(log/info ::request-started
                                                              :url (:url request)
                                                              :attempt attempt
                                                              :source "Square 3"
                                                              :background-job "Square 3")
                                                    (try
                                                      (client/request (assoc request
                                                                             :socket-timeout 10000
                                                                             :connection-timeout 10000
                                                                             #_#_:connection-request-timeout 5000
                                                                             :as :json))
                                                      (catch Throwable e
                                                        (println e)
                                                        (log/warn ::raw-request-failed
                                                                  :exception e)
                                                        (throw e)))))
                          (de/catch
                           (fn [e]
                             (if (>= attempt 5)
                               (throw e)
                               (de/chain
                                (mt/in 10000 (fn [] 1))
                                (fn [_] (de/recur (inc attempt)))))))
                          (de/chain identity)))
                    (fn [result]
                      (de/success! response-deferred result)))
                   (fn [error]
                     (de/error! response-deferred error)))))

         (s/buffer 25)
         (s/realize-each)
         (s/consume (fn [_]
                      #_(log/info ::request-completed
                                  :source "Square 3"
                                  :background-job "Square 3")
                      nil)))
    stream))

(defn manifold-api-call
  ([request] (manifold-api-call request 0))
  ([request attempt]
   (manifold-api-call request attempt (de/deferred)))
  ([request attempt response-deferred]
   (de/chain (s/put! manifold-api-stream [request attempt response-deferred])
             (fn [r]
               (when r
                 response-deferred)))))

(defn client-locations [client]
  (capture-context->lc
   (de/catch
    (de/chain (manifold-api-call
               {:url "https://connect.squareup.com/v2/locations"
                :method :get
                :headers (client-base-headers client)})
              :body
              :locations)
    (fn [error]
      (mu/with-context lc
        (log/error ::no-locations-found
                   :exception error))
      []))))


(def item-cache (atom {}))

(defn fetch-catalog [client i v]
  (capture-context->lc
   (de/chain
    [client i]
    (fn [[client i]]
      (mu/with-context lc
        (manifold-api-call {:url (str "https://connect.squareup.com/v2/catalog/object/" i)
                            :method :get
                            :headers (client-base-headers client)
                            :query-params {"include_related_items" "true"
                                           "catalog_version" v}
                            :as :json})))
    :body
    :object
    #(do (swap! item-cache assoc i %)
         %))))


(defn fetch-catalog-cache [client i version]
  (if (get @item-cache i)
    (de/success-deferred (get @item-cache i))
    (fetch-catalog client i version)))


(defn item->category-name-impl [client item version]
  (capture-context->lc
   (cond (:item_id (:item_variation_data item))
         (de/catch (de/chain (fetch-catalog-cache client (:item_id (:item_variation_data item)) version)
                             (fn [item]
                               (mu/with-context lc
                                 (item->category-name-impl client item version))))
                   (fn [e]
                     (log/warn ::couldnt-fetch-variation
                               :exception e)
                     "Uncategorized"))

         (:category_id (:item_data item))
         (de/catch (de/chain (fetch-catalog-cache client (:category_id (:item_data item)) version)
                             :category_data
                             :name)
                   (fn [e]
                     (log/warn ::couldnt-fetch-category
                               :exception e)
                     "Uncategorized"))

         (:item_data item)
         "Uncategorized"

         :else
         (do
           (log/warn ::no-look-up-item
                     :item item)
           "Uncategorized"))))


(defn item-id->category-name [client i version]
  (capture-context->lc
   (-> [client i]
       (de/chain
        (fn [[client i]]
          (if (str/blank? i)
            "Uncategorized"
            (de/catch (de/chain (fetch-catalog-cache client i version)
                                #(mu/with-context lc
                                   (item->category-name-impl client % version)))
                      (fn [error]
                        (log/warn ::couldnt-fetch-item
                                  :exception error)
                        "Uncategorized"
                        (throw error)))))))))

(defn pc [start end]
  {"query" {"filter" {"date_time_filter"
                      {"updated_at" {"start_at" (->square-date start)
                                     "end_at"   (->square-date end)}}}

            "sort" {"sort_field" "UPDATED_AT"
                    "sort_order" "DESC"}}})

#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn get-order
  ([client location order-id]
   (log/info ::searching-for-order
             :location location
             :order-id order-id)
   (let [result (->> (client/get (str "https://connect.squareup.com/v2/orders/" order-id)
                                 {:headers (client-base-headers client)
                                  :as      :json})
                     :body)]
     result)))

(defn continue-search [client location start end cursor]
  (log/info ::continue-order-search
            :cursor cursor)

  (capture-context->lc
   (de/chain (manifold-api-call
              {:url "https://connect.squareup.com/v2/orders/search"
               :method :post
               :headers (client-base-headers client)
               :body    (json/write-str (cond-> {"location_ids" [(:square-location/square-id location)]
                                                 "limit" 1000
                                                 "cursor" cursor}
                                          start (merge (pc start end))))
               :as      :json})
             :body
             (fn [result]
               (mu/with-context
                 lc
                 (log/info ::orders-found
                           :count (count (:orders result)))
                 (if (not-empty (:cursor result))
                   (de/chain (continue-search client location start end (:cursor result))
                             (fn [continued-results]
                               (mu/with-context
                                 lc
                                 (concat (:orders result) continued-results))))
                   (:orders result)))))))


(defn search
  ([client location start end]
   (capture-context->lc
    (log/info ::searching
              :location (:square-location/client-location location))
    (de/chain (manifold-api-call {:url "https://connect.squareup.com/v2/orders/search"
                                  :method :post
                                  :headers (client-base-headers client)
                                  :body    (json/write-str (cond-> {"location_ids" [(:square-location/square-id location)] "limit" 1000}
                                                             start (merge (pc start end))))
                                  :as      :json})
              :body
              (fn [result]
                (mu/with-context lc
                  (log/info ::orders-found
                            :count (count (:orders result)))
                  (if (not-empty (:cursor result))
                    (de/chain (continue-search client location start end (:cursor result))
                              (fn [continued-results]
                                (mu/with-context lc
                                  (concat (:orders result) continued-results))))
                    (:orders result))))))))


(defn amount->money [amt]
  (* 0.01 (or (:amount amt) 0.0)))


;; to get totals: 
(comment
  (reduce
   (fn [total i]
     (+ total (+ (- (:sales-order/total i)
                    (:sales-order/tax i)
                    (:sales-order/tip i)
                    (:sales-order/service-charge i))
                 (:sales-order/returns i)

                 (:sales-order/discount i))))
   0.0
   []))

(defn scoped-key
  "Client-scoped external id, in the shape sales order keys already use.

  Without the client and location in the key, two clients configured on the same Square location
  collide on a single entity: a refund changes owner every time either client imports, and one
  charge ends up shared between both clients' orders."
  [prefix client location id]
  (str prefix (:client/code client) "-" (:square-location/client-location location) "-" id))

(defn existing-id
  "Entity id of the refund or charge this id already refers to, trying the client-scoped key
  first and the legacy unscoped key second.

  This is what makes re-keying safe. These external ids are `:db.unique/identity`, so the import
  relies on upsert-by-identity; changing the key format on its own would match nothing and
  Datomic would create a SECOND entity for every refund and charge, orphaning the original under
  its legacy key. Pinning the result as `:db/id` makes the write land on the existing entity
  whichever scheme it currently carries."
  [db attr prefix client location id]
  (when id
    (or (dc/entid db [attr (scoped-key prefix client location id)])
        (dc/entid db [attr (str prefix id)]))))

(defn tender->charge [order client location t]
  (remove-nils
   #:charge
    {:type-name (:type t)
     :date (coerce/to-date (time/to-time-zone (coerce/to-date-time (:created_at order)) (time/time-zone-for-id "America/Los_Angeles")))
     :client (:db/id client)
     :note (:note t)
     :location       (:square-location/client-location location)
     :reference-link (str (url/url  "https://squareup.com/receipt/preview" (:id t)))
     :db/id       (existing-id (dc/db conn) :charge/external-id "square/charge/" client location (:id t))
     :external-id (when (:id t)
                    (scoped-key "square/charge/" client location (:id t)))
     :processor   (cond 
                    (#{"OTHER" "THIRD_PARTY_CARD"} (:type t))
                    (condp = (some-> (:note t) str/lower-case)
                      "doordash" :ccp-processor/doordash
                      "dd"       :ccp-processor/doordash
                      "ubereats" :ccp-processor/uber-eats
                      "ue"       :ccp-processor/uber-eats
                      "grubhub"  :ccp-processor/grubhub
                      "grub"     :ccp-processor/grubhub
                      "gh"       :ccp-processor/grubhub
                      (let [src (some-> (:name (:source order)) str/lower-case)]
                        (cond
                          (nil? src)                      :ccp-processor/na
                          (str/includes? src "doordash")  :ccp-processor/doordash
                          (str/includes? src "uber")      :ccp-processor/uber-eats
                          (str/includes? src "grubhub")   :ccp-processor/grubhub
                          (str/includes? src "postmates") :ccp-processor/uber-eats
                          (str/includes? src "koala")     :ccp-processor/koala
                          :else                           :ccp-processor/na)))
                    (= (:type t) "CARD")
                    :ccp-processor/square

                    (= (:type t) "SQUARE_GIFT_CARD")
                    :ccp-processor/square

                    (= (:type t) "CASH")
                    :ccp-processor/na

                    :else
                    :ccp-processor/na)
     :total       (amount->money (:amount_money t))
     :tip         (amount->money (:tip_money t))}))

(defn order->sales-order [client location order]
  (capture-context->lc
   (let [is-order-only-for-charge? (= ["CUSTOM_AMOUNT"]
                                      (mapv :item_type (:line_items order)))]
     (if (and is-order-only-for-charge?
              (not ((set (:client/feature-flags client)) "import-custom-amount")))
       (de/success-deferred
        (->> (:tenders order)
             (map #(tender->charge order client location %))))
       (de/catch
        (de/let-flow [line-items
                      (->>
                       (or (:line_items order) [])
                       (s/->source)
                       (s/transform
                        (map-indexed (fn [i li]
                                       (mu/with-context lc
                                         (->
                                          (de/let-flow [category (item-id->category-name client (:catalog_object_id li) (:catalog_version li))]
                                                       (remove-nils
                                                        #:order-line-item
                                                         {:external-id (str "square/order/" (:client/code client) "-" (:square-location/client-location location) "-" (:id order) "-" i)
                                                          :item-name   (:name li)
                                                          :category    (if (= "GIFT_CARD" (:item_type li))
                                                                         "Gift Card"
                                                                         category)
                                                          :total       (amount->money (:total_money li))
                                                          :tax         (amount->money (:total_tax_money li))
                                                          :discount    (amount->money (:total_discount_money li))}))
                                          (de/catch (fn [e]
                                                      (log/error ::cant-transform
                                                                 :exception e
                                                                 :line-item li))))))))
                       (s/buffer 5)
                       (s/realize-each)
                       (s/reduce conj []))]
                     [(remove-nils
                       #:sales-order
                        {:date        (if (= "Invoices" (:name (:source order)))
                                        (when (:closed_at order)
                                          (coerce/to-date (time/to-time-zone (coerce/to-date-time (:closed_at order)) (time/time-zone-for-id "America/Los_Angeles")))) 
                                        (coerce/to-date (time/to-time-zone (coerce/to-date-time (:created_at order)) (time/time-zone-for-id "America/Los_Angeles"))))
                         :client      (:db/id client)
                         :location    (:square-location/client-location location)
                         :external-id (str "square/order/" (:client/code client) "-" (:square-location/client-location location) "-" (:id order))
                         :source      (or (:name (:source order))
                                          "Square")
                         :vendor      :vendor/ccp-square

                         :reference-link (str (url/url  "https://squareup.com/dashboard/sales/transactions" (:id order) "by-unit" (:square-location/square-id location)))
                         :total          (-> order :net_amounts :total_money amount->money)
                         :tax            (-> order :net_amounts :tax_money amount->money)
                         :tip            (-> order :net_amounts :tip_money amount->money)
                         :discount       (-> order :net_amounts :discount_money amount->money)
                         :service-charge (-> order :net_amounts :service_charge_money amount->money)
                         :returns        (+ (- (-> order :return_amounts :total_money amount->money)
                                               (-> order :return_amounts :tax_money amount->money)
                                               (-> order :return_amounts :tip_money amount->money)
                                               (-> order :return_amounts :service_charge_money amount->money))
                                            (-> order :return_amounts :discount_money amount->money))
                         :charges        (->> (:tenders order)
                                              (map #(tender->charge order client location %)))
                         :line-items     line-items})])
        (fn [e]
          (log/error ::failed-to-transform-order
                     :exception e)))))))

(defn should-import-order? [order]
                                    ;; sometimes orders stay open in square. At least one payment
                                    ;; is needed to import, in order to avoid importing orders in-progress.
  (and
   (if (= "Invoices" (:name (:source order)))
     (boolean (:closed_at order))
     true)
   (or (> (count (:tenders order)) 0)
       (seq (:returns order)))
   (or (= #{} (set (map #(:status (:card_details %)) (:tenders order))))
       (not= #{} (set/difference
                  (set (map #(:status (:card_details %)) (:tenders order)))
                  #{"FAILED" "VOIDED"})))))

(defn daily-results
  ([client location]
   (daily-results client location (time/plus (time/now) (time/days -10)) (time/now)))
  ([client location start end]
   (capture-context->lc
    (->
     (de/chain (search client location start end)
               (fn [search-results]
                 (->> (or search-results [])
                      (s/->source)
                      (s/filter should-import-order?)
                      (s/map #(mu/with-context lc (order->sales-order client location %)))
                      (s/buffer 10)
                      (s/realize-each)
                      (s/reduce into []))))
     (de/catch (fn [e]
                 (log/error ::cant-create-results
                            :exception e
                            :client client
                            :location location)))))))


(defn get-payment [client p]
  (de/chain (manifold-api-call
             {:url (str "https://connect.squareup.com/v2/payments/" p)
              :method :get
              :headers (client-base-headers client)})
            :body
            :payment))


(defn continue-payout-entry-list [c l poi cursor]
  (capture-context->lc lc
                       (de/chain
                        (manifold-api-call
                         {:url     (str "https://connect.squareup.com/v2/payouts/" poi "/payout-entries" "?cursor=" cursor)
                          :method  :get
                          :headers (client-base-headers c "2023-04-19")
                          :as      :json})
                        :body
                        (fn [result]
                          (mu/with-context lc
                            (log/info ::payout-list-found
                                      :count (count (:payout_entries result)))
                            (if (not-empty (:cursor result))
                              (de/chain (continue-payout-entry-list c l poi (:cursor result))
                                        (fn [continued-results]
                                          (mu/with-context lc
                                            (concat (:payout_entries result) continued-results))))
                              (:payout_entries result)))))))

(defn get-payout-entry-list [c l poi]
  (capture-context->lc lc
                       (de/chain
                        (manifold-api-call
                         {:url     (str "https://connect.squareup.com/v2/payouts/" poi "/payout-entries")
                          :method  :get
                          :headers (client-base-headers c "2023-04-19")
                          :as      :json})
                        :body
                        (fn [result]
                          (mu/with-context lc
                            (log/info ::payout-list-found
                                      :count (count (:payout_entries result)))
                            (if (not-empty (:cursor result))
                              (de/chain (continue-payout-entry-list c l poi (:cursor result))
                                        (fn [continued-results]
                                          (mu/with-context lc
                                            (concat (:payout_entries result) continued-results))))
                              (:payout_entries result)))))))

(defn payouts
  ([client location] (payouts client location (time/plus (time/now) (time/days -10)) (time/now)))
  ([client location start end]
   (with-context-as {:location (:square-location/client-location location)} lc
     (de/chain (manifold-api-call
                {:url     (str "https://connect.squareup.com/v2/payouts/?"
                               (url/map->query
                                {:location_id (:square-location/square-id location)
                                 :begin_time  (->square-date start)
                                 :end_time (->square-date end)}))
                 :method  :get
                 :headers (client-base-headers client "2023-04-19")})
               :body
               :payouts
               (fn [payouts]
                 (if (seq payouts)
                   (->> payouts
                        (s/->source)
                        (s/map (fn [payout]
                                 (mu/with-context lc
                                   (log/info ::looking-up-payout
                                             :payout-id (:id payout))
                                   (de/chain (get-payout-entry-list client location (:id payout))
                                             (fn [payout-entries]
                                               (assoc payout :payout_entries payout-entries))))))
                        (s/buffer 10)
                        (s/realize-each)
                        (s/reduce conj []))
                   []))))))

(defn transformed-payouts
  ([client location]
   (transformed-payouts client location (payouts client location)))
  ([client location start end]
   (transformed-payouts client location (payouts client location start end)))
  ([client location payouts]
   (with-context-as {:location (:square-location/client-location location)} lc
     (de/chain payouts
               (fn [payouts]
                 (mu/with-context lc
                   (log/info ::transforming-payouts)
                   (try
                     (->> (for [payout payouts
                                :let       [best-sales-date (some->> (dc/q '[:find ?s4 (count ?s)
                                                                             :in $ [?payout-id ...]
                                                                             :where
                                                                             [?payout :expected-deposit/external-id ?payout-id]
                                                                             [?payout :expected-deposit/charges ?c]
                                                                             [?s :sales-order/charges ?c]
                                                                             [?s :sales-order/date ?sales-date]
                                                                             [(clj-time.coerce/to-date-time ?sales-date) ?s2]
                                                                             [(auto-ap.time/localize ?s2) ?s3]
                                                                             [(clj-time.coerce/to-local-date ?s3) ?s4]]
                                                                           (dc/db conn)
                                                                           [(scoped-key "square/payout/" client location (:id payout))
                                                                            (str "square/payout/" (:id payout))])
                                                                     (sort-by last)
                                                                     last
                                                                     first
                                                                     coerce/to-date-time
                                                                     atime/as-local-time
                                                                     coerce/to-date)

                                            ;; TODO delete this -  this is only needed during the short transformation time
                                            equivalent-already-exists? (seq (dc/q '[:find ?s
                                                                                    :in $ ?c ?a
                                                                                    :where
                                                                                    [?s :expected-deposit/date ?ed]
                                                                                    [(>= ?ed #inst "2023-07-10")]
                                                                                    [(<= ?ed #inst "2023-07-26")]
                                                                                    [?s :expected-deposit/client ?c]
                                                                                    [?s :expected-deposit/external-id ?eid]
                                                                                    [(clojure.string/includes? ?eid "settlement")]
                                                                                    [?s :expected-deposit/total ?t]
                                                                                    [(iol-ion.query/dollars= ?t ?a)]]
                                                                                  (dc/db conn)
                                                                                  (:db/id client)
                                                                                  (amount->money (:amount_money payout))))]
                                :when (not equivalent-already-exists?)]
                            #:expected-deposit {:db/id       (or (existing-id (dc/db conn) :expected-deposit/external-id
                                                                              "square/payout/" client location (:id payout))
                                                                 (str "square/payout/" (:id payout)))
                                                :external-id (scoped-key "square/payout/" client location (:id payout))
                                                :vendor      :vendor/ccp-square
                                                :status      :expected-deposit-status/pending
                                                :total       (amount->money (:amount_money payout))
                                                :client      (:db/id client)
                                                :location    (:square-location/client-location location)
                                                :fee         (- (reduce + 0.0 (map (fn [entry]
                                                                                     (if (= (:type entry) "REFUND")
                                                                                       (- (amount->money (:fee_amount_money entry)))
                                                                                       (amount->money (:fee_amount_money entry))))
                                                                                   (:payout_entries payout))))
                                                :date        (-> (:created_at payout)
                                                                 (coerce/to-date))
                                                :sales-date  (or best-sales-date
                                                                 (-> (:created_at payout)
                                                                     (coerce/to-date)))
                                                :charges     (reverse (->> (:payout_entries payout)
                                                                           (filter (comp :payment_id :type_charge_details))
                                                                           (map (fn [p]
                                                                                  (let [payment-id (:payment_id (:type_charge_details p))]
                                                                                    (remove-nils
                                                                                     {:charge/external-id (scoped-key "square/charge/" client location payment-id)
                                                                                      :db/id              (existing-id (dc/db conn) :charge/external-id "square/charge/" client location payment-id)}))))))})
                          (filter :expected-deposit/date)
                          (into []))
                     (catch Throwable e
                       (log/error ::transform-payout-failed
                                  :exception e)))))))))

(defn refunds
  ([client l]
   (de/chain (manifold-api-call {:url    (str "https://connect.squareup.com/v2/refunds?location_id=" (:square-location/square-id l))
                                 :method :get

                                 :headers (client-base-headers client)
                                 :as      :json})
             :body
             :refunds
             (fn [refunds]
               (->> refunds
                    (filter (fn [r] (= "COMPLETED" (:status r))))
                    (s/->source)
                    (s/map (fn [r]
                             (de/chain
                              (get-payment client (:payment_id r))
                              (fn [payment]
                                #:sales-refund {:db/id       (existing-id (dc/db conn) :sales-refund/external-id "square/refund/" client l (:id r))
                                                :external-id (scoped-key "square/refund/" client l (:id r))
                                                :vendor      :vendor/ccp-square
                                                :total       (amount->money (:amount_money r))
                                                :fee         (transduce
                                                              (comp (filter #(= "ADJUSTMENT" (:type %)))
                                                                    (map :amount_money)
                                                                    (map amount->money))
                                                              +
                                                              0.0
                                                              (:processing_fee r))
                                                :client      (:db/id client)
                                                :location    (:square-location/client-location l)
                                                :date        (coerce/to-date (:created_at r))
                                                :type        (:source_type payment)}))))
                    (s/buffer 5)
                    (s/realize-each)
                    (s/reduce conj []))))))
(defn upsert
  ([client]
   (apply de/zip
          (for [square-location (:client/square-locations client)
                :when           (:square-location/client-location square-location)]
            (upsert client square-location (time/plus (time/now) (time/days -10)) (time/now)))))
  ([client location start end]
   (capture-context->lc
    (de/chain (daily-results client location start end)
              (fn [results]
                (mu/with-context lc
                  (doseq [x (partition-all 100 results)]
                    (log/info ::loading-orders
                              :count (count x))
                    @(dc/transact-async conn x))))))))


(defn upsert-payouts
  ([client]
   (apply de/zip
          (for [square-location (:client/square-locations client)
                :when           (:square-location/client-location square-location)]
            (upsert-payouts client square-location))))
  ([client location]
   (upsert-payouts client location (time/plus (time/now) (time/days -10)) (time/now)))
  ([client location start end]
   (with-context-as {:source "Square payout loading"
                     :client (:client/code client)} lc

     (de/chain (transformed-payouts client location start end)
               (fn [payouts]
                 (mu/with-context lc
                   (log/info ::started-loading-deposits :payout-count (count payouts))
                   (doseq [x (partition-all 20 payouts)]
                     (log/info ::loading-deposits
                               :count (count x))
                     @(dc/transact-async conn x))
                   (log/info ::done-loading-deposits)))))))

(defn upsert-refunds
  ([client]
   (apply de/zip
          (for [square-location (:client/square-locations client)
                :when           (:square-location/client-location square-location)]
            (upsert-refunds client square-location))))
  ([client location]
   (with-context-as {:source "Square refunds loading"
                     :client (:client/code client)} lc

     (de/chain (refunds client location)
               (fn [refunds]
                 (mu/with-context lc
                   (try
                     (doseq [x (partition-all 100 refunds)]
                       (log/info ::loading-refunds
                                 :count (count x)
                                 :sample (first x))
                       @(dc/transact-async conn x))

                     (catch Throwable e
                       (log/error ::upsert-refunds-failed
                                  :exception e)))

                   (log/info ::done-loading-refunds)))))))


(defn get-cash-shift [client id]
  (de/chain (manifold-api-call {:url (str (url/url "https://connect.squareup.com/v2/cash-drawers/shifts" id))
                                :method :get

                                :headers (client-base-headers client "2023-04-19")
                                :as      :json})
            :body
            :cash_drawer_shift))

(defn cash-drawer-shifts
  ([client l]
   (cash-drawer-shifts client l (time/plus (time/now) (time/days -75)) (time/now)))
  ([client l start end]
   (de/chain (manifold-api-call {:url (str "https://connect.squareup.com/v2/cash-drawers/shifts"
                                           "?"

                                           (url/map->query
                                            {:location_id   (:square-location/square-id l)
                                             :begin_time (->square-date start)
                                             :end_time   (->square-date end)
                                             :limit 1000}))
                                 :method :get

                                 :headers (client-base-headers client "2023-04-19")
                                 :as      :json})
             :body
             :cash_drawer_shifts
             (fn [shifts]
               (->> shifts
                    (filter (fn [r] (= "ENDED" (:state r))))
                    (s/->source)
                    (s/map (fn [s]
                             (de/chain
                              (get-cash-shift client (:id s))
                              (fn [cash-drawer-shift]
                                #:cash-drawer-shift {:db/id       (or (existing-id (dc/db conn) :cash-drawer-shift/external-id
                                                                                   "square/cash-drawer-shift/" client l (:id cash-drawer-shift))
                                                                      (str "square/cash-drawer-shift/" (:id cash-drawer-shift)))
                                                     :external-id (scoped-key "square/cash-drawer-shift/" client l (:id cash-drawer-shift))
                                                     :vendor      :vendor/ccp-square
                                                     :paid-in       (amount->money (:cash_paid_in_money cash-drawer-shift))
                                                     :paid-out       (amount->money (:cash_paid_out_money cash-drawer-shift))
                                                     :expected-cash       (amount->money (:expected_cash_money cash-drawer-shift))
                                                     :opened-cash       (amount->money (:opened_cash_money cash-drawer-shift))
                                                     :date (coerce/to-date (:opened_at cash-drawer-shift))
                                                     :client      (:db/id client)
                                                     :location    (:square-location/client-location l)}))))
                    (s/buffer 5)
                    (s/realize-each)
                    (s/reduce conj []))))))

(defn upsert-cash-shifts
  ([client]
   (apply de/zip
          (for [square-location (:client/square-locations client)
                :when           (:square-location/client-location square-location)]
            (upsert-cash-shifts client square-location))))
  ([client location]
   (upsert-cash-shifts client location (time/plus (time/now) (time/days -75)) (time/now)))
  ([client location start end]
   (with-context-as {:source "Square cash shift loading"
                     :client (:client/code client)} lc

     (de/chain (cash-drawer-shifts client location start end)
               (fn [cash-shifts]
                 (mu/with-context lc
                   (try
                     (doseq [x (partition-all 100 cash-shifts)]
                       (log/info ::loading-cash-shifts
                                 :count (count x)
                                 :sample (first x))
                       @(dc/transact-async conn x))

                     (catch Throwable e
                       (log/error ::upsert-cash-shifts-failed
                                  :exception e)))

                   (log/info ::done-loading-cash-shifts)))))))

(def square-read [:db/id
                  :client/code
                  :client/square-auth-token
                  :client/feature-flags
                  {:client/square-locations [:db/id :square-location/name :square-location/square-id :square-location/client-location]}])

(defn get-square-clients
  ([]
   (map first (dc/q '[:find (pull ?c [:db/id
                                      :client/square-integration-status
                                      :client/code
                                      :client/square-auth-token
                                      :client/feature-flags
                                      {:client/square-locations [:db/id :square-location/name :square-location/square-id :square-location/client-location]}])
                      :in $
                      :where [?c :client/square-auth-token]]
                    (dc/db conn))))
  ([& codes]
   (map first (dc/q '[:find (pull ?c [:db/id
                                      :client/code
                                      :client/square-auth-token
                                      :client/feature-flags
                                      {:client/square-locations [:db/id :square-location/name :square-location/square-id :square-location/client-location]}])
                      :in $ [?code ...]
                      :where [?c :client/square-auth-token]
                      [?c :client/code ?code]]
                    (dc/db conn)
                    codes))))

(defn get-square-client-and-location [code]
  (let [[client] (get-square-clients code)]
    (some->> client
             :client/square-locations
             (filter :square-location/client-location)
             seq
             (conj [client]))))

(defn upsert-locations
  ([]
   (apply de/zip
          (for [client (get-square-clients)]
            (upsert-locations client))))
  ([client]
   (let [square-id->id (into {}
                             (map
                              (fn [sl]
                                [(:square-location/square-id sl)
                                 (:db/id sl)])
                              (:client/square-locations client)))]
     (de/chain (client-locations client)
               (fn [client-locations]
                 @(dc/transact-async conn
                                     (for [square-location client-locations]
                                       {:db/id                     (or (square-id->id (:id square-location)) (str (java.util.UUID/randomUUID)))
                                        :client/_square-locations  (:db/id client)
                                        :square-location/name      (:name square-location)
                                        :square-location/square-id (:id square-location)})))))))

#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn reset [client]
  (->>
   (dc/q {:find  ['?e]
          :in    ['$ '?c]
          :where ['(or [?e :sales-order/client ?c]
                       [?e :expected-deposit/client ?c]
                       [?e :sales-refund/client ?c]
                       [?e :charge/client ?c]
                       [?e :cash-drawer-shift/client ?c])]}
         (dc/db conn)
         client)
   (map first)
   (map (fn [x] [:db/retractEntity x]))))

(defn mark-integration-status [client integration-status]
  @(dc/transact-async conn
                      [{:db/id                            (:db/id client)
                        :client/square-integration-status (assoc integration-status
                                                                 :db/id (or (-> client :client/square-integration-status :db/id)
                                                                            (str (java.util.UUID/randomUUID))))}]))
(defn max-date [d1 d2]
  (if (time/after? d1 d2)
    d1
    d2))



(defn remove-voided-orders
  ([client]
   (apply de/zip
          (for [square-location (:client/square-locations client)
                :when           (:square-location/client-location square-location)]
            (remove-voided-orders client square-location (time/plus (time/now) (time/days -10)) (time/now)))))
  ([client location start end]
   (let [start (max-date start (coerce/to-date-time #inst "2024-04-15T00:00:00-08:00"))]
     (capture-context->lc
      (-> (de/chain (search client location start end)
                    (fn [search-results]
                      (->> (or search-results [])
                           (s/->source)
                           (s/filter #(not (should-import-order? %)))
                           (s/map #(mu/with-context lc (order->sales-order client location %)))
                           (s/buffer 10)
                           (s/realize-each)
                           (s/filter (fn already-exists [[o]]
                                       (when (:sales-order/external-id o)
                                         (seq (dc/q '[:find ?i
                                                      :in $ ?ei
                                                      :where [?i :sales-order/external-id ?ei]]
                                                    (dc/db conn)
                                                    (:sales-order/external-id o))))))
                           (s/map (fn [[o]]
                                    [[:db/retractEntity [:sales-order/external-id (:sales-order/external-id o)]]]))
                           
                           (s/reduce into [])))

                    (fn [results]
                      (mu/with-context lc
                        (doseq [x (partition-all 100 results)]
                          (log/info ::removing-orders
                                    :count (count x))
                          @(dc/transact-async conn x)))))
                          (de/catch (fn [e]
                                      (log/warn ::couldnt-remove :error e)
                                      nil) ))))))

#_(comment 
  (require 'auto-ap.time-reader)

 @(let [[c [l]] (get-square-client-and-location "DBFS")  ]
     (log/peek :x  [ c l])
     (search c l #clj-time/date-time "2026-03-28" #clj-time/date-time "2026-03-29")

     ) 

  @(let [[c [l]] (get-square-client-and-location "NGAK")  ]
     (log/peek :x  [ c l])

     (remove-voided-orders c l #clj-time/date-time "2024-04-11" #clj-time/date-time "2024-04-15"))
  (doseq [c (get-square-clients)]
    (try 
      @(remove-voided-orders c)
      (catch Exception e
        nil)))

  
  )

(defn upsert-all [& clients]
  (capture-context->lc
   (log/info ::starting-upsert)
   (->> (apply get-square-clients clients)
        (s/->source)
        (s/filter (fn [client]
                    (seq (filter :square-location/client-location (:client/square-locations client)))))
        (s/map (fn [client]
                 (with-context-as (merge lc {:client (:client/code client)}) lc
                   (log/info ::import-started)
                   (mark-integration-status client {:integration-status/last-attempt (coerce/to-date (time/now))})

                   (->
                    (de/chain (upsert-locations client)
                              (fn [_]
                                (mu/with-context lc
                                  (log/info ::upsert-orders-started)
                                  (upsert client)))
                              (fn [_]
                                (mu/with-context lc
                                  (log/info ::remove-voided-orders-started)
                                  (remove-voided-orders client)))
                              (fn [_]
                                (mu/with-context lc
                                  (log/info ::upsert-payouts-started)
                                  (upsert-payouts client)))
                              (fn [_]
                                (mu/with-context lc
                                  (log/info ::upsert-refunds-started)
                                  (upsert-refunds client)))

                              (fn [_]
                                (mu/with-context lc
                                  (log/info ::upsert-cash-shifts)
                                  (upsert-cash-shifts client)))
                              (fn [_]
                                (mu/with-context lc
                                  (log/info ::upsert-done))
                                (mark-integration-status client {:integration-status/state        :integration-state/success
                                                                 :integration-status/last-updated (coerce/to-date (time/now))})))
                    (de/catch (fn [e]
                                (mu/with-context lc
                                  (let [data (ex-data e)]
                                    (log/info ::upsert-all-failed
                                              :severity :error
                                              :exception e)
                                    (cond (= (:status data) 401)
                                          (mark-integration-status client {:integration-status/state   :integration-state/unauthorized
                                                                           :integration-status/message (-> data :body str)})

                                          (= (:status data) 503)
                                          (mark-integration-status client {:integration-status/state   :integration-state/failed
                                                                           :integration-status/message (-> data :body str)})
                                          :else
                                          (mark-integration-status client {:integration-status/state   :integration-state/failed
                                                                           :integration-status/message (or (ex-message e)
                                                                                                           (str e))}))))))))))
        (s/buffer 5)
        (s/realize-each)
        (s/reduce conj []))))

(defn do-upsert-all [& clients]
  (mu/trace
   ::upsert-all
   [:clients clients]
   @(apply upsert-all clients)))



(comment
  (defn refunds-raw-cont
    ([client l cursor so-far]
     (de/chain (manifold-api-call {:url    (str "https://connect.squareup.com/v2/refunds?location_id=" (:square-location/square-id l)
                                                (when cursor
                                                  (str "&cursor=" cursor))
                                                "&begin_time=2024-03-12T19:59:05.342Z")
                                   :method :get

                                   :headers (client-base-headers client)
                                   :as      :json})
               :body
               (fn [rr]
                 (if (:cursor rr)
                   (refunds-raw-cont client l (:cursor rr)
                                     (concat  so-far (:refunds rr)))
                   (concat  so-far (:refunds rr)))))))

  #_(defn refunds-raw
      ([client l]
       (de/chain (manifold-api-call {:url    (str "https://connect.squareup.com/v2/refunds?location_id=" (:square-location/square-id l) "&begin_time=2024-03-14&end_time=2024-03-15")
                                     :method :get

                                     :headers (client-base-headers client)
                                     :as      :json})
                 :body)))
  (->>
   @(let [[c [l]] (get-square-client-and-location "NGGG")]


      (search c l (time/now) (time/plus (time/now) (time/days -1))))
   
   (filter (fn [r]
             (str/starts-with? (:created_at r) "2024-03-14"))))

  (def refs
    (->>
     @(let [[c [l]] (get-square-client-and-location "NGGG")]


        (refunds-raw-cont c l nil []))
     (filter (fn [r]
               (str/starts-with? (:created_at r) "2024-03-14")))))

  (let [[c [l]] (get-square-client-and-location "NGCL")]

    #_(get-payment c l)

    (map (fn [r] @(get-payment c (:payment_id r))) refs))

  (get-square-client-and-location "NGGB")

  (def my-results
    (let [[c [l]] (get-square-client-and-location "NGFA")]))

  (clojure.data.csv/write-csv *out*
                              (for [c  (get-square-clients)
                                    l (:client/square-locations c)
                                    :when (:square-location/client-location l)
                                    bad-row (try (->> @(search c l (coerce/to-date-time #inst "2024-04-01T00:00:00-07:00") (coerce/to-date-time #inst "2024-04-15T23:59:00-07:00"))
                                                      (filter #(not (should-import-order? %)))
                                                      (map #(first (deref (order->sales-order c l %))))
                                                      (filter (fn already-exists [o]
                                                                (when (:sales-order/external-id o)
                                                                  (seq (dc/q '[:find ?i
                                                                               :in $ ?ei
                                                                               :where [?i :sales-order/external-id ?ei]]
                                                                             (dc/db conn)
                                                                             (:sales-order/external-id o)))))))
                                                 (catch Exception e
                                                   []))]
                                [(:client/code c) (atime/unparse-local (clj-time.coerce/to-date-time (:sales-order/date bad-row)) atime/normal-date) (:sales-order/total bad-row)  (:sales-order/tax bad-row) (:sales-order/tip bad-row) (:db/id bad-row)])
                              :separator \tab)
  



  ;; => 
  


  (require 'auto-ap.time-reader)

  @(upsert-all "NGPG")

  (clojure.pprint/pprint (let [[c [l]] (get-square-client-and-location "NGVT")]
                           l


                           (def z @(search c l #clj-time/date-time "2025-02-23T00:00:00-08:00"
                                           #clj-time/date-time "2025-02-28T00:00:00-08:00"))
                           (take 10 (map #(first (deref (order->sales-order c l %))) z)))


                         )
  
  


  (->> z 
       (filter (fn [o]
                 (seq (filter (comp #{"OTHER"} :type) (:tenders o)))))
       (filter #(not (:name (:source %))))
       (count)
       
       )
  
  
  
  (doseq [[code] (seq (dc/q '[:find ?code
                              :in $
                              :where [?o :sales-order/date ?d]
                              [(>= ?d #inst "2026-01-01")]
                              [?o :sales-order/source "Invoices"]
                              [?o :sales-order/client ?c]
                              [?c :client/code ?code]]
                            (dc/db conn)))
          :let [[c [l]] (get-square-client-and-location code)
                ]
          order @(search c l #clj-time/date-time "2026-01-01T00:00:00-08:00" (time/now))
          :when (= "Invoices" (:name (:source order) ))
          :let [[sales-order] @(order->sales-order c l order)]]
    
    (when (should-import-order? order)
      (println "DATE IS" (:sales-order/date sales-order))
      (when (some-> (:sales-order/date sales-order) coerce/to-date-time (time/after?  #clj-time/date-time "2026-2-16T00:00:00-08:00"))
        (println "WOULD UPDATE" sales-order)
        @(dc/transact auto-ap.datomic/conn [sales-order])
        )
      #_@(dc/transact )
      (println "DONE"))
    
    
    )

  #_(filter (comp #{"OTHER"} :type) (mapcat :tenders z))
  

  @(let [[c [l]] (get-square-client-and-location "NGRY")]
     #_(search c l (clj-time.coerce/from-date #inst "2025-02-28") (clj-time.coerce/from-date #inst "2025-03-01"))

     (order->sales-order c l (:order (get-order c l "KdvwntmfMNTKBu8NOocbxatOs18YY" )))

     )
  
  )
