(ns auto-ap.yodlee.import
  (:require [auto-ap.datomic :refer [audit-transact conn remove-nils uri]]
            [auto-ap.datomic.accounts :as a]
            [auto-ap.datomic.checks :as d-checks]
            [auto-ap.datomic.clients :as d-clients]
            [auto-ap.datomic.transaction-rules :as tr]
            [auto-ap.datomic.transactions :as d-transactions]
            [auto-ap.rule-matching :as rm]
            [auto-ap.time :as time]
            [auto-ap.utils :refer [by dollars=]]
            [auto-ap.yodlee.core :as client]
            [auto-ap.yodlee.core2 :as client2]
            [clj-time.coerce :as coerce]
            [clj-time.core :as t]
            [clojure.string :as str]
            [clojure.tools.logging :as log]
            [datomic.api :as d]
            [digest :refer [sha-256]]
            [mount.core :as mount]
            [unilog.context :as lc]
            [yang.scheduler :as scheduler]))

(defn rough-match [client-id bank-account-id amount]
  (if (and client-id bank-account-id amount)
    (let [[matching-checks] (d-checks/get-graphql {:client-id client-id
                                                   :bank-account-id bank-account-id 
                                                   :amount (- amount)
                                                   :status :payment-status/pending})]
      (if (= 1 (count matching-checks))
        (first matching-checks)
        nil))
    nil))


(defn transaction->existing-payment [_ check-number client-id bank-account-id amount id]
  (log/info "Searching for a matching check for "
            {:client-id client-id
             :check-number check-number
             :bank-account-id bank-account-id
             :amount amount})
  (cond (not (and client-id bank-account-id))
        nil

        (:transaction/payment (d-transactions/get-by-id [:transaction/id (sha-256 (str id))]))
        nil

        check-number
        (or (-> (d-checks/get-graphql {:client-id client-id
                                       :bank-account-id bank-account-id 
                                       :check-number check-number
                                       :amount (- amount)
                                       :status :payment-status/pending})
                first
                first)
            (rough-match client-id bank-account-id amount))

        :else
        (rough-match client-id bank-account-id amount)))



