(ns auto-ap.pdf.ledger
  (:require
   [amazonica.aws.s3 :as s3]
   [auto-ap.datomic :refer [conn pull-attr pull-many]]
   [auto-ap.graphql.utils :refer [<-graphql]]
   [auto-ap.ledger.reports :as l-reports]
   [auto-ap.time :as atime]
   [auto-ap.utils :refer [by dollars-0?]]
   [clj-pdf.core :as pdf]
   [clojure.java.io :as io]
   [clojure.string :as str]
   [auto-ap.logging :as alog]
   [config.core :refer [env]]
   [datomic.api :as dc])
  (:import
   (java.io ByteArrayOutputStream)
   (java.text DecimalFormat)
   (java.util UUID)))

(def ^:dynamic *report-pedantic* false)

(defn cell->pdf [cell]
  (let [cell-contents (cond
                        (and (= :dollar (:format cell))
                             (or (nil? (:value cell))
                               (dollars-0? (:value cell))))
                        "-"

                        (= :dollar (:format cell))
                        (.format (DecimalFormat. "$###,##0.00") (:value cell))

                        (= :percent (:format cell))
                        (.format (DecimalFormat. (if *report-pedantic* "0.00%" "0%")) (:value cell))

                        :else
                        (str (:value cell)))]
    [:pdf-cell
     (cond->  {}

       (:border cell)              (assoc :border true
                             :set-border (:border cell))
       (:colspan cell)             (assoc :colspan (:colspan cell))
       (:align cell)               (assoc :align (:align cell))
       (= :dollar (:format cell))  (assoc :align :right)
       (= :percent (:format cell)) (assoc :align :right)
       (:bold cell)                (assoc-in [:ttf-name] "fonts/calibri-bold.ttf")
       (:color cell)               (assoc :color (:color cell))
       (:bg-color cell)               (assoc :background-color (:bg-color cell)))

     cell-contents
     ]))

(defn cell-count [table]
  (let [counts (map count (:rows table))]
    (if (seq counts)
      (apply max counts)
      0)))

(defn table->pdf [table widths]
  (let [cell-count (cell-count table)]
    (-> [:pdf-table {:header        (mapv
                                      (fn [header]
                                        (map cell->pdf header))
                                      (:header table))
                     :cell-border   false
                     :width-percent (cond (<= cell-count 5)
                                          50

                                          (= 6 cell-count)
                                          60

                                          (= 7 cell-count)
                                          70

                                          (= 8 cell-count)
                                          80

                                          :else
                                          100)}
         widths
         ]

        (into 
          (for [row (:rows table)]
            (into []
                  (for [cell (take cell-count (concat row (repeat nil)))]
                    (cell->pdf cell)
                    ))))
        (conj (take cell-count (repeat (cell->pdf {:value " "})))))))

(defn split-table [table n]
  (let [cell-count (cell-count table)]
    (if (<= cell-count n)
      [table]
      (let [new-table (-> table
                          (update :rows (fn [rows]
                                          (map
                                            (fn [[header & rest]]
                                              (into [header]
                                                    (take (dec n) rest)))
                                            rows)))
                          (update :header (fn [headers]
                                            (map
                                              (fn [[title & header]]
                                                (first 
                                                  (reduce
                                                    (fn [[so-far a] next]
                                                      (let [new-a (+ a (or (:colspan next)
                                                                           1))]
                                                        (if (<= new-a n)
                                                          [(conj so-far next) new-a]
                                                          [so-far new-a])))

                                                    [[title] 1]
                                                    header)))
                                              headers))))
            remaining (-> table
                          (update :rows (fn [rows]
                                          (map
                                            (fn [[header & rest]]
                                              (into [header]
                                                    (drop (dec n) rest)))
                                            rows)))
                          (update :header (fn [headers]
                                            (map
                                              (fn [[title & header]]
                                                (first 
                                                  (reduce
                                                    (fn [[so-far a] next]
                                                      (let [new-a (+ a (or (:colspan next)
                                                                           1))]
                                                        (if (> new-a n)
                                                          [(conj so-far next) new-a]
                                                          [so-far new-a])))

                                                    [[title] 1]
                                                    header)))
                                              headers))))]
        (into [new-table]
              (split-table remaining n))))))

#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn break-apart-tables [pnl-data tables]
  (for [table tables
        table (split-table table (if (:include-deltas (:args pnl-data))
                                   10
                                   9))]
    table))

