(ns auto-ap.graphql.transactions
  (:require [auto-ap.datomic
             :refer
             [audit-transact audit-transact-batch conn remove-nils]]
            [auto-ap.datomic.accounts :as a]
            [auto-ap.datomic.checks :as d-checks]
            [auto-ap.datomic.transaction-rules :as tr]
            [auto-ap.datomic.transactions :as d-transactions]
            [auto-ap.graphql.transaction-rules :as g-tr]
            [auto-ap.yodlee.import :as import]
            [auto-ap.graphql.utils
             :refer
             [->graphql
              <-graphql
              assert-admin
              assert-power-user
              assert-can-see-client
              enum->keyword
              ident->enum-f
              snake->kebab]]
            [auto-ap.rule-matching :as rm]
            [auto-ap.utils :refer [dollars=]]
            [clj-time.coerce :as coerce]
            [clojure.set :as set]
            [clojure.string :as str]
            [clojure.tools.logging :as log]
            [datomic.api :as d]
            [auto-ap.datomic.invoices :as d-invoices]))

(def approval-status->graphql (ident->enum-f :transaction/approval-status))

(defn get-transaction-page [context args value]
  (let [args (assoc (:filters args) :id (:id context))
        [transactions transactions-count] (d-transactions/get-graphql (update (<-graphql args) :approval-status enum->keyword  "transaction-approval-status"))
        transactions (map ->graphql (map approval-status->graphql transactions))]
    {:data transactions
     :total transactions-count
     :count (count transactions)
     :start (:start args 0)
     :end (+ (:start args 0) (count transactions))}))

(defn bulk-change-status [context args value]
  (let [_ (assert-admin (:id context))
        args (assoc args :id (:id context))
        ids (some-> (:filters args)
                    (assoc :id (:id context))
                    (<-graphql)
                    (update :approval-status enum->keyword  "transaction-approval-status")
                    (assoc :per-page Integer/MAX_VALUE)
                    (d-transactions/raw-graphql-ids )
                    :ids)
        specific-ids (d-transactions/filter-ids (:ids args))
        all-ids (into (set ids) specific-ids)]

    (log/info "Unapproving " (count all-ids) args)

    (audit-transact-batch 
     (mapv (fn [i]
             {:db/id i
              :transaction/approval-status (enum->keyword (:status args) "transaction-approval-status")})
           all-ids)
     (:id context))
    {:message (str "Succesfully changed " (count all-ids) " transactions to be " (name (:status args) ) ".")}))


(defn delete-transactions [context args value]
  (let [_ (assert-admin (:id context))
        args (assoc args :id (:id context))
        ids (some-> (:filters args)
                    (<-graphql)
                    (update :approval-status enum->keyword  "transaction-approval-status")
                    (assoc :per-page Integer/MAX_VALUE)
                    (d-transactions/raw-graphql-ids )
                    :ids)
        specific-ids (d-transactions/filter-ids (:ids args))
        all-ids (into (set ids) specific-ids)]

    (log/info "Deleting " (count all-ids) args)
    (audit-transact-batch
     (mapcat (fn [i]
               [[:db/retractEntity i]
                [:db/retractEntity [:journal-entry/original-entity i]]])
             all-ids)
     (:id context))
    {:message (str "Succesfully deleted " (count all-ids) " transactions.")}))

(defn get-potential-autopay-invoices-matches [context args value]
  (assert-power-user (:id context))

  (let [transaction (d-transactions/get-by-id (:transaction_id args))
        _ (assert-can-see-client (:id context) (:transaction/client transaction) )]
    
    (let [matches-set (import/match-transaction-to-unfulfilled-autopayments (:transaction/amount transaction)
                                                                                    (:db/id (:transaction/client transaction)))]
      (->graphql (for [matches matches-set]
                   (for [[_ invoice-id ] matches]
                                             (d-invoices/get-by-id invoice-id)))))))

(defn get-potential-unpaid-invoices-matches [context args value]
  (assert-power-user (:id context))
  (let [transaction (d-transactions/get-by-id (:transaction_id args))
        _ (assert-can-see-client (:id context) (:transaction/client transaction) )]
    
    (let [matches-set (import/match-transaction-to-unpaid-invoices (:transaction/amount transaction)
                                                                   (:db/id (:transaction/client transaction)))]
      (->graphql (for [matches matches-set]
                   (for [[_ invoice-id ] matches]
                     (d-invoices/get-by-id invoice-id)))))))

