(ns auto-ap.ssr.invoices
  (:require
   [auto-ap.client-routes :as client-routes]
   [auto-ap.datomic
             :refer [add-sorter-fields apply-pagination apply-sort-3
                     audit-transact audit-transact-batch conn merge-query
                     observable-query pull-many]]
   [auto-ap.datomic.accounts :as d-accounts]
   [auto-ap.datomic.bank-accounts :as d-bank-accounts]
   [auto-ap.datomic.clients :as d-clients]
   [auto-ap.datomic.invoices :as d-invoices]
   [auto-ap.graphql.checks :as gq-checks :refer [base-payment invoice-payments
                                                 print-checks-internal
                                                 validate-belonging]]
   [auto-ap.graphql.utils :refer [assert-can-see-client assert-not-locked
                                  assert-not-locked-ssr exception->4xx
                                  exception->notification extract-client-ids
                                  notify-if-locked]]
   [auto-ap.logging :as alog]
   [auto-ap.permissions :refer [can? wrap-must]]
   [auto-ap.query-params :refer [wrap-copy-qp-pqp]]
   [auto-ap.routes.invoice :as route]
   [auto-ap.routes.payments :as payment-route]
   [auto-ap.routes.utils
             :refer [wrap-admin wrap-client-redirect-unauthenticated]]
   [auto-ap.rule-matching :as rm]
   [auto-ap.solr :as solr]
   [auto-ap.ssr-routes :as ssr-routes]
   [auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
   [auto-ap.ssr.components :as com]
   [auto-ap.ssr.components.link-dropdown :refer [link-dropdown]]
   [auto-ap.ssr.components.multi-modal :as mm]
   [auto-ap.ssr.form-cursor :as fc]
   [auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
   [auto-ap.ssr.hiccup-helper :as hh]
   [auto-ap.ssr.hx :as hx]
   [auto-ap.ssr.invoice.common :refer [default-read]]
   [auto-ap.ssr.invoice.import :as invoice-import]
   [auto-ap.ssr.invoice.new-invoice-wizard :as new-invoice-wizard :refer [location-select*]]
   [auto-ap.ssr.components.date-range :as dr]
   [auto-ap.ssr.svg :as svg]
   [auto-ap.ssr.utils
             :refer [apply-middleware-to-all-handlers assert-schema
                     clj-date-schema dissoc-nil-transformer entity-id
                     form-validation-error html-response main-transformer
                     many-entity modal-response money percentage
                     ref->enum-schema round-money strip wrap-entity
                     wrap-implied-route-param wrap-merge-prior-hx
                     wrap-schema-enforce]]
   [auto-ap.time :as atime]
   [auto-ap.utils :refer [by dollars-0? dollars=]]
   [bidi.bidi :as bidi]
   [clj-time.coerce :as coerce]
   [clj-time.coerce :as c]
   [clj-time.core :as time]
   [clojure.string :as str]
   [datomic.api :as dc]
   [hiccup.util :as hu]
   [iol-ion.utils :refer [random-tempid]]
   [malli.core :as mc]
   [malli.transform :as mt]
   [malli.util :as mut]
   [slingshot.slingshot :refer [try+]]))


(defn exact-match-id* [request]
  (if (nat-int? (:exact-match-id (:query-params request)))
    [:div {:x-data (hx/json {:exact_match (:exact-match-id (:query-params request))}) :id "exact-match-id-tag"}
     (com/hidden {:name "exact-match-id"
                  "x-model" "exact_match"})
     (com/pill {:color :primary}
               [:span.inline-flex.space-x-2.items-center
                [:div "exact match"]
                [:div.w-3.h-3
                 (com/link {"@click" "exact_match=null; $nextTick(() => $dispatch('change'))"}
                           svg/x)]])]
    [:div {:id "exact-match-id-tag"}]))

(defn filters [request]
  [:form#invoice-filters {"hx-trigger"   "datesApplied, change delay:500ms from:.filter-trigger, keyup changed from:.hot-filter delay:1000ms"
                          "hx-get"       (bidi/path-for ssr-routes/only-routes
                                                        ::route/table)
                          "hx-target"    "#entity-table"
                          "hx-indicator" "#entity-table"}

   (com/hidden {:name "status"
                :value (some-> (:status (:query-params request)) name)})
   [:fieldset.space-y-6
    (com/field {:label "Vendor"}
               (com/typeahead {:name        "vendor"
                               :id          "vendor"
                               :url (bidi/path-for ssr-routes/only-routes :vendor-search)
                               :value       (:vendor (:query-params request))
                               :value-fn :db/id
                               :content-fn :vendor/name
                               :class       "filter-trigger"}))
    (com/field {:label "Account"}
               (com/typeahead {:name        "account"
                               :id          "account"
                               :url (bidi/path-for ssr-routes/only-routes :account-search)
                               :value       (:account (:query-params request))
                               :value-fn :db/id
                               :content-fn #(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read (:db/id %))
                                                                                 (:db/id (:client request))))
                               :class       "filter-trigger"}))
    (dr/date-range-field {:value {:start (:start-date (:query-params request))
                                 :end (:end-date (:query-params request))}
                        :id "date-range"
                        :apply-button? true})
    (com/field {:label "Check #"}
               (com/text-input {:name        "check-number"
                                :id          "check-number"
                                :class       "hot-filter"
                                :value       (:check-number (:query-params request))
                                :placeholder "e.g., 10001"
                                :size        :small}))
    (com/field {:label "Invoice #"}
               (com/text-input {:name        "invoice-number"
                                :id          "invoice-number"
                                :class       "hot-filter"
                                :value       (:invoice-number (:query-params request))
                                :placeholder "e.g., ABC-456"
                                :size        :small}))
   
    (com/field {:label "Amount"}
               [:div.flex.space-x-4.items-baseline
                (com/money-input {:name        "amount-gte"
                                  :id          "amount-gte"
                                  :hx-preserve "true"
                                  :class       "hot-filter  w-20"
                                  :value       (:amount-gte (:query-params request))
                                  :placeholder "0.01"
                                  :size        :small})
                [:div.align-baseline
                 "to"]
                (com/money-input {:name        "amount-lte"
                                  :hx-preserve "true"
                                  :id          "amount-lte"
                                  :class       "hot-filter w-20"
                                  :value       (:amount-lte (:query-params request))
                                  :placeholder "9999.34"
                                  :size        :small})])
    (exact-match-id* request)]])