(defn make-balance-sheet [args data]
  
  (let [data          (<-graphql data)
        args          (<-graphql args)
        args          (assoc args
                    :periods (filter identity (cond-> [(:date args)]
                                                (:include-comparison args) (conj (:comparison-date args)))))
        clients       (pull-many (dc/db conn) [:client/code :client/name :db/id] (:client-ids args))
        data          (concat (->> (:balance-sheet-accounts data)
                                   (map (fn [b]
                                          (assoc b
                                                 :period (:date args)))))
                              (->> (:comparable-balance-sheet-accounts data)
                                   (map (fn [b]
                                          (assoc b
                                                 :period (:comparison-date args))))))
        pnl-data      (l-reports/->PNLData args data (by :db/id :client/code clients))
        client-count (count (set (map :client-id (:data pnl-data))))
        
        report        (l-reports/summarize-balance-sheet pnl-data)
        output-stream (ByteArrayOutputStream.)]
    (pdf/pdf
      (-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15
            :size        :letter
            :font        {:size     6
                          :ttf-name "fonts/calibri-light.ttf"}}
           [:heading (str "Balance Sheet - " (str/join ", " (map :client/name clients)))]]
          (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)])
          (conj 
            (table->pdf report
                         (cond-> (into [30 ] (repeat client-count 13))
                           (:include-comparison args) (into (repeat (* 2 client-count) 13))
                           (and (> client-count 1) (not (:include-comparison args))) (conj 13)))))
      output-stream)
    (.toByteArray output-stream)))

(defn make-pnl [args data]
  (let [data          (<-graphql data)
        args          (<-graphql args)
        clients       (pull-many (dc/db conn) [:client/code :client/name :db/id] (:client-ids args))
        data          (->> data
                           :periods
                           (mapcat (fn [p1 p2]
                                     (map 
                                       (fn [a]
                                         (assoc a :period p1)
                                         )
                                       (:accounts p2))
                                     )
                                   (:periods args)))
        pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients))
        report        (l-reports/summarize-pnl pnl-data)
        output-stream (ByteArrayOutputStream.)]
    (pdf/pdf
      (-> [{:left-margin 10 :right-margin 10 :top-margin 5 :bottom-margin 15
            :size        (cond
                           (and (>= (count (-> pnl-data :args :periods)) 8 )
                                (-> pnl-data :args :include-deltas))
                           :a2

                           (>= (count (-> pnl-data :args :periods)) 4 )
                           :tabloid
                           :else
                           :letter)
            :orientation :landscape
            :font        {:size     6
                          :ttf-name "fonts/calibri-light.ttf"}}
           [:heading (str "Profit and Loss - " (str/join ", " (map :client/name clients)))]]
          (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)])
          (into 
            (for [table (concat (:summaries report)
                                (:details report))]
              (table->pdf table
                          (into [20] (take (dec (cell-count table))
                                           (mapcat identity 
                                                   (repeat
                                                     (if (-> pnl-data :args :include-deltas)
                                                       [13 6 13]
                                                       [13 6])))))))))
     output-stream)
    (.toByteArray output-stream)))

(defn make-cash-flows [args data]
  (let [data          (<-graphql data)
        args          (<-graphql args)
        clients       (pull-many (dc/db conn) '[:client/code :client/name :db/id] (:client-ids args))
        data          (->> data
                           :periods
                           (mapcat (fn [p1 p2]
                                     (map 
                                       (fn [a]
                                         (assoc a :period p1)
                                         )
                                       (:accounts p2))
                                     )
                                   (:periods args)))
        pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients))
        report        (l-reports/summarize-cash-flows pnl-data)
        output-stream (ByteArrayOutputStream.)]
    (pdf/pdf
      (-> [{:left-margin 10 :right-margin 10 :top-margin 5 :bottom-margin 15
            :size        (cond
                           (and (>= (count (-> pnl-data :args :periods)) 8 )
                                (-> pnl-data :args :include-deltas))
                           :a2

                           (>= (count (-> pnl-data :args :periods)) 4 )
                           :tabloid
                           :else
                           :letter)
            :orientation :landscape
            :font        {:size     6
                          :ttf-name "fonts/calibri-light.ttf"}}
           [:heading (str "Statement of Cash Flows - " (str/join ", " (map :client/name clients)))]]
          (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)])
          (into 
            (for [table (concat (:summaries report)
                                (:details report))]
              (table->pdf table
                          (into [20] (take (dec (cell-count table))
                                           (mapcat identity 
                                                   (repeat
                                                     (if (-> pnl-data :args :include-deltas)
                                                       [13 6 13]
                                                       [13 6])))))))))
     output-stream)
    (.toByteArray output-stream)))

(defn make-journal-detail-report [args data]
  (let [data          (<-graphql data)
        args          (<-graphql args)
        clients       (pull-many (dc/db conn) '[:client/code :client/name :db/id] (:client-ids args))
        report        (l-reports/journal-detail-report args data (by :db/id :client/code clients))
        output-stream (ByteArrayOutputStream.)]
    (alog/info ::make-detail-report)
    (pdf/pdf
      (-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15
            :size        :letter
            :font        {:size     6
                          :ttf-name "fonts/calibri-light.ttf"}}
           [:heading (str "Journal Detail Report - " (str/join ", " (map :client/name clients)))]]
          (conj [:paragraph {:color [128 0 0] :size 9} (:warning report)])
          (conj 
            (table->pdf report
                        [80 25 80 25 25 25])))
      output-stream)
    (.toByteArray output-stream)))

