(ns auto-ap.routes.invoices
  (:require [auto-ap.datomic.clients :as d-clients]
            [auto-ap.datomic.vendors :as d-vendors]
            [auto-ap.datomic.invoices :as d-invoices]
            [auto-ap.yodlee.import :refer [manual-import]]
            [auto-ap.utils :refer [by]]
            [auto-ap.datomic :refer [remove-nils uri]]
            [datomic.api :as d]
            [auto-ap.parse :as parse]
            [auto-ap.graphql.utils :refer [assert-admin]]
            [auto-ap.routes.utils :refer [wrap-secure]]
            [clj-time.coerce :refer [to-date]]
            [ring.middleware.json :refer [wrap-json-response]]
            [compojure.core :refer [GET POST context defroutes
                                    wrap-routes]]
            [clojure.string :as str]))

(defn reset-id [i]
  (update i :invoice-number
          (fn [n] (if (re-matches #"#+" n)
                   nil
                   n))))

(defn assoc-client-code [i]
  (-> i 
   (assoc :client-code (first (str/split (:location i) #"-" ))) 
   (assoc :default-location (second (str/split (:location i) #"-" )))))

(defn parse-client [{:keys [client-code client]} clients]
  (if-let [id (:db/id (or (clients client-code)
                       (clients client)))]
    id
    (throw (Exception. (str "Client code '" client-code "' and client named '" client "' not found.")))))

(defn parse-invoice-number [{:keys [invoice-number]}]
  (or invoice-number ""))

(defn parse-vendor [{:keys [vendor-name check]} vendors]
  (let [v (vendors vendor-name)]
    (cond v
          v

          (= "Cash" check)
          nil

          :else
          (throw (Exception. (str "Vendor '" vendor-name "' not found."))))))

(defn parse-vendor-id [{:keys [vendor]}]
  (:db/id vendor))

(defn parse-amount [i]
  (try
    (Double/parseDouble (str/replace (or  (second
                                           (re-matches #"[^0-9\.,\\-]*([0-9\.,\\-]+)[^0-9\.,]*" (:amount i)))
                                          "0")
                                     #"," ""))
    (catch Exception e
      (throw (Exception. (str "Could not parse total from value '" (:amount i) "'") e)))))

(defn parse-default-expense-account [i]
  (try
    (when-let [default-expense-account (:default-expense-account i)]
      (Integer/parseInt  default-expense-account))
    (catch Exception e
      (throw (Exception. (str "Could not parse expense account from value '" (:default-expense-account i) "'") e)))))

(defn parse-account-id [i]
  (try
    (Long/parseLong (second
                       (re-matches #"[^0-9,\\-]*([0-9,\\-]+)[^0-9,]*" (:bank-account-id i))))
    (catch Exception e
      (throw (Exception. (str "Could not parse account from value '" (:bank-account-id i) "'") e)))))



(defn parse-date [{:keys [raw-date]}]
  (try 
    (parse/parse-value :clj-time "MM/dd/yyyy" raw-date)
    (catch Exception e
      (throw (Exception. (str "Could not parse date from '" raw-date "'") e)))))

(defn parse-or-error [key f]
  (fn [x]
    (try
      (assoc x key (f x))
      (catch Exception e
        (update x :errors conj {:info (.getMessage e)
                                :details (str e)})))))

(defn parse-invoice-rows [excel-rows]
  (let [columns [:raw-date :vendor-name :check :location :invoice-number :amount :client-name :bill-entered :bill-rejected :added-on :exported-on :default-expense-account]
        all-vendors (by :vendor/name (d-vendors/get-graphql {})) 
        all-clients (d-clients/get-all)
        all-clients (merge  (by :client/code all-clients) (by :client/name all-clients))
        rows (->> (str/split excel-rows #"\n" )
                  (map #(str/split % #"\t"))
                  (map #(into {} (map (fn [c k] [k c] ) % columns)))
                  (map reset-id)
                  (map assoc-client-code)
                  (map (parse-or-error :client-id #(parse-client % all-clients)))
                  (map (parse-or-error :vendor #(parse-vendor % all-vendors)))
                  (map (parse-or-error :vendor-id #(parse-vendor-id %)))
                  (map (parse-or-error :default-expense-account parse-default-expense-account))
                  (map (parse-or-error :invoice-number parse-invoice-number))
                  (map (parse-or-error :total parse-amount))
                  (map (parse-or-error :date parse-date)))]
    rows))

(defn invoice-rows->transaction [rows]
  (->> rows
       (mapcat (fn [{:keys [vendor-id total client-id amount date invoice-number default-location default-expense-account check vendor]}]
                 (let [invoice #:invoice {:db/id (.toString (java.util.UUID/randomUUID))
                                          :vendor vendor-id
                                          :client client-id
                                          :default-location default-location
                                          :import-status :import-status/imported
                                          :default-expense-account default-expense-account
                                          :total total 
                                          :outstanding-balance (if (= "Cash" check)
                                                                 0.0
                                                                 total)
                                          :status (if (= "Cash" check)
                                                    :invoice-status/paid
                                                    :invoice-status/unpaid)
                                          :invoice-number invoice-number
                                          :date (to-date date)
                                          :expense-accounts [#:invoice-expense-account {:expense-account-id (or default-expense-account (:vendor/default-expense-account vendor))
                                                                                        :location default-location
                                                                                        :amount total}]}
                       payment (if (= :invoice-status/paid (:invoice/status invoice)) 
                                 #:invoice-payment {:invoice (:db/id invoice)
                                                    :amount (:invoice/total invoice)
                                                    :payment (remove-nils #:payment {:db/id (.toString (java.util.UUID/randomUUID))
                                                                                     :vendor (:invoice/vendor invoice)
                                                                                     :client (:invoice/client invoice)
                                                                                     :type :payment-type/cash
                                                                                     :amount (:invoice/total invoice)
                                                                                     :status :payment-status/cleared 
                                                                                     :date (:invoice/date invoice)})}
                                 )]
                   [invoice payment])))
       (filter identity)
       (map remove-nils)
       ))


(defn import-uploaded-invoice [imports]
  (let [clients (d-clients/get-all)
        _ (println imports)

        transactions (reduce (fn [result {:keys [invoice-number customer-identifier total date vendor-code text] :as info}]
                               (let [[matching-vendor default-expense-account] (->> (d/query
                                                                                     (cond-> {:query {:find ['?vendor '?default-expense-account]
                                                                                                      :in ['$ '?vendor-name]
                                                                                                      :where ['[?vendor :vendor/name ?vendor-name]
                                                                                                              '[?vendor :vendor/default-expense-account ?default-expense-account]]}
                                                                                              :args [(d/db (d/connect uri)) vendor-code]}))
                                                                                    first)
                                     matching-client (parse/best-match clients customer-identifier)
                                     _ (println "New invoice matches client" matching-client)
                                     matching-location (parse/best-location-match matching-client text )
                                     [existing-id existing-outstanding-balance existing-status import-status] (->> (d/query
                                                                                                      (cond-> {:query {:find ['?e '?outstanding-balance '?status '?import-status2]
                                                                                                                       :in ['$ '?invoice-number '?vendor '?client]
                                                                                                                       :where '[[?e :invoice/invoice-number ?invoice-number]
                                                                                                                                [?e :invoice/vendor ?vendor]
                                                                                                                                [?e :invoice/client ?client]
                                                                                                                                [?e :invoice/outstanding-balance ?outstanding-balance]
                                                                                                                                [?e :invoice/status ?status]
                                                                                                                                [?e :invoice/import-status ?import-status]
                                                                                                                                [?import-status :db/ident ?import-status2]]}
                                                                                                               :args [(d/db (d/connect uri)) invoice-number matching-vendor (:db/id matching-client)]}))
                                                                                                     first)]
                                 
                                 (if (= :import-status/imported import-status)
                                   result
                                   (conj result (remove-nils #:invoice {:invoice/client (:db/id matching-client)
                                                                        :invoice/vendor matching-vendor
                                                                        :invoice/invoice-number invoice-number
                                                                        :invoice/total (Double/parseDouble total)
                                                                        :invoice/date (to-date date)
                                                                        :invoice/import-status :import-status/pending
                                                                        :invoice/outstanding-balance (or existing-outstanding-balance (Double/parseDouble total))
                                                                        :invoice/status (or existing-status :invoice-status/unpaid)
                                                                        :invoice/expense-accounts (when-not existing-id [#:invoice-expense-account {:expense-account-id default-expense-account
                                                                                                                                                    :location matching-location
                                                                                                                                                    :amount (Double/parseDouble total)}])
                                                                        :db/id existing-id
                                                                        })))
                                 ))
                             []
                             imports)]
    
    @(d/transact (d/connect uri) transactions)
    ))

(defroutes routes
  (wrap-routes
   (context "/" []
            (context "/transactions" []
                     (POST "/batch-upload"
                           {{:keys [data]}  :edn-params user :identity}
                           (assert-admin user)
                           (let [columns [:status :raw-date :description-original :high-level-category nil nil :amount nil nil nil nil nil :bank-account-code :client-code]
                                 all-clients (d-clients/get-all)
                                 all-bank-accounts (mapcat :client/bank-accounts all-clients)
                                 all-clients (merge  (by :client/code all-clients) (by :client/name all-clients))
                                 all-bank-accounts (merge  (by :bank-account/code all-bank-accounts))
                                 rows (->> (str/split data #"\n" )
                                           (drop 1)
                                           (map #(str/split % #"\t"))
                                           (map #(into {} (filter identity (map (fn [c k] [k c] ) % columns))))
                                           
                                           (map (parse-or-error :amount parse-amount))
                                           (map (parse-or-error :date parse-date)))
                                 error-rows (filter :errors rows)
                                 _ (println "importing raw transactions" rows)
                                 
                                 raw-transactions (vec (->> rows
                                                            (filter #(not (seq (:errors %))) )
                                                            (map (fn [{:keys [description-original client-code status high-level-category amount bank-account-code date]}]
                                                                   {:description-original description-original
                                                                    :date date
                                                                    :status status
                                                                    :high-level-category high-level-category
                                                                    :amount amount
                                                                    :client-id (:db/id (all-clients client-code))
                                                                    :bank-account-id  (:db/id (all-bank-accounts bank-account-code))}))))]

                             (manual-import raw-transactions)

                             {:status 200
                              :body (pr-str {:imported (count raw-transactions)
                                             :errors (map #(dissoc % :date) error-rows)})
                              :headers {"Content-Type" "application/edn"}})))

            (context "/invoices" []
                     (POST "/upload"
                           {{  files "file"} :params :as params}
                           (let [{:keys [filename tempfile]} files]
                             (import-uploaded-invoice (parse/parse-file (.getPath tempfile) filename))
                             {:status 200
                              :body (pr-str {})
                              :headers {"Content-Type" "application/edn"}}))
                     (POST "/upload-integreat"
                           {{:keys [excel-rows]}  :edn-params user :identity}
                           (assert-admin user)
                           (let [parsed-invoice-rows (parse-invoice-rows excel-rows)
                                 existing-rows (->> (d-invoices/raw-graphql {})
                                                    (filter (fn [i] (not= :invoice-status/voided (:invoice/status i))))
                                                    (map (fn [{:keys [:invoice/vendor :invoice/client :invoice/invoice-number]}]
                                                           [(:db/id vendor) (:db/id client) invoice-number]))
                                                    set)
                                 grouped-rows (group-by
                                               (fn [i]
                                                 (cond (seq (:errors i))
                                                       :error

                                                       (existing-rows [(:vendor-id i) (:client-id i) (:invoice-number i)])
                                                       :exists

                                                       :else
                                                       :new))
                                               parsed-invoice-rows)
                                 
                                 
                                 vendors-not-found (->> parsed-invoice-rows
                                                        (filter #(and (nil? (:vendor-id %))
                                                                      (not= "Cash" (:check %))))
                                                        (map :vendor-name)
                                                        set)
                                 
                                 inserted-rows @(d/transact (d/connect uri) (invoice-rows->transaction (:new grouped-rows)))]
                             {:status 200
                              :body (pr-str {:imported (count (:new grouped-rows))
                                             :already-imported (count (:exists grouped-rows))
                                             :vendors-not-found vendors-not-found
                                             :errors (:error grouped-rows)})
                              :headers {"Content-Type" "application/edn"}}))))
   wrap-secure))
