(ns auto-ap.graphql.checks
  (:require
   [amazonica.aws.s3 :as s3]
   [auto-ap.datomic :refer [audit-transact conn pull-many remove-nils]]
   [auto-ap.datomic.accounts :as a]
   [auto-ap.datomic.bank-accounts :as d-bank-accounts]
   [auto-ap.datomic.checks :as d-checks]
   [auto-ap.datomic.clients :as d-clients]
   [auto-ap.datomic.invoices :as d-invoices]
   [auto-ap.datomic.transactions :as d-transactions]
   [auto-ap.datomic.vendors :as d-vendors]
   [auto-ap.graphql.utils
    :refer [->graphql <-graphql assert-admin assert-can-see-client
            assert-failure assert-not-locked attach-tracing-resolvers
            enum->keyword]]
   [auto-ap.logging :as alog]
   [auto-ap.numeric :refer [num->words]]
   [auto-ap.solr :as solr]
   [auto-ap.time :refer [iso-date parse]]
   [auto-ap.time :as atime]
   [auto-ap.utils :refer [by dollars-0?]]
   [clj-pdf.core :as pdf]
   [clj-time.coerce :as c]
   [clj-time.coerce :as coerce]
   [clj-time.core :as time]
   [clj-time.format :as f]
   [clojure.edn :as edn]
   [clojure.java.io :as io]
   [clojure.set :as set]
   [clojure.string :as str]
   [com.brunobonacci.mulog :as mu]
   [config.core :refer [env]]
   [datomic.api :as dc]
   [digest])
  (:import
   (java.io ByteArrayOutputStream)
   (java.text DecimalFormat)
   (java.util UUID)))

(def parser (f/formatter "MM/dd/YYYY"))
(defn date->str [t]
  (f/unparse parser t))

