// The JSON commands. A key holds one JSON document, and a JSONPath addresses a part of it:
// `$` is the document root, `$.a.b` a member, `$..x` a recursive match. Values cross the
// wire as JSON text, so a value is a `Json` here and the raw text stays available for the
// calls where formatting or exact bytes matter.
// https://redis.io/docs/latest/commands/?group=json

///|
/// The float array storage `JSON.SET` may be told to use: `FPHA BF16`, `FP16`, `FP32` or
/// `FP64`.
#derive(Eq, Debug)
pub(all) enum Fpha {
  Bf16
  Fp16
  Fp32
  Fp64
}

///|
pub extend Fpha with Debug::{to_repr}

///|
pub extend Fpha with Eq::{not_equal, equal}

///|
/// The keyword `FPHA` takes for this storage.
pub fn Fpha::text(self : Fpha) -> String {
  match self {
    Bf16 => "BF16"
    Fp16 => "FP16"
    Fp32 => "FP32"
    Fp64 => "FP64"
  }
}

///|
/// Parse the JSON text a JSON command answers with.
fn parse_json_text(text : String) -> @json.Json raise RedisError {
  @json.parse(text[:]) catch {
    _ => raise Protocol("JSON reply is not valid JSON: " + text)
  }
}

///|
/// A JSON command's reply as a `Json`, whatever shape it arrived in: RESP2 writes a value
/// as JSON text, RESP3 as a typed value or an array of them.
fn json_reply_value(reply : @resp.RespValue) -> @json.Json raise RedisError {
  match reply {
    @resp.BulkString(b) | @resp.VerbatimString(_, b) =>
      parse_json_text(@utf8.decode_lossy(b[:]))
    @resp.SimpleString(s) => parse_json_text(s)
    @resp.Null => @json.Json::null()
    @resp.Integer(n) => @json.Json::number(n.to_double(), repr=n.to_string())
    @resp.Double(d) => @json.Json::number(d)
    @resp.Boolean(b) => @json.Json::boolean(b)
    @resp.Array(items) => @json.Json::array(items.map(json_reply_value))
    @resp.RespSet(items) => @json.Json::array(items.map(json_reply_value))
    @resp.RespMap(pairs) => {
      let fields : Map[String, @json.Json] = Map([])
      for pair in pairs {
        fields[expect_text(pair.0)] = json_reply_value(pair.1)
      }
      @json.Json::object(fields)
    }
    other =>
      raise Protocol("expected a JSON value, got " + @resp.resp_kind(other))
  }
}

///|
/// A JSON number, or `None` for a null the server wrote for a path that matched nothing.
fn json_number(value : @json.Json) -> Double? {
  match value {
    @json.Number(d, ..) => Some(d)
    _ => None
  }
}

///|
/// The items of a path-addressed reply, as numbers.
fn path_numbers(reply : @resp.RespValue) -> Array[Double?] raise RedisError {
  match reply {
    @resp.BulkString(b) | @resp.VerbatimString(_, b) => {
      let parsed = parse_json_text(@utf8.decode_lossy(b[:]))
      match parsed {
        @json.Array(items) => items.map(json_number)
        other => [json_number(other)]
      }
    }
    other => path_items(other).map(item => json_number(json_reply_value(item)))
  }
}

///|
/// The items of a path-addressed reply, as integers.
fn path_ints(reply : @resp.RespValue) -> Array[Int64?] raise RedisError {
  let out : Array[Int64?] = []
  for item in path_items(reply) {
    match item {
      @resp.Null => out.push(None)
      @resp.Integer(n) => out.push(Some(n))
      @resp.Boolean(b) => out.push(Some(if b { 1 } else { 0 }))
      other => out.push(Some(expect_int64(other)))
    }
  }
  out
}

///|
/// The items of a path-addressed reply, as booleans.
fn path_flags(reply : @resp.RespValue) -> Array[Bool?] raise RedisError {
  let out : Array[Bool?] = []
  for item in path_items(reply) {
    match item {
      @resp.Null => out.push(None)
      other => out.push(Some(expect_bool(other)))
    }
  }
  out
}

