(ns auto-ap.graphql
  (:require
    [com.walmartlabs.lacinia.util :refer [attach-resolvers]]
    [com.walmartlabs.lacinia.schema :as schema]
    [com.walmartlabs.lacinia :refer [execute]]
    [com.walmartlabs.lacinia.executor :as executor]
    [com.walmartlabs.lacinia.resolve :as resolve]
    [buddy.auth :refer [throw-unauthorized]]
    [auto-ap.utils :refer [by]]
    [auto-ap.graphql.utils :refer [assert-admin can-see-client? assert-can-see-client]]
    [auto-ap.datomic :refer [uri merge-query]]
    [datomic.api :as d]
    [auto-ap.expense-accounts :as e-expense-accounts]
    [auto-ap.datomic.clients :as d-clients]
    [auto-ap.datomic.checks :as d-checks]
    [auto-ap.datomic.users :as d-users]
    [auto-ap.datomic.invoices :as d-invoices]
    [auto-ap.datomic.vendors :as d-vendors]
    [auto-ap.graphql.users :as gq-users]
    [auto-ap.graphql.clients :as gq-clients]
    [auto-ap.graphql.vendors :as gq-vendors]
    [auto-ap.graphql.checks :as gq-checks]
    [auto-ap.graphql.expense-accounts :as expense-accounts]
    [auto-ap.graphql.invoices :as gq-invoices]
    [auto-ap.graphql.transactions :as gq-transactions]
    [clojure.walk :as walk]
    [clojure.string :as str])
  (:import
    (clojure.lang IPersistentMap)))


