(ns auto-ap.graphql.invoices
  (:require [auto-ap.graphql.utils :refer [->graphql <-graphql assert-can-see-client assert-admin assert-power-user enum->keyword]]
            
            [auto-ap.datomic.vendors :as d-vendors]
            [auto-ap.datomic.clients :as d-clients]
            [auto-ap.datomic.invoices :as d-invoices]
            [auto-ap.datomic.accounts :as d-accounts]
            [auto-ap.graphql.checks :as gq-checks]
            [auto-ap.time :refer [parse iso-date]]
            [auto-ap.utils :refer [dollars=]]
            [datomic.api :as d]
            [auto-ap.datomic :refer [uri remove-nils audit-transact conn]]
            [clj-time.coerce :as coerce]
            [clj-time.core :as time]
            [clojure.set :as set]
            [clojure.tools.logging :as log]))

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

(defn get-all-invoices [context args value]
  (assert-admin (:id context))
  (map
   ->graphql
   (first (d-invoices/get-graphql (assoc (<-graphql args)
                                         :count Integer/MAX_VALUE)))))

(defn reject-invoices [context {:keys [invoices] :as in} value]
  (assert-power-user (:id context))
  (doseq [i invoices]
    (assert-can-see-client (:id context) (:db/id (:invoice/client (d/entity (d/db conn) i)))))
  (let [transactions (map (fn [i] [:db/retractEntity i ]) invoices)
        transaction-result (audit-transact transactions (:id context))]
    invoices))

(defn approve-invoices [context {:keys [invoices] :as in} value]
  (assert-power-user (:id context))
  (doseq [i invoices]
    (assert-can-see-client (:id context) (:db/id (:invoice/client (d/entity (d/db conn) i)))))
  (let [transactions (map (fn [i] {:db/id i :invoice/import-status :import-status/imported}) invoices)
        transaction-result (audit-transact transactions (:id context))]
    invoices))

(defn assert-no-conflicting [{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in}]
  (when (seq (d-invoices/find-conflicting {:invoice/invoice-number invoice_number
                                           :invoice/vendor vendor_id
                                           :invoice/client client_id}))
    (throw (ex-info (str "Invoice '" invoice_number "' already exists.") {:invoice-number invoice_number :validation-error (str "Invoice '" invoice_number "' already exists.")}))))

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

(defn add-invoice-transaction [{:keys [total invoice_number location scheduled_payment client_id vendor_id vendor_name date due expense_accounts] :as in}]
  (let [vendor (d-vendors/get-by-id vendor_id)
        account (:vendor/default-account vendor)
        due (or (and (:vendor/terms vendor)
                     (coerce/to-date
                      (time/plus date (time/days (d-vendors/terms-for-client-id vendor client_id)))))
                due)
        scheduled_payment (or scheduled_payment
                              (and (d-vendors/automatically-paid-for-client-id? vendor client_id)
                                   due))
        _ (when-not (:db/id account)
            (throw (ex-info (str "Vendor '" (:vendor/name vendor) "' does not have a default expense acount.") {:vendor-id vendor_id} )))]
    (cond-> 
        {:db/id "invoice"
         :invoice/invoice-number invoice_number
         :invoice/client client_id
         :invoice/vendor vendor_id
         :invoice/import-status :import-status/imported
         :invoice/total total
         :invoice/outstanding-balance total
         :invoice/status :invoice-status/unpaid
         :invoice/date  (coerce/to-date date)
         :invoice/expense-accounts (map expense-account->entity
                                        expense_accounts)}
      due (assoc :invoice/due (coerce/to-date due))
      
      scheduled_payment (assoc :invoice/scheduled-payment  (coerce/to-date scheduled_payment)))))


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

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

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

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


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

    (when (nil? (:amount expense-account))
      (throw (ex-info "Expense account does not have an amount specified." {:validation-error "Expense account does not have an amount specified."})))))


(defn assert-invoice-amounts-add-up [{:keys [expense_accounts total]}]
  (let [expense-account-total (reduce + 0 (map (fn [x] (:amount x)) expense_accounts))]
    (when-not (dollars= total expense-account-total) 
      (let [error (str "Expense account total (" expense-account-total ") does not equal invoice total (" total ")")]
        (throw (ex-info error {:validation-error error}))))))


(defn add-invoice [context {{:keys [total expense_accounts invoice_number location client_id vendor_id vendor_name date] :as in} :invoice} value]
  (assert-no-conflicting in)
  (assert-can-see-client (:id context) client_id)
  (assert-valid-expense-accounts expense_accounts)
  (assert-invoice-amounts-add-up in)
  
  (let [transaction-result (audit-transact [(add-invoice-transaction in)] (:id context))]
    (-> (d-invoices/get-by-id (get-in transaction-result [:tempids "invoice"]))
        (->graphql))))

(defn assert-bank-account-belongs [client-id bank-account-id]
  (when-not ((set (map :db/id (:client/bank-accounts (d-clients/get-by-id client-id)))) bank-account-id)
    (throw (ex-info (str "Bank account does not belong to client") {:validation-error "Bank account does not belong to client."} ))))

