(ns auto-ap.graphql.ledger
  (:require
   [auto-ap.datomic
    :refer [audit-transact-batch conn pull-many remove-nils]]
   [auto-ap.datomic.accounts :as a]
   [auto-ap.datomic.ledger :as l]
   [auto-ap.graphql.utils
    :refer [->graphql
            <-graphql
            assert-admin
            assert-can-see-client
            attach-tracing-resolvers
            extract-client-ids
            result->page]]
   [auto-ap.ledger :refer [build-account-lookup]]
   [auto-ap.ledger.reports :as l-reports]
   [auto-ap.logging :as alog]
   [auto-ap.parse.util :as parse]
   [auto-ap.pdf.ledger
    :refer [print-balance-sheet
            print-cash-flows
            print-journal-detail-report
            print-pnl]]
   [auto-ap.solr :as solr]
   [auto-ap.time :as atime]
   [auto-ap.utils :refer [by dollars=]]
   [clj-time.coerce :as coerce]
   [clj-time.core :as t]
   [clojure.data.csv :as csv]
   [com.brunobonacci.mulog :as mu]
   [datomic.api :as dc]
   [iol-ion.tx :refer [random-tempid]])
  (:import
   (org.apache.commons.codec.binary Base64)))

(defn get-ledger-page [context args _]
  (let [args (assoc args :id (:id context))
        _ (when (:client_id (:filters args))
            (assert-can-see-client (:id context) (:client_id (:filters args))))
        clients (or (and (:client_id (:filters args))
                        [{:db/id (:client_id (:filters args))}])
                   (:clients context))
        
        [journal-entries journal-entries-count] (l/get-graphql (assoc (<-graphql (:filters args))
                                                                      :clients clients))

        journal-entries (mapv
                         (fn [je]
                           (->  je
                                (update :journal-entry/original-entity :db/id)))
                         journal-entries)]
    (result->page journal-entries journal-entries-count :journal_entries (:filters args))))

(defn get-ledger-csv [context args _]
  (let [args (assoc args :id (:id context))
        [journal-entries journal-entries-count] (l/get-graphql (assoc (<-graphql (:filters args))
                                                                      :per-page Integer/MAX_VALUE
                                                                      :clients (:clients context)))


        ]
    {:csv_content_b64 (Base64/encodeBase64String
                       (.getBytes 
                        (with-open [w (java.io.StringWriter.)]
                          (csv/write-csv w
                                         (into [["Client" "Vendor" "Date" "Journal Entry" "Journal Entry Line" "Account Code" "Account Name" "Account Type" "Debit" "Credit" "Net"]]
                                               (->> journal-entries
                                                    (mapcat (fn [j]
                                                              (map
                                                               (fn [li]
                                                                 (let [account-type (or (:db/ident (:account/type (:journal-entry-line/account li)))
                                                                                        ({:bank-account-type/check :account-type/asset
                                                                                          :bank-account-type/cash :account-type/asset
                                                                                          :bank-account-type/credit :account-type/liability}
                                                                                         (:db/ident (:bank-account/type (:journal-entry-line/account li)))))]
                                                                   [(-> j :journal-entry/client :client/code)
                                                                    (-> j :journal-entry/vendor :vendor/name)
                                                                    (atime/unparse (coerce/to-date-time (-> j :journal-entry/date))
                                                                                   atime/normal-date)
                                                                    (-> j :db/id)
                                                                    (-> li :db/id)
                                                                    (or (-> li :journal-entry-line/account :account/numeric-code)
                                                                        (-> li :journal-entry-line/account :bank-account/numeric-code))
                                                                    (or (-> li :journal-entry-line/account :account/name)
                                                                        (-> li :journal-entry-line/account :bank-account/name))
                                                                    (some-> account-type name )
                                                                    (-> li :journal-entry-line/debit)
                                                                    (-> li :journal-entry-line/credit)
                                                                    (if (#{:account-type/asset
                                                                           :account-type/dividend
                                                                           :account-type/expense} account-type)
                                                                      (- (or (-> li :journal-entry-line/debit) 0.0) (or (-> li :journal-entry-line/credit) 0.0))
                                                                      (- (or (-> li :journal-entry-line/credit) 0.0) (or (-> li :journal-entry-line/debit) 0.0)))
                                                                    
                                                                    ]))
                                                               (:journal-entry/line-items j))
                                                              ))))
                                         :quote? (constantly true))
                          (.toString w))))}))


