(ns auto-ap.graphql.checks
  (:require [amazonica.aws.s3 :as s3]
            [auto-ap.datomic :refer [audit-transact remove-nils]]
            [auto-ap.datomic.bank-accounts :as d-bank-accounts]
            [auto-ap.datomic.checks :as d-checks]
            [auto-ap.datomic.clients :as d-clients]
            [auto-ap.datomic.invoices :as d-invoices]
            [auto-ap.datomic.transactions :as d-transactions]
            [auto-ap.datomic.vendors :as d-vendors]
            [auto-ap.graphql.utils
             :refer
             [->graphql <-graphql assert-can-see-client enum->keyword]]
            [auto-ap.numeric :refer [num->words]]
            [auto-ap.time :refer [iso-date local-now parse]]
            [auto-ap.utils :refer [by dollars-0?]]
            [clj-pdf.core :as pdf]
            [clj-time.coerce :as c]
            [clj-time.core :as time]
            [clj-time.format :as f]
            [clojure.edn :as edn]
            [clojure.java.io :as io]
            [clojure.string :as str]
            [config.core :refer [env]]
            [clojure.tools.logging  :as log]
            [clojure.set :as set])
  (:import java.io.ByteArrayOutputStream
           java.text.DecimalFormat
           java.util.UUID))

(def parser (f/formatter "MM/dd/YYYY"))
(defn date->str [t] 
  (f/unparse parser t))

