(ns bigml-nodered-core.wz
  (:require clojure.string
            [bigml-nodered-core.bigml :as bigml]
            [bigml-nodered-core.wz.tasks :as tasks]
            [bigml-nodered-core.wz.marshal :as marshal]
            [bigml-nodered-core.wz.stack :as stack]))

;;-- From dblib:
;; (defn- promised [f]
;;   "f is a function that takes a (fn [err res]) callback, or a vector
;; of such functions. promised gives back a promise that resolves to f."
;;   (if (sequential? f)
;;     (.all promise (clj->js (map promised f)))
;;     (new promise
;;          (fn [fulfill reject]
;;            (f (fn [err res]
;;                 (if err
;;                   (reject err)
;;                   (fulfill res))))))))
;;-- End dblib


(defn- wz-fn [fun inputs outputs in-pars out-pars]
  (fn [name msg]
    (stack/add-step
     msg
     name
     (fun name inputs outputs msg in-pars out-pars)
     inputs
     in-pars
     (or outputs out-pars))))

(defn- from-env [node msg key & [default]]
  (let [flow (.-flow (.context node))
        payload (or (.-payload msg) {})]
    (or (aget payload key) (.get flow key) default)))

(defn- pars-from-labels [labels]
  (let [labels (js->clj labels)]
    (if (sequential? labels)
      (into [] (re-seq #"[\w_\-]+" (first labels)))
      nil)))

(defn- extract-diagnostics [result error]
  (if (some? (and result (.-status result)))
    (let [message (.-message (.-status result))
          cause (or (.-cause (.-status result)) {})
          code (or (.-code cause) "unknown")
          extra (or (.-extra cause) "")]
      (str message "(" code ")" " - " (.stringify js/JSON extra)))
    (or error result)))

(defn- results-to-output [name result msg red]
  "For each output port, creates a clone of the message and sets its
  payload by including the output value corresponding to the given
  port label."
  (let [to-out (fn [x]
                 (let [msg (.cloneMessage (.-util red) msg)
                       _ (set! (.-payload msg) x)]
                   msg))]
    (to-out result)))

(defn- set-status-and-run [node status c]
  (.status node (clj->js status)))

(defn- handle-error [err node config msg extra]
  (do
    (set! (.-wzError msg) err)
    (.error node extra msg)
    (println extra)
    msg))

(defn- node-result-cbk [name msg red node config opts error-msg fun]
  "Returns a callback that sets the status of node to either success
  or failure. The callback can be used with asynchronous operations
  spawned by nodes. In case of success, result is multiplexed into a
  Node-RED msg. In case of success, fun (if any) is called before
  setting the node status, giving thus the opportunity to chain an
  additional async operation before returning. The flag argument can
  have two values: :stop, to prevent fun from being called; :ignore,
  to ignore the result."
  (fn [error result & [flag]] 
    (if (and (nil? error) (or (some? result) (= flag :ignore)))
      (let [msg (if (= flag :ignore)
                  msg
                  (results-to-output name result msg red))]
        (if (and (not= flag :stop) (some? fun))
          (fun name opts msg
               (node-result-cbk name msg
                                red node config opts error-msg nil))
          (set-status-and-run
           node
           {"fill" "green" "text" "completed"}
           (.send node msg))))
      (set-status-and-run
       node
       {"fill" "red" "text" "failed"}
       (handle-error (extract-diagnostics result error)
                           node
                           config
                           msg
                           error-msg)))))

(defn- reify-node [msg red node config opts pre wzf i-pars o-pars post]
  (let [name (.-name config)
        [un ak dm] [(from-env node msg "BIGML_USERNAME")
                    (from-env node msg "BIGML_API_KEY")
                    (from-env node msg "BIGML_DOMAIN")]
        trace? (from-env node msg "BIGML_DEBUG_TRACE")
        pre (when (some? pre) (pre red node config i-pars o-pars un ak dm))
        post (when (some? post) (post red node config i-pars o-pars un ak dm))
  
        chain-task
        (fn [task error-msg]
          (node-result-cbk
           name msg red node config opts
           error-msg
           task))
        
        run-wz-task
        (fn [name opts msg cbk]
          (let [whizzml (.-whizzml msg)
                trace #(println "WhizzML for " name ": " whizzml)]
            (bigml/wz-exec
             name whizzml un ak dm
             (fn [e i]
               (do
                 (stack/wz-stack-reset msg)
                 (js-delete msg "wzSendingPort")
                 (if trace? (trace) (js-delete msg "whizzml"))
                 (cbk e (or (and i
                                 (.-execution i)
                                 (.-result (.-execution i))) i)))))))
        
        wz-task
        (fn [name opts msg]
          (let [msg (wzf name msg)
                whizzml (.-whizzml msg)]
            (run-wz-task name opts msg
                         (chain-task post (str "Failed WhizzML: " whizzml)))))
        
        run-task
        #(set-status-and-run
          node
          {"fill" "yellow" "text" "running"}
          (if (some? pre)
            (pre
             name (js->clj config) msg
             (chain-task wz-task "Failed Pre-step"))
            (if (some? wzf)
              (wz-task name config msg)
              (post
               name (js->clj config) msg
               (chain-task nil "Failed Post-step")))))
        
        auth-error 
        #(set-status-and-run
          node
          {"fill" "red" "text" "failed"}
          (handle-error
           "Missing credentials. Check payload or flow context."
           node config msg "whizzml"))]
    
    (if (and (some? un) (some? ak))
      (run-task)
      (auth-error))))

(defn node-fun [fun opts-fun io-fun node config red]
  "Workhorse function for node definition. It is basically responsible
  for producing the node output. This can be pure whizzml code for a
  non-reified node, or the outcome of a whizzml execution for a
  reified node. Whizzml code is propagated downstream through the
  message (whizzml key). Execution results are propagated through the
  payload for easier consumption by downstream nodes (each result is
  assigned the output port name as key). Additionally, each node
  stores the name of its oputput port under the wzSendingPort key. fun
  is a function of name opts msg, typically one of the wz-workflows
  defined above. it can also be a sequence of functions when you want
  to chain several actions together. in such cases, the first action
  is usually one wz-worklows from tasks, which is reified before
  passing control to the subsequent actions. this is used for example
  to implement package creation/execution, storing an event to a local
  file, etc. opts-fun is a function that returns a vector of
  available (field, value) tuples from the node
  configuration. opts-fun is usually auto-generated from the node
  description."
  (fn [msg]
    (try
      (let [name (.-name config)
            payload (js->clj (.-payload msg))
            reify? (or (.-reify config) (from-env node msg "BIGML_DEBUG_REIFY"))
            def-i-o (io-fun)
            i-pars (or (pars-from-labels (.-inputLabels config)) (first def-i-o))
            o-pars (or (pars-from-labels (.-outputLabels config)) (last def-i-o))
            ins (opts-fun (js->clj config) payload)
            ;; last-sending-port (.-wzSendingPort msg)
            ;; ins (if (= (count i-pars) (count last-sending-port))
            ;;   (assoc ins (first i-pars) (get payload (first last-sending-port)))
            ;;          ins)
            outs (get (.-outputLabels config) 0)
            outs (if (or (nil? outs) (sequential? outs)) outs [outs])
            [wzf pre post] (if (map? fun)
                             [(:wz fun) (:pre fun) (:post fun)] [fun nil nil])
            wz-ins (into {} (for [[k v] ins] [k (marshal/wz-marshal v)]))
            wzf (if (some? wzf) (wz-fn wzf wz-ins outs i-pars o-pars) nil)
            ;; _ (set! (.-wzSendingPort msg) o-pars)
            ]
        (if (or reify? (some? pre) (some? post))
          (reify-node msg red node config ins pre wzf i-pars o-pars post)
          (let [msg (wzf name msg)
                whizzml (.-whizzml msg)
                trace? (from-env node msg "BIGML_DEBUG_TRACE")
                trace #(println "WhizzML for " name ": " whizzml)]
            (set-status-and-run
             node
             {"fill" "green" "text" "compiled"}
             (do
               (when trace? (trace))
               (.send node msg))))))
      (catch :default
          e
        (set-status-and-run
         node
         {"fill" "red" "text" "failed"}
         (handle-error e
                       node
                       config
                       msg
                       ""))))))