///|
/// `JSON.SET key path value [NX|XX] [FPHA type]`: store `value` at `path` in `key`.
/// Returns `false` when `nx` or `xx` leaves the key unchanged.
pub async fn Client::json_set(
  self : Client,
  key : String,
  path : String,
  value : @json.Json,
  nx? : Bool,
  xx? : Bool,
  fpha? : Fpha,
) -> Bool raise RedisError {
  let args : Array[Bytes] = [
    b"JSON.SET",
    arg_string(key),
    arg_string(path),
    arg_string(value.stringify()),
  ]
  if nx.unwrap_or(false) {
    args.push(b"NX")
  } else if xx.unwrap_or(false) {
    args.push(b"XX")
  }
  push_some(args, b"FPHA", fpha.map(f => arg_string(f.text())))
  match self.command(args) {
    @resp.SimpleString(_) => true
    @resp.Null => false
    other =>
      raise Protocol("unexpected JSON.SET reply: " + @resp.resp_kind(other))
  }
}

///|
/// `JSON.MSET key path value [key path value ...]`: set several documents at once, all or
/// none.
pub async fn Client::json_mset(
  self : Client,
  triplets : Array[(String, String, @json.Json)],
) -> Unit raise RedisError {
  let args : Array[Bytes] = [b"JSON.MSET"]
  for (key, path, value) in triplets {
    args.push(arg_string(key))
    args.push(arg_string(path))
    args.push(arg_string(value.stringify()))
  }
  status_ok(self.command(args))
}

///|
/// `JSON.MERGE key path value`: merge `value` into the object at `path`, by RFC 7396: a
/// null member removes the member, an object merges recursively, anything else replaces.
pub async fn Client::json_merge(
  self : Client,
  key : String,
  path : String,
  value : @json.Json,
) -> Unit raise RedisError {
  status_ok(
    self.command([
      b"JSON.MERGE",
      arg_string(key),
      arg_string(path),
      arg_string(value.stringify()),
    ]),
  )
}

///|
/// `JSON.GET key [path]`: the document, or the part of it at `path`, or `None` when the key
/// is missing. A JSONPath that matches nothing yields an empty array, which is not `None`.
pub async fn Client::json_get(
  self : Client,
  key : String,
  path? : String,
) -> @json.Json? raise RedisError {
  let args : Array[Bytes] = [b"JSON.GET", arg_string(key)]
  push_arg(args, path.map(arg_string))
  match self.command(args) {
    @resp.Null => None
    other => Some(json_reply_value(other))
  }
}

///|
/// `JSON.GET key [INDENT i] [NEWLINE n] [SPACE s] [path]`: the reply exactly as the server
/// wrote it, formatting included.
pub async fn Client::json_get_text(
  self : Client,
  key : String,
  path? : String,
  indent? : String,
  newline? : String,
  space? : String,
) -> String? raise RedisError {
  let args : Array[Bytes] = [b"JSON.GET", arg_string(key)]
  push_some(args, b"INDENT", indent.map(arg_string))
  push_some(args, b"NEWLINE", newline.map(arg_string))
  push_some(args, b"SPACE", space.map(arg_string))
  push_arg(args, path.map(arg_string))
  match self.command(args) {
    @resp.Null => None
    other => Some(expect_text(other))
  }
}

///|
/// `JSON.MGET key [key ...] path`: the value at `path` in each key, `None` where a key has
/// nothing there.
pub async fn Client::json_mget(
  self : Client,
  keys : Array[String],
  path : String,
) -> Array[@json.Json?] raise RedisError {
  let args : Array[Bytes] = [b"JSON.MGET"]
  for key in keys {
    args.push(arg_string(key))
  }
  args.push(arg_string(path))
  let out : Array[@json.Json?] = []
  for item in expect_array(self.command(args)) {
    match item {
      @resp.Null => out.push(None)
      other => out.push(Some(json_reply_value(other)))
    }
  }
  out
}

///|
/// `JSON.DEL key [path]`: delete the document or the part at `path`, returning how many
/// values were removed.
pub async fn Client::json_del(
  self : Client,
  key : String,
  path? : String,
) -> Int64 raise RedisError {
  let args : Array[Bytes] = [b"JSON.DEL", arg_string(key)]
  push_arg(args, path.map(arg_string))
  expect_int64(self.command(args))
}

///|
/// `JSON.FORGET key [path]`: the older name for `JSON.DEL`.
pub async fn Client::json_forget(
  self : Client,
  key : String,
  path? : String,
) -> Int64 raise RedisError {
  let args : Array[Bytes] = [b"JSON.FORGET", arg_string(key)]
  push_arg(args, path.map(arg_string))
  expect_int64(self.command(args))
}

