///|
/// Structural JSON scanner used only to reject duplicate object members before
/// the standard parser folds them into a Map. Syntax is validated again by
/// `@json.parse`; this scanner enforces resource limits and key uniqueness.
priv struct JsonGuard {
  text : String
  mut offset : Int
  mut nodes : Int
  max_nodes : Int
  max_depth : Int
}

///|
fn JsonGuard::skip_space(self : JsonGuard) -> Unit {
  while self.offset < self.text.length() {
    match self.text[self.offset].to_int() {
      32 | 9 | 10 | 13 => self.offset = self.offset + 1
      _ => break
    }
  }
}

///|
fn JsonGuard::at(self : JsonGuard, ascii : Int) -> Bool {
  self.offset < self.text.length() && self.text[self.offset].to_int() == ascii
}

///|
fn JsonGuard::consume(self : JsonGuard, ascii : Int) -> Bool {
  if self.at(ascii) {
    self.offset = self.offset + 1
    true
  } else {
    false
  }
}

///|
fn JsonGuard::quoted(self : JsonGuard) -> String raise TemporalError {
  if !self.consume(34) {
    raise InvalidArchive("expected JSON string")
  }
  let start = self.offset - 1
  while self.offset < self.text.length() {
    let code = self.text[self.offset].to_int()
    if code == 34 {
      self.offset = self.offset + 1
      let key_json = @json.parse(self.text[start:self.offset]) catch {
        _ => raise InvalidArchive("malformed JSON string")
      }
      return match key_json {
        String(value) => value
        _ => raise InvalidArchive("malformed JSON string")
      }
    }
    if code == 92 {
      self.offset = self.offset + 2
    } else {
      self.offset = self.offset + 1
    }
  }
  raise InvalidArchive("unterminated JSON string")
}

///|
fn JsonGuard::scan_object(
  self : JsonGuard,
  depth : Int,
) -> Unit raise TemporalError {
  ignore(self.consume(123))
  self.skip_space()
  if self.consume(125) {
    return
  }
  let seen : Map[String, Bool] = Map([])
  while true {
    let key = self.quoted()
    if seen.contains(key) {
      raise DuplicateArchiveKey
    }
    seen[key] = true
    self.skip_space()
    if !self.consume(58) {
      raise InvalidArchive("expected colon after object key")
    }
    self.scan_value(depth)
    self.skip_space()
    if self.consume(125) {
      return
    }
    if !self.consume(44) {
      raise InvalidArchive("expected comma between object members")
    }
    self.skip_space()
  }
}

///|
fn JsonGuard::scan_array(
  self : JsonGuard,
  depth : Int,
) -> Unit raise TemporalError {
  ignore(self.consume(91))
  self.skip_space()
  if self.consume(93) {
    return
  }
  while true {
    self.scan_value(depth)
    self.skip_space()
    if self.consume(93) {
      return
    }
    if !self.consume(44) {
      raise InvalidArchive("expected comma between array elements")
    }
    self.skip_space()
  }
}

///|
fn JsonGuard::scan_atom(self : JsonGuard) -> Unit raise TemporalError {
  let start = self.offset
  while self.offset < self.text.length() {
    let code = self.text[self.offset].to_int()
    if code == 44 ||
      code == 93 ||
      code == 125 ||
      code == 32 ||
      code == 9 ||
      code == 10 ||
      code == 13 {
      break
    }
    self.offset = self.offset + 1
  }
  if self.offset == start {
    raise InvalidArchive("empty JSON atom")
  }
}

///|
fn JsonGuard::scan_value(
  self : JsonGuard,
  depth : Int,
) -> Unit raise TemporalError {
  if depth > self.max_depth {
    raise InvalidArchive("JSON nesting limit exceeded")
  }
  self.nodes = self.nodes + 1
  if self.nodes > self.max_nodes {
    raise InvalidArchive("JSON node limit exceeded")
  }
  self.skip_space()
  if self.at(123) {
    self.scan_object(depth + 1)
  } else if self.at(91) {
    self.scan_array(depth + 1)
  } else if self.at(34) {
    ignore(self.quoted())
  } else {
    self.scan_atom()
  }
}

///|
fn reject_duplicate_json_keys(text : String) -> Unit raise TemporalError {
  let scanner = JsonGuard::{
    text,
    offset: 0,
    nodes: 0,
    max_nodes: 1000000,
    max_depth: 128,
  }
  scanner.scan_value(0)
  scanner.skip_space()
  if scanner.offset != text.length() {
    raise InvalidArchive("trailing JSON content")
  }
}