(def integreat-schema
  {
   :scalars {:id {
                  :parse (schema/as-conformer #(when % (Long/parseLong %)))
                  :serialize (schema/as-conformer #(.toString %))}
             :ident {:parse (schema/as-conformer (fn [x] {:db/ident x})) 
                     :serialize (schema/as-conformer #(or (:ident %) (:db/ident %) %))}}
   :objects
   {
    :client
    {:fields {:id {:type :id}
              :name {:type 'String}
              :code {:type 'String}
              :email {:type 'String}
              :address {:type :address}
              :locations {:type '(list String)}
              :bank_accounts {:type '(list :bank_account)}}}
    :contact
    {:fields {:id {:type :id}
              :name {:type 'String}
              :email {:type 'String}
              :phone {:type 'String}}}

    :bank_account
    {:fields {:id {:type :id }
              :type {:type :bank_account_type}
              :number {:type 'String}
              :routing {:type 'String}
              :code {:type 'String}
              :check_number {:type 'Int}
              :name {:type 'String}
              :bank_code {:type 'String}
              :bank_name {:type 'String}
              :yodlee_account_id {:type 'Int}}}
    :address
    {:fields {:street1 {:type 'String}
              :street2 {:type 'String}
              :city {:type 'String}
              :state {:type 'String}
              :zip {:type 'String}}}
    :vendor
    {:fields {:id {:type :id}
              :name {:type 'String}
              :code {:type 'String}

              :print_as {:type 'String}
              :primary_contact {:type :contact}
              :secondary_contact {:type :contact}
              :address {:type :address}

              :default_expense_account {:type 'Int}
              :invoice_reminder_schedule {:type 'String}}}
    :reminder
    {:fields {:id {:type 'Int}
              :email {:type 'String}
              :subject {:type 'String}
              :body {:type 'String}
              :scheduled {:type 'String}
              :sent {:type 'String}
              :vendor {:type :vendor}
              }
     }

    

    :check {:fields {:id {:type :id}
                     :type {:type 'String}
                     :amount {:type 'String}
                     :vendor {:type :vendor}
                     :date {:type 'String}
                     :bank_account {:type :bank_account}
                     :memo {:type 'String}
                     :s3_url {:type 'String}
                     :check_number {:type 'Int}
                     :status {:type 'String}
                     :invoices {:type '(list :invoice_payment)}
                     }}

    :payment {:fields {:id {:type :id}
                     :type {:type :payment_type}
                       :original_id {:type 'Int}
                     :amount {:type 'String}
                     :vendor {:type :vendor}
                     :client {:type :client}
                     :date {:type 'String}
                     :bank_account {:type :bank_account}
                     :memo {:type 'String}
                     :s3_url {:type 'String}
                     :check_number {:type 'Int}
                     :status {:type :ident}
                       :transaction {:type :transaction}
                     :invoices {:type '(list :invoice_payment)}
                     }}

    :transaction {:fields {:id {:type :id}
                           :amount {:type 'String}
                           :description_original {:type 'String}
                           :description_simple {:type 'String}
                           :status {:type 'String}
                           :client {:type :client}
                           :payment {:type :payment}
                           :bank_account {:type :bank_account}
                           :date {:type 'String}
                           :post_date {:type 'String}}}
    :invoice_payment
    {:fields {:id {:type :id}
              :amount {:type 'String}
              :invoice_id {:type 'String}
              :payment_id {:type 'String}
              :payment {:type :payment}
              :invoice {:type :invoice}}}

    :user
    {:fields {:id {:type :id}
              :name {:type 'String}
              :role {:type 'String}
              :clients {:type '(list :client)}}}

    :expense_account {:fields {:id {:type :id}
                               :location {:type 'String}
                               :name {:type 'String}
                               :parent {:type :expense_account
                                        :resolve :get-expense-account-parent}}}

    :invoices_expense_accounts
    {:fields {:id {:type :id}
              :invoice_id {:type 'String}
              :expense_account_id {:type 'Int}
              :location {:type 'String}
              :expense_account {:type :expense_account
                                :resolve :get-expense-account 
                                }
              :amount {:type 'String}}}

    :invoice
    {:fields {:id {:type :id}
              :original_id {:type 'Int}
              :total {:type 'String}
              :outstanding_balance {:type 'String}
              :invoice_number {:type 'String}
              :status {:type 'String}
              :expense_accounts {:type '(list :invoices_expense_accounts)}
              :date {:type 'String}
              :client_id {:type 'Int}
              :payments {:type '(list :invoice_payment)}
              :vendor {:type :vendor}
              :client {:type :client}}}


    :invoice_page {:fields {:invoices {:type '(list :invoice)}
                            :count {:type 'Int}
                            :total {:type 'Int}
                            :start {:type 'Int}
                            :end {:type 'Int}}}

    :payment_page {:fields {:payments {:type '(list :payment)}
                          :count {:type 'Int}
                          :total {:type 'Int}
                          :start {:type 'Int}
                          :end {:type 'Int}}}

    :transaction_page {:fields {:transactions {:type '(list :transaction)}
                                :count {:type 'Int}
                                :total {:type 'Int}
                                :start {:type 'Int}
                                :end {:type 'Int}}}

    :reminder_page {:fields {:reminders {:type '(list :reminder)}
                             :count {:type 'Int}
                             :total {:type 'Int}
                             :start {:type 'Int}
                             :end {:type 'Int}}}
    :check_result {:fields {:invoices {:type '(list :invoice)}
                            :pdf_url {:type 'String}}}

    :expense_account_stat {:fields {:expense_account_id {:type 'Int}
                                    :expense_account_name {:type 'String}
                                    :total {:type 'String}}}

    :invoice_stat {:fields {:name {:type 'String}
                            :paid {:type 'String}
                            :unpaid {:type 'String}}}
    }


   


   :queries
   {:expense_account_stats {:type '(list :expense_account_stat)
                            :args {:client_id {:type :id}}
                            :resolve :get-expense-account-stats}

    :invoice_stats {:type '(list :invoice_stat)
                    :args {:client_id {:type :id}}
                    :resolve :get-invoice-stats}

    :invoice_page {:type '(list :invoice_page)
                   :args {:import_status {:type 'String}
                          :status {:type 'String}
                          :client_id {:type :id}
                          :vendor_id {:type :id}
                          :invoice_number_like {:type 'String}
                          :start {:type 'Int}
                          :sort_by {:type 'String}
                          :asc {:type 'Boolean}}
                   
                   :resolve :get-invoice-page}

    :all_invoices {:type '(list :invoice)
                   :args {:client_id {:type :id}
                          :client_code {:type 'String}
                          :original_id {:type 'Int}
                          :statuses {:type '(list String)}}
                   :resolve :get-all-invoices}

    :all_payments {:type '(list :payment)
                   :args {:client_id {:type :id}
                          :client_code {:type 'String}
                          :original_id {:type 'Int}
                          :statuses {:type '(list String)}}
                   :resolve :get-all-payments}

    :transaction_page {:type '(list :transaction_page)
                   :args {:client_id {:type :id}
                          :bank_account_id {:type :id}
                          :start {:type 'Int}
                          :sort_by {:type 'String}
                          :asc {:type 'Boolean}}
                   
                   :resolve :get-transaction-page}

    :payment_page {:type '(list :payment_page)
                   :args {:client_id {:type :id}
                          :vendor_id {:type :id}
                          :start {:type 'Int}
                          :sort_by {:type 'String}
                          :asc {:type 'Boolean}}
                   
                   :resolve :get-payment-page}
    
    :client {:type '(list :client)
              :resolve :get-client}
    :vendor {:type '(list :vendor)
             :resolve :get-vendor}
    :user {:type '(list :user)
           :resolve :get-user}}

   :input-objects
   {
    :invoice_payment_amount {:fields {:invoice_id {:type :id}
                               :amount {:type 'Float}}}

    :edit_client {:fields {:id {:type :id}
                           :name {:type 'String}
                           :code {:type 'String}
                           :email {:type 'String}
                           :address {:type :add_address}
                           :locations {:type '(list String)}
                           :bank_accounts {:type '(list :edit_bank_account)}}}
    :edit_bank_account
    {:fields {:id {:type :id }
              :code {:type 'String}
              :type {:type :bank_account_type}
              :number {:type 'String}
              :check_number {:type 'Int}
              :name {:type 'String}
              :bank_code {:type 'String}
              :routing {:type 'String}
              :bank_name {:type 'String}
              :yodlee_account_id {:type 'Int}}}
    :edit_user
    {:fields {:id {:type :id}
              :name {:type 'String}
              :role {:type 'String}
              :clients {:type '(list String)}}}

    :add_contact
    {:fields {:id {:type :id}
              :name {:type 'String}
              :email {:type 'String}
              :phone {:type 'String}}}
    :add_address
    {:fields {:street1 {:type 'String}
              :street2 {:type 'String}
              :city {:type 'String}
              :state {:type 'String}
              :zip {:type 'String}}}

    :add_vendor
    {:fields {:id {:type :id}
              :name {:type 'String}
              :code {:type 'String}

              :print_as {:type 'String}
              :primary_contact {:type :add_contact}
              :secondary_contact {:type :add_contact}
              :address {:type :add_address}

              :default_expense_account {:type 'Int}
              :invoice_reminder_schedule {:type 'String}}}

    :edit_expense_account
    {:fields {:id {:type :id}
              :expense_account_id {:type 'Int}
              :location  {:type 'String}
              :amount {:type 'String}}}

    :add_invoice
    {:fields {:id {:type :id}
              :invoice_number {:type 'String}
              :location {:type 'String}
              :date {:type 'String}
              :client_id {:type :id}
              :vendor_id {:type :id}
              :vendor_name {:type 'String}
              :total {:type 'Float}}}

    :edit_invoice
    {:fields {:id {:type :id}
              :invoice_number {:type 'String}
              :date {:type 'String}
              :total {:type 'Float}}}}

   :enums {:payment_type {:values [{:enum-value :check}
                                   {:enum-value :cash}
                                   {:enum-value :debit}]}
           :bank_account_type {:values [{:enum-value :check}
                                        {:enum-value :cash}]}}
   :mutations
   {:reject_invoices {:type '(list :id)
                      :args {:invoices  {:type '(list :id)}}
                      :resolve :mutation/reject-invoices}

    :approve_invoices {:type '(list :id)
                      :args {:invoices  {:type '(list :id)}}
                      :resolve :mutation/approve-invoices}

    :print_checks {:type :check_result
                      :args {:invoice_payments  {:type '(list :invoice_payment_amount)}
                             :bank_account_id {:type :id}
                             :type {:type :payment_type}
                             :client_id {:type :id}}
                      :resolve :mutation/print-checks}

    :add_handwritten_check {:type :check_result
                            :args {:invoice_id {:type :id}
                                   :amount {:type 'Float}
                                   :date {:type 'String}
                                   :check_number {:type 'Int}
                                   :bank_account_id {:type :id}}
                   :resolve :mutation/add-handwritten-check}
    :edit_user {:type :user
                :args {:edit_user {:type :edit_user}}
                :resolve :mutation/edit-user}

    :edit_client {:type :client
                  :args {:edit_client {:type :edit_client}}
                  :resolve :mutation/edit-client}

    :upsert_vendor {:type :vendor
                    :args {:vendor {:type :add_vendor}}
                    :resolve :mutation/upsert-vendor}
    :add_invoice {:type :invoice
                  :args {:invoice {:type :add_invoice}}
                  :resolve :mutation/add-invoice}
    :edit_invoice {:type :invoice
                   :args {:invoice {:type :edit_invoice}}
                   :resolve :mutation/edit-invoice}
    :void_invoice {:type :invoice
                   :args {:invoice_id {:type :id}}
                   :resolve :mutation/void-invoice}
    :unvoid_invoice {:type :invoice
                     :args {:invoice_id {:type :id}}
                     :resolve :mutation/unvoid-invoice}

    :void_payment {:type :payment
                 :args {:payment_id {:type :id}}
                 :resolve :mutation/void-payment}
    :edit_expense_accounts {:type :invoice
                            :args {:invoice_id {:type :id}
                                   :expense_accounts {:type '(list :edit_expense_account)}}
                            :resolve :mutation/edit-expense-accounts}}})


(defn snake->kebab [s]
  (str/replace s #"_" "-"))

(defn kebab [x]
  (keyword (snake->kebab (name x))))

(defn kebab->snake [s]
  (str/replace s #"-" "_"))

(defn snake [x]
  (keyword (kebab->snake (name x))))

(defn ->graphql [m]
  (walk/postwalk
    (fn [node]
      (cond

        (keyword? node)
        (snake node)

        :else
        node))
    m))

(defn <-graphql [m]
  (walk/postwalk
    (fn [node]
      (cond

        (keyword? node)
        (kebab node)

        :else
        node))
    m))






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




(defn get-user [context args value]
  (assert-admin (:id context))

  (doto (let [users (d-users/get-graphql args)]
          (->graphql users))
    println))

(defn get-vendor [context args value]
  (->graphql
   (d-vendors/get-graphql args)))

(defn print-checks [context args value]
  
  (assert-can-see-client (:id context) (:client_id args))
  (->graphql
   (gq-checks/print-checks (map (fn [i] {:invoice-id (:invoice_id i)
                                        :amount (:amount i)})
                                (:invoice_payments args))
                           (:client_id args)
                           (:bank_account_id args)
                           (:type args))))

(defn get-expense-account-stats [context {:keys [client_id] } value]
  (let [result (cond-> {:query {:find ['?expense-account-id '(sum ?amount)]
                                :in ['$]
                                :where []}
                        :args [(d/db (d/connect uri)) client_id]}
                 client_id (merge-query {:query {:in ['?c]}
                                         
                                         :args [client_id]})
                 (not client_id) (merge-query {:query {:where ['[?c :client/name]]}})

                 true (merge-query {:query {:where ['[?i :invoice/client ?c]
                                                    '[?i :invoice/expense-accounts ?expense-account]
                                                    '[?expense-account :invoice-expense-account/expense-account-id ?expense-account-id]
                                                    '[?expense-account :invoice-expense-account/amount ?amount]]}})
                 true (doto println)

                 true (d/query ))]
    (for [[expense-account-id total] result]
      {:expense_account_id expense-account-id :total total :expense_account_name (-> expense-account-id e-expense-accounts/expense-accounts :name)})))

(defn categorize [x]
  (cond (<= x 0) :due
        (<= x 30 ) :due-30
        (<= x 60 ) :due-60
        :else :due-later))

(defn get-invoice-stats [context {:keys [client_id] } value]
  (let [result (cond-> {:query {:find ['?name '(sum ?outstanding-balance) '(sum ?total)]
                                :in ['$]
                                :where []}
                        :args [(d/db (d/connect uri)) client_id]}
                 client_id (merge-query {:query {:in ['?c]}
                                         :args [client_id]})
                 (not client_id) (merge-query {:query {:where ['[?c :client/name]]}})

                 true (merge-query {:query {:where ['[?i :invoice/client ?c]
                                                    '[?i :invoice/outstanding-balance ?outstanding-balance]
                                                    '[?i :invoice/total ?total]
                                                    '[?i :invoice/date ?date]
                                                    '[(.toInstant ^java.util.Date ?date) ?d2]
                                                    '[(.between java.time.temporal.ChronoUnit/DAYS (java.time.Instant/now) ?d2 ) ?d3]
                                                    '[(+ 30 ?d3) ?d4]
                                                    '[(auto-ap.graphql/categorize ?d4) ?name]]}})

                 true (d/query ))
        result (group-by first result)]
    
    (for [[id name] [[:due "Due"] [:due-30 "0-30 days"] [:due-60 "31-60 days"] [:due-later ">60 days"] ]
          :let [[[_ outstanding-balance total] ] (id result nil)
                outstanding-balance (or outstanding-balance 0)
                total (or total 0)]]
      {:name name :unpaid outstanding-balance :paid (if (= :due id)
                                                      0
                                                      (- total outstanding-balance))})))



(def schema
  (-> integreat-schema
      (attach-resolvers {:get-invoice-page gq-invoices/get-invoice-page
                         :get-all-invoices gq-invoices/get-all-invoices
                         :get-all-payments get-all-payments
                         :get-payment-page gq-checks/get-payment-page
                         :get-transaction-page gq-transactions/get-transaction-page
                         :get-expense-account-stats get-expense-account-stats
                         :get-invoice-stats get-invoice-stats
                         
                         :get-client gq-clients/get-client
                         :get-user get-user
                         :mutation/add-handwritten-check gq-checks/add-handwritten-check
                         :mutation/print-checks print-checks
                         :mutation/reject-invoices gq-invoices/reject-invoices
                         :mutation/approve-invoices gq-invoices/approve-invoices
                         :mutation/edit-user gq-users/edit-user
                         :mutation/add-invoice gq-invoices/add-invoice
                         :mutation/edit-invoice gq-invoices/edit-invoice
                         :mutation/edit-client gq-clients/edit-client
                         :mutation/upsert-vendor gq-vendors/upsert-vendor
                         :mutation/void-invoice gq-invoices/void-invoice
                         :mutation/unvoid-invoice gq-invoices/unvoid-invoice
                         :mutation/void-payment gq-checks/void-check
                         :mutation/edit-expense-accounts gq-invoices/edit-expense-accounts
                         :get-vendor get-vendor
                         :get-expense-account expense-accounts/get-expense-account
                         :get-expense-account-parent expense-accounts/get-parent})
      schema/compile))



(defn simplify
  "Converts all ordered maps nested within the map into standard hash maps, and
   sequences into vectors, which makes for easier constants in the tests, and eliminates ordering problems."
  [m]
  (walk/postwalk
    (fn [node]
      (cond
        (instance? IPersistentMap node)
        (into {} node)

        (seq? node)
        (vec node)

        (keyword? node)
        (kebab node)

        :else
        node))
    m))
                 
(defn query
  ([id q]
   (query id q nil ))
  ([id q v]
   (println "executing graphql query" id q v)
   (try 
     (let [result (time (simplify (execute schema q v {:id id})))]
       (when (seq (:errors result))
         (throw (ex-info "GraphQL error" {:result result})))
       result)
     
     (catch Exception e
       (if-let [v (:validation-error (ex-data e))]
         (println "validation error" v)
         (println e))
       
       (throw e)))))
