// JsonB paths: parse and validate the `jsonb->a.b.c` field syntax used in point
// tables, and expose helpers for validation rules and stats.
//
// A JsonB field names the key (and optional dotted sub-keys) under which a
// register is exposed by the data-acquisition pipeline. Keeping a real parser
// here lets the validator give precise diagnostics and lets stats/tools reuse
// the same notion of "path segment".

///|
fn is_alpha(c : Char) -> Bool {
  (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}

///|
fn is_digit(c : Char) -> Bool {
  c >= '0' && c <= '9'
}

///|
fn is_alnum(c : Char) -> Bool {
  is_alpha(c) || is_digit(c)
}

///|
fn is_segment(s : String) -> Bool {
  if s.length() == 0 {
    return false
  }
  for i = 0; i < s.length(); i = i + 1 {
    let c = s[i].to_int().unsafe_to_char()
    if !(is_alnum(c) || c == '_') {
      return false
    }
  }
  true
}

///|
fn starts_with(s : String, prefix : String) -> Bool {
  if s.length() < prefix.length() {
    return false
  }
  for i = 0; i < prefix.length(); i = i + 1 {
    if s[i] != prefix[i] {
      return false
    }
  }
  true
}

///|
/// True when the raw field uses the JsonB prefix `jsonb->`.
pub fn is_jsonb_prefixed(s : String) -> Bool {
  starts_with(s, "jsonb->")
}

///|
/// True when the raw jsonb string is effectively empty / unused.
pub fn jsonb_is_unset(s : String) -> Bool {
  s.length() == 0
}

///|
/// Parse a jsonb field into its dotted segments that follow the `jsonb->`
/// prefix. Returns `Err` with a human message when the value is empty, lacks the
/// prefix, an empty segment appears, or a segment contains an invalid character.
pub fn parse_jsonb_path(s : String) -> Result[Array[String], String] {
  if s.length() == 0 {
    return Err("empty jsonb value")
  }
  if !is_jsonb_prefixed(s) {
    return Err("missing 'jsonb->' prefix")
  }
  let segs : Array[String] = []
  let mut cur = StringBuilder()
  for i = 7; i < s.length(); i = i + 1 {
    let c = s[i].to_int().unsafe_to_char()
    if c == '.' {
      let seg = cur.to_string()
      if seg == "" || !is_segment(seg) {
        return Err("invalid segment '" + seg + "'")
      }
      segs.push(seg)
      cur = StringBuilder()
    } else {
      cur.write_char(c)
    }
  }
  let last = cur.to_string()
  if last == "" || !is_segment(last) {
    return Err("invalid segment '" + last + "'")
  }
  segs.push(last)
  if segs.length() == 0 {
    return Err("empty jsonb path")
  }
  Ok(segs)
}

///|
/// The number of segments in a parseable jsonb path (or 0 when unparseable).
pub fn jsonb_depth(s : String) -> Int {
  match parse_jsonb_path(s) {
    Ok(segs) => segs.length()
    Err(_) => 0
  }
}

///|
/// The final (leaf) segment of a jsonb path, used for naming hints.
pub fn jsonb_leaf(s : String) -> String {
  match parse_jsonb_path(s) {
    Ok(segs) => segs[segs.length() - 1]
    Err(_) => s
  }
}

///|
test "parse_jsonb_path accepts a simple path" {
  match parse_jsonb_path("jsonb->coil_temp") {
    Ok(segs) => {
      assert_eq(segs.length(), 1)
      assert_eq(segs[0], "coil_temp")
    }
    Err(_) => assert_true(false)
  }
}

///|
test "parse_jsonb_path accepts a dotted path" {
  match parse_jsonb_path("jsonb->plant.line.temp") {
    Ok(segs) => assert_eq(segs.length(), 3)
    Err(_) => assert_true(false)
  }
}

///|
test "parse_jsonb_path rejects empty and unprefixed values" {
  assert_eq(parse_jsonb_path("") is Ok(_), false)
  assert_eq(parse_jsonb_path("coil_temp") is Ok(_), false)
  assert_eq(parse_jsonb_path("jsonb->") is Ok(_), false)
}

///|
test "parse_jsonb_path rejects empty or invalid segments" {
  assert_eq(parse_jsonb_path("jsonb->a..b") is Ok(_), false)
  assert_eq(parse_jsonb_path("jsonb->a b") is Ok(_), false)
  assert_eq(parse_jsonb_path("jsonb->a!b") is Ok(_), false)
  assert_eq(parse_jsonb_path("jsonb->.a") is Ok(_), false)
}

///|
test "jsonb_depth and jsonb_leaf" {
  assert_eq(jsonb_depth("jsonb->a.b.c"), 3)
  assert_eq(jsonb_depth("garbage"), 0)
  assert_eq(jsonb_leaf("jsonb->line.temp"), "temp")
  assert_eq(jsonb_leaf("jsonb->x"), "x")
  assert_eq(jsonb_leaf(""), "")
}

///|
test "parse_jsonb_path accepts segments with numbers and underscores" {
  assert_eq(parse_jsonb_path("jsonb->line2.temp_r") is Ok(_), true)
  match parse_jsonb_path("jsonb->area.b") {
    Ok(segs) => assert_eq(segs.length(), 2)
    Err(_) => assert_true(false)
  }
}

///|
test "is_jsonb_prefixed matches only the jsonb-> prefix" {
  assert_eq(is_jsonb_prefixed("jsonb->a"), true)
  assert_eq(is_jsonb_prefixed("jsonb->"), true)
  assert_eq(is_jsonb_prefixed("a"), false)
  assert_eq(is_jsonb_prefixed(""), false)
}

///|
test "jsonb_depth agrees with long dotted paths" {
  assert_eq(jsonb_depth("jsonb->a.b.c.d"), 4)
  assert_eq(jsonb_leaf("jsonb->a.b"), "b")
}