(ns auto-ap.graphql.clients
  (:require [auto-ap.datomic :refer [audit-transact conn remove-nils]]
            [auto-ap.datomic.clients :as d-clients]
            [auto-ap.graphql.utils :refer [->graphql assert-admin can-see-client? is-admin?]]
            [auto-ap.utils :refer [by]]
            [auto-ap.yodlee.core :refer [in-memory-cache]]

            [clj-time.coerce :as coerce]
            [config.core :refer [env]]
            [clojure.string :as str]
            [unilog.context :as lc]
            [clojure.tools.logging :as log]
            [datomic.api :as d]
            [clojure.java.io :as io]
            [amazonica.aws.s3 :as s3]
            [yang.scheduler :as scheduler]
            [mount.core :as mount])
  (:import [org.apache.commons.codec.binary Base64]
           java.util.UUID))

(defn assert-client-code-is-unique [code]
  (when (seq (d/query {:query {:find '[?id]
                               :in ['$ '?code]
                               :where ['[?id :client/code ?code]]}
                       :args [(d/db conn) code]}))
    (throw (ex-info "Client is not unique" {:validation-error (str "Client code '" code "' is not unique.")}))))

(defn upload-signature-data [signature-data]
  (let [prefix "data:image/jpeg;base64,"]
    (when signature-data
      (when-not (str/starts-with? signature-data prefix)
        (throw (ex-info "Invalid signature image" {:validation-error (str "Invalid signature image.")})))
      (let [signature-id (str (UUID/randomUUID))
            raw-bytes (Base64/decodeBase64 (subs signature-data (count prefix)))]
        (s3/put-object :bucket-name "integreat-signature-images" #_(:data-bucket env)
                       :key (str signature-id ".jpg")
                       :input-stream (io/make-input-stream raw-bytes {})
                       :metadata {:content-type "image/jpeg"}
                       :canned-acl "public-read")
        (str "https://integreat-signature-images.s3.amazonaws.com/" signature-id ".jpg")))))

(defn edit-client [context {:keys [edit_client new_bank_accounts] :as args} value]
  (assert-admin (:id context))
  (when-not (:id edit_client)
    (assert-client-code-is-unique (:code edit_client)))
 
  (let [client (when (:id edit_client) (d-clients/get-by-id (:id edit_client)))
        id (or (:db/id client) "new-client")
        signature-file (upload-signature-data (:signature_data edit_client))
        _ (when client
            (audit-transact (into 
                             (mapv (fn [lm] [:db/retractEntity (:db/id lm)]) (:client/location-matches client))
                             (mapv (fn [m] [:db/retract (:db/id client) :client/matches m]) (:client/matches client)))
                            (:id context)))
        reverts (-> []
                    (into (->> (:bank_accounts edit_client)
                               (filter #(nil? (:yodlee_account_id %)))
                               (filter #(:bank-account/yodlee-account-id (d/entity (d/db conn) (:id %))))
                               (map (fn [ba] [:db/retract (:id ba) :bank-account/yodlee-account-id (:bank-account/yodlee-account-id (d/entity (d/db conn) (:id ba)))]))))
                    (into (->> (:bank_accounts edit_client)
                               (filter #(nil? (:yodlee_account %)))
                               (filter #(:bank-account/yodlee-account (d/entity (d/db conn) (:id %))))
                               (map (fn [ba] [:db/retract (:id ba) :bank-account/yodlee-account (:db/id (:bank-account/yodlee-account (d/entity (d/db conn) (:id ba))))]))))
                    (into (->> (:bank_accounts edit_client)
                               (filter #(nil? (:intuit_bank_account %)))
                               (filter #(:bank-account/intuit-bank-account (d/entity (d/db conn) (:id %))))
                               (map (fn [ba] [:db/retract (:id ba) :bank-account/intuit-bank-account (:db/id (:bank-account/intuit-bank-account (d/entity (d/db conn) (:id ba))))])))))
        

        transactions (into [(remove-nils {:db/id id
                                          :client/code (if (str/blank? (:client/code client))
                                                         (:code edit_client)
                                                         (:client/code client))
                                          :client/name (:name edit_client)
                                          :client/matches (:matches edit_client)
                                          :client/signature-file signature-file
                                          :client/email (:email edit_client)
                                          :client/locations (filter identity (:locations edit_client))
                                          :client/week-a-debits (:week_a_debits edit_client)
                                          :client/week-a-credits (:week_a_credits edit_client)
                                          :client/week-b-debits (:week_b_debits edit_client)
                                          :client/week-b-credits (:week_b_credits edit_client)
                                          :client/location-matches (->> (:location_matches edit_client)
                                                                        (filter (fn [lm] (and (:location lm) (:match lm))))
                                                                        (map (fn [lm] {:location-match/location (:location lm)
                                                                                       :location-match/matches [(:match lm)]})))
                                          :client/address (remove-nils {
                                                                        :address/street1  (:street1 (:address edit_client))
                                                                        :address/street2  (:street2 (:address edit_client))
                                                                        :address/city  (:city (:address edit_client))
                                                                        :address/state  (:state (:address edit_client))
                                                                        :address/zip  (:zip (:address edit_client))})
                                          :client/bank-accounts (map #(remove-nils
                                                                       (cond-> {:db/id (:id %)
                                                                                :bank-account/code (:code %)
                                                                                :bank-account/bank-name (:bank_name %)
                                                                                :bank-account/bank-code (:bank_code %)
                                                                                :bank-account/start-date (-> (:start_date %) (coerce/to-date))
                                                                                :bank-account/routing (:routing %)
                                                                                :bank-account/include-in-reports (:include_in_reports %)

                                                                                :bank-account/name (:name %)
                                                                                :bank-account/visible (:visible %)
                                                                                :bank-account/number (:number %)
                                                                                :bank-account/check-number (:check_number %)
                                                                                :bank-account/numeric-code (:numeric_code %)
                                                                                :bank-account/sort-order (:sort_order %)
                                                                                :bank-account/locations (:locations %)

                                                                                :bank-account/yodlee-account-id (:yodlee_account_id %)
                                                                                :bank-account/type (keyword "bank-account-type" (name (:type %)))}
                                                                         (:yodlee_account %) (assoc :bank-account/yodlee-account [:yodlee-account/id (:yodlee_account %)])
                                                                         (:intuit_bank_account %) (assoc :bank-account/intuit-bank-account (:intuit_bank_account %))))
                                                                     (:bank_accounts edit_client))

                                          })
                            [:reset id :client/forecasted-transactions (map #(remove-nils
                                                                              {:db/id (:id %)
                                                                               :forecasted-transaction/day-of-month (:day_of_month %)
                                                                               :forecasted-transaction/identifier (:identifier %)
                                                                               :forecasted-transaction/amount (:amount %)}
                                                                              )
                                                                            (:forecasted_transactions edit_client))]]
                           reverts)
        _ (log/info "upserting client" transactions)
        result (audit-transact transactions (:id context))]
    (-> result :tempids (get id) (or id) d-clients/get-by-id
        (update :client/location-matches
                (fn [lms]
                  (mapcat (fn [lm]
                            (map (fn [m]
                                      {:location-match/match m
                                       :location-match/location (:location-match/location lm)})
                                    (:location-match/matches lm)))
                          lms)))
        ->graphql)))

(defn refresh-bank-account-current-balance [bank-account-id]
  (let [all-transactions (d/query 
                          {:query {:find ['?e '?debit '?credit]
                                   :in ['$ '?bank-account-id]
                                   :where '[[?e :journal-entry-line/account ?bank-account-id]
                                            [(get-else $ ?e :journal-entry-line/debit 0.0) ?debit]
                                            [(get-else $ ?e :journal-entry-line/credit 0.0) ?credit]]}
                           :args [(d/db conn) bank-account-id]})
        debits (->> all-transactions
                    (map (fn [[e debit credit]]
                           debit))
                    (reduce + 0.0))
        credits (->> all-transactions
                     (map (fn [[e debit credit]]
                            credit))
                     (reduce + 0.0))
        current-balance (if (= :bank-account-type/check (:bank-account/type (d/entity (d/db conn) bank-account-id)))
                          (- debits credits)
                          (- credits debits))]
    @(d/transact conn [{:db/id bank-account-id
                        :bank-account/current-balance current-balance}])))

(defn bank-accounts-needing-refresh []
  (let [last-refreshed (->> (d/query
                             {:query {:find ['?ba '?tx]
                                      :in ['$ ]
                                      :where ['[?ba :bank-account/current-balance _ ?tx true]]}
                              :args [(d/history (d/db conn))]})
                            (group-by first)
                            (map (fn [[ba balance-txes]]
                                   [ba (->> balance-txes
                                         (map second)
                                         (sort-by #(- %))
                                         first)])))
        has-newer-transaction (->> (d/query
                                 {:query {:find ['?ba]
                                          :in '[$ [[?ba ?last-refreshed] ...] ]
                                          :where ['[_ :journal-entry-line/account ?ba ?tx]
                                                  '[(>= ?tx ?last-refreshed)]]}
                                  :args [(d/history (d/db conn))
                                         last-refreshed]})
                                (map first)
                                (set))

        no-current-balance (->> (d/query {:query {:find ['?ba]
                                                  :in '[$]
                                                  :where ['[?ba :bank-account/code]
                                                          '(not [?ba :bank-account/current-balance])
                                                          ]}
                                          :args [(d/db conn)]})
                                (map first))]
    (into has-newer-transaction no-current-balance)))

(defn build-current-balance [bank-accounts]
  (doseq [bank-account bank-accounts]
    (log/info "Refreshing bank account" (-> (d/db conn) (d/entity bank-account) :bank-account/code))
    (try 
      (refresh-bank-account-current-balance bank-account)
      (catch Exception e
        (log/error "Can't refresh current balance" e)))))

(defn refresh-all-current-balance []
  (lc/with-context {:source "current-balance-cache"}
    (build-current-balance (->> (d/query {:query {:find ['?ba]
                                                  :in '[$]
                                                  :where ['[?ba :bank-account/code]]}
                                          :args [(d/db conn)]})
                                (map first)))))

(defn refresh-current-balance []
  (lc/with-context {:source "current-balance-cache"}
    (try 
      (log/info "Refreshing running balance cache")
      (build-current-balance (bank-accounts-needing-refresh))
      (catch Exception e
        (log/error e)))))

(mount/defstate current-balance-worker
  :start (scheduler/every (* 17 60 1000) refresh-current-balance)
  :stop (scheduler/stop current-balance-worker))

(defn get-client [context args value]
  (->graphql
   (->> (d-clients/get-all)
        (filter #(can-see-client? (:id context) %))
        (map (fn [c]
               (if (is-admin? (:id context))
                 c
                 (dissoc c :client/yodlee-provider-accounts))))
        (map (fn [c]
               (update c :client/bank-accounts
                       (fn [bank-accounts]
                         (mapv (fn [ba]
                                 ;; TODO remove when new yodlee replaces
                                 (assoc ba :bank-account/yodlee-balance-old (get-in (by :id (mapcat :accounts @in-memory-cache) )
                                                                                    [(:bank-account/yodlee-account-id ba) :balance :amount])))
                               bank-accounts))))))))