(defn distribute [nums]
  (let [sum (reduce + 0 nums)]
    (map #(* 100 (/ % sum)) nums)))

(defn split-memo [memo]
  (str/join "\n"
            (reverse
             (take 6
                   (concat
                    (reduce
                     (fn [[line & rest ] word]
                       (let [line (or line "")]
                         (if (> (+ (count line) (count word)) 50)
                           (concat [word line] rest)
                           (concat [(str line " " word)]
                                   rest))))
                     []
                     (str/split memo #" "))
                    (repeat ""))))))

(defn make-check-pdf [check]
  (let [output-stream (ByteArrayOutputStream.)]
    (pdf/pdf
     [{:left-margin 25 :right-margin 0 :top-margin 0 :bottom-margin 0 :size :letter}
      (let [{:keys [bank-account paid-to client check date amount memo] {print-as :vendor/print-as vendor-name :vendor/name :as vendor} :vendor} check
            df (DecimalFormat. "#,###.00") 
            word-amount (num->words amount)
            
            amount (str "--" (.format df  amount) "--")]
            
        [:table {:num-cols 12 :border false :leading 11 :widths (distribute [2 3 3 3 3 3 3 3 3 2 2 2])}
         [(let [{:keys [:client/name] {:keys [:address/street1 :address/street2 :address/city :address/state :address/zip]} :client/address} client]
            
            [:cell {:colspan 4 } [:paragraph {:leading 14} name "\n" street1 "\n" (str city ", " state " " zip)] ])
          (let [{:keys [:bank-account/bank-name :bank-account/bank-code] } bank-account]
            [:cell {:colspan 6 :align :center} [:paragraph {:style :bold} bank-name] [:paragraph {:size 8 :leading 8} bank-code]])
          [:cell {:colspan 2 :size 13}
           check]]

         [[:cell {:colspan 9}]
          [:cell {:colspan 3 :leading -10} date]] 
         [[:cell {:colspan 12 :size 14}]
          ]

         [[:cell {:size 13 :leading 13} "PAY"]
          [:cell {:size 8 :leading 8 }  "TO THE ORDER OF"]
          [:cell {:colspan 7} (if (seq print-as)
                                print-as
                                vendor-name)]
          [:cell {:colspan 3} amount]]

         [[:cell {}]
          [:cell {:colspan 8}  (str " -- " word-amount " " (str/join "" (take (max
                                                                           2
                                                                           (- 95
                                                                                  (count word-amount)))
                                                                          (repeat "-"))))
           [:line {:line-width 0.15 :color [50 50 50]}]]
          [:cell {:colspan 3}]]

         
         [[:cell {:size 9 :leading 11.5} "\n\n\n\n\nMEMO"]
          [:cell {:colspan 5 :leading 11.5} (split-memo memo) 
           [:line {:line-width 0.15 :color [50 50 50]}]]
          [:cell {:colspan 6  }  (if (:client/signature-file client)
                                                        [:image { :top-margin 90 :xscale 0.30 :yscale 0.30 :align :center}

                                                         (:client/signature-file client)]
                                                        [:spacer])]]

         #_[
          #_[:cell {:colspan 5} #_memo ]
          #_[:cell {:colspan 6}]]

         [[:cell {:colspan 2}]
          [:cell {:colspan 10 :leading 30}
           [:phrase {:size 18 :ttf-name "public/micrenc.ttf"} (str "c" check "c a" (:bank-account/routing bank-account) "a " (:bank-account/number bank-account) "c")]]]
         
         [[:cell {:colspan 12 :leading 18} [:spacer]]]
         
         [[:cell]
          (into
           [:cell {:colspan 9}]
           (let [{:keys [:client/name]
                  {:keys [:address/street1 :address/street2 :address/city :address/state :address/zip ]} :client/address} client]
             (filter identity
                     (list
                      [:paragraph "  " name]
                      [:paragraph "  " street1]
                      (when (not (str/blank? street2)) 
                            [:paragraph "  " street2])
                      [:paragraph "  " city ", " state " " zip]))))
          [:cell {:colspan 2 :size 13}
           check]]

         [[:cell {:colspan 12 :leading 74} [:spacer]]]

         [[:cell]
          [:cell {:colspan 5}  [:paragraph 
                                "             " vendor-name "\n"
                                "             " (:address/street1 (:vendor/address vendor)) "\n"
                                (when (not (str/blank? (:address/street2 (:vendor/address vendor))))
                                  (str "             " (:address/street2 (:vendor/address vendor)) "\n")
                                  )
                                "             " (:address/city (:vendor/address vendor)) ", " (:address/state (:vendor/address vendor)) " " (:address/zip (:vendor/address vendor))]]
          [:cell {:align :right}
           "Paid to:\n"
           "Amount:\n"
           "Date:\n"]

          [:cell {:colspan 5}
           [:paragraph paid-to]
           [:paragraph amount]
           [:paragraph date]]]

         [[:cell {:colspan 3} "Memo:"]
          [:cell {:colspan 9} memo]]

         [[:cell {:colspan 12} [:spacer]]]
         [[:cell {:colspan 12} [:spacer]]]
         [[:cell {:colspan 12} [:spacer]]]
         [[:cell {:colspan 12} [:spacer]]]

         [[:cell {:colspan 5}] 
          [:cell {:align :right :colspan 2}
           "Check:\n"
           "Vendor:\n"
           "Company:\n"
           "Bank Account:\n"
           "Paid To:\n"
           "Amount:\n"
           "Date:\n"]

          [:cell {:colspan 5}
           [:paragraph check]
           [:paragraph vendor-name]
           [:paragraph (:client/name client)]
           [:paragraph (:bank-account/bank-name bank-account)]
           [:paragraph paid-to]
           [:paragraph amount]
           [:paragraph date]]]
         [[:cell {:colspan 3} "Memo:"]
          [:cell {:colspan 9} memo]]
         ])]
     output-stream)
    (.toByteArray output-stream)))

(defn make-pdfs [checks]
  (loop [[check & checks] checks]
    (when check
      (s3/put-object :bucket-name (:data-bucket env)
                     :key (:payment/s3-key check)
                     :input-stream (-> check
                                       :payment/pdf-data
                                       (edn/read-string)



                                       make-check-pdf
                                       (io/make-input-stream {}))
                     :metadata {:content-type "application/pdf"})
      (recur checks))))

(defn merge-pdfs [keys]
  (let [merged-pdf-stream (java.io.ByteArrayOutputStream.)
        uuid (str (UUID/randomUUID))]
    (apply pdf/collate (concat [{:size :letter} merged-pdf-stream] (->> keys
                                                        (map #(s3/get-object (:data-bucket env) %))
                                                        (map :input-stream))))
    (s3/put-object :bucket-name (:data-bucket env)
                   :key (str "merged-checks/" uuid ".pdf")
                   :input-stream (io/make-input-stream (.toByteArray merged-pdf-stream) {})
                   :metadata {:content-length (count (.toByteArray merged-pdf-stream))
                              :content-type "application/pdf"})
    (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/merged-checks/" uuid ".pdf")))



(defmulti invoices->entities (fn [invoices vendor-id client bank-account type index invoice-amounts]
                               type))
(defn invoice-payments [invoices invoice-amounts]
  (->> (for [invoice invoices
                :let [invoice-amount (invoice-amounts (:db/id invoice))]]
            [{:invoice-payment/payment (-> invoice :invoice/vendor :db/id str)
              :invoice-payment/amount invoice-amount
              :invoice-payment/invoice (:db/id invoice)}
             [:pay (:db/id invoice) invoice-amount]])
       (reduce into [])))

(defn base-payment [invoices vendor client bank-account type index invoice-amounts]
  {:db/id (str (:db/id vendor))
   :payment/bank-account (:db/id bank-account)
   :payment/amount (reduce + 0 (map (comp invoice-amounts :db/id) invoices))
   :payment/vendor (:db/id vendor)
   :payment/client (:db/id client)
   :payment/date (c/to-date (time/now)) 
   :payment/invoices (map :db/id invoices)})

(defmethod invoices->entities :payment-type/check [invoices vendor client bank-account type index invoice-amounts]
  (let [uuid (str (UUID/randomUUID))
        memo (str "Invoice #'s: "
                  (str/join ", "
                            (map (fn [i]
                                   (str (:invoice/invoice-number i) "(" (invoice-amounts (:db/id i)) ")"))
                                 invoices)))
        base-payment (base-payment invoices vendor client bank-account type index invoice-amounts)
        payment
        (remove-nils 
         (assoc base-payment
                :payment/s3-uuid (when (> (:payment/amount base-payment) 0) uuid)
                :payment/s3-key (when (> (:payment/amount base-payment) 0) (str "checks/" uuid ".pdf")) 
                :payment/s3-url (when (> (:payment/amount base-payment) 0) (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/checks/" uuid ".pdf"))
                :payment/check-number (+ index (:bank-account/check-number bank-account))
                :payment/type :payment-type/check
                :payment/memo  memo
                :payment/status  :payment-status/pending
                :payment/pdf-data (pr-str {:vendor vendor
                                           :paid-to (or (:vendor/paid-to vendor) (:vendor/name vendor))
                                           :amount (reduce + 0 (map (comp invoice-amounts :db/id) invoices))
                                           :check (str (+ index (:bank-account/check-number bank-account)))
                                           :memo  memo
                                           :date (date->str (local-now))
                                           :client (dissoc client :client/bank-accounts)
                                           :bank-account (dissoc bank-account :bank-account/start-date)
                                           #_#_:client {:name (:name client)
                                                        :address (:address client)
                                                        :signature-file (:signature-file client)
                                                        :bank {:name (:bank-account/bank-name bank-account)
                                                               :acct (:bank-account/bank-code bank-account)
                                                               :routing (:bank-account/routing bank-account)
                                                               :acct-number (:bank-account/number bank-account)}}})))]
    
    (-> []
        (conj payment)
        (into (invoice-payments invoices invoice-amounts)))))


(defmethod invoices->entities :payment-type/debit [invoices vendor client bank-account type index invoice-amounts]
  (let [payment (assoc (base-payment invoices vendor client bank-account type index invoice-amounts)
                       :payment/type :payment-type/debit
                       :payment/memo  (str "Debit Invoice #'s: "
                                           (str/join ", "
                                                     (map (fn [i]
                                                            (str (:invoice/invoice-number i) "(" (invoice-amounts (:db/id i)) ")"))
                                                          invoices)))
                       :payment/status :payment-status/pending)]
    (-> []
        (conj payment)
        (into (invoice-payments invoices invoice-amounts)))))

(defmethod invoices->entities :payment-type/cash [invoices vendor client bank-account type index invoice-amounts]
  (let [payment (assoc (base-payment invoices vendor client bank-account type index invoice-amounts)
                       :payment/type :payment-type/cash
                       :payment/memo  (str "Cash Invoice #'s: "
                                           (str/join ", "
                                                     (map (fn [i]
                                                            (str (:invoice/invoice-number i) "(" (invoice-amounts (:db/id i)) ")"))
                                                          invoices)))
                       :payment/status :payment-status/cleared)]
    (-> []
        (conj payment)
        (into (invoice-payments invoices invoice-amounts)))))

(defn validate-belonging [client-id invoices bank-account]
  (when-not (apply = client-id (map (comp :db/id :invoice/client) invoices ))
    (throw (ex-info "You can't pay for that invoice from this bank account."
                    {:validation-error "You can't pay for that invoice from this bank account."
                     :client-id client-id
                     :invoices (map :invoice/invoice-number invoices)}))
    )
  (when-not (= client-id (:db/id (:client/_bank-accounts bank-account)))
      (throw (ex-info "The selected bank doesn't belong to this client"
                      {:validation-error "The selected bank doesn't belong to this client"
                       :client-id client-id
                       :invoices (map :invoice/invoice-number invoices)}))))

(defn print-checks [invoice-payments client-id bank-account-id type id]
  (let [type (keyword "payment-type" (name type))
        invoices (d-invoices/get-multi (map :invoice-id invoice-payments))
        client (d-clients/get-by-id client-id)
        vendors (by :db/id (d-vendors/get-graphql {}))
        invoice-amounts (by :invoice-id :amount invoice-payments)
        invoices-grouped-by-vendor (group-by (comp :db/id :invoice/vendor) invoices)
        bank-account (d-bank-accounts/get-by-id bank-account-id)
        _ (validate-belonging client-id invoices bank-account)
        _ (when (and (nil? (:bank-account/check-number bank-account))
                     (= type :payment-type/check))
            (let [message (str "The bank account " (:bank-account/name bank-account) " does not have a starting check number. Please ask the integreat staff to initialize it.")]
              (throw (ex-info message
                              {:validation-error message}))))

        _ (if (->> invoices-grouped-by-vendor
                   vals
                   (map (fn [payments]
                          (->> payments
                               (map (comp invoice-amounts :db/id))
                               (reduce + 0.0))))
                   (filter #(<= % 0.0))
                   seq)
            (throw (ex-info "The selected invoices do not have an outstanding balance."
                            {:validation-error "The selected invoices do not have an outstanding balance."})))
        checks (->> (for [[[vendor-id invoices] index] (map vector invoices-grouped-by-vendor (range))]
                      (invoices->entities invoices (vendors vendor-id) client bank-account type index invoice-amounts))
                    (reduce into [])
                    doall)
        checks (if (= type :payment-type/check)
                 (conj checks [:inc (:db/id bank-account) :bank-account/check-number (count invoices-grouped-by-vendor)])
                 checks)]
    (when (= type :payment-type/check)
      (make-pdfs (filter #(and (= :payment-type/check (:payment/type %))
                               (> (:payment/amount %) 0.0))
                         checks)))
    (audit-transact checks id)
    
    
    {:invoices  (d-invoices/get-multi (map :invoice-id invoice-payments))
     :pdf-url (if (= type :payment-type/check)
                (merge-pdfs (filter identity (map :payment/s3-key checks)))
                nil)}))

(defn get-payment-page [context args value]
  (let [args (assoc args :id (:id context))
        [payments checks-count] (d-checks/get-graphql  (update (<-graphql args) :payment-type enum->keyword  "payment-type"))]
    
    [{:payments (->> payments
                     (map (fn [payment]
                            (if (seq (:transaction/_payment payment))
                              (-> payment
                                  (set/rename-keys {:transaction/_payment :transaction})
                                  (update :transaction first))
                              payment)))
                     (map ->graphql ))
      :total checks-count
      :count (count payments)
      :start (:start args 0)
      :end (+ (:start args 0) (count payments))}]))

(defn get-potential-payments [context args value]
  (let [transaction (d-transactions/get-by-id (:transaction_id args))
        _ (assert-can-see-client (:id context) (:transaction/client transaction))
        [payments payments-count] (d-checks/get-graphql {:client-id (:db/id (:transaction/client transaction))
                                                         :bank-account-id (:db/id (:transaction/bank-account transaction)) 
                                                         :amount (- (:transaction/amount transaction))
                                                         :status :payment-status/pending})]
    (map ->graphql payments)))

(defn add-handwritten-check [context args value]
  (let [invoices (d-invoices/get-multi (map :invoice_id (:invoice_payments args))) 
        bank-account-id (:bank_account_id args)
        bank-account (d-bank-accounts/get-by-id bank-account-id)
        _ (doseq [invoice invoices]
            (assert-can-see-client  (:id context) (:invoice/client invoice)))
        _ (validate-belonging (:db/id (:client/_bank-accounts bank-account)) invoices bank-account)
        invoice-payment-lookup (by :invoice_id :amount (:invoice_payments args))
        base-payment (base-payment invoices
                                   (:invoice/vendor (first invoices))
                                   (:invoice/client (first invoices))
                                  bank-account
                                  :payment-type/check
                                  0
                                  invoice-payment-lookup)]
    
    (audit-transact 
     (into [(assoc base-payment
                   :payment/type :payment-type/check
                   :payment/status :payment-status/pending
                   :payment/check-number (:check_number args)
                   :payment/date (c/to-date (parse (:date args) iso-date)))]
           (invoice-payments invoices invoice-payment-lookup))
     (:id context))
    (->graphql
     {:s3-url nil
      :invoices (d-invoices/get-multi (map :invoice_id (:invoice_payments args)))})))


(defn void-check [context {id :payment_id} value]
  (let [check (d-checks/get-by-id id)]
    (assert (or (= :payment-status/pending (:payment/status check))
                (#{:payment-type/cash :payment-type/debit} (:payment/type check))))
    (assert-can-see-client (:id context) (:db/id (:payment/client check)))
    (let [removing-payments (mapcat (fn [x]
                                      (let [invoice (:invoice-payment/invoice x)
                                            new-balance (+ (:invoice/outstanding-balance invoice)
                                                                          (:invoice-payment/amount x))]
                                        [[:db.fn/retractEntity (:db/id x)]
                                         {:db/id (:db/id invoice)
                                          :invoice/outstanding-balance new-balance
                                          :invoice/status (if (dollars-0? new-balance)
                                                            (:invoice/status invoice)
                                                            :invoice-status/unpaid)}]))
                                 (:payment/invoices check))
          updated-payment {:db/id id
                           :payment/amount 0.0
                           :payment/status :payment-status/voided}]
      
      (audit-transact (conj removing-payments updated-payment)
                      (:id context)))
    
    (-> (d-checks/get-by-id id)
        (->graphql))))
