(ns auto-ap.ledger.reports
  #?@
   (:clj
    [(:require
      [auto-ap.time :as atime]
      [auto-ap.time-utils :refer [user-friendly-date]]
      [auto-ap.time-utils :refer [user-friendly-date]]
      [auto-ap.utils :refer [dollars-0? dollars= or-0]]
      [clj-time.coerce :as coerce]
      [clojure.string :as str])]
    :cljs
    [(:require
      [auto-ap.utils :refer [dollars-0? dollars= or-0]]
      [auto-ap.views.utils :as au]
      [clojure.string :as str]
      [auto-ap.time-utils :refer [user-friendly-date]])]))

(defn date->str [d]
  #?(:clj 
     (if (inst? d)
       (atime/unparse-local (coerce/to-date-time d) atime/normal-date)
       (atime/unparse-local d atime/normal-date))
     :cljs (au/date->str d au/pretty)))



(def groupings
  {:assets [["1100 Cash and Bank Accounts" 11000 11999] 
            ["1200 Accounts Receivable" 12000 12999]
            ["1300 Inventory" 13000 13999]
            ["1400 Prepaid Expenses" 14000 14999]
            ["1500 Property and Equipment" 15000 15999]
            ["1600 Intangible Assets" 16000 16999]
            ["1700 Other Assets" 17000 19999]]
   :liabilities [["2000 Accounts Payable" 20100 23999]
                 ["2400 Accrued Expenses" 24000 24999]
                 ["2500 Other Liabilities" 25000 25999]
                 ["2600 Split Accounts" 26000 26999]
                 ["2700 Current Portion of Long-Term Debt" 27000 27999]
                 ["2800 Notes Payable" 28000 28999]]
   :equities [["3000 Owner's Equity" 30000 39999]]
   :sales [["40000-43999 Food Sales " 40000 43999]
           ["44000-46999 Alcohol Sales" 44000 46999]
           ["47000 Merchandise Sales" 47000 47999]
           ["48000 Other Operating Income" 48000 48999]
           ["49000 Non-Business Income" 49000 49999]]
   :cogs [
          ["50000-54000 Food Costs" 50000 53999]
          ["54000-56000 Alcohol Costs" 54000 55999]
          ["56000 Merchandise Costs" 56000 56999]
          ["57000-60000 Other Costs of Sales" 57000 59999]]
   :payroll [["60000 Payroll - General" 60000 60999]
             ["61000 Payroll - Management" 61000 61999]
             ["62000 Payroll - BOH" 62000 62999]
             ["63000-66000 Payroll - FOH" 63000 65999]
             ["66000-70000 Payroll - Other" 66000 69999]]

   :controllable [["70000 72000 GM Controllable Costs - Ops Related" 70000 71999]
                  ["72000 GM Controllable Costs - Customer Related" 72000 72999]
                  ["73000 GM Controllable Costs - Employee Related" 73000 73999]
                  ["74000 GM Controllable Costs - Building & Equipment Related" 74000 74999]
                  ["75000 GM Controllable Costs - Office & Management Related" 75000 75999]
                  ["76000-80000 GM Controllable Costs - Other" 76000 79999]]

   :fixed-overhead [["80000-82000 Operational Costs" 80000 81999]
                    ["82000 Occupancy Costs" 82000 82999]
                    ["83000 Utility Costs" 83000 83999]
                    ["84000 Equipment Rental" 84000 84999]
                    ["85000-87000 Taxes & Insurance" 85000 86999]
                    ["87000-90000 Other Non-Controllable Costs" 87000 89999]]
   :ownership-controllable [["90000-93000 Research & Entertainment" 90000 92999]
                            ["93000 Bank Charges & Interest" 93000 93999]
                            ["94000-96000 Other Owner Controllable Costs" 94000 95999]
                            ["96000 Depreciation" 96000 96999]
                            ["97000 Taxes" 97000 97999]
                            ["98000 Other Expenses" 98000 98999]]

   :operating-activities [
                          ;; BEN EDIT STARTING HERE
                          ["20100-20199 Credit Card Balances" 20100 20199 :add]
                          ["21000-24000 Accounts Payable" 21000 23999 :add]
                          ["25000-28000 Accounts Payable" 25000 27999 :add]
                          ["24000-25000 Accrual Liabilities" 24000 24999 :add]
                          ["12000-13000 Accounts Receivable" 12000 13000 :subtract] 
                          ["96000-97000 Depreciation Expense" 96000 96999 :add]
                          ["13000-15000 Inventory" 13000 14999 :subtract]
                          ;; BEN ENDING HERE
                          ]

   :investment-activities [

                           ;; BEN EDIT STARTING HERE
                           ["15000-18000 Investments" 15000 17999 :subtract]
                           ;; BEN ENDING HERE

                           ]
   :financing-activities [
                          ;; BEN EDIT STARTING HERE
                          ["30000-33000 Other Equity Accounts" 30000 32999 :add]
                          ["33000-34000 Owner's Contributions" 33000 33999 :add]
                          ["34000-35000 Owner's Distributions" 34000 34999 :add]
                          ["35000-36000 Retained Earnings" 35000 35999 :add]
                          ["28000-29000 Loans (payable)" 28000 28999 :add]
                          ;; BEN ENDING HERE
                          ]
   :cash [
          ;; BEN EDIT STARTING HERE
          ["11000-11400 Bank Accounts / Cash" 11000 11399 :add]
          ;; BEN ENDING HERE

          ]})

