(ns claire-common.dialogflow
  (:refer-clojure :exclude [list find])
  (:require
   [claire-common.utils :as utils]
   [clojure.core.async :as async :refer [>! <! go chan]])
  (:require-macros [claire-common.macros
                    :as macros
                    :refer [<? go-try]]))

(def df (js/require "dialogflow"))

(def ^:private ^:const client
  {"EntityType" (utils/new (.-EntityTypesClient df))
   "Intent" (utils/new (.-IntentsClient df))
   "Context" (utils/new (.-ContextsClient df))})

(def ^:private ^:const project-id "newagent-e1a0c")
(def ^:private ^:const agent-path
  (.projectAgentPath (get client "Intent") project-id))

(defn- entity [entity]
  (let [[value synonyms] (if (sequential? entity)
                           [(first entity) entity]
                           [entity [entity]])]
    {:value value
     :synonyms synonyms}))

;; Public stuff

(defn entity-type [name entities & [opts]]
  (let [e (map #(entity %1) entities)
        _ (println "ENTITY TYPE NAME: " name)]
    {:displayName name
     :entities e
     :kind "KIND_MAP"
     :autoExpansionMode
     (or (:auto-expansion opts) "AUTO_EXPANSION_MODE_UNSPECIFIED")}))

(defn context [context & [opts session]]
  (let [session (or session "-")
        name (.contextPath
              (get client "Context") project-id session context)]
    (if (some? opts)
      { "name" name
        "lifespanCount" 2
        "lifespan" 2
        "parameters" (get opts "parameters")}
      name)))

(defn extend-context [env ctx & [merge?]]
  (let [merge? (or merge? false)]
    (utils/make-env env :context ctx :merge? merge?)))

;; Dialogflow Resource manipulation

(defn- phrase-parts [phrase parameters]
  (let [re #"([^$]*)(\$[^ ]*)([^$]*)"
        parts (filter

               #(not (empty? %1))
               (reduce #(concat %1 %2) ()
                       (map #(rest %1) 
                            (or (re-seq re phrase) [["" phrase]]))
                       ))
        is-par? (fn [value] #(= (:value %1) value))]
    (map #(let [par (utils/find-first (is-par? %1) parameters)]
            (if (some? par)
              {:text (:displayName par)
               :entityType (:entityTypeDisplayName par)
               :alias (:displayName par)}
              {:text %1})) parts)))

(defn- training-phrases [phrases parameters]
  (map (fn [phrase]
         {"type" "EXAMPLE"
          "parts" (phrase-parts phrase parameters)})
       phrases))

(defn- list [type & [f]]
  (go-try
   (let [l (<? (utils/call-async-method
                (get client type)
                (str "list" type "s")
                { "parent" agent-path }))]
     (if f (filter f l) l))))

 (defn- find [type name]
  (go-try
   (let [r (<? (list type))
         is-name? #(= (get %1 "displayName") name)]
     (utils/find-first is-name? r))))

(defn- delete [type name]
  (go-try
   (let [r (<? (find type name))]
     (when (some? r)
       (<? (utils/call-async-method (get client type)
                                    (str "delete" type)
                                    { "name" (get r "name") }))))))

(defn delete-all [type]
  (go-try
   (let [root? #(= 0 (count (get %1 "rootFollowupIntentName")))
         default? #(clojure.string/includes?
                    (get %1 "displayName")
                    "Default ")
         f #(or (= type "EntityType") (and (root? %1) (not (default? %1))))
         l (<? (list type f))]
     (doseq [r l]
       (<? (utils/call-async-method (get client type)
                                    (str "delete" type)
                                    { "name" (get r "name") }))))))

(defn- create-or-update [type agent-path res]
  (go-try
   (<? (let [res (dissoc res :followups)
             method (if (some? (:name res))
                      (str "update" type)
                      (str "create" type))
             _ (println "CREATING: " (:displayName res)
                        " METHOD: " method)]
         (utils/call-async-method
          (get client type) method
          {"parent" agent-path
           (utils/lower-first type) res})))))

(defn- make-one [type res pre-fn post-fn]
  (go-try
   (let [name (:displayName res)
         res (if (some? pre-fn) (pre-fn res) res)
         res* (<? (create-or-update type agent-path res))
         res (utils/assoc-some res :name (get res* "name"))]
     (if (some? post-fn) (<? (post-fn res)) res))))

(defn make [type res-list & [pre-fn post-fn]]
  (go-try
   (if (some? (first res-list))
     (let [r (<? (make-one type (first res-list) pre-fn post-fn))
           _ (<? (utils/delay 500))]
       (conj (<? (make type (rest res-list) pre-fn post-fn)) r))
     ())))

(defn- add-followup-context [res]
  (let [ctx (context (str (:displayName res) "-followup") {})
        _ (println "CONTEXT: " ctx)]
    (assoc res :outputContexts (conj (:outputContexts res) ctx))))

(defn- make-followups [parent]
  (when-let
      [followups
       (map
        #(into
          %1
          {:inputContextNames
           (conj
            (:inputContextNames %1)
            (context (str (:displayName parent) "-followup")))
           :followupIntentName (:displayName %1)
           :parentFollowupIntentName (:name parent)})
        (:followups parent))]
    (make "Intent" followups add-followup-context make-followups)))

(defn redirector [to extends]
  (fn [intent query env pars]
    (let [env (if extends
                (extend-context env extends)
                (utils/make-env
                 :context [(context to)]
                 :merge? true))]
      (utils/make-reply
       env
       :export (some? extends)
       :event to))))

(def ^:dynamic ^:private *intent-map* (atom {}))

;; public stuff

(defn parm [name entity prompts & [opts]]
  {:displayName name
   :value (str "$" name)
   :defaultValue ""
   :entityTypeDisplayName (str "@" entity)
   :mandatory (or (:mandatory opts) true)
   :prompts prompts
   :isList (or (:is-list opts) false) })

(defn intent-map [name]
  @*intent-map*)

(defn intent [name phrases messages & [opts]]
  "Returns a map representing an intent"
  (let [webhook (:webhook opts)
        [webhook webhook-state messages]
        (if webhook
          [webhook
           (if (true? (:slotfilling opts))
             "WEBHOOK_STATE_ENABLED_FOR_SLOT_FILLING"
             "WEBHOOK_STATE_ENABLED")
           ["Sorry, I could not get a response for this request."]]
          [nil "WEBHOOK_STATE_UNSPECIFIED" messages])
        parameters (or (:parameters opts) [])
        _ (when webhook (swap!
                         *intent-map* assoc name webhook))]
    {:displayName name
     :trainingPhrases (training-phrases phrases parameters)
     :messages [{:text {:text messages}}]
     :webhookState webhook-state
     :isFallback (or (:fallback opts) (empty? phrases) false)
     :inputContextNames (map context (or (:contexts  opts) []))
     :outputContexts (map context (or (:outputContexts opts) []))
     :parameters parameters
     :events (or (:events opts) [])
     :resetContexts (or (:reset opts) false)
     :followups (:followups opts)}))

(defn make-intents [intents]
  "This will create the given intents on Dialogflow"
  (make "Intent" intents add-followup-context make-followups))

(defn make-entity-types [entity-types]
  "This will create the given entity-types on Dialogflow"
  (make "EntityType" entity-types))