(defn join-names [client-ids]
  (str/replace (->> client-ids (pull-many (dc/db conn) [:client/name]) (map :client/name) (str/join "-")) #"[^\w]" "_" ))

(defn pnl-args->name [args]
  (let [min-date (atime/unparse-local
                   (->> args :periods (map :start) first)
                   atime/iso-date)
        max-date (atime/unparse-local
                   (->> args :periods (map :end) last)
                   atime/iso-date)
        names    (->> args :client_ids join-names)]
    (format "Profit-and-loss-%s-to-%s-for-%s" min-date max-date names)))

(defn cash-flows-args->name [args]
  (let [min-date (atime/unparse-local
                   (->> args :periods (map :start) first)
                   atime/iso-date)
        max-date (atime/unparse-local
                   (->> args :periods (map :end) last)
                   atime/iso-date)
        names    (->> args :client_ids join-names)]
    (format "Cash-flows-%s-to-%s-for-%s" min-date max-date names)))

(defn journal-detail-args->name [args]
  (let [min-date (atime/unparse-local
                   (->> args :date_range :start)
                   atime/iso-date)
        max-date (atime/unparse-local
                   (->> args :date_range :end)
                   atime/iso-date)
        names    (->> args :client_ids join-names)]
    (format "Profit-and-loss-%s-to-%s-for-%s" min-date max-date names)))

(defn balance-sheet-args->name [args]
  (let [date (atime/unparse-local
                   (:date args)
                   atime/iso-date)
        name    (->> args :client_ids join-names)]
    (format "Balance-sheet-%s-for-%s" date name)))

(defn print-pnl [user args data]
  (let [uuid     (str (UUID/randomUUID))
        pdf-data (make-pnl args data)
        name (pnl-args->name args)
        key (str "reports/pnl/" uuid "/" name ".pdf")
        url (str "https://" (:data-bucket env) "/" key)]
    (s3/put-object :bucket-name (:data-bucket env)
                   :key key
                   :input-stream (io/make-input-stream pdf-data {})
                   :metadata {:content-length (count pdf-data)
                              :content-type   "application/pdf"})
    @(dc/transact conn
                 [{:report/name    name
                   :report/client  (:client_ids args)
                   :report/key     key
                   :report/url     url
                   :report/creator (:user user)
                   :report/created (java.util.Date.)}])
    {:report/name name
     :report/url  url }))

(defn print-cash-flows [user args data]
  (let [uuid     (str (UUID/randomUUID))
        pdf-data (make-cash-flows args data)
        name     (cash-flows-args->name args)
        key      (str "reports/cash-flows/" uuid "/" name ".pdf")
        url      (str "https://" (:data-bucket env) "/" key)]
    (s3/put-object :bucket-name (:data-bucket env)
                   :key key
                   :input-stream (io/make-input-stream pdf-data {})
                   :metadata {:content-length (count pdf-data)
                              :content-type   "application/pdf"})
    @(dc/transact conn
                 [{:report/name    name
                   :report/client  (:client_ids args)
                   :report/key     key
                   :report/url     url
                   :report/creator (:user user)
                   :report/created (java.util.Date.)}])
    {:report/name name
     :report/url  url }))

(defn print-balance-sheet [user args data]
  (let [uuid     (str (UUID/randomUUID))
        pdf-data (make-balance-sheet args data)
        name     (balance-sheet-args->name args)
        key      (str "reports/balance-sheet/" uuid "/" name ".pdf")
        url      (str "https://" (:data-bucket env) "/" key)]
    (s3/put-object :bucket-name (:data-bucket env)
                   :key key
                   :input-stream (io/make-input-stream pdf-data {})
                   :metadata {:content-length (count pdf-data)
                              :content-type   "application/pdf"})
    @(dc/transact conn
                 [{:report/name    name
                   :report/client  (:client_ids args)
                   :report/key     key
                   :report/url     url
                   :report/creator (:user user)
                   :report/created (java.util.Date.)}])
    {:report/name name
     :report/url  url }))

(defn print-journal-detail-report [user args data]
  (let [uuid     (str (UUID/randomUUID))
        pdf-data (make-journal-detail-report args data)
        name     (journal-detail-args->name args)
        key      (str "reports/journal-detail/" uuid "/" name ".pdf")
        url      (str "https://" (:data-bucket env) "/" key)]
    (s3/put-object :bucket-name (:data-bucket env)
                   :key key
                   :input-stream (io/make-input-stream pdf-data {})
                   :metadata {:content-length (count pdf-data)
                              :content-type   "application/pdf"})
    @(dc/transact conn
                 [{:report/name    name
                   :report/client  (:client_ids args)
                   :report/key     key
                   :report/url     url
                   :report/creator (:user user)
                   :report/created (java.util.Date.)}])
    {:report/name name
     :report/url  url }))