(defn roll-up-until
  ([lookup-account all-ledger-entries end-date]
   (roll-up-until lookup-account all-ledger-entries end-date nil))
  ([lookup-account all-ledger-entries end-date start-date]
   (->> all-ledger-entries
        (filter (fn [[d]]
                  (if start-date
                    (and
                      (>= (compare d start-date) 0)
                      (<= (compare d end-date) 0))
                    (<= (compare d end-date) 0))))
        (reduce
          (fn [acc [_ _ account location debit credit]]
            (-> acc
                (update-in [[location account] :debit] (fnil + 0.0) debit)
                (update-in [[location account] :credit] (fnil + 0.0) credit)
                (update-in [[location account] :count] (fnil + 0) 1))
            )
          {})
        (reduce-kv
          (fn [acc [location account-id] {:keys [debit credit count]}]
            (let [account      (lookup-account account-id)
                  account-type (:account_type account)]
              
              (conj acc (merge {:id       (str account-id "-" location)
                                :location (or location "")
                                :count    count
                                :debits   debit
                                :credits  credit
                                :amount   (if account-type (if (#{:account-type/asset
                                                                :account-type/dividend
                                                                :account-type/expense} account-type)
                                                           (- debit credit)
                                                           (- credit debit))
                                            0.0)}
                               account))))
          []))))

(defn full-ledger-for-client [client-id]
  (->> (dc/q 
         {:find  ['?d '?jel '?account '?location '?debit '?credit]
          :in    ['$ '?client-id]
          :where '[[?e :journal-entry/client ?client-id]
                   [?e :journal-entry/date ?d]
                   [?e :journal-entry/line-items ?jel]
                   (or-join [?e]
                            (and [?e :journal-entry/original-entity ?i]
                                 (or-join [?e ?i]
                                          (and 
                                            [?i :transaction/bank-account ?b]
                                            (or [?b :bank-account/include-in-reports true]
                                                (not [?b :bank-account/include-in-reports])))
                                          (not [?i :transaction/bank-account])))
                            (not [?e :journal-entry/original-entity ]))
                   [(get-else $ ?jel :journal-entry-line/account :account/unknown) ?account]
                   [(get-else $ ?jel :journal-entry-line/debit 0.0) ?debit ]
                   [(get-else $ ?jel :journal-entry-line/credit 0.0) ?credit]
                   [(get-else $ ?jel :journal-entry-line/location "") ?location]]}
         (dc/db conn) client-id)
       (sort-by first)))

(defn get-balance-sheet [context args _]
  (let [client-id (:client_id args)
        client-ids (->> (or (some-> client-id vector)
                            (filter identity (:client_ids args)))
                        (filter identity))
        _ (when (not (seq client-ids))
            (throw (ex-info "Please select a client." {:validation-error "Please select a client."})))
        _ (doseq [client-id client-ids]
            (assert-can-see-client (:id context) client-id))
        end-date (coerce/to-date (:date args))
        comparable-date (coerce/to-date (:comparison_date args))
        all-ledger-entries (->> client-ids
                                (map (fn [client-id]
                                       [client-id (full-ledger-for-client client-id)]))
                                (into {}))
        lookup-account (->> client-ids
                            (map (fn [client-id]
                                   [client-id (build-account-lookup client-id)]))
                            (into {}))]
    (alog/info ::balance-sheet :params args)
    
    (cond-> {:balance-sheet-accounts (mapcat
                                       #(roll-up-until (lookup-account %) (all-ledger-entries %) end-date )
                                       client-ids)
             }
      (:include_comparison args) (assoc :comparable-balance-sheet-accounts (mapcat
                                                                             #(roll-up-until (lookup-account %) (all-ledger-entries %) comparable-date )
                                                                             client-ids))
      true ->graphql)))

(defn get-profit-and-loss-raw [client-ids periods]
  (let [ all-ledger-entries (->> client-ids
                                 (map (fn [client-id]
                                        [client-id (full-ledger-for-client client-id)]))
                                 (into {}))
        lookup-account (->> client-ids
                            (map (fn [client-id]
                                   [client-id (build-account-lookup client-id)]))
                            (into {}))]
    (->graphql {:periods 
                (->> periods
                     (mapv (fn [{:keys [start end]}]
                             {:accounts (mapcat
                                         #(roll-up-until (lookup-account %) (all-ledger-entries %) (coerce/to-date end) (coerce/to-date start) )
                                         client-ids)})))})))

(defn get-profit-and-loss [context args _]
  (let [client-id (:client_id args)
        client-ids (or (some-> client-id vector)
                       (filter identity (:client_ids args)))
        _ (when (not (seq client-ids))
            (throw (ex-info "Please select a client." {:validation-error "Please select a client."})))
        _ (doseq [client-id client-ids]
            (assert-can-see-client (:id context) client-id))
        _ (when (and (:include_deltas args)
                     (:column_per_location args))
            (throw (ex-info "Please select one of 'Include deltas' or 'Column per location'" {:validation-error "Please select one of 'Include deltas' or 'Column per location'"}))) ]
    (get-profit-and-loss-raw client-ids (:periods args))))