///|
/// `JSON.TYPE key [path]`: the JSON type of each value at `path` — `object`, `array`,
/// `string`, `integer`, `number`, `boolean` or `null` — `None` where nothing matched.
pub async fn Client::json_type(
  self : Client,
  key : String,
  path? : String,
) -> Array[String?] raise RedisError {
  let args : Array[Bytes] = [b"JSON.TYPE", arg_string(key)]
  push_arg(args, path.map(arg_string))
  let out : Array[String?] = []
  for item in path_items(self.command(args)) {
    match item {
      // RESP3 nests one array per path, so a path with several matches arrives one level
      // deeper than the same reply does under RESP2.
      @resp.Array(inner) =>
        for value in inner {
          match value {
            @resp.Null => out.push(None)
            other => out.push(Some(expect_text(other)))
          }
        }
      @resp.Null => out.push(None)
      other => out.push(Some(expect_text(other)))
    }
  }
  out
}

///|
/// `JSON.NUMINCRBY key path value`: add `value` to each number at `path`, returning the new
/// values.
pub async fn Client::json_numincrby(
  self : Client,
  key : String,
  path : String,
  value : Double,
) -> Array[Double?] raise RedisError {
  path_numbers(
    self.command([
      b"JSON.NUMINCRBY",
      arg_string(key),
      arg_string(path),
      arg_double(value),
    ]),
  )
}

///|
/// `JSON.NUMMULTBY key path value`: multiply each number at `path` by `value`, returning
/// the new values.
pub async fn Client::json_nummultby(
  self : Client,
  key : String,
  path : String,
  value : Double,
) -> Array[Double?] raise RedisError {
  path_numbers(
    self.command([
      b"JSON.NUMMULTBY",
      arg_string(key),
      arg_string(path),
      arg_double(value),
    ]),
  )
}

///|
/// `JSON.STRAPPEND key [path] value`: append `value`, a JSON string, to each string at
/// `path`, returning the new lengths.
pub async fn Client::json_strappend(
  self : Client,
  key : String,
  path : String,
  value : String,
) -> Array[Int64?] raise RedisError {
  path_ints(
    self.command([
      b"JSON.STRAPPEND",
      arg_string(key),
      arg_string(path),
      arg_string(@json.Json::string(value).stringify()),
    ]),
  )
}

///|
/// `JSON.STRLEN key [path]`: the length of each string at `path`.
pub async fn Client::json_strlen(
  self : Client,
  key : String,
  path? : String,
) -> Array[Int64?] raise RedisError {
  let args : Array[Bytes] = [b"JSON.STRLEN", arg_string(key)]
  push_arg(args, path.map(arg_string))
  path_ints(self.command(args))
}

///|
/// `JSON.ARRLEN key [path]`: the length of each array at `path`.
pub async fn Client::json_arrlen(
  self : Client,
  key : String,
  path? : String,
) -> Array[Int64?] raise RedisError {
  let args : Array[Bytes] = [b"JSON.ARRLEN", arg_string(key)]
  push_arg(args, path.map(arg_string))
  path_ints(self.command(args))
}

///|
/// `JSON.ARRINDEX key path value [START start] [STOP stop]`: the first position of `value`
/// in each array at `path`, `-1` when absent.
pub async fn Client::json_arrindex(
  self : Client,
  key : String,
  path : String,
  value : @json.Json,
  start? : Int64,
  stop? : Int64,
) -> Array[Int64?] raise RedisError {
  let args : Array[Bytes] = [
    b"JSON.ARRINDEX",
    arg_string(key),
    arg_string(path),
    arg_string(value.stringify()),
  ]
  push_some(args, b"START", start.map(arg_int64))
  push_some(args, b"STOP", stop.map(arg_int64))
  path_ints(self.command(args))
}

///|
/// `JSON.ARRAPPEND key path value [value ...]`: append the values to each array at `path`,
/// returning the new lengths.
pub async fn Client::json_arrappend(
  self : Client,
  key : String,
  path : String,
  values : Array[@json.Json],
) -> Array[Int64?] raise RedisError {
  let args : Array[Bytes] = [
    b"JSON.ARRAPPEND",
    arg_string(key),
    arg_string(path),
  ]
  for value in values {
    args.push(arg_string(value.stringify()))
  }
  path_ints(self.command(args))
}