(defn match-transaction-to-unfulfilled-autopayments [amount client-id]
  (log/info "trying to find uncleared autopay invoices")
  (let [candidate-invoices-vendor-groups (->> (d/query {:query {:find ['?vendor-id '?e '?total '?sd]
                                                                :in ['$ '?client-id]
                                                                :where ['[?e :invoice/client ?client-id]
                                                                        '[?e :invoice/scheduled-payment ?sd]
                                                                        '[?e :invoice/status :invoice-status/paid]
                                                                        '(not [_ :invoice-payment/invoice ?e])
                                                                        '[?e :invoice/vendor ?vendor-id]
                                                                        '[?e :invoice/total ?total]]}
                                                        :args [(d/db conn) client-id]})
                                              (sort-by last)   ;; sort by scheduled payment date
                                              (group-by first) ;; group by vendors
                                              vals)
        considerations (for [candidate-invoices candidate-invoices-vendor-groups
                             invoice-count (range 1 32)
                             consideration (partition invoice-count 1 candidate-invoices)
                             :when (dollars= (reduce (fn [acc [_ _ amount]]
                                                       (+ acc amount)) 0.0 consideration)
                                             (- amount))]
                         consideration)]
    (log/info "Found " (count considerations) "considerations for transaction of" amount)
    considerations
    ))

(defn match-transaction-to-unpaid-invoices [amount client-id]
  (log/info "trying to find unpaid invoices for " client-id amount)
  (let [candidate-invoices-vendor-groups (->> (d/query {:query {:find ['?vendor-id '?e '?outstanding-balance '?d]
                                                                :in ['$ '?client-id]
                                                                :where ['[?e :invoice/client ?client-id]
                                                                        '[?e :invoice/status :invoice-status/unpaid]
                                                                        '(not [_ :invoice-payment/invoice ?e])
                                                                        '[?e :invoice/vendor ?vendor-id]
                                                                        '[?e :invoice/outstanding-balance ?outstanding-balance]
                                                                        '[?e :invoice/date ?d]]}
                                                        :args [(d/db conn) client-id]})
                                              (sort-by last)   ;; sort by scheduled payment date
                                              (group-by first) ;; group by vendors
                                              vals)
        considerations (for [candidate-invoices candidate-invoices-vendor-groups
                             invoice-count (range 1 32)
                             consideration (partition invoice-count 1 candidate-invoices)
                             :when (dollars= (reduce (fn [acc [_ _ amount]]
                                                       (+ acc amount)) 0.0 consideration)
                                             (- amount))]
                         consideration)]
    (log/info "Found " (count considerations) "unpaid invoice considerations for transaction of" amount)
    considerations))

(defn match-transaction-to-single-unfulfilled-autopayments [amount client-id]
  (let [considerations (match-transaction-to-unfulfilled-autopayments amount client-id)]
    (if (= 1 (count considerations))
      (first considerations)
      [])))

(defn add-new-payment [[transaction :as tx]  [[vendor] :as invoice-payments] bank-account-id client-id]
  (log/info "Adding a new payment for transaction " (:transaction/id transaction) " and invoices " invoice-payments)
  (let [payment-id (d/tempid :db.part/user)]
    (-> tx

        (conj {:payment/bank-account bank-account-id
               :payment/client client-id
               :payment/amount (- (:transaction/amount transaction))
               :payment/vendor vendor
               :payment/date (:transaction/date transaction)
               :payment/type :payment-type/debit
               :payment/status :payment-status/cleared
               :db/id payment-id})
        (into (mapcat (fn [[vendor invoice-id invoice-amount]]
                        [{:invoice-payment/invoice invoice-id
                          :invoice-payment/payment payment-id
                          :invoice-payment/amount invoice-amount}
                         {:db/id invoice-id
                          :invoice/outstanding-balance 0.0
                          :invoice/status :invoice-status/paid}])
                      invoice-payments))
        (update 0 assoc
                :transaction/payment payment-id
                :transaction/approval-status :transaction-approval-status/approved
                :transaction/vendor vendor
                :transaction/location "A")
        (conj [:reset (:db/id transaction) :transaction/accounts
               [#:transaction-account
                {:account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"]))
                 :location "A"
                 :amount (Math/abs (:transaction/amount transaction))}]]))))

(defn extract-check-number [{{description-original :original} :description}]
  
  (if-let [[_ _ check-number] (re-find #"(?i)check(card|[^0-9]+([0-9]*))" description-original)]
    (try
      (Integer/parseInt check-number)
      (catch NumberFormatException e
        nil))
    nil))

(defn find-expected-deposit [client-id amount date]
  (when date 
    (-> (d/q
         '[:find ?ed
           :in $ ?c ?a ?d-start 
           :where
           [?ed :expected-deposit/client ?c]
           (not [?ed :expected-deposit/status :expected-deposit-status/cleared])
           [?ed :expected-deposit/date ?d]
           [(>= ?d ?d-start)]
           [?ed :expected-deposit/total ?a2]
           [(auto-ap.utils/dollars= ?a2 ?a)]
           ]
         (d/db conn) client-id amount (coerce/to-date (t/plus date (t/days -10))))
        first
        first)))

(defn transactions->txs [transactions import-id transaction->bank-account apply-rules existing]
  (let [grouped-transactions (group-by
                              (fn [{post-date :postDate
                                    account-id :accountId
                                    date :date
                                    id :id
                                    status :status :as transaction}]
                                (let [bank-account (transaction->bank-account transaction)
                                      bank-account-id (:db/id bank-account)
                                      client (:client/_bank-accounts bank-account) 
                                      client-id (:db/id client)
                                      valid-locations (or (:bank-account/locations bank-account) (:client/locations client))
                                      
                                      date (time/parse date "YYYY-MM-dd")]

                                  (cond (existing (sha-256 (str id)))
                                        :extant

                                        (not client-id)
                                        :error

                                        (not= "POSTED" status)
                                        :not-posted

                                        (and (:start-date bank-account)
                                             (not (t/after? date (:start-date bank-account))))
                                        :not-ready

                                        :else
                                        :import)))
                              transactions)]

    (update grouped-transactions :import
            (fn [transactions]
              (into []

                    (for [transaction transactions
                          :let [{post-date :postDate
                                 account-id :accountId
                                 date :date
                                 id :id
                                 {amount :amount} :amount
                                 {description-original :original
                                  description-simple :simple} :description
                                 {merchant-id :id
                                  merchant-name :name} :merchant
                                 base-type :baseType
                                 type :type
                                 status :status}
                                transaction
                                amount (if (= "DEBIT" base-type)
                                         (-  amount)
                                         amount)
                                check-number (extract-check-number transaction)
                                bank-account (transaction->bank-account transaction)
                                bank-account-id (:db/id bank-account)
                                client (:client/_bank-accounts bank-account) 
                                client-id (:db/id client)
                                valid-locations (or (:bank-account/locations bank-account) (:client/locations client))
                                date (time/parse date "YYYY-MM-dd")]]
                      (let [existing-check (transaction->existing-payment transaction check-number client-id bank-account-id amount id)
                            autopay-invoices-matches (when-not existing-check
                                                       (match-transaction-to-unfulfilled-autopayments amount client-id ))
                            unpaid-invoices-matches (when-not existing-check
                                                      (match-transaction-to-unpaid-invoices amount client-id ))
                            expected-deposit (when (and (> amount 0.0)
                                                        (not existing-check))
                                               (find-expected-deposit client-id amount date))]
                        (cond->
                            [#:transaction
                             {:post-date (coerce/to-date (time/parse post-date "YYYY-MM-dd"))
                              :id (sha-256 (str id))
                              :raw-id (str id)
                              :account-id account-id
                              :date (coerce/to-date date)
                              :amount (double amount)
                              :import-batch/_entry import-id
                              :description-original (some-> description-original (str/replace #"\s+" " "))
                              :description-simple (some-> description-simple (str/replace #"\s+" " "))
                              :approval-status :transaction-approval-status/unapproved
                              :type type
                              :status status
                              :client client-id
                              :check-number check-number
                              :bank-account bank-account-id}]
                          existing-check (update 0 #(assoc % :transaction/approval-status :transaction-approval-status/approved
                                                           :transaction/payment {:db/id (:db/id existing-check)
                                                                                 :payment/status :payment-status/cleared}
                                                           :transaction/vendor (:db/id (:payment/vendor existing-check))
                                                           :transaction/location "A"
                                                           :transaction/accounts [#:transaction-account
                                                                                  {:account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"]))
                                                                                   :location "A"
                                                                                   :amount (Math/abs (double amount))}]))

                          ;; temporarily removed to automatically match autopaid invoices
                          #_(and (not existing-check)
                                 (seq autopay-invoices-matches)) #_(add-new-payment autopay-invoices-matches bank-account-id client-id) 
                          expected-deposit (update 0 #(assoc % :transaction/expected-deposit {:db/id expected-deposit
                                                                                              :expected-deposit/status :expected-deposit-status/cleared}))
                          

                          (and (not (seq autopay-invoices-matches))
                               (not (seq unpaid-invoices-matches))
                               (not expected-deposit)) (update 0 #(apply-rules % valid-locations))
                          true (update 0 remove-nils)))))))))


(defn batch-transact [transactions]
  (reduce (fn [acc batch]
            (into acc (->> batch
                           (d/transact (d/connect uri) )
                           (deref)
                           :tempids
                           vals)))
          []
          (partition-all 100 transactions)))



(defn get-existing []
  (transduce (map first) conj #{}
             (d/query {:query {:find ['?tid]
                               :in ['$]
                               :where ['[_ :transaction/id ?tid]]}
                       :args [(d/db (d/connect uri))]})))

(defn get-all-bank-accounts []
  (->> (d-clients/get-all)
       (mapcat (fn [client]
                 (->> client
                      :client/bank-accounts
                      #_(filter :bank-account/yodlee-account-id)
                      (filter :db/id)
                      (map (fn [{:keys [:db/id :bank-account/yodlee-account-id] :as bank-account}]
                             (assoc bank-account
                                    :client/_bank-accounts client))))))))
(defn start-import [source user]
  (get (:tempids @(d/transact conn [{:db/id "import-batch"
                                     :import-batch/date (coerce/to-date (t/now))
                                     :import-batch/source source
                                     :import-batch/status :import-status/started
                                     :import-batch/user-name user}])) "import-batch"))

(defn finish-import [i]
  @(d/transact conn [(assoc i :import-batch/status :import-status/completed)]))


(defn grouped-import [manual-transactions import-id]
  (lc/with-context {:source "grouped import"}
    (let [transformed-transactions (->> manual-transactions
                                        (filter #(= "posted" (:status %)))
                                        (group-by #(select-keys % [:date :description-original :amount :client-id :bank-account-id]))
                                        (vals)
                                        (mapcat (fn [transaction-group]
                                                  (map
                                                   (fn [index {:keys [date description-original high-level-category amount bank-account-id client-id] :as transaction}]
                                                     {:id (str date "-" bank-account-id "-" description-original "-" amount "-" index "-" client-id)
                                                      :bank-account-id bank-account-id
                                                      :date (time/unparse  date "YYYY-MM-dd")
                                                      :amount {:amount amount}
                                                      :description {:original description-original
                                                                    :simple high-level-category}
                                                      :status "POSTED"})
                                                   (range)
                                                   transaction-group))))
          all-rules (tr/get-all)
          all-bank-accounts (by :db/id (get-all-bank-accounts))
          transaction->bank-account (comp all-bank-accounts :bank-account-id)
          transactions (transactions->txs transformed-transactions import-id transaction->bank-account (rm/rule-applying-fn all-rules) (get-existing))]
      (log/info "Importing " (count (:import transformed-transactions)) " grouped transactions")
      
      (doseq [tx (:import transactions)]
        (audit-transact tx {:user/name "Yodlee import"
                            :user/role ":admin"}))
      (log/info "Imported  grouped transactions")
      transactions)))

(defn do-import
  ([]
   (do-import (client/get-transactions)))
  ([transactions]
   (lc/with-context {:source "Import yodlee transactions"}
     (do
       (log/info "importing from yodlee")
       (let [import-id (start-import :import-source/yodlee "Automated Yodlee User")
         all-bank-accounts (get-all-bank-accounts)
         transaction->bank-account (comp (by :bank-account/yodlee-account-id all-bank-accounts) :accountId)
         all-rules (tr/get-all)
         transactions (transactions->txs transactions import-id transaction->bank-account (rm/rule-applying-fn all-rules) (get-existing))]
       (doseq [tx (:import transactions)]
         (audit-transact tx {:user/name "Yodlee import"
                             :user/role ":admin"}))
       (finish-import {:db/id import-id
                       :import-batch/imported (count (:import transactions))
                       :import-batch/extant (count (:extant transactions))
                       :import-batch/suppressed (count (:suppresed transactions))}))))))

(defn do-import2
  ([]
   (do-import2 (client2/get-transactions "AFH")))
  ([transactions]
   (lc/with-context {:source "Import yodlee transactions"}

     (do
       (log/info "importing from yodlee2")
       (let [import-id (start-import :import-source/yodlee "Automated Yodlee User")
             all-bank-accounts (get-all-bank-accounts)
             transaction->bank-account (comp (by (comp :yodlee-account/id :bank-account/yodlee-account) all-bank-accounts) :accountId)
             all-rules (tr/get-all)
             transactions (transactions->txs transactions import-id transaction->bank-account (rm/rule-applying-fn all-rules) (get-existing))]

         (doseq [tx (:import transactions)]
           
           (audit-transact tx {:user/name "Yodlee import2"
                               :user/role ":admin"}))
         (finish-import {:db/id import-id
                         :import-batch/imported (count (:import transactions))
                         :import-batch/extant (count (:extant transactions))
                         :import-batch/suppressed (count (:suppresed transactions))}))))))




(mount/defstate import-transaction-worker
  :start (scheduler/every (* 1000 60 60 4) do-import)
  :stop (scheduler/stop import-transaction-worker))
