(ns bigml-nodered-core.wz.tasks
  (:require [bigml-nodered-core.wz.marshal :as marshal]
            [bigml-nodered-core.utils :as utils]
            [bigml-nodered-core.bigml :as bigml]
            [bigml-nodered-core.wz.units :as units]))

;; utils
(defn- append-non-empty-string [s1 s2 & [sep]]
  "Given strings s1, s2, and sep, it appends s2 to sep to s1, provided
  s1 and s2 are not null or empty."
  (let [good? #(and (string? %1) (not= "" %1))]
    (cond
     (and (good? s1) (good? s2)) (str s1 sep s2)
     (good? s1) s1
     (good? s2) s2
     :else nil)))

(defn- assoc-not-nil [m k v]
  "Given map m, key k, and value v, it returns a new map built by
  adding [k v] to m, provided v is not nil."
  (if (some? v) (assoc m k v) m))

;; tasks
(defn wz-get [name inputs outputs msg i-pars o-pars]
  "gets a resource json and filters some keypaths"
  (let [in-keypath (.parse js/JSON (get inputs "input_keypath"))
        out-keypath (get inputs "output_keypath")
        opts (dissoc inputs "input_keypath" "output_keypath")
        in-par (append-non-empty-string (get i-pars 0) in-keypath ".")]
    (if (some? in-par)
      (units/wz-fetch in-par out-keypath inputs)
      (throw (js/Error. "Missing input key or port.")))))

(defn wz-find [name inputs outputs msg i-pars o-pars]
  "opts is a map describing the query (e.g. { name__icontains value }"
  (let [t (get inputs "resource-type")
        inputs (dissoc inputs "resource-type")]
    (units/wz-latest t inputs)))

(defn wz-source [name inputs outputs msg i-pars o-pars]
  (let [inputs (dissoc inputs "resource-type")
        [i-par] i-pars]
    (units/wz-create "source" (assoc-not-nil inputs (str i-par) i-par))))

(defn wz-dataset [name inputs outputs msg i-pars o-pars]
  (let [inputs (dissoc inputs "resource-type")
        [i-par] i-pars]
    (units/wz-create "dataset" (assoc-not-nil inputs (str i-par) i-par))))

(defn wz-train [name inputs outputs msg i-pars o-pars]
  (let [t (get inputs "resource-type")
        inputs (dissoc inputs "resource-type")
        [dataset] i-pars]
    (units/wz-create t (assoc-not-nil inputs
                                      (str "(resource-type " dataset ")")
                                      dataset))))

(defn wz-predict [name inputs outputs msg i-pars o-pars]
  (let [t (get inputs "resource-type")
        inputs (dissoc inputs "resource-type")
        [model input-data] i-pars]
    (units/wz-create
      t
      (merge inputs
             {"(resource-type model)"
              model
              "input_data"
              (marshal/wz-marshal-map
               (js->clj (.-inputData (.-payload msg))))}))))

(defn wz-dataset-split [name inputs outputs msg i-pars o-pars]
  (let [[dataset] i-pars]
    (units/wz-random-split dataset
                     (get inputs "rate")
                     (get inputs "firstsplit")
                     (get inputs "secondsplit"))))

(defn wz-eval [name inputs outputs msg i-pars o-pars]
  (let [inputs (dissoc inputs "resource-type")
        [dataset model] i-pars]
    (units/wz-create "evaluation"
                     (assoc inputs
                       (str "(resource-type " model ")") model
                       "dataset" dataset))))

(defn wz-wrap [name inputs outputs msg i-pars o-pars]
  (let [inputs (dissoc inputs "resource-type")]
    (get inputs "whizzml")))

(defn wz-idem [name inputs outputs msg i-pars o-pars]
  "r")

;; The following stuff should be moved to another module
(defn- exec-ok? [e i]
  (and (nil? e)
       (.-execution i)
       (.-result (.-execution i))))

(defn- wz-exec-id [script-id inputs opts payload cbk]
  "Executes a WhizzML script identified by its script-id using the
  provided inputs and creation options. When done, the reult is
  embedded inside of payload and callback cbk is called."
  (bigml/wz-exec-id
   script-id
   inputs
   cbk
   opts))

(defn- apply-payload [inputs input-keys payload]
  "Maps variables in inputs to actual values from payload. Potential
  variables are listed in input-keys. This is useful when passing
  input arguments to a WhizzML package. For example if you
  have [[\"model\" model]] , you will want the second model be
  replaced through the payload.model value coming into the node."
  (let [get-if-key (fn [n]
                     (marshal/wz-marshal
                      (if (some #{n} input-keys) (get payload n) n) true))]
    (clojure.string/replace inputs #"[a-z\"'0-9_-]+" get-if-key)))

(defn- get-input [payload opts name & [cast-to]]
  (or (aget payload name)
      (utils/cast-config-type cast-to (get opts name))))

(defn package-exec [red node config i-pars o-pars un ak dm]
  "Creates all the scripts and libraries defined in (.-meta_input
  config) and upon success executes the last script and update the
  node status."
  (fn [name opts msg cbk]
    (let [ep (js/require "bigml-eventpredict")
          payload (.-payload msg)
          meta-url (get opts "meta_url")
          inputs (apply-payload (get-input payload opts "meta_input")
                                (js->clj i-pars)
                                (js->clj payload))
          inputs { "inputs" (.parse js/JSON inputs) }]
      (.catch
       (.then
        (.createFromMetaGetId (.-WhizzML ep) meta-url name un ak dm)
        (fn [result]
          (wz-exec-id
           result
           inputs
           {:username un :apikey ak}
           payload
           (fn [e i]
             (cbk e (and (nil? e)
                         (let [_ (aset payload (first o-pars)
                                       (or (.-result (.-execution i)) i))]
                           payload)))))))
       (fn [error]
         (println "CATCH: " error))))))

(defn stream-exec [red node config i-pars o-pars un ak dm]
  (fn [name opts msg cbk]
    (let [ep (js/require "bigml-eventpredict")
          payload (aget msg "payload")
          event (get-input payload opts (or (first i-pars) "event"))
          endpoint (get-input payload opts "endpoint")
          label (get-input payload opts "label")
          ebs (get-input payload opts "event_batch_size" :number)
          project (get-input payload opts "project")
          settings (clj->js
                    {:endpoint endpoint
                     :label label
                     :project project
                     :userCreds {:username un :api_key ak :domain dm }
                     :eventBatchSize ebs
                     :retrainPeriod 0
                     :retrain false })]
      (.catch
       (.then
        (.event (.-EventPredict ep) endpoint event settings)
        (fn [result]
          (cbk nil
               (let [_ (aset payload (first o-pars) (clj->js result))]
                 payload))))
       (fn [error]
         (do 
           (println "CATCH STREAM: " error)
           (cbk error nil)))))))

(def fs (js/require "fs"))

(defn upload-source [red node config i-pars o-pars un ak dm]
  "Uploads a CSV to BigML to create a source."
  (fn [name opts msg cbk]
    (let [payload (.-payload msg)
          file (get-input payload opts "file")]
      (if (and (some? file) (.existsSync fs file))
        (bigml/upload-source
         file un ak dm {}
         (fn [e i]
           (let [_ (aset payload (first o-pars)
                         (clj->js (and (some? i) (.-resource i))))]
             (cbk e payload :stop))))
        (cbk nil nil :ignore)))))

(defn test-exec [red node config un ak]
  (fn [name opts msg cbk]
    (let [_ (println "TEST-EXEC")]
      (cbk nil { :result "test-run"} ))))


