(ns auto-ap.square.core
  (:require [auto-ap.datomic :refer [conn remove-nils]]
            [auto-ap.utils :refer [by]]
            [clj-http.client :as client]
            [clj-time.coerce :as coerce]
            [clj-time.core :as time]
            [clj-time.format :as f]
            [config.core :refer [env] :as cfg ]
            [clojure.string :as str]
            [clojure.data.json :as json]
            [clojure.tools.logging :as log]
            [datomic.api :as d]
            [mount.core :as mount]
            [unilog.context :as lc]
            [yang.scheduler :as scheduler]
            [clojure.core.async :as async]))

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

(defn lookup-dates []
  (->> (clj-time.periodic/periodic-seq (time/plus (time/now) (time/days -15))
                                       (time/now)
                                       (time/days 5))
       (map (fn [d]
              [(auto-ap.time/unparse (time/plus d (time/days 1)) auto-ap.time/iso-date)

               (auto-ap.time/unparse (time/plus d (time/days 5)) auto-ap.time/iso-date)]))))

(defn locations [client]
  (->> (client/get "https://connect.squareup.com/v2/locations"
                   {:headers (base-headers client)
                    :as :json}) 
       :body
       :locations))

(defn fetch-catalog [client i]
  (if i 
    (try 
      (log/info "looking up catalog for" (str "https://connect.squareup.com/v2/catalog/object/" i))
      (->> (client/get (str "https://connect.squareup.com/v2/catalog/object/" i)
                       {:headers (base-headers client)
                        :query-params {"include_related_items" "true"}
                        :as :json}) 
           :body
           :object)
      (catch Exception e
        (log/error e)
        nil))
    (log/warn "Trying to look up non existant ")))

(def fetch-catalog-fast (memoize fetch-catalog))

(defn item-id->category-name [client i]
  (let [item (fetch-catalog-fast client i)]
    (cond (:item_variation_data item)
          (item-id->category-name client (:item_id (:item_variation_data item)))

          (:category_id (:item_data item))
          (:name (:category_data (fetch-catalog-fast client (:category_id (:item_data item)))))

          (:item_data item)
          "Uncategorized"

          :else
          (do 
            (log/warn "couldn't look up" i)
            "Uncategorized"))))

(defn categories []
  (by :id (comp :name :category_data) ))

