(ns auto-ap.ssr.invoice.new-invoice-wizard
  (:require
   [auto-ap.client-routes :as client-routes]
   [auto-ap.datomic
             :refer [audit-transact conn pull-attr]]
   [auto-ap.datomic.accounts :as d-accounts]
   [auto-ap.datomic.invoices :as d-invoices]
   [auto-ap.graphql.utils :refer [assert-can-see-client assert-not-locked
                                  exception->4xx]]
   [auto-ap.logging :as alog]
   [auto-ap.routes.invoice :as route]
   [auto-ap.routes.utils
             :refer [wrap-client-redirect-unauthenticated]]
   [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.multi-modal :as mm]
   [auto-ap.ssr.form-cursor :as fc]
   [auto-ap.ssr.hx :as hx]
   [auto-ap.ssr.invoice.common :refer [default-read]]
   [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
   [auto-ap.ssr.svg :as svg]
   [auto-ap.ssr.utils
             :refer [->db-id apply-middleware-to-all-handlers check-allowance
                     check-location-belongs clj-date-schema entity-id
                     form-validation-error html-response money strip
                     wrap-schema-enforce]]
   [auto-ap.time :as atime]
   [bidi.bidi :as bidi]
   [clj-time.coerce :as coerce]
   [clj-time.core :as time]
   [datomic.api :as dc]
   [hiccup.util :as hu]
   [iol-ion.query :refer [dollars=]]
   [malli.core :as mc]
   [malli.util :as mut]))

(defn get-vendor [vendor-id]
  (dc/pull
   (dc/db conn)
   [:vendor/terms
    :vendor/automatically-paid-when-due
    {:vendor/default-account d-accounts/default-read
     :vendor/account-overrides
     [:vendor-account-override/client
      {:vendor-account-override/account d-accounts/default-read}]}
    {:vendor/terms-overrides
     [:vendor-terms-override/client :vendor-terms-override/terms]}]
   vendor-id))





(defn check-vendor-default-account [vendor-id]
  (some?  (:vendor/default-account (get-vendor vendor-id))))

(def new-form-schema
  [:map
   [:db/id {:optional true} [:maybe entity-id]]
   [:customize-due-and-scheduled? {:optional true :default false :decode/arbitrary (fn [x] (if (= "" x)
                                                                                             false
                                                                                             x))} [:maybe :boolean]]
   [:customize-accounts {:optional true :default :default} [:enum :default :customize]]

   [:invoice/client {:optional true} [:maybe entity-id]]
   [:invoice/date clj-date-schema]
   [:invoice/due {:optional true} [:maybe clj-date-schema]]
   [:invoice/scheduled-payment {:optional true} [:maybe clj-date-schema]]
   [:invoice/vendor {:optional true}
    [:and entity-id
     [:fn {:error/message "Vendor is missing default expense account"}
      check-vendor-default-account]]]
   [:invoice/invoice-number {:optional true} [:string {:min 1 :decode/string strip}]]
   [:invoice/total money]
   [:invoice/expense-accounts
    [:vector {:coerce? true}
     [:and
      [:map
       [:invoice-expense-account/account [:and entity-id
                                          [:fn {:error/message "Not an allowed account."}
                                           #(check-allowance % :account/invoice-allowance)]]]
       [:invoice-expense-account/location :string]
       [:invoice-expense-account/amount  :double]]
      [:fn {:error/fn (fn [r x] (:type r))
            :error/path [:invoice-expense-account/location]} 
       (fn [iea]
         (check-location-belongs (:invoice-expense-account/location iea)
                                 (:invoice-expense-account/account iea)))]]]]])

(defn wrap-schema [s]
  [:and s
   [:fn (fn [{:keys [:db/id :invoice/invoice-number :invoice/vendor :invoice/client] :as z}]
          (if id
            true
            (and invoice-number vendor client)))]])


(defn clientize-vendor [{:vendor/keys [terms-overrides automatically-paid-when-due default-account account-overrides] :as vendor} client-id]
  (if (nil? vendor)
    nil
    (let [terms-override (->> terms-overrides
                              (filter (fn [to]
                                        (= (->db-id (:vendor-terms-override/client to))
                                           client-id)))
                              (map :vendor-terms-override/terms)
                              first)
          account  (or (->> account-overrides
                            (filter (fn [to]
                                      (= (->db-id (:vendor-account-override/client to))
                                         client-id)))
                            (map :vendor-account-override/account)
                            first)
                       default-account)
          account (d-accounts/clientize account client-id)

          automatically-paid-when-due (->> automatically-paid-when-due
                                           (filter (fn [to]
                                                     (= (->db-id to)
                                                        client-id)))
                                           seq
                                           boolean)
          vendor (cond-> vendor
                   terms-override (assoc :vendor/terms terms-override)
                   true (assoc :vendor/automatically-paid-when-due automatically-paid-when-due
                               :vendor/default-account account)
                   true (dissoc :vendor/account-overrides :vendor/terms-overrides))]
      vendor)))

(defn account-prediction* [{:keys [multi-form-state]}]
  (let [vendor (clientize-vendor (get-vendor (:invoice/vendor (:step-params multi-form-state)))
                                 (->db-id (:invoice/client (:step-params multi-form-state))))
        account-name (:account/name (:vendor/default-account vendor))
        value (mm/get-mfs-field multi-form-state :customize-accounts)]
    (when vendor
      (com/radio-list  {:name "step-params[customize-accounts]"

                        :value (name value)
                        :options (filter identity
                                         [(when account-name {:value (name :default)
                                                              :content (com/pill {:color :primary} account-name)})
                                          {:value (name :customize)
                                           :content [:div "Customize accounts"]}])}))))


(defrecord BasicDetailsStep [linear-wizard]
  mm/ModalWizardStep
  (step-name [_]
    "Basic Details")
  (step-key [_]
    :basic-details)

  (edit-path [_ _]
    [])

  (step-schema [_]
    (wrap-schema (mut/select-keys (mm/form-schema linear-wizard) #{:invoice/client :invoice/vendor :invoice/date :invoice/due :invoice/scheduled-payment :invoice/total :invoice/invoice-number :db/id :customize-due-and-scheduled? :customize-accounts})))

  (render-step
    [this {:keys [multi-form-state] :as request}]
    (let [extant? (mm/get-mfs-field multi-form-state :db/id)]
      (mm/default-render-step
       linear-wizard this
       :head [:div.p-2 (if extant?
                         "Edit invoice"
                         "New invoice")]
       :body (mm/default-step-body
              {}
              [:div {:x-data (hx/json {:clientId (or (fc/field-value (:invoice/client fc/*current*))
                                                     (:db/id (:client request)))
                                       :vendorId (fc/field-value (:invoice/vendor fc/*current*))
                                       :date (-> (fc/field-value (:invoice/date fc/*current*))
                                                 (atime/unparse-local atime/normal-date))
                                       :due (some-> (fc/field-value (:invoice/due fc/*current*))
                                                    (atime/unparse-local atime/normal-date))
                                       :scheduledPayment (some-> (fc/field-value (:invoice/scheduled-payment fc/*current*))
                                                                 (atime/unparse-local atime/normal-date))
                                       :customizeDueAndScheduled (fc/field-value (:customize-due-and-scheduled? fc/*current*))})}
               (fc/with-field :db/id
                 (when extant?
                   (com/hidden {:name (fc/field-name)
                                :value (fc/field-value)})))


               (fc/with-field :customize-due-and-scheduled?
                 (com/hidden {:name (fc/field-name)
                              :value (fc/field-value)
                              :x-model "customizeDueAndScheduled"}))
               (fc/with-field :invoice/client
                 (if (or (:client request) extant?)
                   (com/hidden {:name (fc/field-name)
                                :value (or  (mm/get-mfs-field multi-form-state :invoice/client)
                                            (:db/id (:client request)))})
                   (com/validated-field
                    {:label  "Client"
                     :errors (fc/field-errors)}
                    [:div.w-96
                     (com/typeahead {:name        (fc/field-name)
                                     :error?      (fc/error?)
                                     :class       "w-96"
                                     :placeholder "Search..."
                                     :url         (bidi/path-for ssr-routes/only-routes :company-search)
                                     :value       (fc/field-value)
                                     :content-fn  (fn [c] (pull-attr (dc/db conn) :client/name c))
                                     :x-model "clientId"})])))
               (fc/with-field :invoice/vendor
                 (com/validated-field
                  {:label  "Vendor"
                   :errors (fc/field-errors)}
                  [:div.w-96
                   (com/typeahead {:name        (fc/field-name)
                                   :error?      (fc/error?)
                                   :disabled (boolean (-> request :multi-form-state :snapshot :db/id))
                                   :class       "w-96"
                                   :placeholder "Search..."
                                   :url         (bidi/path-for ssr-routes/only-routes :vendor-search)
                                   :value       (fc/field-value)
                                   :content-fn  (fn [c] (pull-attr (dc/db conn) :vendor/name c))
                                   :x-model "vendorId"})]))
               [:div.mb-4
                [:span.text-sm.text-gray-500 "Can't find the vendor?  "
                 (com/link {:href (bidi.bidi/path-for
                                   client-routes/routes
                                   :new-vendor)
                            :target "new"}
                           "Add new vendor")
                 " in a new window, then return here."]]


               [:div.flex.items-center.gap-2
                (fc/with-field :invoice/date
                  (com/validated-field
                   {:label "Date"
                    :errors (fc/field-errors)}
                   [:div {:class "w-24"}
                    (com/date-input {:value (some-> (fc/field-value)
                                                    (atime/unparse-local atime/normal-date))
                                     :name (fc/field-name)
                                     :error? (fc/field-errors)
                                     :x-model "date"
                                     :placeholder "1/1/2024"})]))
                [:div {:x-show "!customizeDueAndScheduled"}
                 (com/link {"@click" "customizeDueAndScheduled=true"
                            :x-show "!due && !scheduledPayment"}
                           "Add due / scheduled payment date")
                 (com/link {"@click" "customizeDueAndScheduled=true"
                            :x-show "due || scheduledPayment"}
                           "Change due / scheduled payment date")]]
               (fc/with-field :invoice/due
                 (com/validated-field
                  (hx/alpine-appear {:label "Due (optional)"
                                     :errors (fc/field-errors)
                                     :x-show "customizeDueAndScheduled"})
                  [:div {:class "w-24"
                         :hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/due-date)
                         :x-dispatch:changed "[clientId, vendorId, date]"
                         :hx-trigger "changed"
                         :hx-target "this"
                         :hx-swap "innerHTML"}
                   (com/date-input {:value (some-> (fc/field-value)
                                                   (atime/unparse-local atime/normal-date))
                                    :name (fc/field-name)
                                    :x-model "due"

                                    :error? (fc/field-errors)
                                    :placeholder "1/1/2024"})]))
               (fc/with-field :invoice/scheduled-payment
                 (com/validated-field
                  (hx/alpine-appear {:label "Scheduled payment (optional)"
                                     :errors (fc/field-errors)
                                     :x-show "customizeDueAndScheduled"})
                  [:div {:class "w-24"}
                   [:div {:class "w-24"

                          :hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/scheduled-payment-date)
                          :x-dispatch:changed "[clientId, vendorId, due]"
                          :hx-trigger "changed"
                          :hx-target "this"
                          :hx-swap "innerHTML"}
                    (com/date-input {:value (some-> (fc/field-value)
                                                    (atime/unparse-local atime/normal-date))
                                     :name (fc/field-name)
                                     :error? (fc/field-errors)
                                     :placeholder "1/1/2024"})]]))

               (fc/with-field :invoice/invoice-number
                 (com/validated-field
                  {:label "Invoice Number"
                   :errors (fc/field-errors)}
                  [:div {:class "w-24"}
                   (com/text-input {:value (-> (fc/field-value))
                                    #_#_:disabled (boolean (-> request :multi-form-state :snapshot :db/id))
                                    :name (fc/field-name)
                                    :error? (fc/field-errors)
                                    :placeholder "HA-123"})]))
               (fc/with-field :invoice/total
                 (com/validated-field
                  {:label "Total"
                   :errors (fc/field-errors)}
                  [:div {:class "w-16"}
                   (com/money-input {:value (-> (fc/field-value))
                                     :name (fc/field-name)
                                     :class "w-24"
                                     :error? (fc/field-errors)
                                     :placeholder "212.44"})]))

               [:div#expense-account-prediction
                (hx/alpine-appear
                 {:x-dispatch:bryce "[vendorId]"
                  :hx-trigger "bryce"
                  :hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/account-prediction)
                  :hx-target "this"
                  :hx-swap "innerHTML"})
                (account-prediction* request)]])

       :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"
                                                         :hx-put (bidi.bidi/path-for ssr-routes/only-routes
                                                                                     ::route/new-wizard-navigate)} "Save"))
       :validation-route ::route/new-wizard-navigate)))

  mm/CustomNext
  (custom-next-handler
    [_ request]
    (if (= (get-in request [:multi-form-state :step-params :customize-accounts])
           :customize)
      (mm/navigate-handler {:request request
                            :to-step :accounts})

      (html-response [:div]
                     :headers {"location" (bidi.bidi/path-for ssr-routes/only-routes ::route/new-invoice-submit)}
                     :status 308)
      #_(mm/navigate-handler {:request request
                              :to-step :next-steps}))))


(defn location-select*
  [{:keys [name account-location client-locations value]}]
  (let [options (into (cond account-location
                            [[account-location account-location]]

                            (seq client-locations)
                            (into [["Shared" "Shared"]]
                                  (for [cl client-locations]
                                    [cl cl]))
                            :else
                            [["Shared" "Shared"]]))]
    (com/select {:options options
                 :name  name
                 :value (ffirst options)
                 :class "w-full"})))

(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)
                                        {:client-id client-id
                                         :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)))})])

(defn- invoice-expense-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 (:invoice-expense-account/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 :invoice-expense-account/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 :invoice-expense-account/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)
                           :client-id client-id})
        :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->> (:invoice-expense-account/account @value)
                                                                 (nat-int? (:invoice-expense-account/account @value)) (dc/pull (dc/db conn)
                                                                                                                               '[:account/location])))
                          :client-locations (pull-attr (dc/db conn) :client/locations client-id)
                          :value            (fc/field-value)}))))
   (fc/with-field :invoice-expense-account/amount
     (com/data-grid-cell
      {}
      (com/validated-field
       {:errors (fc/field-errors)}
       (com/money-input {:name  (fc/field-name)
                         :class "w-16 amount-field"
                         :value (fc/field-value)}))))
   (com/data-grid-cell {:class "align-top"}
                       (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))

(defn invoice-expense-account-total* [request]
  (let [total (->> (-> request
                       :multi-form-state
                       :step-params
                       :invoice/expense-accounts)
                   (map (fnil :invoice-expense-account/amount 0.0))
                   (filter number?)
                   (reduce + 0.0))]
    (format "$%,.2f" total)))

(defn invoice-expense-account-balance* [request]
  (let [total (->> (-> request
                       :multi-form-state
                       :step-params
                       :invoice/expense-accounts)
                   (map (fnil :invoice-expense-account/amount 0.0))
                   (filter number?)
                   (reduce + 0.0))
        balance (-
                      (-> request :multi-form-state :snapshot :invoice/total)
                      total)]
    [:span {:class (when-not (dollars= 0.0 balance)
                     "text-red-300")}
     (format "$%,.2f" balance)]))

(defn invoice-expense-account-total [request]
  (html-response (invoice-expense-account-total* request)))

(defn invoice-expense-account-balance [request]
  (html-response (invoice-expense-account-balance* request)))

(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) #{:invoice/expense-accounts}))

  (render-step [this {{:keys [snapshot] :as multi-form-state} :multi-form-state :as request}]
    (mm/default-render-step
     linear-wizard this
     :head [:div.p-2 "Invoice accounts "]
     :body (mm/default-step-body
            {}
            [:div {}
             (pull-attr (dc/db conn)  :client/name (:invoice/client snapshot))
             (fc/with-field :invoice/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 #(invoice-expense-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/new-wizard-new-account)
                                                       :row-offset 0
                                                       :index     (count (fc/field-value))
                                                       :tr-params {:hx-vals (hx/json {:client-id (:invoice/client snapshot)})}}
                                                      "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/expense-account-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/expense-account-balance)
                                                                       :hx-target "this"
                                                                       :hx-swap "innerHTML"}
                                                                      (invoice-expense-account-balance* 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 "INVOICE TOTAL"])
                                                  (com/data-grid-cell {:class "text-right"}
                                                                      (format "$%,.2f" (:invoice/total snapshot)))
                                                  (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))
  mm/Initializable
  (init-step-params
    [_ current request]
    (if (not (seq (:invoice/expense-accounts (:step-params current))))
      (assoc (:step-params current)  :invoice/expense-accounts [{:db/id "123"
                                                                 :invoice-expense-account/location "Shared"
                                                                 :invoice-expense-account/account (:db/id (:vendor/default-account (clientize-vendor (get-vendor (->db-id (:invoice/vendor (:snapshot current))))
                                                                                                                                                     (->db-id (:invoice/client (:snapshot current))))))
                                                                 :invoice-expense-account/amount (:invoice/total (:step-params current))}])
      (:step-params current))))

(defrecord NextSteps [linear-wizard]
  mm/ModalWizardStep
  (step-name [_]
    "Next Steps")
  (step-key [_]
    :next-steps)

  (edit-path [_ _]
    [])

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

  (render-step [this {{:keys [snapshot] :as multi-form-state} :multi-form-state :as request}]

    (mm/default-render-step
     linear-wizard this
     :head [:div.p-2 "Invoice accounts "]
     :body (mm/default-step-body
            {}
            [:p.text-lg "Would you like to pay this invoice now?"]

            (com/navigation-button-list {}
                                        (com/navigation-button (-> {:class "w-48"
                                                                    :hx-get (hu/url (bidi.bidi/path-for ssr-routes/only-routes ::route/pay-wizard)
                                                                                    {:selected (:db/id snapshot)
                                                                                     :replace-modal true})}
                                                                   hx/trigger-click-or-enter) "Pay now")
                                        (com/navigation-button (-> {:class "w-48"
                                                                    :hx-get (hu/url (bidi.bidi/path-for ssr-routes/only-routes ::route/new-wizard)
                                                                                    {:replace-modal true})}
                                                                   hx/trigger-click-or-enter) "Add another")
                                        (com/navigation-button {:class "w-48" :next-arrow? false
                                                                "@click" "$dispatch('modalclose') "
                                                                "@keyup.enter.stop" "$dispatch('modalclose')"}
                                                               "Close")))
     :footer
     nil
     :validation-route ::route/new-wizard-navigate)))


(defn assert-no-conflicting [{:invoice/keys [invoice-number client vendor] :db/keys [id]}]
  (when (seq (d-invoices/find-conflicting {:invoice/invoice-number invoice-number
                                           :invoice/vendor (->db-id vendor)
                                           :invoice/client (->db-id client)
                                           :db/id id}))
    (form-validation-error (str "Invoice '" invoice-number "' already exists."))))


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


(defn- calculate-spread
  "Helper function to calculate the amount to be assigned to each location"
  [shared-amount total-locations]
  (let [base-amount (int (/ shared-amount total-locations))
        remainder (- shared-amount (* base-amount total-locations))]
    {:base-amount base-amount
     :remainder  remainder}))


(defn- spread-expense-account
  "Spreads the expense account amount across the given locations"
  [locations expense-account]
  (if (= "Shared" (:invoice-expense-account/location expense-account))
    (let [{:keys [base-amount remainder]} (calculate-spread (:invoice-expense-account/amount expense-account) (count locations))]
      (map-indexed (fn [idx _]
                     (assoc expense-account
                            :invoice-expense-account/amount (+ base-amount (if (< idx remainder) 1 0))
                            :invoice-expense-account/location (nth locations idx)))
                   locations))
    [expense-account]))

(defn $->cents [x]
  (int
   (let [result (* 100M (bigdec x))]
     (.setScale result 0 java.math.BigDecimal/ROUND_HALF_UP))))

(defn cents->$ [x]
  (double
   (let [result (* 0.01M (bigdec x))]
     (.setScale result 2 java.math.BigDecimal/ROUND_HALF_UP))))

(defn- apply-total-delta-to-account [invoice-total eas]
  (when (seq eas)
    (let [leftover (- invoice-total (reduce + 0 (map :invoice-expense-account/amount eas)))
          leftover-beyond-a-single-cent? (or (< leftover -1)
                                             (> leftover 1))
          leftover (if leftover-beyond-a-single-cent?
                     0
                     leftover)
          [first-eas & rest] eas]
      (cons
       (update first-eas :invoice-expense-account/amount #(+ % leftover))
       rest))))


(defn maybe-spread-locations
  "Converts any expense account for a \"Shared\" location into a separate expense account for all valid locations for that client"
  ([invoice]
   (maybe-spread-locations invoice (pull-attr (dc/db conn) :client/locations (:invoice/client invoice))))
  ([invoice locations]
   (update-in invoice
              [:invoice/expense-accounts]
              (fn [expense-accounts]
                (->> expense-accounts
                     (map (fn [ea] (update ea :invoice-expense-account/amount $->cents)))
                     (mapcat (partial spread-expense-account locations))
                     (apply-total-delta-to-account ($->cents (:invoice/total invoice)))
                     (map (fn [ea] (update ea :invoice-expense-account/amount cents->$))))))))



(defrecord NewWizard2 [_ 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 :basic-details)))
  (render-wizard [this {:keys [multi-form-state] :as request}]
    (mm/default-render-wizard
     this request
     :form-params
     (-> mm/default-form-props
         (assoc :hx-post
                (str (bidi/path-for ssr-routes/only-routes ::route/new-invoice-submit))))
     :render-timeline? false))
  (steps [_]
    [:basic-details
     :accounts
     :next-steps])
  (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 {:basic-details (->BasicDetailsStep this)
            :accounts (->AccountsStep this)
            :next-steps (->NextSteps this)}
           step-key)))
  (form-schema [_]
    new-form-schema)
  (submit [this {:keys [multi-form-state request-method identity] :as request}]
    (let [invoice (:snapshot multi-form-state)

          _ (alog/peek invoice)
          extant? (:db/id invoice)
          client-id (->db-id (:invoice/client invoice))
          vendor-id (->db-id (:invoice/vendor invoice))
          paid-amount (if-let [outstanding-balance
                               (and extant?
                                    (-
                                     (pull-attr (dc/db conn)
                                                :invoice/total
                                                (:db/id invoice))
                                     (pull-attr (dc/db conn)
                                                :invoice/outstanding-balance
                                                (:db/id invoice))))]
                        outstanding-balance
                        0.0)
          outstanding-balance (- (or
                                  (:invoice/total (:step-params multi-form-state))
                                  (:invoice/total (:snapshot multi-form-state)))
                                 paid-amount)

          transaction [:upsert-invoice (-> multi-form-state
                                           :snapshot
                                           (assoc :db/id (or (:db/id invoice) "invoice"))
                                           (dissoc :customize-due-and-scheduled? :invoice/journal-entry :invoice/payments :customize-accounts)
                                           (assoc :invoice/expense-accounts (if (= :customize (:customize-accounts invoice))
                                                                              (-> multi-form-state :step-params :invoice/expense-accounts)
                                                                              [{:db/id "123"
                                                                                :invoice-expense-account/location "Shared"
                                                                                :invoice-expense-account/account (:db/id (:vendor/default-account (clientize-vendor (get-vendor vendor-id)
                                                                                                                                                                    client-id)))
                                                                                :invoice-expense-account/amount (or (:invoice/total (:step-params multi-form-state))
                                                                                                                    (:invoice/total (:snapshot multi-form-state)))}]))
                                           (assoc
                                            :invoice/outstanding-balance outstanding-balance
                                            :invoice/import-status       :import-status/imported
                                            :invoice/status              (if (dollars= 0.0 outstanding-balance)
                                                                           :invoice-status/paid
                                                                           :invoice-status/unpaid))
                                           (maybe-spread-locations)
                                           (update :invoice/date coerce/to-date)
                                           (update :invoice/due coerce/to-date)
                                           (update :invoice/scheduled-payment coerce/to-date))]]
      (assert-invoice-amounts-add-up (second transaction))
      (assert-no-conflicting invoice)
      (exception->4xx #(assert-can-see-client (:identity request) client-id))

      (exception->4xx #(assert-not-locked  client-id (:invoice/date invoice)))
      (let [transaction-result (audit-transact [transaction] (:identity request))]
        (try 
          (solr/touch-with-ledger (get-in transaction-result [:tempids "invoice"]))
          (catch Exception e
            (alog/error ::cant-save-solr 
                        :error e
                        ))
          )
        (if extant?

          (html-response
           (@(resolve 'auto-ap.ssr.invoices/row*) identity (dc/pull (dc/db conn) default-read (:db/id invoice)) {:flash? true
                                                                                                                 :request request})
           :headers (cond-> {"hx-trigger"  "modalclose"
                             "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id invoice))
                             "hx-reswap" "outerHTML"}))

          (assoc-in (mm/navigate-handler {:request (assoc-in request [:multi-form-state :snapshot :db/id] (get-in transaction-result [:tempids "invoice"]))
                                          :to-step :next-steps})
                    [:headers "hx-trigger"] "invalidated"))))))

(def new-wizard (->NewWizard2 nil nil))


(defn initial-new-wizard-state [request]
  (mm/->MultiStepFormState {:invoice/date (time/now)
                            :customize-accounts :default}
                           []
                           {:invoice/date (time/now)
                            :customize-accounts :default}))




(defn initial-edit-wizard-state [request]
  (let [entity (dc/pull (dc/db conn) default-read (:db/id (:route-params request)))
        entity (select-keys entity (mut/keys new-form-schema))]

    (mm/->MultiStepFormState (assoc entity
                                    :customize-accounts :customize)
                             []
                             (assoc entity
                                    :customize-accounts :customize))))

(defn location-select [{{:keys [name account-id client-id value] :as qp} :query-params}]
  (html-response (location-select* {:name             name
                                    :value            value
                                    :account-location (some->> account-id
                                                               (pull-attr (dc/db conn) :account/location))
                                    :client-locations (some->> client-id
                                                               (pull-attr (dc/db conn) :client/locations))})))


(defn due-date [{:keys [multi-form-state]}]
  (let [vendor (clientize-vendor (get-vendor (:invoice/vendor (:step-params multi-form-state)))
                                 (->db-id (:invoice/client (:step-params multi-form-state))))
        good-date
        (or (when (and (:invoice/date (:step-params multi-form-state))
                       (-> vendor :vendor/terms))
              (time/plus (:invoice/date (:step-params multi-form-state))
                         (time/days (-> vendor :vendor/terms))))
            (:invoice/due (:step-params multi-form-state)))]

    (html-response
     (com/date-input {:value (some-> good-date
                                     (atime/unparse-local atime/normal-date))
                      :name "step-params[invoice/due]"
                      :x-init (format "due='%s'" (some-> good-date
                                                         (atime/unparse-local atime/normal-date)))
                      :x-model "due"
                      :error? false
                      :placeholder "1/1/2024"}))))

(defn scheduled-payment-date [{:keys [multi-form-state]}]
  (let [vendor (clientize-vendor (get-vendor (:invoice/vendor (:step-params multi-form-state)))
                                 (->db-id (:invoice/client (:step-params multi-form-state))))
        good-date
        (when (and (:invoice/due (:step-params multi-form-state))
                   (:vendor/automatically-paid-when-due vendor))

          (:invoice/due (:step-params multi-form-state)))]

    (html-response
     (com/date-input {:value (some-> good-date
                                     (atime/unparse-local atime/normal-date))
                      :name "step-params[invoice/scheduled-payment]"
                      :error? false
                      :placeholder "1/1/2024"}))))


(defn account-prediction [{:keys [multi-form-state form-errors] :as request}]
  (html-response
   (account-prediction* request)))

(def key->handler
  (apply-middleware-to-all-handlers
   {::route/new-wizard (-> mm/open-wizard-handler
                           (mm/wrap-wizard new-wizard)
                           (mm/wrap-init-multi-form-state initial-new-wizard-state))
    ::route/edit-wizard (-> mm/open-wizard-handler
                            (mm/wrap-wizard new-wizard)
                            (mm/wrap-init-multi-form-state initial-edit-wizard-state)
                            (wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
    ::route/due-date (-> due-date
                         (mm/wrap-wizard new-wizard)
                         (mm/wrap-decode-multi-form-state)
                         (wrap-nested-form-params))
    ::route/account-prediction (-> account-prediction
                                   (mm/wrap-wizard new-wizard)
                                   (mm/wrap-decode-multi-form-state)
                                   (wrap-nested-form-params))
    ::route/scheduled-payment-date (-> scheduled-payment-date
                                       (mm/wrap-wizard new-wizard)
                                       (mm/wrap-decode-multi-form-state)
                                       (wrap-nested-form-params))
    ::route/expense-account-total (-> invoice-expense-account-total
                                      (mm/wrap-wizard new-wizard)
                                      (mm/wrap-decode-multi-form-state))
    ::route/expense-account-balance (-> invoice-expense-account-balance
                                        (mm/wrap-wizard new-wizard)
                                        (mm/wrap-decode-multi-form-state))

    ::route/location-select   (-> location-select
                                  (wrap-schema-enforce :query-schema [:map
                                                                      [:name :string]
                                                                      [:client-id {:optional true}
                                                                       [:maybe entity-id]]
                                                                      [:account-id {:optional true}
                                                                       [:maybe entity-id]]]))

    ::route/new-invoice-submit              (-> mm/submit-handler
                                                (mm/wrap-wizard new-wizard)
                                                (mm/wrap-decode-multi-form-state))
    ::route/new-wizard-navigate (-> mm/next-handler
                                    (mm/wrap-wizard new-wizard)
                                    (mm/wrap-decode-multi-form-state))
    ::route/new-wizard-new-account (->
                                    (add-new-entity-handler [:step-params :invoice/expense-accounts]
                                                            (fn render [cursor request]
                                                              (invoice-expense-account-row*
                                                               {:value cursor
                                                                :client-id (:client-id (:query-params request))}))
                                                            (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]]]))}
   (fn [h]
     (-> h
         (wrap-client-redirect-unauthenticated)))))