///|
/// `JSON.ARRINSERT key path index value [value ...]`: insert the values at `index` in each
/// array at `path`, returning the new lengths.
pub async fn Client::json_arrinsert(
  self : Client,
  key : String,
  path : String,
  index : Int64,
  values : Array[@json.Json],
) -> Array[Int64?] raise RedisError {
  let args : Array[Bytes] = [
    b"JSON.ARRINSERT",
    arg_string(key),
    arg_string(path),
    arg_int64(index),
  ]
  for value in values {
    args.push(arg_string(value.stringify()))
  }
  path_ints(self.command(args))
}

///|
/// `JSON.ARRPOP key [path] [index]`: pop an element from each array at `path`, `index` -1
/// being the last, returning the popped values.
pub async fn Client::json_arrpop(
  self : Client,
  key : String,
  path : String,
  index? : Int64,
) -> Array[@json.Json?] raise RedisError {
  let args : Array[Bytes] = [b"JSON.ARRPOP", arg_string(key), arg_string(path)]
  push_arg(args, index.map(arg_int64))
  let out : Array[@json.Json?] = []
  for item in path_items(self.command(args)) {
    match item {
      @resp.Null => out.push(None)
      other => out.push(Some(json_reply_value(other)))
    }
  }
  out
}

///|
/// `JSON.ARRTRIM key path start stop`: trim each array at `path` to the inclusive range,
/// returning the new lengths.
pub async fn Client::json_arrtrim(
  self : Client,
  key : String,
  path : String,
  start : Int64,
  stop : Int64,
) -> Array[Int64?] raise RedisError {
  path_ints(
    self.command([
      b"JSON.ARRTRIM",
      arg_string(key),
      arg_string(path),
      arg_int64(start),
      arg_int64(stop),
    ]),
  )
}

///|
/// `JSON.OBJKEYS key [path]`: the member names of each object at `path`.
pub async fn Client::json_objkeys(
  self : Client,
  key : String,
  path? : String,
) -> Array[Array[String]?] raise RedisError {
  let args : Array[Bytes] = [b"JSON.OBJKEYS", arg_string(key)]
  push_arg(args, path.map(arg_string))
  let out : Array[Array[String]?] = []
  for item in path_items(self.command(args)) {
    match item {
      @resp.Null => out.push(None)
      other => out.push(Some(expect_text_list(other)))
    }
  }
  out
}

///|
/// `JSON.OBJLEN key [path]`: the number of members of each object at `path`.
pub async fn Client::json_objlen(
  self : Client,
  key : String,
  path? : String,
) -> Array[Int64?] raise RedisError {
  let args : Array[Bytes] = [b"JSON.OBJLEN", arg_string(key)]
  push_arg(args, path.map(arg_string))
  path_ints(self.command(args))
}

///|
/// `JSON.TOGGLE key path`: flip each boolean at `path`, returning the new values.
pub async fn Client::json_toggle(
  self : Client,
  key : String,
  path : String,
) -> Array[Bool?] raise RedisError {
  path_flags(self.command([b"JSON.TOGGLE", arg_string(key), arg_string(path)]))
}

///|
/// `JSON.CLEAR key [path]`: empty the arrays and objects at `path`, returning how many
/// values were cleared.
pub async fn Client::json_clear(
  self : Client,
  key : String,
  path? : String,
) -> Int64 raise RedisError {
  let args : Array[Bytes] = [b"JSON.CLEAR", arg_string(key)]
  push_arg(args, path.map(arg_string))
  expect_int64(self.command(args))
}

///|
/// `JSON.RESP key [path]`: the value as RESP itself, with `[` and `{` markers for arrays
/// and objects. It is the one JSON command whose reply is not a JSON document.
pub async fn Client::json_resp(
  self : Client,
  key : String,
  path? : String,
) -> @resp.RespValue raise RedisError {
  let args : Array[Bytes] = [b"JSON.RESP", arg_string(key)]
  push_arg(args, path.map(arg_string))
  self.command(args)
}

///|
/// `JSON.DEBUG MEMORY key [path]`: the bytes the value at `path` occupies.
pub async fn Client::json_debug_memory(
  self : Client,
  key : String,
  path? : String,
) -> Int64 raise RedisError {
  let args : Array[Bytes] = [b"JSON.DEBUG", b"MEMORY", arg_string(key)]
  push_arg(args, path.map(arg_string))
  expect_int64(self.command(args))
}