(def potential-query
  {"query" {"filter" {"date_time_filter"
                      {
                       "created_at" {
                                     "start_at" "2020-08-28T00:00:00-07:00",
                                     "end_at" "2020-08-28T23:59:59-07:00"
                                     }

                       

                       }
                      "state_filter" {"states" ["COMPLETED"]}}

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


(defn pc [d] {"query" {"filter" {"date_time_filter"
                      {
                       "created_at" {
                                     "start_at" (f/unparse (f/formatter "YYYY-MM-dd'T'HH:mm:ssZZ") (time/to-time-zone  (coerce/to-date-time d) (time/time-zone-for-id "America/Los_Angeles")))
                                     "end_at" (f/unparse (f/formatter "YYYY-MM-dd'T'HH:mm:ssZZ") (time/to-time-zone  (time/plus (coerce/to-date-time d) (time/days 1)) (time/time-zone-for-id "America/Los_Angeles")))
                                     }

                       

                       }}

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


(defn search
  ([client]
   (search client (get-in env [:square-config client :square-location])))
  ([client l]
   (search client l nil))
  ([client l d]
   (log/info "Searching for" l)
   (let [result (->> (client/post "https://connect.squareup.com/v2/orders/search"
                            {:headers (base-headers client)
                            :body (json/write-str (cond-> {"location_ids" [l] "limit" 10000}
                                                    d (merge (pc d))))
                            :as :json}) 
              :body
              :orders)]
     (log/info "found " (count result))
     result)))


(defn order
  
  ([client o]
   (log/info "Searching for" o)
   (let [result (->> (client/get (str "https://connect.squareup.com/v2/orders/" o) 
                                  {:headers (base-headers client)
                                   :as :json}) 
                     :body)]
      
     (log/info "found " (count result))
     result)))

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

(defn location_id->client-location [location]
  (get-in env [:square-location location]))

;; 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 order->sales-order [client location order]
  (remove-nils
   #:sales-order
   {:date (coerce/to-date (time/to-time-zone (coerce/to-date-time (:created_at order)) (time/time-zone-for-id "America/Los_Angeles"))) 
    :client [:client/code client]
    :location location
    :external-id (str "square/order/" client "-" location "-" (:id order))
    :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 (fn [t]
                         (remove-nils 
                          #:charge
                          {:type-name (:type t)
                           :processor (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
                                        (condp = (:name (:source order))
                                          "GRUBHUB" :ccp-processor/grubhub
                                          "UBEREATS" :ccp-processor/uber-eats
                                          "DOORDASH" :ccp-processor/doordash
                                          :ccp-processor/na))
                           :total (amount->money (:amount_money t))
                           :tip (amount->money (:tip_money t))}))))
    :line-items (->> (:line_items order)
                     (map (fn [li]
                            (remove-nils 
                             #:order-line-item
                             {:item-name (:name li)
                              :category (if (= "GIFT_CARD" (:item_type li))
                                          "Gift Card"
                                          (item-id->category-name client (:catalog_object_id li)))
                              :total (amount->money (:total_money li))
                              :tax (amount->money (:total_tax_money li))
                              :discount (amount->money (:total_discount_money li))}))))}))

(defn daily-results
  ([client]
   (daily-results client nil))
  ([client d]
   (daily-results client (get-in env [:square-config client :square-location]) d))
  ([client location d]
   (->> (search client location d)
        (filter (fn [order]
                  ;; sometimes orders stay open in square. At least one payment
                  ;; is needed to import, in order to avoid importing orders in-progress.
                  (and (or (> (count (:tenders order)) 0)
                           (seq (:returns order)))
                       (not= #{"FAILED"}
                             (set (map #(:status (:card_details %)) (:tenders order)))))))
        (filter (fn [order]
                  (and 
                   (not= "Koala" (:name (:source order)))
                   (not= "koala-production" (:name (:source order))))))
        (map #(order->sales-order client (get-in env [:square-config client :location]) %)))))

#_(daily-results)


(defn retry
  ([f] (retry f 0))
  ([f i]
   (if (< i 5)
     (try
       (f)
       (catch Exception e
         (log/warn "error pulling http " e)
         (retry f (inc i))))
     (log/error "Too many failures"))))

(defn get-payment [client p]
  (:payment (:body (retry #(client/get (str "https://connect.squareup.com/v2/payments/" p)
                                       {:headers (base-headers client)
                                        :as :json
                                        :retry-handler (fn [ex try-count http-context]
                                                         (log/warn "Retrying after failure " ex)
                                                         (if (> try-count 4) false true))})))))

(defn get-settlement-sales-date [client settlement]
  (let [concurrent 10
        output-chan (async/chan)]
    (async/pipeline-blocking concurrent
                             output-chan
                             (map (fn [p]
                                    (lc/with-context {:source "Square settlements loading "}
                                      (log/info "looking up payment " p " for settlement " (:id settlement))
                                      (or 
                                       (-> (get-payment client p)
                                           :created_at
                                           coerce/to-date)
                                       (coerce/to-date (time/now))))))
                             (async/to-chan (->> settlement
                                                 :entries
                                                 (filter #(= "CHARGE" (:type %)))
                                                 (map :payment_id)
                                                 (filter identity)
                                                 (set)
                                                 (take 20)
                                                 ))
                             true
                             (fn [e]
                               (lc/with-context {:source "Square settlements loading "}
                                 (log/error "Error loading sales date details" e))))
    (->> (async/<!! (async/into [] output-chan))
         sort
         (drop 2)
         first)))

(defn get-settlement-details [client settlements] ;; pairs of [location settlement]
  (log/info "getting settlement details for " settlements)
  (let [concurrent 10
        output-chan (async/chan)]
    (async/pipeline-blocking concurrent
                             output-chan
                             (map (fn [[location s :as g]]
                                    (lc/with-context {:source "Square settlements loading "}
                                      (log/info "Looking up settlement " s " for location " location)
                                      (let [settlement (:body (retry #(client/get (str "https://connect.squareup.com/v1/" location "/settlements/" s)
                                                                                  {:headers (base-headers client)
                                                                                   :as :json
                                                                                   :retry-handler (fn [ex try-count http-context]
                                                                                                    (log/warn "Retrying after failure " ex)
                                                                                                    (if (> try-count 4) false true))})))]
                                        
                                        (log/info "sales date for " s " is " (get-settlement-sales-date client settlement))
                                        (assoc settlement :sales-date (get-settlement-sales-date client settlement) )))))
                             (async/to-chan settlements)
                             true
                             (fn [e]
                               (lc/with-context {:source "Square settlements loading "}
                                 (log/error "Error loading settlements details" e))))
    (async/<!! (async/into [] output-chan))))

(defn settlements
  ([client l] (settlements client l (lookup-dates)))
  ([client l lookup-dates]
   (log/info "Searching for" l)
   (->> lookup-dates
        (mapcat (fn [[start-date end-date]]
                  (log/info "looking up settlements for " l " on dates " start-date " to " end-date)
                  (let [settlements (->> (client/get (str "https://connect.squareup.com/v1/" l "/settlements")
                                                     {:headers (base-headers client)
                                                      :query-params {"begin_time" start-date
                                                                     "end_time" end-date}
                                                      :as :json}) 
                                         :body
                                         (map :id))]
                    settlements)))
        set
        seq
        (map (fn [s]
               [l s]))
        (get-settlement-details client)
        )))

(defn daily-settlements
  ([client] (daily-settlements client
                               (get-in env [:square-config client :square-location])))
  ([client location-id]
   (->> (for [settlement (settlements client location-id)]
          #:expected-deposit {:external-id (str "square/settlement/" (:id settlement))
                              :total (amount->money (:total_money settlement))
                              :client [:client/code client]
                              :location (get-in env [:square-config client :location])
                              :fee (- (reduce + 0.0 (map (fn [entry]
                                                           (if (= (:type entry) "REFUND")
                                                             (- (amount->money (:fee_money entry)))
                                                             (amount->money (:fee_money entry))))
                                                         (:entries settlement))))
                              :date (-> (:initiated_at settlement)
                                        (coerce/to-date))
                              :sales-date (or (:sales-date settlement)
                                              (-> (:initiated_at settlement)
                                                  (coerce/to-date)))})
    
    (filter :expected-deposit/date))))

(defn refunds
  ([client]
   (refunds client (get-in env [:square-config client :square-location])))
  ([client l]
               (let [refunds (:refunds (:body (client/get (str "https://connect.squareup.com/v2/refunds?location_id=" l)
                                                          {:headers (base-headers client)
                                                           :as :json
                                                           :retry-handler (fn [ex try-count http-context]
                                                                            (log/warn "Retrying after failure " ex)
                                                                            (if (> try-count 4) false true))})))]
                 (->> refunds
                      (filter (fn [r] (= "COMPLETED" (:status r))))
                      (map (fn [r]
                             #:sales-refund {:external-id (str "square/refund/" (:id r))
                                             :total (amount->money (:amount_money r))
                                             :fee (transduce
                                                   (comp (filter #(= "ADJUSTMENT" (:type %)))
                                                         (map :amount_money)
                                                         (map amount->money))
                                                   +
                                                   0.0
                                                   (:processing_fee r))
                                             :client [:client/code client]
                                             :location (get-in env [:square-config client :location])
                                             :date (coerce/to-date (:created_at r))
                                             :type (:source_type (get-payment client (:payment_id r)))}))))))

(defn upsert
  ([client ]
   (upsert client nil))
  ([client d]
   (lc/with-context {:source "Square loading"}
     (try 
       (let [existing (->> (d/query {:query {:find ['?external-id]
                                             :in ['$ '?client]
                                             :where ['[?o :sales-order/client ?client]
                                                     '[?o :sales-order/external-id ?external-id]]}
                                     :args [(d/db conn) [:client/code client]]})
                           (map first)
                           set)
             _ (log/info (count existing) "Sales orders already exist")
             to-create (filter #(not (existing (:sales-order/external-id %)))
                               (daily-results client d))]
         (doseq [x (partition-all 20 to-create)]
           (log/info "Loading " (count x))
           @(d/transact conn x)))
       (catch Exception e
         (log/error e))))))

(defn upsert-settlements
  ([client] (upsert-settlements client (get-in env [:square-config client :square-location])))
  ([client location-id]
   (lc/with-context {:source "Square settlements loading"}
     (try 
       (let [existing (->> (d/query {:query {:find ['?external-id]
                                             :in ['$ '?client]
                                             :where ['[?d :expected-deposit/client ?client]
                                                     '[?d :expected-deposit/external-id ?external-id]]}
                                     :args [(d/db conn) [:client/code client]]})
                           (map first)
                           set)
             _ (log/info (count existing) "settlements already exist")
             to-create (filter  #(not (existing (:expected-deposit/external-id %)))
                                (daily-settlements client location-id))]
         (doseq [x (partition-all 20 to-create)]
           (log/info "Loading expected deposit" (count x))
           @(d/transact conn x)))
       (catch Exception e
         (log/error e)))
     (log/info "Done loading settlements"))))

(defn upsert-refunds
  ([client] (upsert-refunds client (get-in env [:square-config client :square-location])))
  ([client location]
   (lc/with-context {:source "Loading Square Settlements"
                     :client client}
     (try 
       (let [existing (->> (d/query {:query {:find ['?external-id]
                                             :in ['$ '?client]
                                             :where ['[?r :sales-refund/client ?client]
                                                     '[?r :sales-refund/external-id ?external-id]]}
                                     :args [(d/db conn) [:client/code client]]})
                           (map first)
                           set)
             _ (log/info (count existing) "refunds already exist")
             to-create (filter  #(not (existing (:sales-refund/external-id %)))
                                (refunds client location))]
         (doseq [x (partition-all 20 to-create)]
           (log/info "Loading refund" (count x))
           @(d/transact conn x)))
       (catch Exception e
         (log/error e)))
     (log/info "Done loading refunds"))))

(defn reset []
  (->> 
   (d/query {:query {:find ['?e]
                     :in ['$]
                     :where ['[?e :sales-order/date]]}
             :args [(d/db conn)]})
   (map first)
   (map (fn [x] [:db/retractEntity x]))))

(defn upsert-all []
  (doseq [[client-code] (get-in env [:square-config])]
    (lc/with-context {:source (str "Square loading for " client-code)
                      :client client-code}
      (log/info "Loading Orders")
      (upsert client-code)
      (log/info "Loading Settlements")
      (upsert-settlements client-code)
      (log/info "Loading refunds")
      (upsert-refunds client-code)
      )))

(mount/defstate square-loader
  :start (scheduler/every (* 4 59 60 1000) upsert-all)
  :stop (scheduler/stop square-loader))





(comment
  (daily-results)
  (mount/stop (mount/only #{'auto-ap.square.core/square-settlement-loader}))

  (mount/stop (mount/only #{'auto-ap.square.core/square-refund-loader}))
  
  (mount/stop (mount/only #{'auto-ap.square.core/square-loader}))

  (mount/start (mount/only #{'auto-ap.square.core/square-settlement-loader}))
  (mount/start (mount/only #{'auto-ap.square.core/square-refund-loader}))
  (mount/start (mount/only #{'auto-ap.square.core/square-loader}))
  (do (upsert) nil)
  
  (do @(d/transact conn (reset)) nil))