(defn unlink-transaction [context args value]
  (let [_ (assert-power-user (:id context))
        
        args (assoc args :id (:id context))
        transaction-id (:transaction_id args)
        transaction (d/pull (d/db conn)
                            [:transaction/approval-status
                             :transaction/status
                             :transaction/location
                             :transaction/vendor
                             :transaction/accounts
                             :transaction/client [:db/id]
                             
                             {:transaction/payment [{:payment/status [:db/ident]} :db/id]} ]
                            transaction-id)
        _ (assert-can-see-client (:id context) (:transaction/client transaction) )
        _ (log/info "Unlinking" transaction)
        payment (-> transaction :transaction/payment ) 
        is-autopay-payment? (some->> (doto (d/query {:query {:find ['?sp]
                                                             :in ['$ '?payment]
                                                             :where ['[?ip :invoice-payment/payment ?payment]
                                                                     '[?ip :invoice-payment/invoice ?i]
                                                                     '[(get-else $ ?i :invoice/scheduled-payment "N/A") ?sp]]}
                                                     :args [(d/db conn) (:db/id payment)]})
                                       log/info)
                                     seq
                                     (map first)
                                     (every? #(instance? java.util.Date %)))
        ]

    (log/info (:db/id payment))

    (log/info "Unlinking " transaction-id " from payment " payment " - Deleting the payment due to autopay? " is-autopay-payment?)
    (when (not= :payment-status/cleared (-> payment :payment/status :db/ident))
      (throw (ex-info "Payment can't be undone because it isn't cleared." {:validation-error "Payment can't be undone because it isn't cleared."})))
    (if is-autopay-payment?
      (audit-transact
       (->> [{:db/id (:db/id payment)
              :payment/status :payment-status/pending}
             {:db/id transaction-id
              :transaction/approval-status :transaction-approval-status/unapproved}

             [:db/retractEntity (:db/id payment) ]
             [:db/retract transaction-id :transaction/payment (:db/id payment)]
             [:db/retract transaction-id :transaction/vendor (:db/id (:transaction/vendor transaction))]
             [:db/retract transaction-id :transaction/location (:transaction/location transaction)]]
            (into (map (fn [a]
                         [:db/retract transaction-id :transaction/accounts (:db/id a)])
                       (:transaction/accounts transaction)))
            (into (map (fn [[invoice-payment]]
                         [:db/retractEntity invoice-payment])
                       (d/query {:query {:find ['?ip]
                                         :in ['$ '?p]
                                         :where ['[?ip :invoice-payment/payment ?p]]}
                                 :args [(d/db conn) (:db/id payment)]} ))))
       (:id context))
      (audit-transact
       (into [{:db/id (:db/id payment)
               :payment/status :payment-status/pending}
              {:db/id transaction-id
               :transaction/approval-status :transaction-approval-status/unapproved}
              [:db/retract transaction-id :transaction/payment (:db/id payment)]
              [:db/retract transaction-id :transaction/vendor (:db/id (:transaction/vendor transaction))]
              [:db/retract transaction-id :transaction/location (:transaction/location transaction)]]

             (map (fn [a]
                    [:db/retract transaction-id :transaction/accounts (:db/id a)])
                  (:transaction/accounts transaction)))
       (:id context)))
    (-> (d-transactions/get-by-id transaction-id)
        approval-status->graphql
        ->graphql)))


(defn transaction-account->entity [{:keys [id account_id amount location]}]
  (remove-nils #:transaction-account {:amount amount
                                      :db/id id
                                      :account account_id
                                      :location location}))


(defn deleted-accounts [transaction accounts]
  (let [current-accounts (:transaction/accounts transaction)
        specified-ids (->> accounts
                           (map :id)
                           set)
        existing-ids (->> current-accounts
                          (map :db/id)
                          set)]
    (set/difference existing-ids specified-ids)))

(defn assert-valid-expense-accounts [accounts]
  (doseq [trans-account accounts
          :let [account (d/entity (d/db conn) (:account_id trans-account))]]
    (when (empty? (:location trans-account))
      (throw (ex-info "Account is missing location" {:validation-error "Account is missing location"})))

    (when (and (not (empty? (:account/location account)))
               (not= (:location trans-account)
                     (:account/location account)))
      (let [err (str "Account uses location '" (:location trans-account) "' but expects '" (:account/location account) "'")]
        (throw (ex-info err
                {:validation-error err}))))

    (when (and (empty? (:account/location account))
               (= "A" (:location trans-account)))
      (let [err (str "Account uses location '" (:location trans-account) "', which is reserved for liabilities, equities, and assets.")]
        (throw (ex-info err
                        {:validation-error err}))))


    (when (nil? (:account_id trans-account))
      (throw (ex-info "Account is missing account" {:validation-error "Account is missing account"})))))

(defn edit-transaction [context {{:keys [id accounts vendor_id approval_status forecast_match] :as transaction} :transaction} value]
  (let [existing-transaction (d-transactions/get-by-id id)
        _ (assert-can-see-client (:id context) (:transaction/client existing-transaction) )
        _ (assert-valid-expense-accounts accounts)
        deleted (deleted-accounts existing-transaction accounts)
        account-total (reduce + 0 (map (fn [x] (:amount x)) accounts))
        missing-locations  (seq (set/difference
                                 (->> accounts
                                      (map :location)
                                      set)
                                 (-> (:transaction/client existing-transaction)
                                     :client/locations
                                     set
                                     (conj "A")
                                     (conj "HQ"))))]
     
    (when-not (dollars= (Math/abs (:transaction/amount existing-transaction)) account-total) 
      (let [error (str "Expense account total (" account-total ") does not equal transaction total (" (Math/abs (:transaction/amount existing-transaction)) ")")]
          (throw (ex-info error {:validation-error error}))))
    (when missing-locations
      (throw (ex-info (str "Location '" (str/join ", " missing-locations) "' not found on client.") {})) )
    
    (audit-transact (concat [(remove-nils {:db/id id
                                           :transaction/vendor vendor_id
                                           :transaction/approval-status (some->> approval_status
                                                                                 name
                                                                                 snake->kebab
                                                                                 (keyword "transaction-approval-status"))
                                           :transaction/accounts (map transaction-account->entity accounts)
                                           })
                             ]
                            (cond forecast_match
                                  [[:db/add id :transaction/forecast-match forecast_match]]

                                  (:db/id (:transaction/forecast-match existing-transaction))
                                  [[:db/retract id :transaction/forecast-match (:db/id (:transaction/forecast-match existing-transaction))]]

                                  :else
                                  [])

                            (map (fn [d]
                                   [:db/retract id :transaction/accounts d])
                                 deleted))
                    (:id context))
    (-> (d-transactions/get-by-id id)
        approval-status->graphql
        ->graphql)))

(defn match-transaction [context {:keys [transaction_id payment_id]} value]
  (let [transaction (d-transactions/get-by-id transaction_id)
        payment (d-checks/get-by-id payment_id)
        _ (assert-can-see-client (:id context) (:transaction/client transaction) )
        _ (assert-can-see-client (:id context) (:payment/client payment) )]
    (when (not= (:db/id (:transaction/client transaction))
                (:db/id (:payment/client payment)))
      (throw (ex-info "Clients don't match" {:validation-error "Payment and client do not match."})))

    (when-not (dollars= (- (:transaction/amount transaction))
                        (:payment/amount payment))
      (throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"})))
    (audit-transact (into 
                     [{:db/id (:db/id payment)
                       :payment/status :payment-status/cleared
                       :payment/date (coerce/to-date (first (sort [(:payment/date payment)
                                                                   (:transaction/date transaction)])))}

                      {:db/id (:db/id transaction)
                       :transaction/payment (:db/id payment)
                       :transaction/vendor (:db/id (:payment/vendor payment))
                       :transaction/location "A"
                       :transaction/approval-status :transaction-approval-status/approved
                       :transaction/accounts [{:transaction-account/account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"]))
                                               :transaction-account/location "A"
                                               :transaction-account/amount (Math/abs (:transaction/amount transaction))}]}]
                     (map (fn [x] [:db/retractEntity (:db/id x)] )
                          (:transaction/accounts transaction)))
                    (:id context)))
  (-> (d-transactions/get-by-id transaction_id)
      approval-status->graphql
      ->graphql))

(defn match-transaction-autopay-invoices [context {:keys [transaction_id autopay_invoice_ids]} value]
  (let [_ (assert-power-user (:id context))
        transaction (d-transactions/get-by-id transaction_id)
        _ (assert-can-see-client (:id context) (:transaction/client transaction) )
        db (d/db conn)
        invoice-clients  (set (map (comp :db/id :invoice/client #(d/entity db %)) autopay_invoice_ids))
        invoice-amount  (reduce + 0.0 (map (comp :invoice/total #(d/entity db %)) autopay_invoice_ids))]
    (when (:transaction/payment transaction)
      (throw (ex-info "Transaction already linked" {:validation-error "Transaction already linked"})))
    (when (or (> (count invoice-clients) 1)
              (not= (:db/id (:transaction/client transaction))
                    (first invoice-clients)))
      (throw (ex-info "Clients don't match" {:validation-error "Invoice(s) and transaction client do not match."})))

    (when-not (dollars= (- (:transaction/amount transaction))
                        invoice-amount)
      (throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"})))
    #_(log/info [#_(select-keys (d/entity db transaction_id) #{:transaction/amount :db/id})]
              (->> autopay_invoice_ids
                  (map (fn [id]
                         (let [entity (d/entity db id)]
                           [(-> entity :invoice/vendor :db/id)
                            (-> entity :db/id)
                            (-> entity :invoice/total)])))))
    (let [payment-tx (import/add-new-payment [(select-keys (d/entity db transaction_id) #{:transaction/amount :transaction/date :db/id})]
                                             (map (fn [id]
                                                    (let [entity (d/entity db id)]
                                                      [(-> entity :invoice/vendor :db/id)
                                                       (-> entity :db/id)
                                                       (-> entity :invoice/total)]))
                                                  autopay_invoice_ids)
                                             (:db/id (:transaction/bank-account transaction))
                                             (:db/id (:transaction/client transaction)))]
      (log/info "Adding a new payment" payment-tx)
      @(d/transact conn payment-tx))
    
    (-> (d-transactions/get-by-id transaction_id)
        approval-status->graphql
        ->graphql)))

(defn match-transaction-unpaid-invoices [context {:keys [transaction_id unpaid_invoice_ids]} value]
  (let [_ (assert-power-user (:id context))
        transaction (d-transactions/get-by-id transaction_id)
      
        _ (assert-can-see-client (:id context) (:transaction/client transaction) )
        db (d/db conn)
        invoice-clients  (set (map (comp :db/id :invoice/client #(d/entity db %)) unpaid_invoice_ids))
        invoice-amount  (reduce + 0.0 (map (comp :invoice/outstanding-balance #(d/entity db %)) unpaid_invoice_ids))]
    (when (or (> (count invoice-clients) 1)
              (not= (:db/id (:transaction/client transaction))
                    (first invoice-clients)))
      (throw (ex-info "Clients don't match" {:validation-error "Invoice(s) and transaction client do not match."
                                             :invoice-clients (str invoice-clients)})))

    (when-not (dollars= (- (:transaction/amount transaction))
                        invoice-amount)
      (throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"})))
    (when (:transaction/payment transaction)
      (throw (ex-info "Transaction already linked" {:validation-error "Transaction already linked"})))
    
    (let [payment-tx (import/add-new-payment [(select-keys (d/entity db transaction_id) #{:transaction/amount :transaction/date :db/id})]
                                             (map (fn [id]
                                                    (let [entity (d/entity db id)]
                                                      [(-> entity :invoice/vendor :db/id)
                                                       (-> entity :db/id)
                                                       (-> entity :invoice/total)]))
                                                  unpaid_invoice_ids)
                                             (:db/id (:transaction/bank-account transaction))
                                             (:db/id (:transaction/client transaction)))]
      (log/info "Adding a new payment" payment-tx)
      @(d/transact conn payment-tx))

    (-> (d-transactions/get-by-id transaction_id)
        approval-status->graphql
        ->graphql)))

(defn match-transaction-rules [context {:keys [transaction_ids transaction_rule_id all]} value]
  (let [_ (assert-admin (:id context))
        transaction_ids (if all
                          (->> (g-tr/run-transaction-rule context {:transaction_rule_id transaction_rule_id
                                                                   :count Integer/MAX_VALUE} nil)

                               (filter #(not (:payment %)))
                               (map :id ))

                          
                          transaction_ids)
        transactions (transduce
                      (comp
                       (map d-transactions/get-by-id)
                       (map #(update % :transaction/date coerce/to-date)))
                      conj
                      []
                      transaction_ids)
        transaction-rule (update (tr/get-by-id transaction_rule_id) :transaction-rule/description #(some-> %  rm/->pattern))]
    (doseq [transaction transactions]
      (when (not (rm/rule-applies? transaction transaction-rule))
        (throw (ex-info "Transaction rule does not apply" {:validation-error "Transaction rule does not apply"
                                                           :transaction-rule transaction-rule
                                                           :transaction transaction})))

      (when (:transaction/payment transaction)

        (throw (ex-info "Transaction already associated with a payment" {:validation-error "Transaction already associated with a payment"}))))
    (audit-transact (transduce
                     (map #(into 
                            [(remove-nils (rm/apply-rule {:db/id (:db/id %)
                                                          :transaction/amount (:transaction/amount %)}
                                                         transaction-rule

                                                         (or (-> % :transaction/bank-account :bank-account/locations)
                                                             (-> % :transaction/client :client/locations))))]
                            (map (fn [x] [:db/retractEntity (:db/id x)] )
                                 (:transaction/accounts %))))
                     into
                     []
                     transactions)
                    (:id context))
    )
  (transduce
   (comp
    (map d-transactions/get-by-id)
    (map approval-status->graphql)
    (map ->graphql))
   conj
   []
   transaction_ids ))