;; profit and loss based off of index
#_(defn get-profit-and-loss [context args _]
    (let [client-id                  (:client_id args)
          client-ids                 (or (some-> client-id vector)
                                         (filter identity (:client_ids args)))
          _                          (when (not (seq client-ids))
                                       (throw (ex-info "Please select a client." {:validation-error "Please select a client."})))
          _                          (doseq [client-id client-ids]
                                       (assert-can-see-client (:id context) client-id))
          _                          (when (and (:include_deltas args)
                                                (:column_per_location args))
                                       (throw (ex-info "Please select one of 'Include deltas' or 'Column per location'" {:validation-error "Please select one of 'Include deltas' or 'Column per location'"})))
          db                         (dc/db conn)
          all-used-account-locations (dc/q '[:find ?c ?a ?l
                                             :in $ [?c ...]
                                             :where
                                             (or-join [?c ?a ?l]
                                                      (and 
                                                        [?a :account/numeric-code]
                                                        (not [?a :account/location])
                                                        [?c :client/locations ?l])
                                                      (and
                                                        [?a :account/numeric-code]
                                                        [?a :account/location ?l]
                                                        [?c :client/locations ?l])
                                                      (and
                                                        [?c :client/bank-accounts ?a]
                                                        [(ground "A") ?l]))]
                                           (dc/db conn)
                                           client-ids)
          lookup-account             (->> client-ids
                                          (map (fn [client-id]
                                                 [client-id (build-account-lookup client-id)]))
                                          (into {}))]
      (->graphql
        {:periods 
         (->> (:periods args)
              (mapv (fn [{:keys [start end]}]
                      (let [start (coerce/to-date start)
                            end   (coerce/to-date end)]
                        {:accounts (mapcat
                                     (fn [[c a l]]
                                       (let [start-point (->> (dc/index-pull db
                                                                             {:index    :avet
                                                                              :selector [:db/id :journal-entry-line/running-balance :journal-entry-line/client+account+location+date]
                                                                              :start    [:journal-entry-line/client+account+location+date [c a l start]]
                                                                              :reverse  true
                                                                              :limit    1})
                                                              (take-while (fn [result]
                                                                            (= [c a l]
                                                                               (take 3 (:journal-entry-line/client+account+location+date result)))))
                                                              (drop-while (fn [{[_ _ _ date] :journal-entry-line/client+account+location+date}]
                                                                            (>= (compare date start) 0)))
                                                              first)
                                             end-point   (->> (dc/index-pull db
                                                                             {:index    :avet
                                                                              :selector [:db/id :journal-entry-line/running-balance :journal-entry-line/client+account+location+date]
                                                                              :start    [:journal-entry-line/client+account+location+date [c a l end]]
                                                                              :reverse  true
                                                                              :limit    1})
                                                              (take-while (fn [result]
                                                                            (= [c a l]
                                                                               (take 3 (:journal-entry-line/client+account+location+date result)))))
                                                              (take 1)
                                                              (drop-while (fn [{[_ _ _ date] :journal-entry-line/client+account+location+date}]
                                                                            (>= (compare date end) 0)))
                                                              first)]
                                         (when end-point
                                           [(merge {:id       (str a "-" l)
                                                    :location (or l "")
                                                    :count    0
                                                    :debits   0
                                                    :credits  0
                                                    :amount   (- (or (:journal-entry-line/running-balance end-point) 0.0)
                                                                 (or (:journal-entry-line/running-balance start-point) 0.0))
                                                    }
                                                   ((lookup-account c) a))])))
                                     all-used-account-locations)}))))})))

(defn profit-and-loss-pdf [context args value]
  (let [data   (get-profit-and-loss context args value)
        result (print-pnl (:id context) args data)]

    (->graphql result)))

(defn cash-flows-pdf [context args value]
  (let [data   (get-profit-and-loss context args value)
        result (print-cash-flows (:id context) args data)]
    (->graphql result)))

(defn balance-sheet-pdf [context args value]
  (let [valid-clients (extract-client-ids (:clients context)
                                          (:client_ids args))
        data   (get-balance-sheet context args value)
        result (print-balance-sheet (:id context) args data)]

    (->graphql result)))


(defn assoc-error [f]
  (fn [entry]
    (try 
      (f entry)
      (catch Exception e
        (assoc entry :error (.getMessage e)
               :status (or (:status (ex-data e))
                           :error))))))

(defn all-ids-not-locked [all-ids]
  (->> all-ids
       (dc/q '[:find ?t
              :in $ [?t ...]
              :where
              [?t :journal-entry/client ?c]
              [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
              [?t :journal-entry/date ?d]
              [(>= ?d ?lu)]]
            (dc/db conn))
       (map first)))

