(ns jepsen.antietcd.client
  "HTTP client wrapper for antietcd"
  (:require
    [jepsen.independent :as independent]
    [clojure.string :refer [trim]]
    [org.httpkit.client :as http]
    [clojure.data.json :as json]
  )
  (:import java.util.Base64)
)

(defn node-url
  "An HTTP url for connecting to a node on a particular port."
  [node]
  (str "http://" node ":2379"))

(defn base64_encode [to-encode]
  (.encodeToString (Base64/getEncoder) (.getBytes to-encode))
)

(defn base64_decode [to-decode]
  (String. (.decode (Base64/getUrlDecoder) to-decode))
)

(defn retry_all [fn]
  (loop []
    (let
      [res (fn)]
      (cond
        (= (:type res) :ok) res
        true (do (Thread/sleep (long (rand-int 50))) (recur))
      )
    )
  )
)

(defn throw [fn]
  (loop []
    (let
      [res (fn)]
      (cond
        (= (:type res) :ok) res
        (= (:error res) :mismatch) res
        true (throw (java.util.concurrent.TimeoutException. (str (:error res))))
      )
    )
  )
)

(defn antietcd_result [res]
  (case (:status res)
    200 {:type :ok, :body (json/read-str (:body res) :key-fn keyword)}
    503 {:type :fail, :error (or (if (:body res) (trim (:body res))) "quorum error")}
    408 {:type :fail, :error (or (if (:body res) (trim (:body res))) "timed out")}
    {:type :fail, :error (or (:body res) (ex-message (:error res)) "unknown")}
  )
)

(defn antietcd_request [url body]
  (let
    [res (deref (http/post
      url
      {
        :headers {"Content-Type" "application/json"}
        :body (json/write-str body)
        :connect-timeout 5000
      }
    ) 5000 {:status 408})]
    (antietcd_result res)
  )
)

(defn antietcd_put [url key value]
  (antietcd_request (str url "/v3/kv/put") {:key key :value value})
)

(defn antietcd_txn [url body]
  (let
    [res (antietcd_request (str url "/v3/kv/txn") body)]
    (case (:type res)
      :ok (if (:succeeded (:body res)) res {:type :fail, :error :mismatch})
      res
    )
  )
)

(defn antietcd_cas [url key old new]
  (antietcd_txn url {
    :compare [ { :key key :target :VALUE :value old } ]
    :success [ { :request_put { :key key :value new } } ]
  })
)

(defn antietcd_update
  [url key modfn]
  (let
    [res (antietcd_request (str url "/v3/kv/range") {:key key})]
    (if (= (:type res) :ok)
      (let
        [
          val (:value (first (:kvs (:body res))))
          new (modfn val)
          res2 (antietcd_request (str url "/v3/kv/txn") {
            :compare [
              (if (:kvs (:body res))
                { :key key :target :MOD :result :LESS :mod_revision (+ (:mod_revision (first (:kvs (:body res)))) 1) }
                { :key key :target :VERSION :version 0 }
              )
            ]
            :success [ { :request_put { :key key :value new } } ]
          })
        ]
        (if (= (:type res2) :ok)
          (if (:succeeded (:body res2))
            {:type :ok}
            {:type :fail, :error :mismatch}
          )
          res2
        )
      )
      res
    )
  )
)

(defn get_primary!
  "Determine the current primary node"
  [nodes]
  (->>
    nodes
    (map (fn [node] (http/post (str (node-url node) "/v3/maintenance/status") {
      :headers {"Content-Type" "application/json"}
      :body "{}"
      :timeout 1000
      :connect-timeout 1000
    })))
    (doall)
    (map (fn [res] (antietcd_result @res)))
    (reduce
      (fn [acc val] (if
        (or
          (nil? (:raftTerm (:body val)))
          (> (:raftTerm acc) (:raftTerm (:body val)))
        )
        acc
        (:body val)
      ))
      {:raftTerm 0, :leader nil}
    )
    (:leader)
  )
)