(defn distribute [nums]
  (let [sum (reduce + 0 nums)]
    (map #(* 100 (/ % sum)) nums)))

(defn split-memo [memo]
  (str/join "\n"
            (reverse
             (take 6
                   (concat
                    (reduce
                     (fn [[line & rest] word]
                       (let [line (or line "")]
                         (if (> (+ (count line) (count word)) 50)
                           (concat [word line] rest)
                           (concat [(str line " " word)]
                                   rest))))
                     []
                     (str/split memo #" "))
                    (repeat ""))))))

(defn make-check-pdf [check]
  (let [output-stream (ByteArrayOutputStream.)]
    (pdf/pdf
     [{:left-margin 25 :right-margin 0 :top-margin 0 :bottom-margin 0 :size :letter}
      (let [{:keys [bank-account paid-to client check date amount memo] {print-as :vendor/print-as vendor-name :vendor/name :as vendor} :vendor} check
            df (DecimalFormat. "#,###.00")
            word-amount (num->words amount)
            amount (str "--" (.format df  amount) "--")]
        [:table {:num-cols 12 :border false :leading 11 :widths (distribute [2 3 3 3 3 3 3 3 3 2 2 2])}
         [(let [{:keys [:client/name] {:keys [:address/street1 :address/city :address/state :address/zip]} :client/address} client]
            [:cell {:colspan 5} [:paragraph {:leading 14} name "\n" street1 "\n" (str city ", " state " " zip)]])
          (let [{:keys [:bank-account/bank-name :bank-account/bank-code]} bank-account]
            [:cell {:colspan 5 :align :center} [:paragraph {:style :bold} bank-name] [:paragraph {:size 8 :leading 8} bank-code]])
          [:cell {:colspan 2 :size 13}
           check]]

         [[:cell {:colspan 9}]
          [:cell {:colspan 3 :leading -10} date]]
         [[:cell {:colspan 12 :size 14}]]

         [[:cell {:size 13 :leading 13} "PAY"]
          [:cell {:size 8 :leading 8}  "TO THE ORDER OF"]
          [:cell {:colspan 7} (if (seq print-as)
                                print-as
                                vendor-name)]
          [:cell {:colspan 3} amount]]

         [[:cell {}]
          [:cell {:colspan 8 :size 7}  (str " -- " word-amount " " (str/join "" (take (max
                                                                                       2
                                                                                       (- 95
                                                                                          (count word-amount)))
                                                                                      (repeat "-"))))
           [:line {:line-width 0.15 :color [50 50 50]}]]
          [:cell {:colspan 3}]]


         [[:cell {:size 9 :leading 11.5} "\n\n\n\n\nMEMO"]
          [:cell {:colspan 5 :leading 11.5} (split-memo memo)
           [:line {:line-width 0.15 :color [50 50 50]}]]
          [:cell {:colspan 6}  (if (:client/signature-file client)
                                 [:image {:top-margin 90 :xscale 0.30 :yscale 0.30 :align :center}

                                  (:client/signature-file client)]
                                 [:spacer])]]

         #_[#_[:cell {:colspan 5} #_memo]
            #_[:cell {:colspan 6}]]

         [[:cell {:colspan 2}]
          [:cell {:colspan 10 :leading 30}
           [:phrase {:size 18 :ttf-name "public/micrenc.ttf"} (str "c" check "c a" (:bank-account/routing bank-account) "a " (:bank-account/number bank-account) "c")]]]
         [[:cell {:colspan 12 :leading 18} [:spacer]]]
         [[:cell]
          (into
           [:cell {:colspan 9}]
           (let [{:keys [:client/name]
                  {:keys [:address/street1 :address/street2 :address/city :address/state :address/zip]} :client/address} client]
             (filter identity
                     (list
                      [:paragraph "  " name]
                      [:paragraph "  " street1]
                      (when (not (str/blank? street2))
                        [:paragraph "  " street2])
                      [:paragraph "  " city ", " state " " zip]))))
          [:cell {:colspan 2 :size 13}
           check]]

         [[:cell {:colspan 12 :leading 74} [:spacer]]]

         [[:cell]
          [:cell {:colspan 5}  [:paragraph
                                "             " vendor-name "\n"
                                "             " (:address/street1 (:vendor/address vendor)) "\n"
                                (when (not (str/blank? (:address/street2 (:vendor/address vendor))))
                                  (str "             " (:address/street2 (:vendor/address vendor)) "\n"))
                                "             " (:address/city (:vendor/address vendor)) ", " (:address/state (:vendor/address vendor)) " " (:address/zip (:vendor/address vendor))]]
          [:cell {:align :right}
           "Paid to:\n"
           "Amount:\n"
           "Date:\n"]

          [:cell {:colspan 5}
           [:paragraph paid-to]
           [:paragraph amount]
           [:paragraph date]]]

         [[:cell {:colspan 3} "Memo:"]
          [:cell {:colspan 9} memo]]

         [[:cell {:colspan 12} [:spacer]]]
         [[:cell {:colspan 12} [:spacer]]]
         [[:cell {:colspan 12} [:spacer]]]
         [[:cell {:colspan 12} [:spacer]]]

         [[:cell {:colspan 3}]
          [:cell {:align :right :colspan 2}
           "Check:\n"
           "Vendor:\n"
           "Company:\n"
           "Bank Account:\n"
           "Paid To:\n"
           "Amount:\n"
           "Date:\n"]

          [:cell {:colspan 7}
           [:paragraph check]
           [:paragraph vendor-name]
           [:paragraph (:client/name client)]
           [:paragraph (:bank-account/bank-name bank-account)]
           [:paragraph paid-to]
           [:paragraph amount]
           [:paragraph date]]]
         [[:cell {:colspan 3} "Memo:"]
          [:cell {:colspan 9} memo]]])]
     output-stream)
    (.toByteArray output-stream)))

(defn make-pdfs [checks]
  (loop [[check & checks] checks]
    (when check
      (let [check-bytes (-> check
                            :payment/pdf-data
                            (edn/read-string)



                            make-check-pdf)]
        (s3/put-object :bucket-name (:data-bucket env)
                       :key (:payment/s3-key check)
                       :input-stream (io/make-input-stream check-bytes {})
                       :metadata {:content-type "application/pdf"
                                  :content-length (count check-bytes)}))
      (recur checks))))

(defn merge-pdfs [keys]
  (let [merged-pdf-stream (java.io.ByteArrayOutputStream.)
        uuid (str (UUID/randomUUID))]
    (apply pdf/collate (concat [{:size :letter} merged-pdf-stream] (->> keys
                                                                        (map #(s3/get-object (:data-bucket env) %))
                                                                        (map :input-stream))))
    (s3/put-object :bucket-name (:data-bucket env)
                   :key (str "merged-checks/" uuid ".pdf")
                   :input-stream (io/make-input-stream (.toByteArray merged-pdf-stream) {})
                   :metadata {:content-length (count (.toByteArray merged-pdf-stream))
                              :content-type "application/pdf"})
    (str "https://" (:data-bucket env) "/merged-checks/" uuid ".pdf")))

#_{:clj-kondo/ignore [:unused-binding]}
(defmulti invoices->entities (fn [invoices vendor-id client bank-account type index invoice-amounts date]
                               type))
(defn invoice-payments [invoices invoice-amounts]
  (->> (for [invoice invoices
             :let [invoice-amount (invoice-amounts (:db/id invoice))]
             :when (and invoice-amount
                        (not= :invoice-status/voided (:invoice/status invoice)))]
         [{:invoice-payment/payment (-> invoice :invoice/vendor :db/id str)
           :invoice-payment/amount invoice-amount
           :invoice-payment/invoice (:db/id invoice)}
          [:pay (:db/id invoice) invoice-amount]])
       (reduce into [])))

(defn base-payment [invoices vendor client bank-account _ _ invoice-amounts date]
  {:db/id (str (:db/id vendor))
   :payment/bank-account (:db/id bank-account)
   :payment/amount (reduce + 0 (map (comp invoice-amounts :db/id) invoices))
   :payment/vendor (:db/id vendor)
   :payment/client (:db/id client)
   :payment/date (c/to-date (or date (time/now)))
   :payment/invoices (map :db/id invoices)})

(defmethod invoices->entities :payment-type/check [invoices vendor client bank-account type index invoice-amounts date]
  (when (<= (->> invoices
                 (map (comp invoice-amounts :db/id))
                 (reduce + 0.0))
            0.001)
    (throw (ex-info "The selected invoices do not have an outstanding balance."
                    {:validation-error "The selected invoices do not have an outstanding balance."})))
  (let [uuid         (str (UUID/randomUUID))
        date (or date (time/now))
        memo         (str "Invoice #'s: "
                          (str/join ", "
                                    (map (fn [i]
                                           (str (:invoice/invoice-number i) "(" (invoice-amounts (:db/id i)) ")"))
                                         invoices)))
        base-payment (base-payment invoices vendor client bank-account type index invoice-amounts date)
        payment
        (remove-nils
         (cond-> (assoc base-payment
                        :payment/s3-uuid (when (> (:payment/amount base-payment) 0) uuid)
                        :payment/s3-key (when (> (:payment/amount base-payment) 0) (str "checks/" uuid ".pdf"))
                        :payment/s3-url (when (> (:payment/amount base-payment) 0) (str "https://" (:data-bucket env) "/checks/" uuid ".pdf"))
                        :payment/check-number (+ index (:bank-account/check-number bank-account))
                        :payment/type :payment-type/check
                        :payment/memo  memo
                        :payment/status  :payment-status/pending
                        :payment/pdf-data (pr-str {:vendor vendor
                                                   :paid-to (or (:vendor/paid-to vendor) (:vendor/name vendor))
                                                   :amount (reduce + 0 (map (comp invoice-amounts :db/id) invoices))
                                                   :check (str (+ index (:bank-account/check-number bank-account)))
                                                   :memo  memo
                                                   :date (date->str date)
                                                   :client (dissoc client :client/bank-accounts :client/square-integration-status :client/ezcater-locations :client/locked-until :client/emails :client/square-auth-token :client/square-locations)
                                                   :bank-account (dissoc bank-account :bank-account/start-date :bank-account/integration-status)
                                                   #_#_:client {:name (:name client)
                                                                :address (:address client)
                                                                :signature-file (:signature-file client)
                                                                :bank {:name (:bank-account/bank-name bank-account)
                                                                       :acct (:bank-account/bank-code bank-account)
                                                                       :routing (:bank-account/routing bank-account)
                                                                       :acct-number (:bank-account/number bank-account)}}}))))]

    (-> []
        (conj payment)
        (into (invoice-payments invoices invoice-amounts)))))


(defmethod invoices->entities :payment-type/debit [invoices vendor client bank-account type index invoice-amounts date]
  (when (<= (->> invoices
                 (map (comp invoice-amounts :db/id))
                 (reduce + 0.0))
            0.001)
    (throw (ex-info "The selected invoices do not have an outstanding balance."
                    {:validation-error "The selected invoices do not have an outstanding balance."})))
  (let [payment (assoc (base-payment invoices vendor client bank-account type index invoice-amounts date)
                       :payment/type :payment-type/debit
                       :payment/memo  (str "Debit Invoice #'s: "
                                           (str/join ", "
                                                     (map (fn [i]
                                                            (str (:invoice/invoice-number i) "(" (invoice-amounts (:db/id i)) ")"))
                                                          invoices)))
                       :payment/status :payment-status/pending)]
    (-> []
        (conj payment)
        (into (invoice-payments invoices invoice-amounts)))))


(defmethod invoices->entities :payment-type/balance-credit [invoices invoice-amounts]
  (when (<= (->> invoices
                 (map (comp invoice-amounts :db/id))
                 (reduce + 0.0))
            0.001)
    (throw (ex-info "The selected invoices do not have an outstanding balance."
                    {:validation-error "The selected invoices do not have an outstanding balance."}))))

(defmethod invoices->entities :payment-type/credit [invoices vendor client bank-account type index invoice-amounts date]
  (when (>= (->> invoices
                 (map (comp invoice-amounts :db/id))
                 (reduce + 0.0))
            0.001)
    (throw (ex-info "The selected invoices do not have an outstanding balance."
                    {:validation-error "The selected invoices do not have an outstanding balance."})))
  (let [payment (assoc (base-payment invoices vendor client bank-account type index invoice-amounts date)
                       :payment/type :payment-type/credit
                       :payment/memo  (str "Debit Invoice #'s: "
                                           (str/join ", "
                                                     (map (fn [i]
                                                            (str (:invoice/invoice-number i) "(" (invoice-amounts (:db/id i)) ")"))
                                                          invoices)))
                       :payment/status :payment-status/pending)]
    (-> []
        (conj payment)
        (into (invoice-payments invoices invoice-amounts)))))

(defmethod invoices->entities :payment-type/cash [invoices vendor client bank-account type index invoice-amounts date]
  (when (<= (->> invoices
                 (map (comp invoice-amounts :db/id))
                 (reduce + 0.0))
            0.001)
    (throw (ex-info "The selected invoices do not have an outstanding balance."
                    {:validation-error "The selected invoices do not have an outstanding balance."})))
  (let [base-payment (base-payment invoices vendor client bank-account type index invoice-amounts date)
        transaction-id (str (UUID/randomUUID))
        memo (str "Cash Invoice #'s: "
                  (str/join ", "
                            (map (fn [i]
                                   (str (:invoice/invoice-number i) "(" (invoice-amounts (:db/id i)) ")"))
                                 invoices)))
        payment (assoc base-payment
                       :payment/type :payment-type/cash
                       :payment/date (coerce/to-date (last (sort (map :invoice/date invoices))))
                       :payment/memo  memo
                       :payment/status :payment-status/cleared)
        transaction [:upsert-transaction {:db/id                            (str "transaction-" (:db/id vendor))
                                          :transaction/amount               (- (:payment/amount base-payment))
                                          :transaction/payment              (str (:db/id vendor))
                                          :transaction/client               (:db/id client)
                                          :transaction/status "POSTED"
                                          :transaction/bank-account         (:db/id bank-account)
                                          :transaction/id                   #_{:clj-kondo/ignore [:unresolved-var]} (digest/sha-256 transaction-id)
                                          :transaction/raw-id               transaction-id
                                          :transaction/vendor               (:db/id vendor)
                                          :transaction/description-original memo
                                          :transaction/date                 (:payment/date payment)
                                          :transaction/approval-status      :transaction-approval-status/approved
                                          :transaction/accounts             [{:db/id (str "transaction-account-" (:db/id vendor))
                                                                              :transaction-account/account  (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"]))
                                                                              :transaction-account/location "A"
                                                                              :transaction-account/amount   (Math/abs (:payment/amount base-payment))}]}]]
    (-> []
        (conj payment)
        (conj transaction)
        (into (invoice-payments invoices invoice-amounts)))))

(defn validate-belonging [client-id invoices bank-account]
  (when-not (apply = client-id (map (comp :db/id :invoice/client) invoices))
    (throw (ex-info "You can't pay for that invoice from this bank account."
                    {:validation-error "You can't pay for that invoice from this bank account."
                     :client-id client-id
                     :invoices (map :invoice/invoice-number invoices)})))
  (when-not (= client-id (:db/id (:client/_bank-accounts bank-account)))
    (throw (ex-info "The selected bank doesn't belong to this client"
                    {:validation-error "The selected bank doesn't belong to this client"
                     :client-id client-id
                     :invoices (map :invoice/invoice-number invoices)}))))

(defn print-checks-internal
  ([invoice-payments client-id bank-account-id type id date]
   (let [type (keyword "payment-type" (name type))
         invoices (d-invoices/get-multi (map :invoice-id invoice-payments))
         client (d-clients/get-by-id client-id)
         invoice-amounts (by :invoice-id :amount invoice-payments)
         invoices-grouped-by-vendor (group-by (comp :db/id :invoice/vendor) invoices)
         vendors (->> (pull-many (dc/db conn) d-vendors/default-read (keys invoices-grouped-by-vendor))
                      (by :db/id))
         bank-account (d-bank-accounts/get-by-id bank-account-id)
         _ (validate-belonging client-id invoices bank-account)
         _ (when (and (nil? (:bank-account/check-number bank-account))
                      (= type :payment-type/check))
             (let [message (str "The bank account " (:bank-account/name bank-account) " does not have a starting check number. Please ask the integreat staff to initialize it.")]
               (throw (ex-info message
                               {:validation-error message}))))
         checks (mu/trace ::build-checks []
                          (->> (for [[[vendor-id invoices] index] (map vector invoices-grouped-by-vendor (range))]
                                 (invoices->entities invoices (vendors vendor-id) client bank-account type index invoice-amounts date))
                               (reduce into [])
                               doall))
         checks (if (= type :payment-type/check)
                  (conj checks [:plus (:db/id bank-account) :bank-account/check-number (count invoices-grouped-by-vendor)])
                  checks)]
     (when (= type :payment-type/check)
       (mu/trace ::making-pdfs [:checks checks]
                 (make-pdfs (filter #(and (= :payment-type/check (:payment/type %))
                                          (> (:payment/amount %) 0.0))
                                    checks))))
     (let [result (audit-transact (map #(if (map? %)
                                          (dissoc % :payment/pdf-data)
                                          %) checks) id)]

       (try
         (doseq [[_ i] (:tempids result)]
           (solr/touch-with-ledger i))
         (catch Exception e
           (alog/error ::cant-save-solr
                       :error e))))
     {:invoices  (d-invoices/get-multi (map :invoice-id invoice-payments))
      :pdf-url (if (= type :payment-type/check)
                 (mu/trace ::merge-pdfs
                           []
                           (merge-pdfs (filter identity (map :payment/s3-key checks))))
                 nil)})))

(defn get-payment-page [context args _]
  (let [[payments checks-count] (d-checks/get-graphql  (-> args
                                                           :filters
                                                           (<-graphql)
                                                           (assoc :clients (:clients context))
                                                           (update  :payment-type enum->keyword  "payment-type")
                                                           (update  :status enum->keyword  "payment-status")))]

    [{:payments (->> payments
                     (map (fn [payment]
                            (if (seq (:transaction/_payment payment))
                              (-> payment
                                  (set/rename-keys {:transaction/_payment :transaction})
                                  (update :transaction first))
                              payment)))
                     (map ->graphql))
      :total checks-count
      :count (count payments)
      :start (-> args :filters (:start 0))
      :end (+ (-> args :filters (:start 0)) (count payments))}]))

(defn get-potential-payments [context args _]
  (let [transaction (d-transactions/get-by-id (:transaction_id args))
        _ (assert-can-see-client (:id context) (:transaction/client transaction))
        [payments _] (d-checks/get-graphql {:client-id       (:db/id (:transaction/client transaction))
                                            :clients [{:db/id (:db/id (:transaction/client transaction))}]
                                            :bank-account-id (:db/id (:transaction/bank-account transaction))
                                            :amount          (- (:transaction/amount transaction))
                                            :status          :payment-status/pending
                                            :date-range {:start (time/plus (:transaction/date transaction) (time/days -90))}})]
    (map ->graphql (reverse (sort-by :payment/date  payments)))))

(defn add-handwritten-check [context args _]
  (let [invoices (d-invoices/get-multi (map :invoice_id (:invoice_payments args)))
        bank-account-id (:bank_account_id args)
        bank-account (d-bank-accounts/get-by-id bank-account-id)
        _ (doseq [invoice invoices]
            (assert-can-see-client  (:id context) (:invoice/client invoice)))
        client-id (:db/id (:invoice/client (first invoices)))
        _ (validate-belonging (:db/id (:client/_bank-accounts bank-account)) invoices bank-account)
        _ (assert-not-locked client-id (:date args))
        invoice-payment-lookup (by :invoice_id :amount (:invoice_payments args))
        base-payment (base-payment invoices
                                   (:invoice/vendor (first invoices))
                                   (:invoice/client (first invoices))
                                   bank-account
                                   :payment-type/check
                                   0
                                   invoice-payment-lookup
                                   (parse (:date args) iso-date))]

    (let [result (audit-transact
                  (into [(assoc base-payment
                                :payment/type :payment-type/check
                                :payment/status :payment-status/pending
                                :payment/check-number (:check_number args)
                                #_#_:payment/date (c/to-date (parse (:date args) iso-date)))]
                        (invoice-payments invoices invoice-payment-lookup))
                  (:id context))]
      (doseq [[_ i] (:tempids result)]
        (solr/touch-with-ledger i)))
    (->graphql
     {:s3-url nil
      :invoices (d-invoices/get-multi (map :invoice_id (:invoice_payments args)))})))


(defn void-payment [context {id :payment_id} _]
  (let [check (d-checks/get-by-id id)]
    (assert (or (= :payment-status/pending (:payment/status check))
                (#{:payment-type/cash :payment-type/debit :payment-type/balance-credit} (:payment/type check)))
            (pr-str check))
    (assert-can-see-client (:id context) (:db/id (:payment/client check)))
    (assert-not-locked (:db/id (:payment/client check)) (:payment/date check))
    (let [removing-payments (mapcat (fn [x]
                                      (let [invoice (:invoice-payment/invoice x)
                                            new-balance (+ (:invoice/outstanding-balance invoice)
                                                           (:invoice-payment/amount x))]
                                        [[:db/retractEntity (:db/id x)]
                                         [:upsert-invoice {:db/id                       (:db/id invoice)
                                                           :invoice/outstanding-balance new-balance
                                                           :invoice/status              (if (dollars-0? new-balance)
                                                                                          (:invoice/status invoice)
                                                                                          :invoice-status/unpaid)}]]))
                                    (:payment/invoices check))
          updated-payment {:db/id id
                           :payment/amount 0.0
                           :payment/status :payment-status/voided}]
      (audit-transact (conj removing-payments updated-payment)
                      (:id context)))
    (-> (d-checks/get-by-id id)
        (->graphql))))

(defn void-payments-internal [all-ids id]
  (audit-transact (->> all-ids
                       (dc/q '[:find (pull ?p [:db/id
                                               {:invoice-payment/_payment [:invoice-payment/amount
                                                                           :db/id
                                                                           {:invoice-payment/invoice [:db/id :invoice/outstanding-balance]}]}])
                               :in $ [?p ...]
                               :where
                               (not [_ :transaction/payment ?p])
                               (not [?p :payment/status :payment-status/voided])
                               [?p :payment/client ?c]
                               [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
                               [?p :payment/date ?d]
                               [(>= ?d ?lu)]]
                             (dc/db conn))
                       (map first)
                       (mapcat (fn [{:keys    [:db/id]
                                     invoices :invoice-payment/_payment}]
                                 (into
                                  [{:db/id          id
                                    :payment/amount 0.0
                                    :payment/status :payment-status/voided}]
                                  (->> invoices
                                       (mapcat (fn [{:keys [:invoice-payment/invoice :db/id :invoice-payment/amount]}]
                                                 (let [new-balance (+ (:invoice/outstanding-balance invoice)
                                                                      amount)]
                                                   [[:db.fn/retractEntity id]
                                                    [:upsert-invoice {:db/id                       (:db/id invoice)
                                                                      :invoice/outstanding-balance new-balance
                                                                      :invoice/status              (if (dollars-0? new-balance)
                                                                                                     (:invoice/status invoice)
                                                                                                     :invoice-status/unpaid)}]]))))))))
                  id))


(defn void-payments [context args _]
  (assert-admin (:id context))
  (let [args         (assoc args :clients (:clients context))
        ids          (some-> args
                             :filters
                             (assoc :clients (:clients context))
                             (<-graphql)
                             (update  :payment-type enum->keyword  "payment-type")
                             (update  :status enum->keyword  "payment-status")
                             (assoc :per-page Integer/MAX_VALUE)
                             d-checks/raw-graphql-ids
                             :ids)
        specific-ids (d-checks/filter-ids (:ids args))
        all-ids      (into (set ids) specific-ids)]
    (alog/info ::voiding-payments :count (count all-ids))

    (void-payments-internal all-ids (:id context))
    {:message (str "Succesfully voided " (count all-ids))}))

(defn get-all-payments [context args _]
  (assert-admin (:id context))
  (map
   ->graphql
   (first (d-checks/get-graphql (assoc (<-graphql (assoc args :clients (:clients context))) :count Integer/MAX_VALUE)))))

(defn print-checks [context args _]
  (assert-can-see-client (:id context) (:client_id args))
  (->graphql
   (print-checks-internal (map (fn [i] {:invoice-id (:invoice_id i)
                                        :amount     (:amount i)})
                               (:invoice_payments args))
                          (:client_id args)
                          (:bank_account_id args)
                          (:type args)
                          (:id context)
                          (time/now))))

(defn pay-invoices-from-balance [context {invoices :invoices
                                          client-id :client_id} _]
  (assert-can-see-client (:id context) client-id)
  (let [invoices (d-invoices/get-multi invoices)
        client (d-clients/get-by-id client-id)
        _ (when (> (count (set (map :invoice/vendor invoices))) 1)
            (assert-failure "Balance payments can only be done on one vendor at a time."))
        _ (when (> (reduce + 0 (map :invoice/outstanding-balance invoices)) 0.001)
            (assert-failure "There isn't a positive balance to pay from."))
        invoices-to-be-paid (filter
                             (fn [i]
                               (> (:invoice/outstanding-balance i)
                                  0.001))
                             invoices)
        credit-invoices (filter
                         (fn [i]
                           (< (:invoice/outstanding-balance i)
                              0.001))
                         invoices)


        total-to-pay (reduce + 0 (map :invoice/outstanding-balance invoices-to-be-paid))
        _ (when (<= total-to-pay  0.001)
            (assert-failure "You must select invoices that need to be paid."))

        invoice-amounts (->> invoices-to-be-paid
                             (map (fn [i]
                                    [(:db/id i)
                                     (:invoice/outstanding-balance i)]))
                             (concat (->> credit-invoices
                                          (reduce
                                           (fn [[remaining-to-pay invoice-amounts] invoice]

                                             (cond (dollars-0? (+ remaining-to-pay (:invoice/outstanding-balance invoice)))
                                                   (reduced (conj invoice-amounts
                                                                  [(:db/id invoice)
                                                                   (:invoice/outstanding-balance invoice)]))

                                                   (< (+ remaining-to-pay (:invoice/outstanding-balance invoice)) 0.0)
                                                   (reduced (conj invoice-amounts
                                                                  [(:db/id invoice)
                                                                   (- remaining-to-pay)]))

                                                   :else
                                                   [(+ remaining-to-pay (:invoice/outstanding-balance invoice))
                                                    (conj invoice-amounts [(:db/id invoice)
                                                                           (:invoice/outstanding-balance invoice)])]))
                                           [total-to-pay []])))
                             (into {}))



        vendor-id (:db/id (:invoice/vendor (first invoices)))
        payment {:db/id            (str vendor-id)
                 :payment/amount   total-to-pay
                 :payment/vendor   vendor-id
                 :payment/client   (:db/id client)
                 :payment/date     (c/to-date (time/now))
                 :payment/invoices (map :db/id invoices)
                 :payment/type     :payment-type/balance-credit
                 :payment/status   :payment-status/cleared}]

    (let [result (audit-transact (-> []
                                     (conj payment)
                                     (into (invoice-payments invoices invoice-amounts))) (:id context))]
      (doseq [[_ n] (:tempids result)]
        (solr/touch-with-ledger n)))
    (->graphql {:invoices (d-invoices/get-multi (map :db/id invoices))})))

(def objects
  {:payment {:fields {:id           {:type :id}
                      :type         {:type :payment_type}
                      :original_id  {:type 'Int}
                      :amount       {:type 'String}
                      :vendor       {:type :vendor}
                      :client       {:type :client}
                      :date         {:type 'String}
                      :bank_account {:type :bank_account}
                      :memo         {:type 'String}
                      :s3_url       {:type 'String}
                      :check_number {:type 'Int}
                      :status       {:type :ident}
                      :transaction  {:type :transaction}
                      :invoices     {:type '(list :invoice_payment)}}}
   :invoice_payment
   {:fields {:id         {:type :id}
             :amount     {:type 'String}
             :invoice_id {:type 'String}
             :payment_id {:type 'String}
             :payment    {:type :payment}
             :invoice    {:type :invoice}}}
   :payment_page {:fields {:payments {:type '(list :payment)}
                           :count    {:type 'Int}
                           :total    {:type 'Int}
                           :start    {:type 'Int}
                           :end      {:type 'Int}}}})

(def queries
  {:all_payments {:type    '(list :payment)
                  :args    {:client_id   {:type :id}
                            :client_code {:type 'String}
                            :original_id {:type 'Int}
                            :statuses    {:type '(list String)}}
                  :resolve :get-all-payments}
   :payment_page {:type    '(list :payment_page)
                  :args    {:filters {:type :payment_filters}}
                  :resolve :get-payment-page}
   :potential_payment_matches {:type    '(list :payment)
                               :args    {:transaction_id {:type :id}}
                               :resolve :get-potential-payments}})

(def mutations
  {:print_checks {:type    :check_result
                  :args    {:invoice_payments {:type '(list :invoice_payment_amount)}
                            :bank_account_id  {:type :id}
                            :type             {:type :payment_type}
                            :client_id        {:type :id}}
                  :resolve :mutation/print-checks}
   :pay_invoices_from_balance {:type    :check_result
                               :args    {:invoices  {:type '(list :id)}
                                         :client_id {:type :id}}
                               :resolve :mutation/pay-invoices-from-balance}

   :add_handwritten_check {:type    :check_result
                           :args    {:invoice_payments {:type '(list :invoice_payment_amount)}
                                     :date             {:type 'String}
                                     :check_number     {:type 'Int}
                                     :bank_account_id  {:type :id}}
                           :resolve :mutation/add-handwritten-check}
   :void_payment {:type    :payment
                  :args    {:payment_id {:type :id}}
                  :resolve :mutation/void-payment}

   :void_payments {:type    :message
                   :args    {:filters {:type :payment_filters}
                             :ids     {:type '(list :id)}}
                   :resolve :mutation/void-payments}})

(def input-objects
  {:invoice_payment_amount {:fields {:invoice_id {:type :id}
                                     :amount     {:type :money}}}
   :payment_filters {:fields {:vendor_id         {:type :id}
                              :payment_type      {:type :payment_type}
                              :status            {:type :payment_status}
                              :exact_match_id    {:type :id}
                              :date_range        {:type :date_range}
                              :amount_lte        {:type :money}
                              :amount_gte        {:type :money}
                              :check_number_like {:type 'String}
                              :invoice_number    {:type 'String}
                              :start             {:type 'Int}
                              :per_page          {:type 'Int}
                              :sort              {:type '(list :sort_item)}}}})

(def enums
  {:payment_type {:values [{:enum-value :check}
                           {:enum-value :cash}
                           {:enum-value :debit}
                           {:enum-value :credit}
                           {:enum-value :balance_credit}]}
   :payment_status {:values [{:enum-value :voided}
                             {:enum-value :pending}
                             {:enum-value :cleared}]}})


(def resolvers
  {:get-potential-payments get-potential-payments
   :get-payment-page get-payment-page
   :get-all-payments get-all-payments
   :mutation/void-payment void-payment
   :mutation/void-payments void-payments
   :mutation/print-checks print-checks
   :mutation/pay-invoices-from-balance pay-invoices-from-balance
   :mutation/add-handwritten-check add-handwritten-check})

(defn attach [schema]
  (->
   (merge-with merge schema
               {:objects       objects
                :queries       queries
                :mutations     mutations
                :input-objects input-objects
                :enums         enums})
   (attach-tracing-resolvers resolvers)))