(defn delete-external-ledger [context args _]
  (let [_ (assert-admin (:id context))
        args (assoc args :clients (:clients context))
        ids (some-> (:filters args)
                    (assoc :clients (:clients context))
                    (assoc :only-external true)
                    (<-graphql)
                    (assoc :per-page Integer/MAX_VALUE)
                    (#(l/raw-graphql-ids (dc/db conn) %))
                    :ids)
        _ (alog/info ::trying-to-delete
            :count (count ids)
            :sample (take 3 ids))
        specific-ids (l/filter-ids (:ids args))
        all-ids (all-ids-not-locked (into (set ids) specific-ids))]
    (if (> (count all-ids) 1000)
      {:message (str "You can only delete 1000 ledger entries at a time.")}
      (do
        (alog/info ::deleting :args args)
        (audit-transact-batch
         (map (fn [i]
                [:db/retractEntity i])
                 all-ids)
         (:id context))
        {:message (str "Succesfully deleted " (count all-ids) " ledger entries.")}))))

(defn import-ledger [context args _]
  (assert-admin (:id context))
  (let [used-vendor-names (set (map :vendor_name (:entries args)))
        all-vendors              (mu/trace ::get-all-vendors
                                   []
                                   (->> (dc/q '[:find ?e
                                               :in $ [?name ...]
                                               :where [?e :vendor/name ?name]]
                                             (dc/db conn)
                                             used-vendor-names)
                                        (map first)
                                        (pull-many (dc/db conn) [:db/id :vendor/name])
                                        (by :vendor/name)))
        client-locked-lookup              (mu/trace ::get-all-clients []
                                                    (->> (dc/q '[:find ?code ?locked-until
                                                                 :in $
                                                                 :where [?c :client/code ?code]
                                                                 [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?locked-until]]
                                                               (dc/db conn))
                                                         (into {})))
        all-client-bank-accounts (mu/trace ::get-all-client-bank-accounts
                                   []
                                   (->> (dc/q '[:find ?code ?ba-code
                                                :in $
                                                :where [?c :client/code ?code]
                                                [?c :client/bank-accounts ?ba]
                                                [?ba :bank-account/code ?ba-code]]
                                              (dc/db conn))
                                        (reduce
                                          (fn [acc [code ba-code]]
                                            (update acc code (fnil conj #{}) ba-code))
                                          {})))
        
        all-client-locations     (mu/trace ::get-all-client-locations
                                           []
                                           (->> (dc/q '[:find ?code ?location
                                                        :in $
                                                        :where [?c :client/code ?code]
                                                        [?c :client/locations ?location]]
                                                      (dc/db conn))
                                                (reduce
                                                  (fn [acc [code ba-code]]
                                                    (update acc code (fnil conj #{"HQ" "A"}) ba-code))
                                                  {})))
        
        new-hidden-vendors       (reduce
                                   (fn [new-vendors {:keys [vendor_name]}]
                                     (if (or (all-vendors vendor_name)
                                             (new-vendors vendor_name))
                                       new-vendors
                                       (assoc new-vendors vendor_name
                                              {:vendor/name   vendor_name
                                               :vendor/hidden true
                                               :db/id         vendor_name})))
                                   {}
                                   (:entries args))
        _                        (mu/trace ::upsert-new-vendors
                                   []
                                   (audit-transact-batch (vec (vals new-hidden-vendors)) (:id context)))
        all-vendors              (->> (dc/q '[:find ?e
                                             :in $ [?name ...]
                                             :where [?e :vendor/name ?name]]
                                           (dc/db conn)
                                           used-vendor-names)
                                      (map first)
                                      (pull-many (dc/db conn) [:db/id :vendor/name])
                                      (by :vendor/name))
        all-accounts             (mu/trace ::get-all-accounts []
                                           (transduce (map (comp str :account/numeric-code)) conj #{} (a/get-accounts)))
        transaction              (mu/trace ::build-transaction
                                   [:count (count (:entries args))]
                                   (doall (map
                                                       (assoc-error (fn [entry]
                                                                      (let [vendor (all-vendors (:vendor_name entry))]
                                                                        (when-not (client-locked-lookup (:client_code entry))
                                                                          (throw (ex-info (str "Client '" (:client_code entry )"' not found.") {:status :error}) ))
                                                                        (when-not vendor
                                                                          (throw (ex-info (str "Vendor '" (:vendor_name entry) "' not found.") {:status :error})))
                                                                        (when-not (re-find #"\d{1,2}/\d{1,2}/\d{4}" (:date entry))
                                                                          (throw (ex-info (str "Date must be MM/dd/yyyy") {:status :error})))
                                                                        (when-let [locked-until (client-locked-lookup (:client_code entry))]
                                                                          (when (and (not (t/after? (coerce/to-date-time (coerce/to-date (parse/parse-value :clj-time "MM/dd/yyyy" (:date entry))))
                                                                                                    (coerce/to-date-time locked-until)))
                                                                                     (not (t/equal? (coerce/to-date-time (coerce/to-date (parse/parse-value :clj-time "MM/dd/yyyy" (:date entry))))
                                                                                                    (coerce/to-date-time locked-until))))
                                                                            (throw (ex-info (str "Client's data is locked until " locked-until) {:status :error}))))
                                                                        
                                                                        (when-not (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line_items entry)))
                                                                                            (reduce (fnil + 0.0 0.0) 0.0 (map :credit (:line_items entry))))
                                                                          (throw (ex-info (str "Debits '"
                                                                                               (reduce (fnil + 0.0 0.0) 0 (map :debit (:line_items entry)))
                                                                                               "' and credits '"
                                                                                               (reduce (fnil + 0.0 0.0) 0 (map :credit (:line_items entry)))
                                                                                               "' do not add up.")
                                                                                          {:status :error})))
                                                                        (when (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line_items entry)))
                                                                                        0.0)
                                                                          (throw (ex-info (str "Cannot have ledger entries that total $0.00")
                                                                                          {:status :ignored})))
                                                                        (assoc entry
                                                                               :status :success
                                                                               :tx
                                                                               [:upsert-ledger 
                                                                                (remove-nils 
                                                                                  {:journal-entry/source          (:source entry)
                                                                                   :journal-entry/client          [:client/code (:client_code entry)]
                                                                                   :journal-entry/date            (coerce/to-date (parse/parse-value :clj-time "MM/dd/yyyy" (:date entry)))
                                                                                   :journal-entry/external-id     (:external_id entry)
                                                                                   :journal-entry/vendor          (:db/id (all-vendors (:vendor_name entry)))
                                                                                   :journal-entry/amount          (:amount entry)
                                                                                   :journal-entry/note            (:note entry)
                                                                                   :journal-entry/cleared-against (:cleared_against entry)

                                                                                   :journal-entry/line-items
                                                                                   (mapv (fn [ea]
                                                                                           (let [debit  (or (:debit ea) 0.0)
                                                                                                 credit (or (:credit ea) 0.0)]
                                                                                             (when (and (not (get
                                                                                                               (get all-client-locations (:client_code entry))
                                                                                                               (:location ea)))
                                                                                                        (not= "A" (:location ea)))
                                                                                               (throw (ex-info (str "Location '" (:location ea) "' not found.")
                                                                                                               {:status :error})))
                                                                                             (when (and (<= debit 0.0)
                                                                                                        (<= credit 0.0))
                                                                                               (throw (ex-info (str "Line item amount " (or debit credit) " must be greater than 0.")
                                                                                                               {:status :error})))
                                                                                             (when (and (not (all-accounts (:account_identifier ea)))
                                                                                                        (not (get
                                                                                                               (get all-client-bank-accounts (:client_code entry))
                                                                                                               (:account_identifier ea))))
                                                                                               (throw (ex-info (str "Account '" (:account_identifier ea) "' not found.")
                                                                                                               {:status :error})))
                                                                                             (let [matching-account (when (re-matches #"^[0-9]+$" (:account_identifier ea))
                                                                                                                      (a/get-account-by-numeric-code-and-sets (Integer/parseInt (:account_identifier ea)) ["default"]))]
                                                                                               (when (and matching-account
                                                                                                          (:account/location matching-account)
                                                                                                          (not= (:account/location matching-account)
                                                                                                                (:location ea)))
                                                                                                 (throw (ex-info (str "Account '"
                                                                                                                      (:account/numeric-code matching-account)
                                                                                                                      "' requires location '"
                                                                                                                      (:account/location matching-account)
                                                                                                                      "' but got '"
                                                                                                                      (:location ea)
                                                                                                                      "'")
                                                                                                                 {:status :error})))
                                                                                               (when (and matching-account
                                                                                                          (not (:account/location matching-account))
                                                                                                          (= "A" (:location ea)))
                                                                                                 (throw (ex-info (str "Account '"
                                                                                                                      (:account/numeric-code matching-account)
                                                                                                                      "' cannot use location '"
                                                                                                                      (:location ea)
                                                                                                                      "'")
                                                                                                                 {:status :error})))
                                                                                               (remove-nils (cond-> {:db/id (random-tempid)
                                                                                                                     :journal-entry-line/location (:location ea)
                                                                                                                     :journal-entry-line/debit (when (> debit 0)
                                                                                                                                                 debit)
                                                                                                                     :journal-entry-line/credit (when (> credit 0)
                                                                                                                                                  credit)}
                                                                                                              matching-account       (assoc :journal-entry-line/account (:db/id matching-account))
                                                                                                              (not matching-account) (assoc :journal-entry-line/account [:bank-account/code (:account_identifier ea)]))))))
                                                                                         (:line_items entry))
                                                                                   
                                                                                   :journal-entry/cleared true})]))))
                                                       (:entries args))))
        errors            (filter #(= (:status %) :error) transaction)
        ignored           (filter #(= (:status %) :ignored) transaction)
        success           (filter #(= (:status %) :success) transaction)
        retraction        (mapv (fn [x] [:db/retractEntity [:journal-entry/external-id (:external_id x)]])
                                success)
        ignore-retraction (->> ignored
                               (map :external_id )
                               (dc/q '[:find ?je
                                      :in $ [?ei ...]
                                      :where [?je :journal-entry/external-id ?ei]]
                                    (dc/db conn)
                                    )
                               (map first)
                               (map (fn [je] [:db/retractEntity je])))]
    (alog/info ::manual-import
               :errors (count errors)
               :sample (take 3 errors))
    

    (mu/trace ::retraction-tx
      [:count (count retraction)]
      (audit-transact-batch retraction (:id context)))
    (mu/trace ::ignore-retraction-tx
      [:count (count ignore-retraction)]
      (when (seq ignore-retraction)
        (audit-transact-batch ignore-retraction (:id context))))
    (let [invalidated 
          (mu/trace ::success-tx
                    [:count (count success)]
                    (for [[_ n] (:tempids (audit-transact-batch (map :tx success) (:id context)))]
                      n))]
      (future                           ;
        (mu/log ::indexing-solr :count (count invalidated))
        (mu/trace ::indexed-external-solr
                  [:count (count invalidated)]
                  (doseq [n invalidated]
                    (solr/touch n)))))
    
    {:successful (map (fn [x] {:external_id (:external_id x)}) success)
     :ignored    (map (fn [x]
                        {:external_id (:external_id x)})
                      ignored)
     :existing   []
     :errors     (map (fn [x] {:external_id (:external_id x)
                               :error       (:error x)}) errors)}))


(defn get-journal-detail-report [context input _]
  (let [category-totals (atom {})
        base-categories (into []
                              (for [client-id                 (:client_ids input)
                                    :let                      [_ (assert-can-see-client (:id context) client-id)
                                                               account-lookup (build-account-lookup client-id)
                                                               c (dc/pull (dc/db conn) '[:client/locations] client-id)]
                                    location                  (:client/locations c)
                                    category                  (:categories input)
                                    :let                      [category (<-graphql category)
                                                               all-journal-entries (->> (get-ledger-page (assoc context
                                                                                                                :clients [{:db/id client-id}])
                                                                                                         {:filters {:location          location
                                                                                                                    :date_range        (:date_range input)
                                                                                                                    :from_numeric_code (l-reports/min-numeric-code category )
                                                                                                                    :to_numeric_code   (l-reports/max-numeric-code category )
                                                                                                                    :per_page          Integer/MAX_VALUE}}
                                                                                                         nil)
                                                                                        :journal_entries
                                                                                        (mapcat (fn [je]
                                                                                                  (->> (je :line_items)
                                                                                                       (filter (fn  [jel]
                                                                                                                 (when-let [account (account-lookup (:id (:account jel)))] 
                                                                                                                   (and 
                                                                                                                     (l-reports/account-belongs-in-category? (:numeric_code account) category)  
                                                                                                                     (= location (:location jel)))))
                                                                                                               )
                                                                                                       (map (fn [jel ]
                                                                                                              {:date        (:date je)
                                                                                                               :debit       (:debit jel)
                                                                                                               :credit      (:credit jel)
                                                                                                               :description (or (:name (:vendor je))
                                                                                                                                (:alternate_description je))
                                                                                                               :account     (:account jel)
                                                                                                               :location    (:location jel)})))))
                                                                                        (into []))
                                                               _ (swap! category-totals assoc-in [client-id location  category]
                                                                        (- (or (reduce + 0.0 (map #(or (:credit %) 0.0) all-journal-entries)) 0.0)
                                                                           (or (reduce + 0.0 (map #(or (:debit %) 0.0) all-journal-entries)) 0.0)) )
                                                               journal-entries-by-account (group-by #(account-lookup (get-in % [:account :id])) all-journal-entries)]
                                    [account journal-entries] (conj (vec journal-entries-by-account) [nil all-journal-entries])
                                    :let                      [journal-entries (first (reduce
                                                                                        (fn [[acc last-je] je]
                                                                                          (let [next-je (assoc je :running_balance
                                                                                                               (- (+ (or (:running_balance last-je 0.0) 0.0)
                                                                                                                     (or (:credit je 0.0) 0.0))
                                                                                                                  (or (:debit je 0.0) 0.0)))]
                                                                                            [(conj acc next-je) next-je]))
                                                                                        []
                                                                                        (sort-by :date journal-entries)))]]
                                {:category        (->graphql category)
                                 :client_id       client-id
                                 :location        location
                                 :account         (or account
                                                      {:name (str "All " (name category))})
                                 :journal_entries (when account journal-entries)
                                 :total           (- (or (reduce + 0.0 (map #(or (:credit %) 0.0) journal-entries)) 0.0)
                                                     (or (reduce + 0.0 (map #(or (:debit %) 0.0) journal-entries)) 0.0))}))
        result          {:categories 
                         (into base-categories
                               (for [client-id (:client_ids input)
                                     :let      [_ (assert-can-see-client (:id context) client-id)
                                                account-lookup (build-account-lookup client-id)
                                                c (dc/pull (dc/db conn) '[:client/locations] client-id)]
                                     location  (:client/locations c)
                                     line [{:client_id       client-id
                                            :location        location
                                            :account         {:name "Gross Profit"}
                                            :journal_entries nil
                                            :total           (+ (get-in @category-totals [client-id location :sales] 0.0)
                                                                (get-in @category-totals [client-id location :cogs] 0.0)
                                                                (get-in @category-totals [client-id location :payroll] 0.0))}
                                           {:client_id       client-id
                                            :location        location
                                            :account         {:name "Overhead"}
                                            :journal_entries nil
                                            :total           (+ (get-in @category-totals [client-id location :controllable] 0.0)
                                                                (get-in @category-totals [client-id location :fixed-overhead] 0.0)
                                                                (get-in @category-totals [client-id location :ownership-controllable] 0.0))}
                                           {:client_id       client-id
                                            :location        location
                                            :account         {:name "Net Profit"}
                                            :journal_entries nil
                                            :total           (+ (get-in @category-totals [client-id location :sales] 0.0)
                                                                (get-in @category-totals [client-id location :cogs] 0.0)
                                                                (get-in @category-totals [client-id location :payroll] 0.0)
                                                                (get-in @category-totals [client-id location :controllable] 0.0)
                                                                (get-in @category-totals [client-id location :fixed-overhead] 0.0)
                                                                (get-in @category-totals [client-id location :ownership-controllable] 0.0))}]]
                                 line))}]
    result))



(defn journal-detail-report-pdf [context args value]
  (let [data   (get-journal-detail-report context args value)
        result (print-journal-detail-report (:id context) args data)]

    (->graphql result)))


(def objects
  {:balance_sheet_account
   {:fields {:id           {:type 'String}
             :amount       {:type 'String}
             :debits       {:type :money}
             :credits      {:type :money}
             :location     {:type 'String}
             :client_id    {:type :id}
             :count        {:type 'Int}
             :numeric_code {:type 'Int}
             :account_type {:type :account_type}
             :name         {:type 'String}}}

   :report_pdf
   {:fields {:url {:type 'String}
             :name {:type 'String}}}

   :balance_sheet
   {:fields {:balance_sheet_accounts            {:type '(list :balance_sheet_account)}
             :comparable_balance_sheet_accounts {:type '(list :balance_sheet_account)}}}

   :profit_and_loss_report_period
   {:fields {:accounts {:type '(list :balance_sheet_account)}}}

   :profit_and_loss_report
   {:fields {:periods {:type '(list :profit_and_loss_report_period)}}}

   :journal_entry_line
   {:fields {:id              {:type :id}
             :account         {:type :account}
             :location        {:type 'String}
             :debit           {:type 'String}
             :credit          {:type 'String}
             :running_balance {:type :money}}}

   :journal_entry
   {:fields {:id                    {:type :id}
             :source                {:type 'String}
             :external_id           {:type 'String}
             :original_entity       {:type :id}
             :amount                {:type 'String}
             :note                  {:type 'String}
             :cleared_against       {:type 'String}
             :client                {:type :client}
             :vendor                {:type :vendor}
             :alternate_description {:type 'String}
             :date                  {:type 'String}
             :line_items            {:type '(list :journal_entry_line)}}}

   :journal_detail_report_row
   {:fields {:client                {:type :client}
             :description {:type 'String}
             :date                  {:type 'String}
             :account         {:type :account}
             :location        {:type 'String}
             :debit           {:type 'String}
             :credit          {:type 'String}
             :running_balance {:type :money}}}

   :ledger_page
   {:fields {:journal_entries {:type '(list :journal_entry)}
             :count           {:type 'Int}
             :total           {:type 'Int}
             :start           {:type 'Int}
             :end             {:type 'Int}}}

   :import_ledger_entry_result
   {:fields {:external_id {:type 'String}
             :error       {:type 'String}
             :status      {:type 'String}}}

   :import_ledger_result
   {:fields {:successful {:type '(list :import_ledger_entry_result)}
             :existing   {:type '(list :import_ledger_entry_result)}
             :ignored    {:type '(list :import_ledger_entry_result)}
             :errors     {:type '(list :import_ledger_entry_result)}}}

   :journal_detail_report_category
   {:fields {:category {:type :ledger_category}
             :account         {:type :account}
             :total {:type :money}
             :client_id {:type :id}
             :location {:type 'String}
             :journal_entries {:type '(list :journal_detail_report_row)}}}
   :journal_detail_report
   {:fields {:categories {:type '(list :journal_detail_report_category)}}}})

(def queries
  {:balance_sheet {:type    :balance_sheet
                   :args    {:client_ids          {:type '(list :id)}
                             :include_comparison {:type 'Boolean}
                             :date               {:type :iso_date}
                             :comparison_date    {:type :iso_date}}
                   :resolve :get-balance-sheet}

   :profit_and_loss {:type    :profit_and_loss_report
                     :args    {:client_id  {:type :id}
                               :client_ids {:type '(list :id)}
                               :periods    {:type '(list :date_range)}
                               :include_deltas {:type 'Boolean}
                               :column_per_location {:type 'Boolean}}
                     :resolve :get-profit-and-loss}

   :journal_detail_report {:type    :journal_detail_report
                           :args    {:client_id           {:type :id}
                                     :client_ids          {:type '(list :id)}
                                     :date_range             {:type :date_range}
                                     :categories {:type '(list :ledger_category)}}
                           :resolve :get-journal-detail-report}

   :journal_detail_report_pdf {:type    :report_pdf
                               :args    {:client_id  {:type :id}
                                         :client_ids {:type '(list :id)}
                                         :date_range {:type :date_range}
                                         :categories {:type '(list :ledger_category)}}
                               :resolve :journal-detail-report-pdf}

   :profit_and_loss_pdf {:type    :report_pdf
                         :args    {:client_id  {:type :id}
                                   :client_ids {:type '(list :id)}
                                   :periods    {:type '(list :date_range)}
                                   :include_deltas {:type 'Boolean}
                                   :column_per_location {:type 'Boolean}}
                         :resolve :profit-and-loss-pdf}

   :cash_flows_pdf {:type    :report_pdf
                    :args    {:client_id           {:type :id}
                              :client_ids          {:type '(list :id)}
                              :periods             {:type '(list :date_range)}
                              :include_deltas      {:type 'Boolean}
                              :column_per_location {:type 'Boolean}}
                    :resolve :cash-flows-pdf}

   :balance_sheet_pdf {:type    :report_pdf
                       :args    {:client_ids          {:type '(list :id)}
                                 :include_comparison {:type 'Boolean}
                                 :date               {:type :iso_date}
                                 :comparison_date    {:type :iso_date}}
                       :resolve :balance-sheet-pdf}

   :ledger_page {:type    :ledger_page
                 :args    {:filters {:type :ledger_filters}}
                 :resolve :get-ledger-page}

   :ledger_csv {:type    :csv
                :args    {:filters {:type :ledger_filters}}
                :resolve :get-ledger-csv}})

(def mutations
  {:import_ledger
   {:type    :import_ledger_result
    :args    {:entries {:type '(list :import_ledger_entry)}}
    :resolve :mutation/import-ledger}

   :delete_external_ledger
   {:type    :message
    :args    {:filters {:type :ledger_filters}
              :ids     {:type '(list :id)}}
    :resolve :mutation/delete-external-ledger}})

(def input-objects
  {:numeric_code_range
   {:fields {:from {:type 'Int}
            :to {:type 'Int}}}
   :ledger_filters
   {:fields {:client_id         {:type :id}
             :vendor_id         {:type :id}
             :account_id        {:type :id}
             :exact_match_id    {:type :id}
             :amount_lte        {:type :money}
             :amount_gte        {:type :money}
             :bank_account_id   {:type :id}
             :date_range        {:type :date_range}
             :location          {:type 'String}
             :locations          {:type '(list String)}
             :numeric_code      {:type '(list :numeric_code_range)}
             :start             {:type 'Int}
             :per_page          {:type 'Int}
             :only_external     {:type 'Boolean}
             :external_id_like  {:type 'String}
             :source            {:type 'String}
             :sort              {:type '(list :sort_item)}}}

   :import_ledger_line_item
   {:fields {:account_identifier {:type 'String}
             :location           {:type 'String}
             :debit              {:type :money}
             :credit             {:type :money}}}
   :import_ledger_entry
   {:fields {:source          {:type 'String}
             :external_id     {:type 'String} 
             :client_code     {:type 'String}
             :date            {:type 'String}
             :vendor_name     {:type 'String}
             :amount          {:type :money}
             :note            {:type 'String}
             :cleared_against {:type 'String}
             :line_items      {:type '(list :import_ledger_line_item)}}}
   })

(def enums
  {:ledger_category {:values [{:enum-value :sales}
                                  {:enum-value :cogs}
                                  {:enum-value :payroll}
                              {:enum-value :controllable}
                              {:enum-value :fixed_overhead}
                              {:enum-value :ownership_controllable}]}})


(def resolvers
  {:get-ledger-page get-ledger-page
   :get-balance-sheet get-balance-sheet
   :get-profit-and-loss get-profit-and-loss
   :profit-and-loss-pdf profit-and-loss-pdf
   :cash-flows-pdf cash-flows-pdf
   :journal-detail-report-pdf journal-detail-report-pdf
   :balance-sheet-pdf balance-sheet-pdf
   :get-ledger-csv get-ledger-csv
   :get-journal-detail-report get-journal-detail-report
   :mutation/delete-external-ledger delete-external-ledger
   :mutation/import-ledger import-ledger})

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