(def cashflow-aggregation 
  (->> (select-keys groupings [:operating-activities :investment-activities :financing-activities])
       vals
       (mapcat identity)
       (map (fn [[_ start end rule]]
                 [start end rule]))
       (into [])))

(defn cashflow-account->amount [account-code amount]
  (let [operation (->> cashflow-aggregation
                       (filter (fn [[start end]]
                                 (<= start account-code end)))
                       first
                       last)]
    (cond (= operation :add)
          amount

          (= operation :subtract)
          (- amount)

          :else
          amount)))

(defn min-numeric-code [category]
  (->> (groupings category)
       (map second)
       (apply min)))

(defn max-numeric-code [category]
  (->> (groupings category)
       (map second)
       (apply max)))


(def flat-categories 
  (for [[category groups] groupings
        [_ start end] groups]
    [category start end]))

(defn in-range? [code]
  (if code 
    (reduce
      (fn [acc [_ start end]]
        (if (<= start code end)
          (reduced true)
          acc))
      false
      flat-categories)
    false))

(defn client-locations [pnl-data]
  (->> pnl-data
       :data
       (filter (comp in-range? :numeric-code))
       (filter #(not= "A" (:location %)))
       (group-by (juxt :client-id :location))
       (filter (fn [[_ as]] 
                 (not (dollars-0?  (reduce + 0 (map (comp or-0 :amount) as))))))
       (mapcat second)
       (map (fn [a]
              (if (or (not (:client-id a))
                      (empty? (:location a)))
                nil
                [(:client-id a)
                 (:location a)])))
       (filter identity)
       (set)
       
       (sort-by (fn [x]
                  [(:client-id x)
                   (if (= (:location x) "HQ" )
                     "ZZZZZZ"
                     (:location x))]))))

(defn aggregate-accounts [pnl-data]
  (reduce (fnil + 0.0) 0.0 (map (comp or-0 :amount) (:data pnl-data))))

(defn aggregate-cashflow-accounts [pnl-data]
  (reduce (fnil + 0.0) 0.0 (map (comp or-0 (fn [d]
                                             (cashflow-account->amount (:numeric-code d) (:amount d))))
                                (:data pnl-data))))

(defn aggregate-credits [pnl-data]
  (reduce (fnil + 0.0) 0.0 (map :credits (:data pnl-data))))

(defn aggregate-debits [pnl-data]
  (reduce (fnil + 0.0) 0.0 (map :debits (:data pnl-data))))

(defn best-category [a]
  (->> flat-categories
       (filter (fn [[category start end]]
                 (and (:numeric-code a)
                      (<= start (:numeric-code a) end))))
       first
       first))

(defn filter-client [pnl-data client]
  (-> pnl-data
      (update :data (fn [data]
                               ((group-by :client-id data) client)))
      (update :filters (fn [f]
                         (assoc f :client-id client)))))

(defn filter-location [pnl-data location]
  (-> pnl-data
      (update :data (fn [data]
                      ((group-by :location data) location)))
      (update :filters (fn [f]
                         (assoc f :location location)))))

(defn filter-locations [pnl-data locations]
  (-> pnl-data
      (update :filters (fn [f]
                         (assoc f :locations locations)))))

(defn filter-numeric-code [pnl-data from to]
  (-> pnl-data 
      (update :data (fn [data]
                      (filter
                        #(<= from (or (:numeric-code %) 0) to)
                        data)))
      (update :filters (fn [f]
                         (assoc f
                                :numeric-code [{:from from
                                                :to to}])))))

(defn account-belongs-in-category? [numeric-code category]
  (->> (groupings category)
       (some (fn [[_ from to]]
               (<= from (or numeric-code 0) to)))))

;; TODO make click-through work for multiple ranges :fliters
(defn filter-categories [pnl-data categories]
  (-> pnl-data
      (update :data
              (fn [data]
                (for [account data
                      :when (some #(account-belongs-in-category? (:numeric-code account) %) categories)]
                  account)))
      (update :filters
              (fn [f]
                (assoc f :numeric-code
                       (for [category categories
                             [_ from to] (groupings category)]
                         {:from from
                          :to to}))))))

(defn filter-period [pnl-data period]
  (-> pnl-data
    (update :data (fn [data]
                             ((group-by :period data) period)))
    (update :filters (fn [f]
                       (assoc f :date-range period)))))

(defn zebra [pnl-data i]
  (if (odd? i)
    (assoc-in pnl-data [:cell-args :bg-color] [240 240 240])
    pnl-data))



(defn negate [pnl-data types]
  (update pnl-data :data 
          (fn [accounts]
            (map 
              (fn [account]
                (if (types (best-category account))
                  (update account :amount #(- (or % 0.0)))
                  account))
              accounts))))


(defn used-accounts [pnl-datas]
  (->>
    pnl-datas
    (mapcat :data)
    (map #(select-keys % [:numeric-code :name]))
    (set)
    (sort-by :numeric-code)))

(defn subtotal-by-column-row [pnl-datas title & [cell-args]]
  (into [{:value   title
          :bold    true}]
        (map
          (fn [p]
            (merge 
              {:format  :dollar
               :value   (aggregate-accounts p)
               :filters (when (:numeric-code (:filters p)) ;; don't allow filtering when you don't at least filter numeric codes
                          (:filters p))}
              (:cell-args p)
              cell-args))
          pnl-datas)))

(defn cashflow-subtotal-by-column-row [pnl-datas title & [cell-args]]
  (into [{:value   title
          :bold    true}]
        (mapcat
         (fn [p]
           [
            (merge 
             {:format  :text
              :value   nil}
             (:cell-args p)
             cell-args)
            (merge 
             {:format  :text
              :value   nil}
             (:cell-args p)
             cell-args)
            (merge 
             {:format  :dollar
              :value   (aggregate-cashflow-accounts p)
              :filters (when (:numeric-code (:filters p)) ;; don't allow filtering when you don't at least filter numeric codes
                         (:filters p))}
             (:cell-args p)
             cell-args)])
         pnl-datas)))

(defn calc-percent-of-sales [table pnl-datas]
  (let [sales  (map
                (fn [p]
                  (aggregate-accounts (filter-categories p [:sales])))
                pnl-datas)]
    (->> table
         (map (fn [[_ & values]]
                (map
                  (fn [v s]
                    {:border (:border v)
                     :bg-color (:bg-color v)
                     :format (if (string? (:value v))
                               :text
                               :percent)
                     :color [128 128 128]
                     :value  (cond
                               (string? (:value v))
                               ""

                               (dollars-0? s)
                              0.0
                              :else
                              (/ (:value v) s))})
                  values sales))))))

(defn calc-deltas [table]
  (->> table 
       (map (fn [[_ & values]]
              (->> values
                   (partition 2 1)
                   (map (fn [[a b]]
                          (if (or (string? (:value b))
                                  (string? (:value a)))
                            {:value ""
                             :format :text
                             :bg-color (:bg-color b)}
                            {:border (:border b)
                             :format :dollar
                             :value  (- (:value b)
                                        (:value a))
                             :bg-color (:bg-color b)}))))))))

(defn combine-tables
  ([[pnl-data] table percent-of-sales deltas]
   (map (fn [[title & row] percent-of-sales deltas ]
          (let [deltas (cons {:value  0.0
                              :format :dollar
                              :border (:border (first row))} deltas)]
            (into [title]
                  (mapcat
                    (fn [v p d]
                      (if (:include-deltas (:args pnl-data))
                        [v p d]
                        [v p]))
                    row
                    percent-of-sales
                    deltas))))
        table
        percent-of-sales
        deltas)))

(defn headers [[pnl-data :as pnl-datas] header-title]
  (let [big-header (into [{:value header-title
                           :bold true}]
                         (map-indexed (fn [i p]
                                        (cond-> {:value 
                                                 (str (date->str (:start p))
                                                      " - "
                                                      (date->str (:end p)))
                                                 :colspan (cond
                                                            (-> pnl-data :args :include-deltas)
                                                            3

                                                            (-> pnl-data :args :column-per-location)
                                                            (* 2 (/ (count pnl-datas)
                                                                    (count (-> pnl-data :args :periods))))

                                                            :else
                                                            2)
                                                 :align :center
                                                 :bold  true}
                                          (odd? i) (assoc :bg-color [240 240 240])))
                              (:periods (:args pnl-data))))
        sub-header (into [{:value ""}]
                         (if (-> pnl-data :args :column-per-location)
                           (mapcat
                             (fn [p]
                               (cond-> [(merge {:value (or (when (-> p :filters :location)
                                                             (str ((-> p :client-codes) (-> p :filters :client-id)) "-" (-> p :filters :location)))
                                                           "Total")
                                                :align :right}
                                               (:cell-args p))
                                        (merge {:value "%"
                                                :align :right}
                                               (:cell-args p))]
                                 (-> pnl-data :args :include-deltas) (conj (merge {:value "+/-"
                                                                                   :align :right}
                                                                                  (:cell-args p)))))
                             pnl-datas)
                           (mapcat
                             (fn [p]
                               (cond-> [(merge {:value "Amt"
                                                :align :right}
                                               (:cell-args p))
                                        (merge {:value "%"
                                                :align :right}
                                               (:cell-args p))]
                                 (-> pnl-data :args :include-deltas) (conj (merge {:value "+/-"
                                                                                   :align :right}
                                                                                  (:cell-args p)))))
                             pnl-datas)))]
    [big-header
     sub-header]))

(defn location-summary-table [pnl-datas title]
  (let [table [(subtotal-by-column-row (map #(filter-categories % [:sales])
                                  pnl-datas)
                             "Sales")
               (subtotal-by-column-row (map #(filter-categories % [:cogs ]) pnl-datas)
                                       "Cogs")

               (subtotal-by-column-row (map #(filter-categories % [:payroll ]) pnl-datas)
                                       "Payroll")

               (subtotal-by-column-row (map #(-> %
                                                (filter-categories [:sales :payroll :cogs])
                                                (negate #{:payroll :cogs}))
                                            pnl-datas)
                                       "Gross Profits")

               (subtotal-by-column-row (map #(filter-categories % [:controllable :fixed-overhead :ownership-controllable])
                                            pnl-datas)
                                       "Overhead")

               (subtotal-by-column-row (map #(-> %
                                                (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
                                                (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
                                            pnl-datas)
                                       "Net Income")]
        percent-of-sales (calc-percent-of-sales table pnl-datas)
        deltas           (calc-deltas table)]
    {:header (headers pnl-datas title)
     :rows   (combine-tables pnl-datas table percent-of-sales deltas)}))


(defn detail-rows
  [pnl-datas grouping title]
  (let [pnl-datas    (map #(filter-categories % [grouping])
                          pnl-datas)
        individual-accounts
        (for [[grouping-name from to] (groupings grouping)
              :let                    [pnl-datas (map #(filter-numeric-code % from to)
                                                      pnl-datas)
                                       account-codes (used-accounts pnl-datas)]
              :when                   (seq account-codes)
              row                     (-> [(into [{:value (str "---" grouping-name "---")}]
                                                 (map
                                                   (fn [p]
                                                     (assoc (:cell-args p) :value "" :format ""))
                                                   pnl-datas)
                                                 )]
                                          (into (for [{:keys [numeric-code name]} account-codes]
                                                  (into [{:value (str name ":" numeric-code)}]
                                                        (map
                                                          (fn [p]
                                                            (let [pnl-data (-> p (filter-numeric-code numeric-code numeric-code))
                                                                  this-name-exists? (->> (:data p)
                                                                                         (filter (comp #{name} :name))
                                                                                         seq)]
                                                              (merge 
                                                                (if this-name-exists? 
                                                                  {:format  :dollar
                                                                   :filters (:filters pnl-data)
                                                                   :value   (aggregate-accounts pnl-data)}
                                                                  {:filters (:filters pnl-data)
                                                                   :value   ""})
                                                                (:cell-args p))))

                                                          pnl-datas))))
                                          (conj (subtotal-by-column-row pnl-datas "" {:border [:top]})))]
          row)]
    (-> [(into [{:value title
                 :bold  true}]
               (map
                 (fn [p]
                   (assoc (:cell-args p) :value "" :format ""))
                 pnl-datas))]
        (into individual-accounts)
        (conj (subtotal-by-column-row pnl-datas title)))))

(defn location-detail-table [pnl-datas client-datas title prefix]
  (let [table            (-> []
                             (into (detail-rows pnl-datas
                                                :sales
                                                (str prefix " Sales")))
                             (into (detail-rows pnl-datas
                                                :cogs
                                                (str prefix " COGS")))
                             (into (detail-rows
                                     pnl-datas
                                     :payroll
                                     (str prefix " Payroll")))
                             (conj (subtotal-by-column-row (map #(filter-categories % [:payroll :cogs])
                                                                pnl-datas)
                                                 (str prefix " Prime Costs")))
                             (conj (subtotal-by-column-row (map #(-> %
                                                                     (filter-categories [:sales :payroll :cogs])
                                                                     (negate #{:payroll :cogs}))
                                                                pnl-datas)
                                                 (str prefix " Gross Profits")))
                             (into (detail-rows
                                     pnl-datas
                                     :controllable
                                     (str prefix " Controllable Expenses")))
                             (into (detail-rows
                                     pnl-datas
                                     :fixed-overhead
                                     (str prefix " Fixed Overhead")))
                             (into (detail-rows
                                     pnl-datas
                                     :ownership-controllable
                                     (str prefix " Ownership Controllable")))
                             (conj (subtotal-by-column-row (map #(filter-categories % [:controllable :fixed-overhead :ownership-controllable]) pnl-datas)
                                                 (str prefix " Overhead")))
                             (conj (subtotal-by-column-row (map #(-> %
                                                          (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
                                                          (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
                                                      pnl-datas)
                                                 (str prefix " Net Income"))))
        table (if (seq client-datas)
                (conj table (subtotal-by-column-row (map #(-> %
                                                              (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
                                                              (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
                                                         client-datas)
                                                    "All Location Net Income"))
                table)
        percent-of-sales (calc-percent-of-sales table pnl-datas)
        deltas           (into [] (calc-deltas table))]
    {:header (headers pnl-datas title)
     :rows   (combine-tables pnl-datas table percent-of-sales deltas)}))

(defn cash-flow-headers [[pnl-data :as pnl-datas] header-title]
  (let [big-header (into [{:value header-title
                           :bold true}]
                         (map-indexed (fn [i p]
                                        (cond-> {:value 
                                                 (str (date->str (:start p))
                                                      " - "
                                                      (date->str (:end p)))
                                                 :colspan 3
                                                 :align :center
                                                 :bold  true}
                                          (odd? i) (assoc :bg-color [240 240 240])))
                              (:periods (:args pnl-data))))
        sub-header (into [{:value "Account"}]
                         (mapcat
                           (fn [p]
                             [(merge {:value "Increases"
                                      :align :right}
                                     (:cell-args p))
                              (merge {:value "Decreases"
                                      :align :right}
                                     (:cell-args p))
                              (merge {:value "+/- in Cash"
                                      :align :right}
                                     (:cell-args p))])
                           pnl-datas))]
    [big-header sub-header]))

(defn cash-flow-detail-rows
  [pnl-datas grouping title]
  (let [pnl-datas    (map #(filter-categories % [grouping])
                          pnl-datas)
        individual-accounts
        (for [[grouping-name from to add-or-subtract] (groupings grouping)
              :let                    [pnl-datas (map #(filter-numeric-code % from to)
                                                      pnl-datas)
                                       account-codes (used-accounts pnl-datas)]
              :when                   (seq account-codes)
              row                     (-> [(into [{:value (str "---" grouping-name "---")}]
                                                 (mapcat
                                                  (fn [p]
                                                    [(assoc (:cell-args p) :value "" :format "")
                                                     (assoc (:cell-args p) :value "" :format "")
                                                     (assoc (:cell-args p) :value "" :format "")])
                                                  pnl-datas)
                                                 )]
                                          (into (for [{:keys [numeric-code name]} account-codes]
                                                  (into [{:value name}]
                                                        (mapcat
                                                         (fn [p]
                                                           (let [pnl-data (-> p (filter-numeric-code numeric-code numeric-code))
                                                                 aggregated (aggregate-accounts pnl-data)
                                                                 credits (aggregate-credits pnl-data)
                                                                 debits (aggregate-debits pnl-data)]
                                                             (if (dollars= (- debits credits) aggregated)
                                                               [(merge 
                                                                  {:format  :dollar
                                                                   :filters (:filters pnl-data)
                                                                   :value   debits}
                                                                  (:cell-args p))
                                                                (merge 
                                                                  {:format  :dollar
                                                                   :filters (:filters pnl-data)
                                                                   :value   credits}
                                                                  (:cell-args p))
                                                                (merge 
                                                                  {:format  :dollar
                                                                   :filters (:filters pnl-data)
                                                                   :value   (cashflow-account->amount numeric-code aggregated)}
                                                                  (:cell-args p))]
                                                               [(merge 
                                                                  {:format  :dollar
                                                                   :filters (:filters pnl-data)
                                                                   :value   credits}
                                                                  (:cell-args p))
                                                                (merge 
                                                                  {:format  :dollar
                                                                   :filters (:filters pnl-data)
                                                                   :value   debits}
                                                                  (:cell-args p))
                                                                (merge 
                                                                  {:format  :dollar
                                                                   :filters (:filters pnl-data)
                                                                   :value   (cashflow-account->amount numeric-code aggregated)}
                                                                  (:cell-args p))])))

                                                         pnl-datas))))
                                          (conj (cashflow-subtotal-by-column-row pnl-datas "" {:border [:top]})))]
          row)]
    (-> [(into [{:value title
                 :bold  true}]
               (mapcat
                (fn [p]
                  [(assoc (:cell-args p) :value "" :format "")
                   (assoc (:cell-args p) :value "" :format "")
                   (assoc (:cell-args p) :value "" :format "")])
                pnl-datas))]
        (into individual-accounts)
        (conj (cashflow-subtotal-by-column-row pnl-datas title)))))

(defn cash-flows-table [pnl-datas #_client-datas title prefix]
  (let [table            (-> []
                             (conj (cashflow-subtotal-by-column-row (map #(-> %
                                                                     (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
                                                                     (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
                                                                pnl-datas)
                                                           "Net Income"))
                             (into (cash-flow-detail-rows pnl-datas
                                                :operating-activities
                                                (str prefix " Operating Activities")))
                             (into (cash-flow-detail-rows pnl-datas
                                                :investment-activities
                                                (str prefix " Investment Activities")))
                             (into (cash-flow-detail-rows pnl-datas
                                                :financing-activities
                                                (str prefix " Financing Activities")))

                             (conj (cashflow-subtotal-by-column-row (map #(-> %
                                                                              (filter-categories [:operating-activities :investment-activities :financing-activities :sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
                                                                              (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
                                                                         pnl-datas)
                                                                    "Change in Cash and Cash Equivalents"))

                             (into (cash-flow-detail-rows pnl-datas
                                                          :cash
                                                          (str prefix " Bank Accounts / Cash")))

                             )]
    {:header (cash-flow-headers pnl-datas "Cash Flow")
     :rows   table}))

(defn warning-message [pnl-data]
  (let [errors (->> pnl-data
                    :data
                    (filter (fn [{:keys [numeric-code]}]
                              (nil? numeric-code))))
        error-count (count errors)]
    (when (> error-count 0)
      (str "This report does not include "
           (str/join ", "
                     (map #(str (:count %) " unresolved ledger entries for " (if (str/blank? (:location %))
                                                                               " all locations"
                                                                               (:location %)))
                          errors))))))

(defn invalid-ids [pnl-data]
  (let [errors (->> pnl-data
                    :data
                    (filter (fn [{:keys [numeric-code]}]
                              (nil? numeric-code)))
                    (map :sample)) ]
    errors))

(defn summarize-pnl [pnl-data]
  {:warning (warning-message pnl-data)
   :invalid-ids  (invalid-ids pnl-data)
   :summaries
   (if (-> pnl-data :args :column-per-location)
     [(location-summary-table (mapcat identity (for [[period i] (map vector (-> pnl-data :args :periods ) (range))]
                                                 (concat 
                                                   (for [[client-id location] (client-locations pnl-data)]
                                                     (-> pnl-data
                                                         (filter-client client-id)
                                                         (filter-location location)
                                                         (filter-period period)
                                                         (zebra i)))
                                                   [(zebra (filter-locations (filter-period pnl-data period)
                                                                             (map second (client-locations pnl-data))) i)])))
                              "All location Summary")]
     (for [[client-id location] (client-locations pnl-data)]
       (location-summary-table (for [[period i] (map vector (-> pnl-data :args :periods ) (range))]
                                 (-> pnl-data
                                     (filter-client client-id)
                                     (filter-location location)
                                     (filter-period period)
                                     (zebra i)))
                               (str (-> pnl-data :clients-by-id (get client-id)) " (" location ") Summary"))))
   :details
   (doall (if (-> pnl-data :args :column-per-location)
            [(location-detail-table (mapcat identity (for [[period i] (map vector (-> pnl-data :args :periods ) (range))]
                                                       (concat
                                                         (for [[client-id location] (client-locations pnl-data)]
                                                           (-> pnl-data
                                                               (filter-client client-id)
                                                               (filter-location location)
                                                               (filter-period period)
                                                               (zebra i)))
                                                         [(-> pnl-data
                                                              (filter-period period)
                                                              (filter-locations (map second (client-locations pnl-data)))
                                                              (zebra i))])))
                                    nil
                                    "All location Detail"
                                    "")]
            (for [[client-id location] (client-locations pnl-data)]
              (location-detail-table (for [[period i] (map vector (-> pnl-data :args :periods ) (range))]
                                       (-> pnl-data
                                           (filter-client client-id)
                                           (filter-location location)
                                           (filter-period period)
                                           (zebra i)))
                                     (for [[period i] (map vector (-> pnl-data :args :periods ) (range))]
                                       (-> pnl-data
                                           (filter-client client-id)
                                           (filter-period period)
                                           (filter-locations (map second (client-locations pnl-data)))
                                           (zebra i)))
                                     (str (-> pnl-data :clients-by-id (get client-id)) " (" location ") Detail")
                                     location))))})

(defn summarize-cash-flows [pnl-data]
  (let [client-ids (->> (client-locations pnl-data)
                        (map first)
                        set)]
    
    {:warning (warning-message pnl-data)
     :details
     (doall (for [client-id client-ids]
              (cash-flows-table (for [[period i] (map vector (-> pnl-data :args :periods ) (range))]
                                  (-> pnl-data
                                      (filter-client client-id)
                                      (filter-period period)
                                      (zebra i)))
                                (str (-> pnl-data :clients-by-id (get client-id)) "  Detail")
                                "")))}))


(defn balance-sheet-headers [pnl-data]
  (let [period-count (count (:periods (:args pnl-data)))
        client-ids   (set (map :client-id (:data pnl-data)))
        client-count (count client-ids)
        show-total?  (and (> client-count 1) (= 1 period-count))]
    (cond->  []
      (> client-count 1)
      (conj (cond-> (into [{:value "Client"}]
                          (mapcat identity 
                                  (for [client client-ids]
                                    (cond-> [{:value (str (-> pnl-data :client-codes (get client)))}]
                                      (> period-count 1)
                                      (into (apply concat (repeat (dec period-count) ["" ""])))))))
              show-total? (conj {:value "Total" :bold true :border [:left]})))

      true
      (conj (cond-> (into [{:value "Period Ending"}]
                          (for [client client-ids 
                                [index p] (map vector (range) (:periods (:args pnl-data)))
                                :let [is-first? (= 0 index)
                                      period-date (date->str p)
                                      period-headers (if (or is-first?
                                                             (not (:include-deltas (:args pnl-data))))
                                                      [{:value period-date}]
                                                      [{:value period-date}
                                                       {:value "+/-"}])]
                                header period-headers]
                            header))
              show-total? (conj {:value (date->str (first (:periods (:args pnl-data)))) :border [:left]}))))))

(defn append-deltas [table]
  (->> table
       (map (fn [[title & values]]
              (loop [result [title]
                     previous nil
                     [current :as values] values]
                (if current
                  (recur
                   (cond-> result
                     true (conj current)

                     (and (:value current) (:value previous)
                          (number? (:value current)) (number? (:value previous))
                          (= (:client-id (:filters previous))
                             (:client-id (:filters current))))
                     (conj {:border (:border previous)
                            :format :dollar
                            :value  (- (or (:value current) 0.0)
                                       (or (:value previous) 0.0))}))
                   current
                   (rest values))
                  result))))))

#_(defn summarize-balance-sheet [pnl-data]
  (reduce
    (fn [result table]
      (-> result
          (update :header into (:header table))
          (update :rows
                  (fn [current-rows]
                    (if (seq current-rows)
                      (map
                        concat
                        current-rows
                        (:rows table))
                      (:rows table))))))
    {:header []
     :rows   []}
    (for [client-id (set (map :client-id (:data pnl-data)))]
      (let [pnl-datas (map (fn [p]
                             (-> pnl-data 
                                 (filter-client client-id)
                                 (filter-period p)))
                           (:periods (:args pnl-data)))
            table     (-> []
                          (into (detail-rows pnl-datas
                                             :assets
                                             "Assets"))
                          (into (detail-rows pnl-datas
                                             :liabilities
                                             "Liabilities"))
                          (into (detail-rows pnl-datas
                                             :equities
                                             "Owner's Equity"))
                          (conj (subtotal-by-column-row
                                  (map #(-> %
                                            (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
                                            (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
                                       pnl-datas)
                                  "Retained Earnings")))
            table     (if (:include-comparison (:args pnl-data))
                        (append-deltas table)
                        table)]
        {:header (balance-sheet-headers pnl-data)
         :rows   table})))
  )

(defn add-total-border [rows]
  (map (fn [row]
         (let [last-idx (dec (count row))]
           (map-indexed
            (fn [i cell]
              (if (= i last-idx)
                (let [borders (or (:border cell) [])]
                  (assoc cell :border (conj borders :left)))
                cell))
            row)))
       rows))

(defn summarize-balance-sheet [pnl-data]
  (let [client-ids   (set (map :client-id (:data pnl-data)))
        client-count (count client-ids)
        period-count (count (:periods (:args pnl-data)))
        show-total?  (and (> client-count 1) (= 1 period-count))
        pnl-datas    (for [client-id client-ids
                           p         (:periods (:args pnl-data))]
                       (-> pnl-data 
                           (filter-client client-id)
                           (filter-period p)))
        total-data   (when show-total?
                       (-> pnl-data
                           (filter-period (first (:periods (:args pnl-data))))
                           (assoc :cell-args {:bold true})))
        pnl-datas    (concat pnl-datas (when total-data [total-data]))]
    (let [table     (-> []
                        (into (detail-rows pnl-datas
                                           :assets
                                           "Assets"))
                        (into (detail-rows pnl-datas
                                           :liabilities
                                           "Liabilities"))
                        (into (detail-rows pnl-datas
                                           :equities
                                           "Owner's Equity"))
                        (conj (subtotal-by-column-row
                               (map #(-> %
                                         (filter-categories [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable])
                                         (negate #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable}))
                                    pnl-datas)
                               "Retained Earnings")))
          table     (if (and (> period-count 1)
                             (:include-deltas (:args pnl-data))) 
                       (append-deltas table)
                       table)
          table     (if show-total? (add-total-border table) table)]
      {:warning (warning-message pnl-data)
       :header (balance-sheet-headers pnl-data)
       :rows   table}))
  )


(defn journal-detail-report [args data client-codes]
  {:header [[{:value "Category"}
             {:value "Date"}
             {:value "Description"}
             {:value "Debit"}
             {:value "Credit"}
             {:value "Running Balance"}]]
   :rows   (reduce
             (fn [rows category]
               (into rows
                     ;; TODO colspan ?
                     (concat (when (seq (:journal-entries category))
                               [[
                                 {:value (str (client-codes (:client-id category)) " - " (:location category) " - " (:name (:account category)))}
                                 {:value ""}
                                 {:value ""}
                                 {:value ""}
                                 {:value ""}
                                 {:value ""}]])
                             (map
                              (fn [je]
                                [{:value ""}
                                 {:value (user-friendly-date (:date je))}
                                 {:value (:description je "")}
                                 {:value (get je :debit)
                                  :format :dollar}
                                 {:value (get je :credit)
                                  :format :dollar}
                                 {:value (get je :running-balance)
                                  :format :dollar}])
                              (:journal-entries category))
                             [[
                               {:value  (str (client-codes (:client-id category)) " - " (:location category) " - " (:name (:account category)))
                                :bold   true
                                :border [:top]}
                               {:value  ""
                                :border [:top]}
                               {:value  (str "Total"  )
                                :bold   true
                                :border [:top]} 
                               {:value  ""
                                :border [:top]}
                               {:value  ""
                                :border [:top]}
                               {:value  (:total category)
                                :format :dollar
                                :bold   true
                                :border [:top]}]]))

               )
             []
             (:categories data))}
  )


(defrecord PNLData [args data client-codes])