(defn add-and-print-invoice [context {{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in} :invoice bank-account-id :bank_account_id type :type} value]
  (assert-no-conflicting in)
  (assert-can-see-client (:id context) client_id)
  (assert-bank-account-belongs client_id bank-account-id)
  (assert-valid-expense-accounts (:expense_accounts in))
  (assert-invoice-amounts-add-up in)
  (let [transaction-result (audit-transact [(add-invoice-transaction in)] (:id context))]
    (-> (gq-checks/print-checks [{:invoice-id (get-in transaction-result [:tempids "invoice"])
                                  :amount total}]
                                client_id
                                bank-account-id
                                type
                                (:id context))
        ->graphql)))


(defn edit-invoice [context {{:keys [id due invoice_number total vendor_id date client_id expense_accounts scheduled_payment] :as in} :invoice} value]
  (let [invoice (d-invoices/get-by-id id)
        _ (when (seq (d-invoices/find-conflicting {:db/id id
                                                   :invoice/invoice-number invoice_number
                                                   :invoice/vendor (:db/id (:invoice/vendor invoice))
                                                   :invoice/client (:db/id (:invoice/client invoice))}))
            (throw (ex-info (str "Invoice '" invoice_number "' already exists.") {:invoice-number invoice_number})))

        
        
        paid-amount (- (:invoice/total invoice) (:invoice/outstanding-balance invoice))
        _ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))
        deleted (deleted-expense-accounts invoice expense_accounts)
        _ (assert-valid-expense-accounts expense_accounts)
        _ (assert-invoice-amounts-add-up in)
        
        
        updated-invoice (cond-> {:db/id id
                                 :invoice/invoice-number invoice_number
                                 :invoice/date (coerce/to-date date)
                                 
                                 :invoice/total total 
                                 :invoice/outstanding-balance (- total paid-amount)
                                 :invoice/expense-accounts (map expense-account->entity
                                                                expense_accounts)}
                          due (assoc :invoice/due (coerce/to-date due))
                          scheduled_payment (assoc :invoice/scheduled-payment (coerce/to-date scheduled_payment)))]
    (audit-transact (concat [updated-invoice]
                            (map (fn [d] [:db/retract id :invoice/expense-accounts d]) deleted))
                    (:id context))
    (-> (d-invoices/get-by-id id)
        (->graphql))))

(defn void-invoice [context {id :invoice_id} value]
 (let [invoice (d-invoices/get-by-id id)
  _ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))]
   (audit-transact [{:db/id id
                     :invoice/total 0.0
                     :invoice/outstanding-balance 0.0
                     :invoice/status :invoice-status/voided
                     :invoice/expense-accounts (map (fn [ea] {:db/id (:db/id ea)
                                                              :invoice-expense-account/amount 0.0})
                                                    (:invoice/expense-accounts invoice))}]
                   (:id context))

   (-> (d-invoices/get-by-id id) (->graphql))))


(defn unvoid-invoice [context {id :invoice_id} value]
  (let [invoice (d-invoices/get-by-id id)
        _ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))
        conn (d/connect uri)
        history (d/history (d/db conn))
        txs (d/query {:query {:find ['?tx '?e '?original-status '?original-outstanding '?total '?ea '?ea-amount]
                              :where ['[?e :invoice/status :invoice-status/voided ?tx true]
                                      '[?e :invoice/status ?original-status ?tx false]
                                      '[?e :invoice/outstanding-balance ?original-outstanding ?tx false]
                                      '[?e :invoice/total ?total ?tx false]
                                      '[?ea :invoice-expense-account/amount ?ea-amount ?tx false]]
                              :in ['$ '?e]}
                      :args [history id]})
        [last-transaction] (->> txs (sort-by first) (last))]
    (audit-transact [(->> txs
                          (filter (fn [[tx]] (= tx last-transaction)))
                          (reduce (fn [new-transaction [_ entity original-status original-outstanding total expense-account expense-account-amount]]
                                    (-> new-transaction 
                                        (assoc :db/id entity
                                               :invoice/total total
                                               :invoice/status original-status
                                               :invoice/outstanding-balance original-outstanding)

                                        (update :invoice/expense-accounts conj {:db/id expense-account :invoice-expense-account/amount expense-account-amount}))
                                    ) {}))]
                    (:id context))

    (-> (d-invoices/get-by-id id)
        (->graphql))))

(defn unautopay-invoice [context {id :invoice_id} value]
  (let [invoice (d/entity (d/db conn) id)
        _ (assert (:invoice/client invoice))
        _ (assert-can-see-client (:id context) (:db/id (:invoice/client invoice)))]
    (audit-transact [[:db/add id :invoice/status :invoice-status/unpaid]
                     [:db/add id :invoice/outstanding-balance (:invoice/total invoice)]
                     [:db/retract id :invoice/scheduled-payment (:invoice/scheduled-payment invoice)]]
                    (:id context))

    (-> (d-invoices/get-by-id id)
        (->graphql))))

(defn edit-expense-accounts [context args value]
  (assert-can-see-client (:id context) (:db/id (:invoice/client (d-invoices/get-by-id (:invoice_id args)))))
  (let [invoice-id (:invoice_id args)
        invoice (d-invoices/get-by-id invoice-id)
        _ (assert-valid-expense-accounts (:expense_accounts args))
        deleted (deleted-expense-accounts invoice (:expense_accounts args))
        updated {:db/id  invoice-id
                 :invoice/expense-accounts (map
                                            expense-account->entity
                                            (:expense_accounts args))}]

    (audit-transact (concat [updated]
                            (map (fn [d] [:db/retract invoice-id :invoice/expense-accounts d])deleted))
                    (:id context))
    (->graphql
     (d-invoices/get-by-id (:invoice_id args)))))