(defn fetch-ids [db {:keys [query-params route-params] :as request}]
  (let [valid-clients (extract-client-ids (:clients request)
                                          (:client-id request)
                                          (when (:client-code request)
                                            [:client/code (:client-code request)]))
        query
        (if (:exact-match-id query-params)
          {:query {:find  '[?e]
                   :in    '[$ ?e [?c ...]]
                   :where '[[?e :invoice/client ?c]]}
           :args  [db
                   (:exact-match-id query-params)
                   valid-clients]}
          (cond-> {:query {:find []
                           :in '[$ [?clients ?start ?end]]
                           :where '[[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]]}
                   :args [db
                          [valid-clients
                           (some-> (:start-date query-params) coerce/to-date)
                           (some-> (:end-date query-params) coerce/to-date)]]}


            (:client-id query-params)
            (merge-query {:query {:in    ['?client-id]
                                  :where ['[?e :invoice/client ?client-id]]}
                          :args  [(:client-id query-params)]})

            (:client-code query-params)
            (merge-query {:query {:in    ['?client-code]
                                  :where ['[?e :invoice/client ?client-id]
                                          '[?client-id :client/code ?client-code]]}
                          :args  [(:client-code query-params)]})


            (:start (:due-range query-params)) (merge-query {:query {:in    '[?start-due]
                                                                     :where ['[?e :invoice/due ?due]
                                                                             '[(>= ?due ?start-due)]]}
                                                             :args  [(coerce/to-date (:start (:due-range query-params)))]})

            (:end (:due-range query-params)) (merge-query {:query {:in    '[?end-due]
                                                                   :where ['[?e :invoice/due ?due]
                                                                           '[(<= ?due ?end-due)]]}
                                                           :args  [(coerce/to-date (:end (:due-range query-params)))]})


            (:import-status query-params)
            (merge-query {:query {:in    ['?import-status]
                                  :where ['[?e :invoice/import-status ?import-status]]}
                          :args  [(:import-status query-params)]})

            (not (:import-status query-params))
            (merge-query {:query { :where ['[?e :invoice/import-status :import-status/imported]]} })

            (:status route-params)
            (merge-query {:query {:in    ['?status]
                                  :where ['[?e :invoice/status ?status]]}
                          :args  [(:status route-params)]})
            (:vendor query-params)
            (merge-query {:query {:in    ['?vendor-id]
                                  :where ['[?e :invoice/vendor ?vendor-id]]}
                          :args  [(:db/id (:vendor query-params))]})

            (:account query-params)
            (merge-query {:query {:in    ['?account-id]
                                  :where ['[?e :invoice/expense-accounts ?iea ?]
                                          '[?iea :invoice-expense-account/account ?account-id]]}
                          :args  [(:db/id (:account query-params))]})

            (:amount-gte query-params)
            (merge-query {:query {:in    ['?amount-gte]
                                  :where ['[?e :invoice/total ?total-filter]
                                          '[(>= ?total-filter ?amount-gte)]]}
                          :args  [(:amount-gte query-params)]})

            (:amount-lte query-params)
            (merge-query {:query {:in    ['?amount-lte]
                                  :where ['[?e :invoice/total ?total-filter]
                                          '[(<= ?total-filter ?amount-lte)]]}
                          :args  [(:amount-lte query-params)]})

            (not-empty (:invoice-number query-params))
            (merge-query {:query {:in    ['?invoice-number-like]
                                  :where ['[?e :invoice/invoice-number ?invoice-number]
                                          '[(.contains ^String ?invoice-number ?invoice-number-like)]]}
                          :args  [(:invoice-number query-params)]})

            (:scheduled-payments query-params)
            (merge-query {:query {:in    []
                                  :where ['[?e :invoice/scheduled-payment]]}
                          :args  []})

            (:unresolved query-params)
            (merge-query {:query {:in    []
                                  :where ['(or-join [?e]
                                                    (not [?e :invoice/expense-accounts])
                                                    (and [?e :invoice/expense-accounts ?ea]
                                                         (not [?ea :invoice-expense-account/account])))]}
                          :args  []})

            (seq (:location query-params))
            (merge-query {:query {:in    ['?location]
                                  :where ['[?e :invoice/expense-accounts ?eas]
                                          '[?eas :invoice-expense-account/location ?location]]}
                          :args  [(:location query-params)]})

            (:sort query-params) (add-sorter-fields {"client"               ['[?e :invoice/client ?c]
                                                                             '[?c :client/name ?sort-client]]
                                                     "vendor"               ['[?e :invoice/vendor ?v]
                                                                             '[?v :vendor/name ?sort-vendor]]
                                                     "description-original" ['[?e :transaction/description-original ?sort-description-original]]
                                                     "location"             ['[?e :invoice/expense-accounts ?iea]
                                                                             '[?iea :invoice-expense-account/location ?sort-location]]
                                                     "date"                 ['[?e :invoice/date ?sort-date]]
                                                     "due"                  ['[(get-else $ ?e :invoice/due #inst "2050-01-01") ?sort-due]]
                                                     "invoice-number"       ['[?e :invoice/invoice-number ?sort-invoice-number]]
                                                     "total"                ['[?e :invoice/total ?sort-total]]
                                                     "outstanding-balance"  ['[?e :invoice/outstanding-balance ?sort-outstanding-balance]]}
                                                    query-params)
            true
            (merge-query {:query {:find  ['?sort-default '?e]}})))]

    (->> (observable-query query)
         (apply-sort-3 (assoc query-params :default-asc? false))
         (apply-pagination query-params))))


(defn hydrate-results [ids db _]
  (let [results (->> (pull-many db default-read ids)
                     (group-by :db/id))
        refunds (->> ids
                     (map results)
                     (map first))]
    refunds))

(defn sum-outstanding [ids]
  
  (->> 
    (dc/q {:find  ['?id '?o]
           :in    ['$ '[?id ...]]
           :where ['[?id :invoice/outstanding-balance ?o]]}
          (dc/db conn)
          ids)
   (map last)
   (reduce
    +
    0.0)))

(defn sum-total-amount [ids]
  
  (->> 
    (dc/q {:find  ['?id '?o]
           :in    ['$ '[?id ...]]
           :where ['[?id :invoice/total ?o]]
           }
          (dc/db conn)
          ids)
    (map last)
    (reduce
      +
      0.0)))

(defn fetch-page [request]
  (let [db                                           (dc/db conn)
        {ids-to-retrieve :ids matching-count :count
         all-ids :all-ids} (fetch-ids db request)]

    [(->> (hydrate-results ids-to-retrieve db request))
     matching-count
     (sum-outstanding all-ids)
     (sum-total-amount all-ids)]))

(def query-schema (mc/schema
                   [:maybe [:map {:date-range [:date-range :start-date :end-date]}
                            [:sort {:optional true} [:maybe [:any]]]
                            [:per-page {:optional true :default 25} [:maybe :int]]
                            [:start {:optional true :default 0} [:maybe :int]]
                            [:amount-gte {:optional true} [:maybe :double]]
                            [:amount-lte {:optional true} [:maybe :double]]
                            [:vendor {:optional true :default nil} [:maybe  [:entity-map {:pull [:db/id :vendor/name]}]]]
                            [:account {:optional true :default nil} [:maybe  [:entity-map {:pull [:db/id :account/name]}]]]
                            [:check-number {:optional true} [:maybe [:string {:decode/string strip}]]]
                            [:invoice-number {:optional true} [:maybe [:string {:decode/string strip}]]]
                            [:status {:optional true} [:maybe (ref->enum-schema "invoice-status")]]
                            [:exact-match-id {:optional true} [:maybe entity-id]]
                            [:all-selected {:optional true :default nil} [:maybe :boolean]]
                            [:selected {:optional true :default nil} [:maybe [:vector {:coerce? true}
                                                                              entity-id]]]
                            [:start-date {:optional true}
                             [:maybe clj-date-schema]]
                            [:end-date {:optional true}
                             [:maybe clj-date-schema]]]]))

(comment
  (mc/decode query-schema
             {:start " "}
             main-transformer))

(defn selected->ids [request params]
  (let [all-selected (:all-selected params)
        selected (:selected params)
        ids (cond
              all-selected
              (:ids (fetch-ids (dc/db conn) (-> request
                                                (assoc :query-params params)
                                                (assoc-in [:query-params :start] 0)
                                                (assoc-in [:query-params :per-page]  250))))


              :else
              selected)]
    ids))

(defn assert-can-undo-autopayment [invoice]
  (assert-not-locked-ssr  (:db/id (:invoice/client invoice)) (:invoice/date invoice))
  (when  (not= :invoice-status/paid (:invoice/status invoice))
    (throw (ex-info "This invoice isn't paid." {:type :warning})))

  (when-not (:invoice/scheduled-payment invoice)
    (throw (ex-info "This invoice didn't have a scheduled payment" {:type :warning})))

  (when (:invoice/payments invoice)
    (throw (ex-info "This invoice has a linked payment. Void the payment to undo the payment." {:type :warning}))))

(defn can-undo-autopayment [invoice]
  (try+
    (assert-can-undo-autopayment invoice)
   true
   (catch [:type :warning] {}
          false)))


(defn pay-button* [params]
  (let [ids (:ids params)
        ids (if (seq ids) 
              (map first
                               (dc/q '[:find ?i
                                       :in $ [?i ...]
                                       :where (not [?i :invoice/scheduled-payment])]
                                     (dc/db conn)
                                     ids))
              ids)
        selected-client-count (if (seq ids)
                                (ffirst
                                 (dc/q '[:find (count ?c)
                                         :in $ [?i ...]
                                         :where [?i :invoice/client ?c]]
                                       (dc/db conn)
                                       ids))

                                0)
        outstanding-balances (if (seq ids)
                               (->>
                                (dc/q '[:find ?i ?v ?ob
                                        :in $ [?i ...]
                                        :where [?i :invoice/vendor ?v]
                                        [?i :invoice/outstanding-balance ?ob]]
                                      (dc/db conn)
                                      ids)))

        vendor-totals (if (seq ids)
                        (->>
                         outstanding-balances
                         (reduce (fn [acc [_ v ob]]
                                   (update acc v (fnil + 0) ob))
                                 {})
                         (vals)))
        all-credits-or-debits (or (every? #(<= % 0.0) vendor-totals)
                                  (every? #(>= % 0.0) vendor-totals))
        at-least-one-positive-payment (some (fn [[_ _ ob]]
                                              (> ob 0.001))
                                            outstanding-balances)
        total (reduce + 0.0 vendor-totals)
        paying-credit? (and (> (count ids) 1)
                        (= 1 (count vendor-totals))
                        at-least-one-positive-payment
                        (dollars-0? total))]


    [:div  (cond-> {:hx-target "this"
           
                    :hx-trigger "click from:#pay-button"
                    :x-tooltip "{allowHTML: true, content: () => $refs.template.innerHTML, appendTo: $root}"}
             paying-credit? (assoc :hx-post (bidi/path-for ssr-routes/only-routes ::route/pay-using-credit))
             (not paying-credit? ) (assoc :hx-get (bidi/path-for ssr-routes/only-routes
                                                              ::route/pay-wizard)))
     (com/button {:color :primary
                  :id "pay-button"
                  :disabled (or (= (count (:ids params)) 0)
                                (not= 1 selected-client-count)
                                (not all-credits-or-debits))
                  "x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
                  "hx-include" "#invoice-filters"
                  :hx-get (bidi/path-for ssr-routes/only-routes ::route/pay-button)
                  :hx-swap "outerHTML"
                  :hx-trigger "selectedChanged from:body, htmx:afterSwap from:#entity-table"
                  :x-ref "source"
                  :minimal-loading? true
                  :class "relative"}
                 (cond
                   paying-credit?
                   "Pay invoices using credit"
                   
                   (> (count ids) 0)
                   
                   (format "Pay %d invoices ($%,.2f)"
                           (count ids)
                           (or total 0.0))
                   
                   
                   (or (= 0 (count ids))
                       (> selected-client-count 1))
                   (list "Pay " (com/badge {} "!"))
                   :else
                   "Pay"))
     [:template {:x-ref "template"}
      (cond
        (not all-credits-or-debits)
        [:div "All vendor totals must be either positive or negative."]
        (and (= 0 (count ids))
             (not= (count (:ids params))
                   0))
        [:div "No " [:span.font-bold "payable"] " invoices selected."]

        (= 0 (count ids))
        [:div "Please select some invoices to pay"]
        (> selected-client-count 1)
        [:div "Can only pay for one client at a time"]
        :else
        [:div "Click to choose a bank account"])]]))


(defn pay-button [request]
  (html-response
   (pay-button* {:ids (selected->ids request
                                     (:query-params request))})))


;; TODO test as a real user
(def grid-page
  (helper/build {:id                 "entity-table"
                 :nav                com/main-aside-nav
                 :check-boxes? true
                 :check-box-warning? (fn [e]
                                       (some? (:invoice/scheduled-payment e)))
                 :page-specific-nav  filters
                 :fetch-page         fetch-page
                 :oob-render
                 (fn [request]
                   [(assoc-in (dr/date-range-field {:value {:start (:start-date (:query-params request))
                                                           :end (:end-date (:query-params request))}
                                                   :id "date-range"
                                                   :apply-button? true}) [1 :hx-swap-oob] true)
                    (assoc-in (exact-match-id* request)  [1 :hx-swap-oob] true)])
                 :query-schema query-schema
                 :parse-query-params (fn [p]
                                       (alog/peek ::PARSE
                                                  (mc/decode query-schema p main-transformer)))
                 :action-buttons (fn [request]
                                   (let [[_ _ outstanding total] (:page-results request)]
                                     [(com/pill {:color :primary} "Outstanding: "
                                                (format "$%,.2f" outstanding))
                                      (com/pill {:color :secondary} "Total: "
                                                (format "$%,.2f" total))

                                      (when (can? (:identity request) {:subject :invoice :activity :bulk-delete})
                                        (com/button {:hx-get    (str (bidi/path-for ssr-routes/only-routes ::route/bulk-delete))
                                                     "x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
                                                     "hx-include" "#invoice-filters"
                                                     :color     :red}
                                                    "Void selected"))
                                      (when (can? (:identity request) {:subject :invoice :activity :bulk-edit})
                                        (com/button {:hx-get    (str (bidi/path-for ssr-routes/only-routes ::route/bulk-edit))
                                                     "x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
                                                     "hx-include" "#invoice-filters"
                                                     :color     :secondary}
                                                    "Bulk Edit"))
                                      (when (can? (:identity request) {:subject :invoice :activity :pay})
                                        (pay-button* {:ids (selected->ids request
                                                                          (:query-params request))}))
                                      (when (can? (:identity request) {:subject :invoice :activity :create})
                                        (com/button {:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-wizard)}
                                                    "New invoice"))]))
                 :row-buttons (fn [request entity]
                                [(when (and (= :invoice-status/unpaid (:invoice/status entity))
                                            (can? (:identity request) {:subject :invoice :activity :delete}))
                                   (com/icon-button {:hx-delete (bidi/path-for ssr-routes/only-routes
                                                                               ::route/delete
                                                                               :db/id (:db/id entity))
                                                     :hx-confirm "Are you sure you want to void this invoice?"}
                                                    svg/trash))
                                 (when (and (can? (:identity request) {:subject :invoice :activity :edit})
                                            (#{:invoice-status/unpaid  :invoice-status/paid} (:invoice/status entity)))
                                   (com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes
                                                                            ::route/edit-wizard
                                                                            :db/id (:db/id entity))}
                                                    svg/pencil))
                                 (when (and (can? (:identity request) {:subject :invoice :activity :edit})
                                            (#{:invoice-status/voided} (:invoice/status entity)))
                                   (com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes
                                                                            ::route/unvoid
                                                                            :db/id (:db/id entity))}
                                                    svg/undo))
                                 (when (and (can? (:identity request) {:subject :invoice :activity :edit})
                                            (can-undo-autopayment entity)
                                            )
                                   (com/button {:hx-put (bidi/path-for ssr-routes/only-routes
                                                                            ::route/undo-autopay
                                                                            :db/id (:db/id entity))}
                                                    "Undo autopay"))])

                 :breadcrumbs        [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
                                       "Invoices"]]
                 :title       (fn [r]
                                (str
                                 (some-> r :route-params :status name str/capitalize (str " "))
                                 "Invoices"))
                 :entity-name "invoices"
                 :route       ::route/table
                 :break-table (fn [request entity]
                                (when (= (-> request :query-params :sort first :name) "Vendor")
                                  (-> entity :invoice/vendor :vendor/name)))
                 :headers     [{:key      "client"
                                :name     "Client"
                                :sort-key "client"
                                :hide?    (fn [args]
                                            (and (= (count (:clients args)) 1)
                                                 (= 1 (count (:client/locations (:client args))))))
                                :render   (fn [x] [:div.flex.items-center.gap-2 (-> x :invoice/client :client/name)
                                                   (map #(com/pill {:color :primary} (-> % :invoice-expense-account/location))
                                                        (:invoice/expense-accounts x)) ])}
                               {:key      "vendor"
                                :name     "Vendor"
                                :sort-key "vendor"
                                :render   #(-> % :invoice/vendor :vendor/name)}
                               {:key      "invoice-number"
                                :name     "Invoice number"
                                :sort-key "invoice-number"
                                :render   :invoice/invoice-number}
                               {:key           "date"
                                :sort-key "date"
                                :name          "Date"
                                :show-starting "lg"
                                :render        (fn [{:invoice/keys [date]}]
                                                 (some-> date (atime/unparse-local atime/normal-date)))}
                               {:key           "due"
                                :sort-key "due"
                                :name          "Due"
                                :show-starting "xl" ;; xl:table-cell
                                :render        (fn [{:invoice/keys [due]}]
                                                 (if-let [due-date (some-> due (atime/unparse-local atime/normal-date)) ]
                                                   (let [
                                                         today (time/now)
                                                         [start end] (if (time/before? due today)
                                                                       [due today]
                                                                       [today due])
                                                         i (time/interval start end )
                                                         days (if (time/before? due today)
                                                                (- (time/in-days i))
                                                                (time/in-days i))]
                                                     (cond (= 0 days)
                                                           [:div.text-primary-700 "today"]
                                                           (> days 0)
                                                           [:div.text-primary-700 (format "in %d days", days)]
                                                           :else 
                                                           [:div.text-red-700 (format "%d days ago", (- days))]))))} 
                               {:key "status"
                                :name "Status"
                                :render (fn [{:invoice/keys [status scheduled-payment]}]
                                          (cond  (= status :invoice-status/paid)
                                            (com/pill {:color :primary} "Paid")
                                            (= status :invoice-status/voided)
                                            (com/pill {:color :red} "Voided")
                                            
                                            scheduled-payment
                                            (com/pill {:color :yellow} "Scheduled")

                                            (= status :invoice-status/unpaid)
                                            (com/pill {:color :secondary} "Unpaid")
                                            :else
                                            ""))}
                               {:key "accounts"
                                :name "Account"
                                :show-starting "lg"
                                :render (fn [{:invoice/keys [expense-accounts client]}]
                                          [:div.flex.flex-col.gap-y-2
                                           (when (first expense-accounts)
                                             [:div.flex-initial
                                              (com/pill {:color :primary}
                                                        (:account/name
                                                         (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read (:db/id (:invoice-expense-account/account (first expense-accounts))))

                                                                               (:db/id client))))])
                                           (when (> (count expense-accounts) 1)
                                             [:div.flex-initial
                                              (com/pill {:color :secondary}
                                                        "+ " (dec (count expense-accounts)) " more")])])}

                               {:key           "outstanding"
                                :name          "Outstanding"
                                :sort-key "outstanding-balance"
                                :class "text-right"
                                :render        (fn [{:invoice/keys [outstanding-balance total]}]
                                                 [:div
                                                  (some->> outstanding-balance (format "$%,.2f"))
                                                  (when-not (dollars= outstanding-balance total)
                                                    [:div.text-xs.text-gray-400 (format "of $%,.2f" total)])])}
                               {:key           "links"
                                :name          "Links"
                                :show-starting "lg"
                                :class "w-8"
                                :render        (fn [i]
                                                 (link-dropdown
                                                   (into []
                                                         (concat (->> i
                                                                      :invoice/payments
                                                                      (map :invoice-payment/payment)
                                                                      (filter (fn [p]
                                                                                (not= :payment-status/voided
                                                                                      (:payment/status p))))
                                                                      (mapcat (fn [p]
                                                                                (cond-> [{:link (hu/url (bidi/path-for ssr-routes/only-routes
                                                                                                                       ::payment-route/all-page)
                                                                                                        {:exact-match-id (:db/id p)})
                                                                                          :content (str (format "$%,.2f" (:payment/amount p))
                                                                                                        (some-> (:payment/date p) coerce/to-date-time (atime/unparse-local atime/normal-date) (#(str " payment on " %))))}]
                                                                                  (:payment/transaction p) (conj {:link (hu/url (bidi/path-for client-routes/routes :transactions)
                                                                                                                                {:exact-match-id (:db/id (first (:payment/transaction p)))})
                                                                                                                  :color :secondary
                                                                                                                  :content "Transaction"})))))
                                                                 (when (:invoice/journal-entry i)
                                                                   [{:link (hu/url (bidi/path-for client-routes/routes :ledger)
                                                                                   {:exact-match-id (:db/id (first (:invoice/journal-entry i)))})
                                                                     :color :yellow
                                                                     :content "Ledger entry"}])
                                                                 (when (:invoice/source-url i)
                                                                   [{:link (:invoice/source-url i)
                                                                     :color :secondary
                                                                     :content "File"}])))))}]}))

(def row* (partial helper/row* grid-page))

(defn unvoid-invoice [{:as request :keys [identity entity]}]
  (let [invoice entity
        id (:db/id entity)
        _ (assert-can-see-client identity (:db/id (:invoice/client invoice)))
        _ (assert-not-locked  (:db/id (:invoice/client invoice)) (:invoice/date invoice))
        history (dc/history (dc/db conn))
        txs (dc/q {:find  ['?tx '?e '?original-status '?original-outstanding '?total '?ea '?ea-amount]
                   :where ['[?e :invoice/status :invoice-status/voided ?tx true]
                           '[?e :invoice/status ?original-status ?tx false]
                           '[?e :invoice/outstanding-balance ?original-outstanding ?tx false]
                           '[?e :invoice/total ?total ?tx false]
                           '[?ea :invoice-expense-account/amount ?ea-amount ?tx false]]
                   :in    ['$ '?e]}
                  history id)
        [last-transaction] (->> txs (sort-by first) (last))
        tx [[:upsert-invoice
             (->> txs
                  (filter (fn [[tx]] (= tx last-transaction)))
                  (reduce (fn [new-transaction [_ entity original-status original-outstanding total expense-account expense-account-amount]]
                            (-> new-transaction
                                (assoc :db/id entity
                                       :invoice/total total
                                       :invoice/status original-status
                                       :invoice/outstanding-balance original-outstanding)
                                (update :invoice/expense-accounts (fnil conj []) {:db/id expense-account :invoice-expense-account/amount expense-account-amount})))
                          {}))]]
        _  (audit-transact tx identity)]
    (alog/info ::unvoiding-invoice :transaction :tx)
    (html-response
     (row* identity (dc/pull (dc/db conn) default-read id) {:flash? true
                                                            :request request})
     :headers (cond-> {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" id)
                       "hx-reswap" "outerHTML"}))))

(defn undo-autopay [{:as request :keys [identity entity]}]
  (let [invoice entity
        id (:db/id entity)
        _ (assert-can-see-client identity (:db/id (:invoice/client invoice)))
        ]
    (alog/info ::undoing-autopay :transaction :tx)
    (assert-can-undo-autopayment invoice)
    (audit-transact 
      [[:upsert-invoice {:db/id id
                         :invoice/status :invoice-status/unpaid 
                         :invoice/outstanding-balance (:invoice/total entity)
                         :invoice/scheduled-payment nil}]]
     identity)


    (html-response
     (row* identity (dc/pull (dc/db conn) default-read id) {:flash? true
                                                            :request request})
     :headers (cond-> {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" id)
                       "hx-reswap" "outerHTML"}))))

(defn delete [{invoice :entity :as request identity :identity}]
  (exception->notification
   #(when-not (= :invoice-status/unpaid (:invoice/status invoice))
      (throw (ex-info  "Cannot void an invoice if it is paid. First void the payment." {}))))

  (when (->> invoice :invoice/payments
             (filter (fn [p]
                       (not= :payment-status/voided
                             (:payment/status p))))
             seq)
    (throw (ex-info "This invoice has linked payments. Void the payments first." {:type :notification})))

  (exception->notification
   #(assert-can-see-client identity (:db/id (:invoice/client invoice))))
  (notify-if-locked (:db/id (:invoice/client invoice))
                    (:invoice/date invoice))
  (audit-transact [[:upsert-invoice {:db/id                       (:db/id invoice)
                                     :invoice/total               0.0
                                     :invoice/outstanding-balance 0.0
                                     :invoice/status              :invoice-status/voided
                                     :invoice/expense-accounts    (map (fn [ea] {:db/id                          (:db/id ea)
                                                                                 :invoice-expense-account/amount 0.0})
                                                                       (:invoice/expense-accounts invoice))}]]
                  identity)

  (html-response (row* (:identity request) (dc/pull (dc/db conn) default-read (:db/id invoice))
                       {:class "live-removed"})
                 :headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id invoice))}))

(defn bulk-delete-dialog [request]
  (let [all-selected (:all-selected (:query-params request))
        selected (:selected (:query-params request))
        ids (cond
              all-selected
              (:ids (fetch-ids (dc/db conn) (-> request
                                                (assoc-in [:query-params :start] 0)
                                                (assoc-in [:query-params :per-page]  250))))
              :else
              selected)]
    (modal-response
     (com/modal {}
                (com/modal-card-advanced
                 {}

                 (com/modal-body {}
                                 [:div.flex.flex-col.mt-4.space-y-4.items-center
                                  [:div.w-24.h-24.bg-red-50.rounded-full.p-4.text-red-300

                                   svg/alert]
                                  [:div "You are about to void " (count ids) " invoices. Are you sure you want to do this?"]])
                 (com/modal-footer {} [:div.flex.justify-end (com/button {:color :primary
                                                                          :hx-vals (hx/json (mc/encode
                                                                                             query-schema
                                                                                             (dissoc (:query-params request) :sort)
                                                                                             (mt/transformer
                                                                                              main-transformer
                                                                                              dissoc-nil-transformer
                                                                                              mt/strip-extra-keys-transformer)))
                                                                          :hx-delete (hu/url (bidi/path-for ssr-routes/only-routes
                                                                                                            ::route/bulk-delete-confirm))}
                                                                         "Void invoices")])))
     :headers (-> {}
                  (assoc "hx-retarget" ".modal-stack")
                  (assoc "hx-reswap" "beforeend")))))

(defn void-invoices-internal [all-ids id]
  (let [all-ids (->> all-ids
                     (dc/q '[:find (pull ?i [:db/id :invoice/date {:invoice/expense-accounts [:db/id]}])
                             :in $ [?i ...]
                             :where (not [_ :invoice-payment/invoice ?i])
                             [?i :invoice/client ?c]
                             [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
                             [?i :invoice/date ?d]
                             [(>= ?d ?lu)]]
                           (dc/db conn)))
        voidable-cash-payments (->> (dc/q '[:find ?p
                                            :in $ [?i ...]
                                            :where [?ip :invoice-payment/invoice ?i]
                                            [?ip :invoice-payment/payment ?p]
                                            [?p :payment/type :payment-type/cash]
                                            [?i :invoice/client ?c]
                                            [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
                                            [?i :invoice/date ?d]
                                            [(>= ?d ?lu)]]
                                          (dc/db conn)
                                          (map :db/id all-ids))
                                    (map first))]
    (alog/info ::void-payments :count (count voidable-cash-payments))
    (gq-checks/void-payments-internal voidable-cash-payments id)

    (alog/info ::voiding-invoices :count (count all-ids))
    (audit-transact
     (->> all-ids
          (map
           (fn [[i]]
             [:upsert-invoice {:db/id                       (:db/id i)
                               :invoice/total               0.0
                               :invoice/outstanding-balance 0.0
                               :invoice/status              :invoice-status/voided
                               :invoice/expense-accounts    (mapv
                                                             (fn [iea]
                                                               {:db/id                          (:db/id iea)
                                                                :invoice-expense-account/amount 0.0})
                                                             (:invoice/expense-accounts i))}])))
     id)
    (count all-ids)))



(defn bulk-delete-dialog-confirm [request]
  (alog/peek (:form-params request))
  (let [ids (selected->ids request (:form-params request))
        updated-count (void-invoices-internal ids (:identity request))]

    (html-response [:div]
                   :headers {"hx-trigger" (hx/json {:modalclose   ""
                                                    :notification (format "Successfully voided %d of %d invoices."
                                                                          updated-count
                                                                          (count ids))})})))

#_(defn pay-invoices-from-balance [context {invoices :invoices
                                          client-id :client_id} _]
  )

(defn pay-using-credit [request]
  (alog/peek (:form-params request))
  (let [invoices (selected->ids request (:form-params request))

        _ (alog/peek invoices)
        invoices (d-invoices/get-multi invoices)
        client->invoices (group-by (comp  :db/id :invoice/client)
                                   invoices)

        client-id (first (keys client->invoices))
        _ (when (> (count (keys client->invoices)) 1)
            (throw (ex-info "Can only pay from one customer's balance at a time" {:type :form-validation})))
        _ (when-not (can? (:identity request) {:activity :pay :subject :invoice
                                               :client client-id})
            (throw (ex-info "You can't pay these invoices" {:type :form-validation})))
        client (d-clients/get-by-id client-id)

        _ (when (> (count (set (map :invoice/vendor invoices))) 1)
            (throw (ex-info "Balance payments can only be for one vendor at a time." {:type :form-validation})))
        _ (when (> (reduce + 0 (map :invoice/outstanding-balance invoices)) 0.001)
            (throw (ex-info "There isn't a positive balance to pay from" {:type :form-validation})))
        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)
            (throw (ex-info "Select some invoices that need to be paid" {:type :form-validation})))

        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}
        result (audit-transact (-> []
                                   (conj payment)
                                   (into (invoice-payments invoices invoice-amounts))) (:identity request))]

    (doseq [[_ n] (:tempids result)]
      (solr/touch-with-ledger n))
    (html-response [:div]
                   :headers {"hx-trigger" (hx/json {:modalclose   ""
                                                    :invalidated ""
                                                    :notification (format "Successfully paid %d invoices."
                                                                          (count invoices))})})))


(defn does-amount-exceed-outstanding? [amount outstanding-balance]
  (let [outstanding-balance (round-money outstanding-balance)
        amount (round-money amount)]
    (or (and (> outstanding-balance 0)
             (> amount outstanding-balance))
        (and (> outstanding-balance 0)
             (<= amount 0))
        (and (< outstanding-balance 0)
             (< amount outstanding-balance))
        (and (< outstanding-balance 0)
             (>= amount 0))
        (and (= outstanding-balance 0.0)
             (not= amount 0.0)))))

(def payment-form-schema
  (mc/schema
   [:map
    [:client entity-id]
    [:invoices [:and
                [:vector {:coerce? true}
                 [:map
                  [:invoice-id entity-id]
                  [:amount  money]]]
                [:fn {:error/message "All payments must not exceed their outstanding balance."}
                 (fn [invoices]
                   (let [outstanding-balances (->> (dc/q '[:find ?i ?ob
                                                           :in $ [?i ...]
                                                           :where [?i :invoice/outstanding-balance ?ob]]
                                                         (dc/db conn)
                                                         (map :invoice-id invoices))
                                                   (into {}))]
                     (every? (fn [%]
                               (not (does-amount-exceed-outstanding?  (:amount %) (outstanding-balances (:invoice-id %)))))
                             invoices)))]]]
    [:has-warning? :boolean]
    [:bank-account entity-id]
    [:check-number {:optional true} :int]
    [:handwritten-date {:optional true} [:maybe clj-date-schema]]
    [:mode [:enum :simple :advanced]]
    [:method [:enum :debit :print-check :cash :handwrite-check :credit]]]))

(defn bank-account-card-base [{:keys [bg-color text-color icon bank-account can-handwrite? credit-only?]}]
  [:div {:class "w-[30em]"}
   (com/card {:class "w-full"}
             [:div.flex.items-stretch {}
              (com/hidden {:name "item"
                           :value (:db/id bank-account)})
              [:div.grow-0.flex.flex-col.justify-center
               [:div.p-1.m-2.rounded-full
                {:class
                 bg-color}
                [:div {:class
                       (hh/add-class "p-1.5 w-8 h-8" text-color)}
                 icon]]]
              [:div.flex.flex-col.grow.m-2
               [:div.font-medium.text-gray-700  (:bank-account/name bank-account)]
               [:div.font-light.text-gray-600 (:bank-account/bank-name bank-account)]]
              [:div.grow-0.m-2.self-center {:x-data (hx/json {})}
               (if credit-only?
                 (com/button {:color :primary
                              :minimal-loading? true
                              :hx-vals (hx/json {"step-params[bank-account]" (:db/id bank-account)
                                                 "step-params[method]" "credit"})
                              :hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/pay-wizard-navigate)
                                              {:from (mm/encode-step-key :choose-method)
                                               :to (mm/encode-step-key :payment-details)})}
                             "Credit")
                 (com/button {:x-ref "button"
                              "@click.prevent.capture" "$tooltip($refs.tooltip.innerHTML, {allowHTML: true, onMount(i) {htmx.process(i.popper)}, interactive:true, theme:\"light\", timeout:5000})" }
                             "Pay"))
               [:template { :x-ref "tooltip"}
                [:div.flex.flex-col.gap-2 {
                                           :data-key "vis"
                                           :class "p-4 w-max" }
                 (when (= :bank-account-type/check
                          (:bank-account/type bank-account))
                   (com/button {:color :primary
                                :minimal-loading? true
                                :hx-vals (hx/json {"step-params[bank-account]" (:db/id bank-account)
                                                   "step-params[method]" "print-check"})
                                :hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/pay-wizard-navigate)
                                                {:from (mm/encode-step-key :choose-method)
                                                 :to (mm/encode-step-key :payment-details)})}
                               "Print check"))
                 (when (= :bank-account-type/cash
                          (:bank-account/type bank-account))
                   (com/button {:minimal-loading? true
                                :hx-vals (hx/json {"step-params[bank-account]" (:db/id bank-account)
                                                   "step-params[method]" "cash"})
                                :hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/pay-wizard-navigate)
                                                {:from (mm/encode-step-key :choose-method)
                                                 :to (mm/encode-step-key :payment-details)})}
                               "With cash"))
                 (when (not= :bank-account-type/cash
                             (:bank-account/type bank-account))
                   (com/button {:color (when (= :bank-account-type/credit
                                                (:bank-account/type bank-account))
                                         :primary)
                                :minimal-loading? true
                                :hx-vals (hx/json {"step-params[bank-account]" (:db/id bank-account)
                                                   "step-params[method]" "debit"})
                                :hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/pay-wizard-navigate)
                                                {:from (mm/encode-step-key :choose-method)
                                                 :to (mm/encode-step-key :payment-details)})}
                               "Debit"))
                 (when (and (= :bank-account-type/check (:bank-account/type bank-account))
                            can-handwrite?)
                   (com/button {:minimal-loading? true
                                :hx-vals (hx/json {"step-params[bank-account]" (:db/id bank-account)
                                                   "step-params[method]" "handwrite-check"})
                                :hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/pay-wizard-navigate)
                                                {:from (mm/encode-step-key :choose-method)
                                                 :to (mm/encode-step-key :payment-details)})}
                               "Handwrite check"))]]]])])

(defmulti bank-account-card  (fn [ba _ _]
                               (:bank-account/type ba)))
(defmethod bank-account-card :bank-account-type/cash [bank-account can-handwrite? credit-only?]
  (bank-account-card-base {:bg-color "bg-green-50"
                           :text-color "text-green-600"
                           :icon svg/dollar
                           :bank-account bank-account
                           :can-handwrite? can-handwrite?
                           :credit-only? credit-only?}))

(defmethod bank-account-card
  :bank-account-type/credit
  [bank-account can-handwrite? credit-only?]
  (bank-account-card-base {:bg-color "bg-purple-50"
                           :text-color "text-purple-600"
                           :icon svg/credit-card
                           :bank-account bank-account
                           :can-handwrite? can-handwrite?
                           :credit-only? credit-only?}))

(defmethod bank-account-card
  :bank-account-type/check [bank-account can-handwrite? credit-only?]
  (bank-account-card-base {:bg-color "bg-blue-50"
                           :text-color "text-blue-600"
                           :icon svg/check
                           :bank-account bank-account
                           :can-handwrite? can-handwrite?
                           :credit-only? credit-only?}))


(defn can-handwrite? [invoices]
  (let [selected-vendors (set (map (comp :db/id :invoice/vendor) invoices))]
    (and
     (= 1 (count selected-vendors))
     (> (reduce + 0 (map :invoice/outstanding-balance invoices)) 0.0))))

(defn credit-only? [invoices]
  (->> invoices
       (group-by :invoice/vendor)
       vals
       (map (fn [is]
              (alog/peek ::invoices is)
              (reduce + 0.0 (map :invoice/outstanding-balance is))))
       (every? #(<= % 0.0))))


(defrecord ChoosePaymentMethodModal [linear-wizard]
  mm/ModalWizardStep
  (step-name [_]
    "Payment method")
  (step-key [_]
    :choose-method)

  (edit-path [_ _]
    [])

  (step-schema [_]
    (mut/select-keys (mm/form-schema linear-wizard) #{:bank-account :method}))

  (render-step
    [this request]
    (let [invoices (:invoices (:snapshot (:multi-form-state request)))
          can-handwrite? (can-handwrite? (map (comp (:invoice-by-id linear-wizard) :invoice-id) invoices))
          credit-only? (credit-only? (map (comp (:invoice-by-id linear-wizard) :invoice-id) invoices))]
      (mm/default-render-step
       linear-wizard this
       :head [:div.p-2.inline-flex.gap-2.items-center "Pay " (count invoices) " invoices"
              (when (:has-warning? (:snapshot (:multi-form-state request)))
                (com/pill {:color :yellow}
                          "Some of the selected invoices may be locked or paid."))]
       :body (mm/default-step-body
              {}
              [:div.flex.flex-col.space-y-2
               (for [ba (:bank-accounts linear-wizard)
                     :when (:bank-account/visible ba)]
                 (bank-account-card ba can-handwrite? credit-only?))])
       :footer
       nil
       :validation-route ::route/pay-wizard-navigate))))

(defrecord PaymentDetailsStep [linear-wizard]
  mm/ModalWizardStep
  (step-name [_]
    "Details")
  (step-key [_]
    :payment-details)

  (edit-path [_ _]
    [])

  (step-schema [_]
    (mut/select-keys (mm/form-schema linear-wizard) #{:invoices :check-number :handwritten-date :mode}))

  (render-step [this request]
    (mm/default-render-step
     linear-wizard this
     :head [:div.p-2 "Pay " (count (:invoices (:snapshot (:multi-form-state request)))) " invoices"]
     :body (mm/default-step-body
            {}
            [:div {}
             (when (= :handwrite-check (:method (:snapshot (:multi-form-state request))))
               (fc/with-field :check-number
                 (com/validated-field
                  {:errors (fc/field-errors)
                   :label "Check number"}
                  (com/int-input {:value (fc/field-value)
                                  :name (fc/field-name)
                                  :error? (fc/field-errors)
                                  :placeholder "10001"}))))
             (when (#{:handwrite-check :print-check} (:method (:snapshot (:multi-form-state request))))
               (fc/with-field :handwritten-date
                 (com/validated-field
                  {:errors (fc/field-errors)
                   :label "Date"}
                  (com/date-input {:value (-> (fc/field-value)
                                              (atime/unparse-local atime/normal-date))
                                   :name (fc/field-name)
                                   :error? (fc/field-errors)
                                   :placeholder "1/1/2020"}))))
             (com/radio-list {:x-model "mode"
                              :name "step-params[mode]"
                              :options [{:value "simple"
                                         :content (let [total (reduce + 0.0
                                                                      (map (comp :invoice/outstanding-balance (:invoice-by-id linear-wizard) :invoice-id)
                                                                           (:invoices (:snapshot (:multi-form-state request)))))]
                                                    (if (< total 0)
                                                      (format "Credit in full ($%,.2f)" total)
                                                      (format "Pay in full ($%,.2f)" total)))}
                                        {:value "advanced"
                                         :content "Customize payments"}]})
             
             [:div.space-y-4
              (fc/with-field :invoices
                (com/validated-field
                 {:errors (fc/field-errors)}
                 (com/data-grid
                  (hx/alpine-appear {:headers [(com/data-grid-header {} "Vendor")
                                               (com/data-grid-header {} "Invoice Number")
                                               (com/data-grid-header {:class "text-right"} "Total")
                                               (com/data-grid-header {:class "text-right"} "Pay")]
                                     :x-show "mode==\"advanced\""})
                  (fc/cursor-map
                   (fn [i]
                     (com/data-grid-row
                      {}
                      (com/data-grid-cell
                       {}

                       (-> (fc/field-value) :invoice :invoice/vendor :vendor/name))
                      (com/data-grid-cell
                       {}
                       (fc/with-field :invoice-id
                         (com/hidden {:name (fc/field-name)
                                      :value (fc/field-value)}))
                       (-> (fc/field-value) :invoice :invoice/invoice-number))
                      (com/data-grid-cell
                       {:class "text-right"}
                       [:span.inline-flex.gap-2
                        (format "$%,.2f" (-> (fc/field-value) :invoice :invoice/outstanding-balance))])
                      (com/data-grid-cell
                       {:class "w-20"}
                       (fc/with-field :amount
                         (com/validated-field {:errors (fc/field-errors)}
                                              (com/money-input {:value (format "%.2f" (fc/field-value)) :class "w-20"
                                                                :name (fc/field-name)
                                                                :error?      (fc/error?)}))))))))))]])
     :footer
     (mm/default-step-footer linear-wizard this :validation-route ::route/pay-wizard-navigate
                             :next-button-content "Pay")
     :validation-route ::route/pay-wizard-navigate)))

(defn add-handwritten-check [request wizard snapshot]
  (let [invoices (d-invoices/get-multi (map :invoice-id (:invoices snapshot)))
        bank-account-id (:bank-account snapshot)
        bank-account (d-bank-accounts/get-by-id bank-account-id)
        _ (when-not (= 1 (count (set (map (comp :db/id :invoice/vendor) invoices))))
            (throw (ex-info "Can only write a handwritten check for a single vendor." {:type :form-validation})))
        _ (doseq [invoice invoices]
            (assert-can-see-client  (:identity request) (: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 (:handwritten-date snapshot))
        invoice-payment-lookup (by :invoice-id :amount (:invoices snapshot))
        base-payment (base-payment invoices
                                   (:invoice/vendor (first invoices))
                                   (:invoice/client (first invoices))
                                   bank-account
                                   :payment-type/check
                                   0
                                   invoice-payment-lookup
                                   (:handwritten-date snapshot))]
    (let [result (audit-transact
                  (into [(assoc base-payment
                                :payment/type :payment-type/check
                                :payment/status :payment-status/pending
                                :payment/check-number (:check-number snapshot))]
                        (invoice-payments invoices invoice-payment-lookup))
                  (:identity request))]
      (try
        (doseq [[_ i] (:tempids result)]
          (solr/touch-with-ledger i))
        (catch Exception e
          (alog/error ::cant-save-solr
                      :error e))))))

;; TODO support crediting from balance
(defrecord PayWizard [form-params current-step invoice-by-id]
  mm/LinearModalWizard
  (hydrate-from-request
    [this request]
    (let [invoices (->> (dc/q '[:find (pull ?i [{:invoice/vendor [:vendor/name :db/id]
                                                 :invoice/client [:db/id]}
                                                :invoice/outstanding-balance
                                                :invoice/invoice-number
                                                :db/id])
                                :in $ [?i ...]]
                              (dc/db conn)
                              (map :invoice-id (get-in request [:multi-form-state :snapshot :invoices])))
                        (map first)
                        (sort-by (juxt (comp :invoice/vendor :vendor/name)
                                       :invoice/invoice-number)))]
      (assoc this :invoice-by-id (by :db/id invoices)
             :bank-accounts (->> (dc/q '[:find (pull ?ba [:bank-account/name :bank-account/sort-order :bank-account/visible
                                                          :bank-account/bank-name
                                                          :db/id
                                                          {[:bank-account/type :xform iol-ion.query/ident] [:db/ident]}])
                                         :in $ ?c
                                         :where [?c :client/bank-accounts ?ba]]
                                       (dc/db conn)
                                       (:client (:snapshot (:multi-form-state request))))
                                 (map first)
                                 (sort-by :bank-account/sort-order)))))
  (navigate [this step-key]
    (assoc this :current-step step-key))
  (get-current-step [this]
    (if current-step
      (mm/get-step this current-step)
      (mm/get-step this :choose-method)))
  (render-wizard [this {:keys [multi-form-state] :as request}]
    (let [request (update-in request [:multi-form-state :step-params :invoices]
                             (fn [form-invoices]
                               (->> form-invoices
                                    (map (fn [form-invoice]
                                           (assoc form-invoice :invoice ((:invoice-by-id this) (:invoice-id form-invoice)))))
                                    (sort-by
                                     (juxt (comp :vendor/name :invoice/vendor :invoice)
                                           (comp :invoice/invoice-number :invoice)))
                                    (into []))))]
      (mm/default-render-wizard
       this request
       :form-params
       (-> mm/default-form-props
           (assoc :hx-post
                  (str (bidi/path-for ssr-routes/only-routes ::route/pay-submit)))
           (assoc :x-data (hx/json {:mode (some-> multi-form-state
                                                  :step-params
                                                  :mode
                                                  name)}))))))

  (steps [_]
    [:choose-method
     :payment-details])

  (get-step [this step-key]
    (let [step-key-result (mc/parse  mm/step-key-schema step-key)
          [step-key-type step-key] step-key-result]
      (if (= :step step-key-type)
        (get {:choose-method (->ChoosePaymentMethodModal this)
              :payment-details (->PaymentDetailsStep this)}
             step-key)

        (get {:bank-account (->ChoosePaymentMethodModal this)}
             (first step-key)))))
  (form-schema [_] payment-form-schema)
  (submit [this {:keys [multi-form-state request-method identity] :as request}]
    (let [snapshot (mc/decode
                    payment-form-schema
                    (:snapshot multi-form-state)
                    mt/strip-extra-keys-transformer)


          _ (assert-schema payment-form-schema snapshot)

          _ (exception->4xx
             #(if (= :handwrite-check (:method snapshot))
                (when (or (not (some? (:check-number snapshot)))
                          (= "" (:check-number snapshot)))
                  (throw (Exception. "Check number is required")))
                true))
          
          result (exception->4xx
                  #(do
                     (when (:handwritten-date snapshot)
                       (let [invoices (d-invoices/get-multi (map :invoice-id (:invoices snapshot)))]
                         (assert-not-locked (:db/id (:invoice/client (first invoices))) (:handwritten-date snapshot))))
                     (if  (= :handwrite-check (:method snapshot))
                       (add-handwritten-check request this snapshot)
                       (try
                         (print-checks-internal (map (fn [i] {:invoice-id (:invoice-id i)
                                                              :amount     (:amount i)})
                                                     (:invoices snapshot))
                                                (:client snapshot)
                                                (:bank-account snapshot)
                                                (cond (= :print-check (:method snapshot))
                                                      :payment-type/check
                                                      (= :debit (:method snapshot))
                                                      :payment-type/debit
                                                      (= :cash (:method snapshot))
                                                      :payment-type/cash
                                                      (= :credit (:method snapshot))
                                                      :payment-type/credit
                                                      :else :payment-type/debit)
                                                identity
                               (:handwritten-date snapshot))
                         (catch Exception e
                           (println e))))))]
      (modal-response
       (com/modal {}
                  (com/modal-card-advanced
                   {:class "transition duration-300 ease-in-out htmx-swapping:-translate-x-2/3 htmx-swapping:opacity-0 htmx-swapping:scale-0 htmx-added:translate-x-2/3 htmx-added:opacity-0 htmx-added:scale-0 scale-100 translate-x-0 opacity-100"}
                   (com/modal-body {}
                                   [:div.flex.flex-col.mt-4.space-y-4.items-center
                                    [:div.w-24.h-24.bg-green-50.rounded-full.p-4.text-green-300.animate-gg
                                     svg/thumbs-up]
                                    (when-not (:pdf-url result)
                                      [:div "That's a wrap. Your payment is complete."])
                                    (when (:pdf-url result)
                                      [:div "Your checks are ready. Click "
                                       (com/link {:href (:pdf-url result) :target "_new"} "here")
                                       " to download and print."])
                                    (when (:pdf-url result)
                                      [:div.text-xs.italic [:em "Remember to turn off all scaling and margins."]])])))
       :headers {"hx-trigger" "invalidated"}))))

(def pay-wizard
  (->PayWizard nil nil nil))

(defn wrap-status-from-source [handler]
  (fn [{:keys [matched-current-page-route] :as request}]
    (let [request (cond-> request
                    (= ::route/paid-page matched-current-page-route) (assoc-in [:route-params :status] :invoice-status/paid)
                    (= ::route/unpaid-page matched-current-page-route) (assoc-in [:route-params :status] :invoice-status/unpaid)
                    (= ::route/voided-page matched-current-page-route) (assoc-in [:route-params :status] :invoice-status/voided)
                    (= ::route/all-page matched-current-page-route) (assoc-in [:route-params :status] nil))]
      (handler request))))

(defn payable-ids [ids]
  (->> (dc/q '[:find ?i
               :in $ [?i ...]
               :where [?i :invoice/status :invoice-status/unpaid]
               [?i :invoice/client ?c]
               (not [?i :invoice/scheduled-payment])]
             (dc/db conn)
             ids)
       (map first)))

(defn initial-pay-wizard-state [request]
  (exception->notification
   #(let [selected-ids (selected->ids request (:query-params request))
          selected-ids (payable-ids selected-ids)
          _  (when (= 0 (count selected-ids))
               (throw (ex-info "No selected invoices are applicable for payment" {:type :notification})))

          has-warning? (and (:selected (:query-params request))
                            (not= (count selected-ids)
                                  (count (:selected (:query-params request)))))
          invoices (->> (dc/q '[:find (pull ?i [{:invoice/vendor [:vendor/name :db/id]
                                                 :invoice/client [:db/id]}
                                                :invoice/outstanding-balance
                                                :invoice/invoice-number
                                                :db/id])
                                :in $ [?i ...]]
                              (dc/db conn)
                              selected-ids)
                        (map first)
                        (sort-by (juxt (comp :invoice/vendor :vendor/name)
                                       :invoice/invoice-number)))]
      (mm/->MultiStepFormState {:invoices (mapv (fn [i] {:invoice-id (:db/id i)
                                                         :amount (:invoice/outstanding-balance i)})
                                                invoices)
                                :mode :simple
                                :client (-> invoices first :invoice/client :db/id)
                                :has-warning? (boolean has-warning?)
                                :handwritten-date (time/now)}
                               []
                               {:mode :simple
                                :has-warning? (boolean has-warning?)}))))
(defn redirect-handler [target-route]
  (fn handle [request]
    {:status 302
     :headers {"Location" (str (hu/url (bidi.bidi/path-for ssr-routes/only-routes 
                                                           target-route)
                                       (:query-params request)))}}))


(defn initial-bulk-edit-state [request]
  (mm/->MultiStepFormState {:search-params (:query-params request)
                            :expense-accounts [{:db/id "123"
                                                :location "Shared"
                                                :account nil
                                                :percentage 1.0}]}
                           []
                           {:search-params (:query-params request)
                            :expense-accounts [{:db/id "123"
                                                :location "Shared"
                                                :account nil
                                                :percentage 1.0}]}))

(defn- account-typeahead*
  [{:keys [name value client-id x-model]}]
  [:div.flex.flex-col
   (com/typeahead {:name        name
                   :placeholder "Search..."
                   :url         (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
                                        { :purpose "invoice"})
                   :id          name
                   :x-model     x-model
                   :value       value
                   :content-fn  (fn [value]
                                  (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
                                                                       client-id)))})])



;; TODO clientize
(defn all-ids-not-locked [all-ids]
  (->> all-ids
       (dc/q '[:find ?i
               :in $ [?i ...]
               :where
               [?i :invoice/client ?c]
               [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
               [?i :invoice/date ?d]
               [(>= ?d ?lu)]]
             (dc/db conn))
       (map first)))
(defn- bulk-edit-account-row* [{:keys [value client-id]}]

  (com/data-grid-row
   (-> {:x-data   (hx/json {:show      (boolean (not (fc/field-value (:new? value))))
                            :accountId (fc/field-value (:account value))})
        :data-key "show"
        :x-ref    "p"}
       hx/alpine-mount-then-appear)
   (fc/with-field :db/id
     (com/hidden {:name  (fc/field-name)
                  :value (fc/field-value)}))
   (fc/with-field :account
     (com/data-grid-cell
      {}
      (com/validated-field
       {:errors (fc/field-errors)}
       (account-typeahead* {:value     (fc/field-value)
                            :client-id client-id
                            :name      (fc/field-name)
                            :x-model   "accountId"}))))
   (fc/with-field :location
     (com/data-grid-cell
      {}
      (com/validated-field
       {:errors (fc/field-errors)
        :x-hx-val:account-id "accountId"
        :hx-vals (hx/json {:name (fc/field-name) })
        :x-dispatch:changed "accountId"
        :hx-trigger "changed"
        :hx-get     (bidi/path-for ssr-routes/only-routes ::route/location-select)
        :hx-target "find *"
        :hx-swap "outerHTML"}
       (location-select* {:name             (fc/field-name)
                          :account-location (:account/location (cond->> (:account @value)
                                                                 (nat-int? (:account @value)) (dc/pull (dc/db conn)
                                                                                                                               '[:account/location])))
                          :value            (fc/field-value)}))))
   (fc/with-field :percentage
     (com/data-grid-cell
      {}
      (com/validated-field
       {:errors (fc/field-errors)}
       (com/money-input {:name  (fc/field-name)
                            :class "w-16 amount-field"
                            :value (some-> (fc/field-value)
                                           (* 100)
                                           (long))}))))
   (com/data-grid-cell {:class "align-top"}
                       (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))

(defrecord AccountsStep [linear-wizard]
  mm/ModalWizardStep
  (step-name [_]
    "Expense Accounts")
  (step-key [_]
    :accounts)

  (edit-path [_ _]
    [])

  (step-schema [_]
    (mut/select-keys (mm/form-schema linear-wizard) #{:expense-accounts}))

  (render-step [this {{:keys [snapshot] :as multi-form-state} :multi-form-state :as request}]
    (let [selected-ids (selected->ids (assoc request :query-params (:search-params snapshot)) (:search-params snapshot))
          all-ids (all-ids-not-locked selected-ids)]
      (mm/default-render-step
       linear-wizard this
       :head [:div.p-2 "Bulk editing " (count all-ids) " invoices"]
       :body (mm/default-step-body
              {}
              [:div {}
               (fc/with-field :expense-accounts
                 (com/validated-field
                  {:errors (fc/field-errors)}
                  (com/data-grid {:headers [(com/data-grid-header {} "Account")
                                            (com/data-grid-header {:class "w-32"} "Location")
                                            (com/data-grid-header {:class "w-16"} "%")
                                            (com/data-grid-header {:class "w-16"})]}
                                 (fc/cursor-map #(bulk-edit-account-row* {:value %
                                                                          :client-id (:invoice/client snapshot)}))

                                 (com/data-grid-new-row {:colspan   4
                                                         :hx-get    (bidi/path-for ssr-routes/only-routes
                                                                                   ::route/bulk-edit-new-account)
                                                         :row-offset 0
                                                         :index     (count (fc/field-value)) }
                                                        "New account")
                                 (com/data-grid-row {}
                                                    (com/data-grid-cell {})
                                                    (com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"])
                                                    (com/data-grid-cell {:id "total"
                                                                         :class "text-right"
                                                                         :hx-trigger "change from:closest form target:.amount-field"
                                                                         :hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/bulk-edit-total)
                                                                         :hx-target "this"
                                                                         :hx-swap "innerHTML"}
                                                                        #_(invoice-expense-account-total* request))
                                                    (com/data-grid-cell {}))

                                 (com/data-grid-row {}
                                                    (com/data-grid-cell {})
                                                    (com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "BALANCE"])
                                                    (com/data-grid-cell {:id "total"
                                                                         :class "text-right"
                                                                         :hx-trigger "change from:closest form target:.amount-field"
                                                                         :hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/bulk-edit-balance)
                                                                         :hx-target "this"
                                                                         :hx-swap "innerHTML"}
                                                                        #_(invoice-expense-account-balance* request))
                                                    (com/data-grid-cell {})))))])
       :footer
       (mm/default-step-footer linear-wizard this :validation-route ::route/new-wizard-navigate
                               :next-button (com/button {:color :primary :x-ref "next" :class "w-32"} "Save"))
       :validation-route ::route/new-wizard-navigate))))


(defn maybe-code-accounts [invoice account-rules valid-locations]
  (with-precision 2
    (let [accounts (vec (mapcat
                         (fn [ar]
                           (let [cents-to-distribute (int (Math/round (Math/abs (* (:percentage ar)
                                                                                   (:invoice/total invoice)
                                                                                   100))))]
                             (if (= "Shared" (:location ar))
                               (do
                                 (->> valid-locations
                                      (map
                                       (fn [cents location]
                                         {:db/id (random-tempid)
                                          :invoice-expense-account/account  (:account ar)
                                          :invoice-expense-account/amount   (* 0.01 cents)
                                          :invoice-expense-account/location location})
                                       (rm/spread-cents cents-to-distribute (count valid-locations)))))
                               [(cond-> {:db/id (random-tempid)
                                         :invoice-expense-account/account (:account ar)
                                         :invoice-expense-account/amount (* 0.01 cents-to-distribute)}
                                  (:location ar) (assoc :invoice-expense-account/location (:location ar)))])))
                         account-rules))
          accounts (mapv
                    (fn [a]
                      (update a :invoice-expense-account/amount
                              #(with-precision 2
                                 (double (.setScale (bigdec %) 2 java.math.RoundingMode/HALF_UP)))))
                    accounts)
          leftover (with-precision 2 (.round (bigdec (- (Math/abs (:invoice/total invoice))
                                                        (Math/abs (reduce + 0.0 (map #(:invoice-expense-account/amount %) accounts)))))
                                             *math-context*))
          accounts (if (seq accounts)
                     (update-in accounts [(dec (count accounts)) :invoice-expense-account/amount] #(+ % (double leftover)))
                     [])]
      accounts)))

(defn assert-percentages-add-up [{:keys [expense-accounts]}]
  (let [expense-account-total (reduce + 0 (map (fn [x] (:percentage x)) expense-accounts))]
    (when-not (dollars= 1.0 expense-account-total)
      (form-validation-error (str "Expense account total (" expense-account-total ") does not equal 100%")))))

(defrecord BulkEditWizard [_ current-step]
  mm/LinearModalWizard
  (hydrate-from-request
    [this request]
    this)
  (navigate [this step-key]
    (assoc this :current-step step-key))
  (get-current-step [this]
                    (if current-step
                      (mm/get-step this current-step)
                      (mm/get-step this :accounts)))
  (render-wizard [this {:keys [multi-form-state] :as request}]
    (mm/default-render-wizard
     this request
     :form-params
     (-> mm/default-form-props
         (assoc :hx-put
                (str (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-submit))))
     :render-timeline? false))
  (steps [_]
    [:accounts])
  (get-step [this step-key]
    (let [step-key-result (mc/parse  mm/step-key-schema step-key)
          [step-key-type step-key] step-key-result]
      (get {:accounts (->AccountsStep this) }
           step-key)))
  (form-schema [_]
    (mc/schema [:map
                [:expense-accounts
                    (many-entity {:min 1}
                                 [:account entity-id]
                                 [:location [:string {:min 1 :error/message "required"}]]
                                 [:percentage percentage])]]))
  (submit [this {:keys [multi-form-state request-method identity] :as request}]
          (let [selected-ids (selected->ids (assoc request :query-params (:search-params (:snapshot multi-form-state))) (:search-params (:snapshot multi-form-state)))
                all-ids (all-ids-not-locked selected-ids)
                invoices (pull-many (dc/db conn) '[:db/id :invoice/total {:invoice/client [:client/locations]}] (vec all-ids)) ]
(assert-percentages-add-up (:snapshot multi-form-state))
            
            (doseq [a    (-> multi-form-state :snapshot :expense-accounts)
                    :let [{:keys [:account/location :account/name]} (dc/pull (dc/db conn) [:account/location :account/name] (:account a))]]
              (when (and location (not= location (:location a)))
                (let [err (str "Account " name " uses location " (:location a) ", but is supposed to be " location)]
                  (throw (ex-info err {:validation-error err})))))
            (alog/info ::bulk-code :count (count all-ids))
            (audit-transact-batch
             (map (fn [i]
                    [:upsert-invoice {:db/id                    (:db/id i)
                                      :invoice/expense-accounts (maybe-code-accounts i (-> multi-form-state :snapshot :expense-accounts) (-> i :invoice/client :client/locations))}])
                  invoices)
             (:identity request))
            
            (html-response
             [:div]
             :headers (cond-> {"hx-trigger"  (hx/json { "modalclose" ""
                                                       "invalidated" ""
                                                       "notification" (str "Successfully coded " (count all-ids) " invoices.")}) 
                               "hx-reswap" "outerHTML"})))))

(def bulk-edit-wizard (->BulkEditWizard nil nil))


(defn bulk-edit-total* [request]
  (let [total (->> (-> request
                       :multi-form-state
                       :step-params
                       :expense-accounts)
                   (map (fnil :percentage 0.0))
                   (filter number?)
                   (reduce + 0.0))]
    (format "%.1f%%" (* 100.0 total))))

(defn bulk-edit-balance* [request]
  (let [total (->> (-> request
                       :multi-form-state
                       :step-params
                       :expense-accounts)
                   (map (fnil :percentage 0.0))
                   (filter number?)
                   (reduce + 0.0))
        balance (- 100.0
                      (* 100.0 total))]
    [:span {:class (when-not (dollars= 0.0 balance)
                     "text-red-300")}
     (format "%.1f%%" balance)]))

(defn bulk-edit-total [request]
  (html-response (bulk-edit-total* request)))

(defn bulk-edit-balance [request]
  (html-response (bulk-edit-balance* request)))

(def key->handler
  (apply-middleware-to-all-handlers
   (->
    {::route/all-page              (-> (helper/page-route grid-page)
                                       (wrap-implied-route-param :status nil))
     ::route/paid-page              (-> (helper/page-route grid-page)
                                        (wrap-implied-route-param :status :invoice-status/paid))
     ::route/unpaid-page              (-> (helper/page-route grid-page)
                                          (wrap-implied-route-param :status :invoice-status/unpaid))
     ::route/voided-page              (-> (helper/page-route grid-page)
                                          (wrap-implied-route-param :status :invoice-status/voided))
     ::route/legacy-invoices (redirect-handler ::route/all-page)
     ::route/legacy-import-invoices (redirect-handler ::route/import-page)
     ::route/legacy-unpaid-invoices (redirect-handler ::route/unpaid-page)
     ::route/legacy-paid-invoices (redirect-handler ::route/paid-page)
     ::route/legacy-voided-invoices (redirect-handler ::route/voided-page)
     ::route/legacy-new-invoice (redirect-handler ::route/new-wizard)
     ::route/bulk-edit (-> mm/open-wizard-handler
                      (mm/wrap-wizard bulk-edit-wizard)
                      (mm/wrap-init-multi-form-state initial-bulk-edit-state))
::route/bulk-edit-submit (-> mm/submit-handler
                             (mm/wrap-wizard bulk-edit-wizard)
                             (mm/wrap-decode-multi-form-state)
                             (wrap-must {:subject :invoice :activity :bulk-edit}))
::route/bulk-edit-total (-> bulk-edit-total
                            (mm/wrap-wizard bulk-edit-wizard)
                            (mm/wrap-decode-multi-form-state)
                            (wrap-must {:subject :invoice :activity :bulk-edit}))
::route/bulk-edit-balance (-> bulk-edit-balance
                              
                              (mm/wrap-wizard bulk-edit-wizard)
                              (mm/wrap-decode-multi-form-state)
                              (wrap-must {:subject :invoice :activity :bulk-edit}))
::route/bulk-edit-new-account (->
                               (add-new-entity-handler [:step-params :expense-accounts]
                                                       (fn render [cursor request]
                                                         (bulk-edit-account-row*
                                                          {:value cursor }))
                                                       (fn build-new-row [base _]
                                                         (assoc base :invoice-expense-account/location "Shared")))
                               (wrap-schema-enforce :query-schema [:map
                                                                   [:client-id {:optional true}
                                                                    [:maybe entity-id]]]))

     ::route/undo-autopay (-> undo-autopay
                              (wrap-entity [:route-params :db/id] default-read)
                              (wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
     ::route/unvoid (-> unvoid-invoice
                        (wrap-entity [:route-params :db/id] default-read)
                        (wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
     ::route/pay-button (-> pay-button
                            (wrap-schema-enforce :query-schema  query-schema))
     ::route/delete (-> delete
                        (wrap-entity [:route-params :db/id] default-read)
                        (wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
     ::route/bulk-delete-confirm  (-> bulk-delete-dialog-confirm
                                      (wrap-schema-enforce :form-schema  query-schema)
                                      (wrap-admin))
     ::route/bulk-delete  (-> bulk-delete-dialog
                              (wrap-admin))

     ::route/pay-using-credit  (-> pay-using-credit
                                   (wrap-schema-enforce :form-schema  query-schema))
     ::route/pay-wizard (-> mm/open-wizard-handler

                            (mm/wrap-wizard pay-wizard)
                            (mm/wrap-init-multi-form-state initial-pay-wizard-state))

     ::route/pay-submit              (-> mm/submit-handler

                                         (mm/wrap-wizard pay-wizard)
                                         (mm/wrap-decode-multi-form-state))

     ::route/pay-wizard-navigate
     (-> mm/next-handler
         (mm/wrap-wizard pay-wizard)
         (mm/wrap-decode-multi-form-state))

     ::route/table             (helper/table-route grid-page :parse-query-params? false)}
    (merge new-invoice-wizard/key->handler)
    (merge invoice-import/key->handler))
   (fn [h]
     (-> h
         (wrap-copy-qp-pqp)
         (wrap-status-from-source)
         (wrap-apply-sort grid-page)
         (wrap-merge-prior-hx)
         (wrap-schema-enforce :query-schema query-schema)
         (wrap-schema-enforce :hx-schema query-schema)
         (wrap-client-redirect-unauthenticated)))))