///|
/// A three-state field used when the wire protocol distinguishes an omitted
/// property from an explicit JSON null.
pub(all) enum ProtocolNullable[T] {
  Omitted
  Null
  Value(T)
} derive(Eq, Debug)

///|
/// Roles used by ACP annotations.
pub(all) enum Role {
  User
  Assistant
} derive(Eq, Debug)

///|
/// Stable ACP identifiers are strings on the wire.
pub type SessionId = String

///|
pub type MessageId = String

///|
pub type ToolCallId = String

///|
pub type TerminalId = String

///|
pub type PermissionOptionId = String

///|
pub type SessionConfigId = String

///|
pub type SessionConfigValueId = String

///|
pub type SessionConfigGroupId = String

///|
pub type SessionModeId = String

///|
/// Structured failures raised while decoding the stable ACP data model.
///
/// The path is always a wire-property path (with array indexes where
/// applicable).  No decoder in this package turns an invalid value into a
/// default value.
pub(all) suberror ProtocolDecodeError {
  JsonParse(message~ : String)
  ExpectedObject(path~ : String)
  ExpectedArray(path~ : String)
  ExpectedString(path~ : String)
  ExpectedBoolean(path~ : String)
  ExpectedNumber(path~ : String)
  MissingField(path~ : String)
  UnknownField(path~ : String)
  InvalidField(path~ : String, reason~ : String)
  InvalidDiscriminator(path~ : String, value~ : String)
  InvalidPath(path~ : String, value~ : String)
  InvalidBase64(path~ : String)
} derive(Eq, Debug)

///|
/// Parse one JSON value for the protocol data-model codecs.
pub fn protocol_parse_json(source : String) -> Json raise ProtocolDecodeError {
  @json.parse(source) catch {
    error => raise JsonParse(message=error.to_string())
  }
}

///|
/// Stringify one protocol JSON value without adding a framing newline.
pub fn protocol_stringify_json(value : Json) -> String {
  value.stringify()
}

///|
pub fn protocol_field(
  fields : Map[String, Json],
  key : String,
) -> ProtocolNullable[Json] {
  match fields.get(key) {
    None => Omitted
    Some(Null) => Null
    Some(value) => Value(value)
  }
}

///|
pub fn protocol_require_object(
  value : Json,
  path~ : String,
) -> Map[String, Json] raise ProtocolDecodeError {
  match value {
    Object(fields) => fields
    _ => raise ExpectedObject(path~)
  }
}

///|
pub fn protocol_require_array(
  value : Json,
  path~ : String,
) -> Array[Json] raise ProtocolDecodeError {
  match value {
    Array(values) => values
    _ => raise ExpectedArray(path~)
  }
}

///|
pub fn protocol_required(
  fields : Map[String, Json],
  key : String,
  path~ : String,
) -> Json raise ProtocolDecodeError {
  match fields.get(key) {
    Some(value) => value
    None => raise MissingField(path~)
  }
}

///|
pub fn protocol_required_string(
  fields : Map[String, Json],
  key : String,
  path~ : String,
) -> String raise ProtocolDecodeError {
  match protocol_required(fields, key, path~) {
    String(value) => value
    _ => raise ExpectedString(path~)
  }
}

///|
pub fn protocol_decode_string(
  value : Json,
  path~ : String,
) -> String raise ProtocolDecodeError {
  match value {
    String(text) => text
    _ => raise ExpectedString(path~)
  }
}

///|
fn protocol_decode_number(
  value : Json,
  path~ : String,
) -> Double raise ProtocolDecodeError {
  match value {
    Number(number, ..) => number
    _ => raise ExpectedNumber(path~)
  }
}

///|
pub fn protocol_decode_boolean(
  value : Json,
  path~ : String,
) -> Bool raise ProtocolDecodeError {
  match value {
    True => true
    False => false
    _ => raise ExpectedBoolean(path~)
  }
}

///|
fn protocol_decode_int64(
  value : Json,
  path~ : String,
) -> Int64 raise ProtocolDecodeError {
  match value {
    String(_) =>
      @json.from_json(value) catch {
        _ =>
          raise InvalidField(path~, reason="expected a signed 64-bit integer")
      }
    Number(number, repr~) => protocol_decode_number_int64(number, repr, path~)
    _ => raise InvalidField(path~, reason="expected a signed 64-bit integer")
  }
}

///|
fn protocol_decode_number_int64(
  number : Double,
  repr : String?,
  path~ : String,
) -> Int64 raise ProtocolDecodeError {
  match protocol_parse_int64_number(number, repr) {
    Ok(value) => value
    Err(Malformed | NonFinite | Fractional | OutOfRange | PrecisionUnavailable) =>
      raise InvalidField(path~, reason="expected a signed 64-bit integer")
  }
}

///|
fn protocol_decode_uint64(
  value : Json,
  path~ : String,
) -> UInt64 raise ProtocolDecodeError {
  match value {
    String(_) =>
      @json.from_json(value) catch {
        _ =>
          raise InvalidField(
            path~,
            reason="expected a non-negative 64-bit integer",
          )
      }
    Number(number, repr~) => protocol_decode_number_uint64(number, repr, path~)
    _ =>
      raise InvalidField(path~, reason="expected a non-negative 64-bit integer")
  }
}

///|
fn protocol_decode_number_uint64(
  number : Double,
  repr : String?,
  path~ : String,
) -> UInt64 raise ProtocolDecodeError {
  match protocol_parse_uint64_number(number, repr) {
    Ok(value) => value
    Err(Malformed | NonFinite | Fractional | OutOfRange | PrecisionUnavailable) =>
      raise InvalidField(path~, reason="expected a non-negative 64-bit integer")
  }
}

///|
priv enum ProtocolUInt64NumberError {
  Malformed
  NonFinite
  Fractional
  OutOfRange
  PrecisionUnavailable
}

///|
/// Decode an unsigned integer-valued JSON number without trusting a rounded
/// `Double`.  The JSON parser retains the original decimal representation in
/// `repr` when the value is not losslessly represented by a finite `Double`.
fn protocol_parse_uint64_number(
  number : Double,
  repr : String?,
) -> Result[UInt64, ProtocolUInt64NumberError] {
  match repr {
    Some(text) => protocol_parse_uint64_repr(text)
    None =>
      if number.is_nan() || number.is_inf() {
        Err(NonFinite)
      } else if number != number.floor() {
        Err(Fractional)
      } else if number < 0.0 {
        Err(OutOfRange)
      } else if number > 9007199254740991.0 {
        Err(PrecisionUnavailable)
      } else {
        Ok(number.to_uint64())
      }
  }
}

///|
fn protocol_parse_uint64_repr(
  text : String,
) -> Result[UInt64, ProtocolUInt64NumberError] {
  let chars = text.to_array()
  let length = chars.length()
  if length == 0 {
    return Err(Malformed)
  }
  let mut index = 0
  if chars[index] == '-' {
    index += 1
    if index == length {
      return Err(Malformed)
    }
  }
  let start = index
  if chars[index] == '0' {
    index += 1
    if index < length && protocol_is_digit(chars[index]) {
      return Err(Malformed)
    }
  } else if protocol_is_digit(chars[index]) {
    index += 1
    while index < length && protocol_is_digit(chars[index]) {
      index += 1
    }
  } else {
    return Err(Malformed)
  }
  let decimal_index : Int? = if index < length && chars[index] == '.' {
    let decimal = index
    index += 1
    let fraction_start = index
    while index < length && protocol_is_digit(chars[index]) {
      index += 1
    }
    if index == fraction_start {
      return Err(Malformed)
    }
    Some(decimal)
  } else {
    None
  }
  let exponent_index : Int? = if index < length &&
    (chars[index] == 'e' || chars[index] == 'E') {
    let exponent = index
    index += 1
    if index < length && (chars[index] == '+' || chars[index] == '-') {
      index += 1
    }
    let exponent_start = index
    while index < length && protocol_is_digit(chars[index]) {
      index += 1
    }
    if index == exponent_start {
      return Err(Malformed)
    }
    Some(exponent)
  } else {
    None
  }
  if index != length {
    return Err(Malformed)
  }
  match (decimal_index, exponent_index) {
    (None, None) => protocol_parse_uint64_text(text)
    _ =>
      protocol_parse_uint64_decimal(chars, start, decimal_index, exponent_index)
  }
}

///|
fn protocol_is_digit(value : Char) -> Bool {
  let digit = value.to_int() - '0'
  digit >= 0 && digit <= 9
}

///|
fn protocol_parse_uint64_text(
  text : String,
) -> Result[UInt64, ProtocolUInt64NumberError] {
  let chars = text.to_array()
  let length = chars.length()
  if length == 0 {
    return Err(OutOfRange)
  }
  let negative = chars[0] == '-'
  let start = if negative { 1 } else { 0 }
  if start == length {
    return Err(OutOfRange)
  }
  let max_value : UInt64 = 18446744073709551615UL
  let mut value : UInt64 = 0UL
  for index in start.. 9 {
      return Err(OutOfRange)
    }
    let digit64 = digit.to_uint64()
    if negative {
      if digit64 != 0UL {
        return Err(OutOfRange)
      }
    } else {
      if value > max_value / 10UL ||
        (value == max_value / 10UL && digit64 > 5UL) {
        return Err(OutOfRange)
      }
      value = value * 10UL + digit64
    }
  }
  Ok(value)
}

///|
priv enum ProtocolDecimalShift {
  Finite(negative~ : Bool, magnitude~ : Int)
  Huge(negative~ : Bool)
}

///|
fn protocol_decimal_shift(
  chars : Array[Char],
  start : Int,
  end : Int,
) -> ProtocolDecimalShift {
  let negative = chars[start] == '-'
  let digit_start = if negative || chars[start] == '+' {
    start + 1
  } else {
    start
  }
  let mut magnitude = 0
  let mut huge = false
  for index in digit_start.. 100000 / 10 || magnitude * 10 > 100000 - digit {
        huge = true
      } else {
        magnitude = magnitude * 10 + digit
      }
    }
  }
  if huge {
    Huge(negative~)
  } else {
    Finite(negative~, magnitude~)
  }
}

///|
fn protocol_trim_leading_zeroes(text : String) -> String {
  let chars = text.to_array()
  let mut first = 0
  for index in 0.. Result[UInt64, ProtocolUInt64NumberError] {
  let mantissa_end = match exponent_index {
    Some(index) => index
    None => chars.length()
  }
  let fractional_digits = match decimal_index {
    Some(index) => mantissa_end - index - 1
    None => 0
  }
  let digits_builder = StringBuilder(size_hint=mantissa_end - start)
  let mut nonzero = false
  for index in start.. protocol_decimal_shift(chars, index + 1, chars.length())
    None => Finite(negative=false, magnitude=0)
  }
  let scale = match shift {
    Finite(negative=true, magnitude~) => -magnitude - fractional_digits
    Finite(negative=false, magnitude~) => magnitude - fractional_digits
    Huge(negative=true) => return Err(Fractional)
    Huge(negative=false) => return Err(OutOfRange)
  }
  if scale < 0 {
    let remove = -scale
    if remove >= digits.length() {
      return Err(Fractional)
    }
    let digit_chars = digits.to_array()
    for index in (digits.length() - remove).. 20 {
      return Err(OutOfRange)
    }
    let zeros = "0".repeat(scale)
    let sign = if start == 1 { "-" } else { "" }
    protocol_parse_uint64_text(sign + digits + zeros)
  }
}

///|
priv enum ProtocolInt64NumberError {
  Malformed
  NonFinite
  Fractional
  OutOfRange
  PrecisionUnavailable
}

///|
/// Decode a signed integer-valued JSON number without trusting a rounded
/// `Double`.  A repr-less Double is accepted only inside the exact binary
/// integer range; larger values must retain their original JSON text.
fn protocol_parse_int64_number(
  number : Double,
  repr : String?,
) -> Result[Int64, ProtocolInt64NumberError] {
  match repr {
    Some(text) => protocol_parse_int64_repr(text)
    None =>
      if number.is_nan() || number.is_inf() {
        Err(NonFinite)
      } else if number != number.floor() {
        Err(Fractional)
      } else if number.abs() > 9007199254740991.0 {
        Err(PrecisionUnavailable)
      } else {
        Ok(number.to_int64())
      }
  }
}

///|
fn protocol_parse_int64_repr(
  text : String,
) -> Result[Int64, ProtocolInt64NumberError] {
  let chars = text.to_array()
  let length = chars.length()
  if length == 0 {
    return Err(Malformed)
  }
  let mut index = 0
  if chars[index] == '-' {
    index += 1
    if index == length {
      return Err(Malformed)
    }
  }
  let start = index
  if chars[index] == '0' {
    index += 1
    if index < length && protocol_is_digit(chars[index]) {
      return Err(Malformed)
    }
  } else if protocol_is_digit(chars[index]) {
    index += 1
    while index < length && protocol_is_digit(chars[index]) {
      index += 1
    }
  } else {
    return Err(Malformed)
  }
  let decimal_index : Int? = if index < length && chars[index] == '.' {
    let decimal = index
    index += 1
    let fraction_start = index
    while index < length && protocol_is_digit(chars[index]) {
      index += 1
    }
    if index == fraction_start {
      return Err(Malformed)
    }
    Some(decimal)
  } else {
    None
  }
  let exponent_index : Int? = if index < length &&
    (chars[index] == 'e' || chars[index] == 'E') {
    let exponent = index
    index += 1
    if index < length && (chars[index] == '+' || chars[index] == '-') {
      index += 1
    }
    let exponent_start = index
    while index < length && protocol_is_digit(chars[index]) {
      index += 1
    }
    if index == exponent_start {
      return Err(Malformed)
    }
    Some(exponent)
  } else {
    None
  }
  if index != length {
    return Err(Malformed)
  }
  match (decimal_index, exponent_index) {
    (None, None) => protocol_parse_int64_text(text)
    _ =>
      protocol_parse_int64_decimal(chars, start, decimal_index, exponent_index)
  }
}

///|
fn protocol_parse_int64_text(
  text : String,
) -> Result[Int64, ProtocolInt64NumberError] {
  let chars = text.to_array()
  let length = chars.length()
  if length == 0 {
    return Err(OutOfRange)
  }
  let negative = chars[0] == '-'
  let start = if negative { 1 } else { 0 }
  if start == length {
    return Err(OutOfRange)
  }
  let min_value : Int64 = -9223372036854775808L
  let max_value : Int64 = 9223372036854775807L
  let mut value : Int64 = 0L
  for index in start.. 9 {
      return Err(OutOfRange)
    }
    let digit64 = digit.to_int64()
    if negative {
      if value < min_value / 10L || (value == min_value / 10L && digit64 > 8L) {
        return Err(OutOfRange)
      }
      value = value * 10L - digit64
    } else {
      if value > max_value / 10L || (value == max_value / 10L && digit64 > 7L) {
        return Err(OutOfRange)
      }
      value = value * 10L + digit64
    }
  }
  Ok(value)
}

///|
fn protocol_parse_int64_decimal(
  chars : Array[Char],
  start : Int,
  decimal_index : Int?,
  exponent_index : Int?,
) -> Result[Int64, ProtocolInt64NumberError] {
  let mantissa_end = match exponent_index {
    Some(index) => index
    None => chars.length()
  }
  let fractional_digits = match decimal_index {
    Some(index) => mantissa_end - index - 1
    None => 0
  }
  let digits_builder = StringBuilder(size_hint=mantissa_end - start)
  let mut nonzero = false
  for index in start.. protocol_decimal_shift(chars, index + 1, chars.length())
    None => Finite(negative=false, magnitude=0)
  }
  let scale = match shift {
    Finite(negative=true, magnitude~) => -magnitude - fractional_digits
    Finite(negative=false, magnitude~) => magnitude - fractional_digits
    Huge(negative=true) => return Err(Fractional)
    Huge(negative=false) => return Err(OutOfRange)
  }
  if scale < 0 {
    let remove = -scale
    if remove >= digits.length() {
      return Err(Fractional)
    }
    let digit_chars = digits.to_array()
    for index in (digits.length() - remove).. 19 {
      return Err(OutOfRange)
    }
    let zeros = "0".repeat(scale)
    let sign = if start == 1 { "-" } else { "" }
    protocol_parse_int64_text(sign + digits + zeros)
  }
}

///|
fn protocol_decode_wire_uint64(
  value : Json,
  path~ : String,
) -> UInt64 raise ProtocolDecodeError {
  match value {
    Number(number, repr~) => protocol_decode_number_uint64(number, repr, path~)
    _ => raise InvalidField(path~, reason="expected a non-negative integer")
  }
}

///|
fn protocol_encode_uint64(value : UInt64) -> Json {
  Json::number(value.to_double(), repr=value.to_string())
}

///|
pub fn protocol_nullable_string(
  fields : Map[String, Json],
  key : String,
  path~ : String,
) -> ProtocolNullable[String] raise ProtocolDecodeError {
  match protocol_field(fields, key) {
    Omitted => Omitted
    Null => Null
    Value(value) => Value(protocol_decode_string(value, path~))
  }
}

///|
fn protocol_nullable_json(
  fields : Map[String, Json],
  key : String,
) -> ProtocolNullable[Json] {
  match protocol_field(fields, key) {
    Omitted => Omitted
    Null => Null
    Value(value) => Value(value)
  }
}

///|
pub fn protocol_meta(
  fields : Map[String, Json],
  path~ : String,
) -> ProtocolNullable[Json] raise ProtocolDecodeError {
  match protocol_nullable_json(fields, "_meta") {
    Value(value) =>
      match value {
        Object(_) => Value(value)
        _ => raise InvalidField(path~, reason="_meta must be an object or null")
      }
    other => other
  }
}

///|
pub fn[T] protocol_put_nullable(
  fields : Map[String, Json],
  key : String,
  value : ProtocolNullable[T],
  encode : (T) -> Json,
) -> Unit {
  match value {
    Omitted => ()
    Null => fields[key] = Json::null()
    Value(value) => fields[key] = encode(value)
  }
}

///|
pub fn protocol_put_meta(
  fields : Map[String, Json],
  value : ProtocolNullable[Json],
) -> Unit raise ProtocolDecodeError {
  match value {
    Omitted => ()
    Null => fields["_meta"] = Json::null()
    Value(value) =>
      match value {
        Object(_) => fields["_meta"] = value
        _ =>
          raise InvalidField(
            path="_meta",
            reason="_meta must be an object or null",
          )
      }
  }
}

///|
pub fn protocol_reject_unknown(
  fields : Map[String, Json],
  allowed : ArrayView[String],
  prefix~ : String,
) -> Unit raise ProtocolDecodeError {
  for key, _ in fields {
    if !allowed.contains(key) {
      let path = if prefix == "" { key } else { prefix + "." + key }
      raise UnknownField(path~)
    }
  }
}

///|
fn protocol_path_is_absolute(path : String) -> Bool {
  if path.has_prefix("/") || path.has_prefix("\\\\") {
    true
  } else {
    match (path.get_char(0), path.get_char(1), path.get_char(2)) {
      (Some(first), Some(':'), Some('/' | '\\')) =>
        (first >= 'A' && first <= 'Z') || (first >= 'a' && first <= 'z')
      _ => false
    }
  }
}

///|
pub fn protocol_validate_absolute_path(
  path : String,
  field_path~ : String,
) -> String raise ProtocolDecodeError {
  if protocol_path_is_absolute(path) {
    path
  } else {
    raise InvalidPath(path=field_path, value=path)
  }
}

///|
fn protocol_validate_base64(
  value : String,
  field_path~ : String,
) -> String raise ProtocolDecodeError {
  ignore(
    @base64.decode(value) catch {
      _ => raise InvalidBase64(path=field_path)
    },
  )
  value
}

///|
fn protocol_encode_role(role : Role) -> Json {
  match role {
    User => Json::string("user")
    Assistant => Json::string("assistant")
  }
}

///|
fn protocol_decode_role(
  value : Json,
  path~ : String,
) -> Role raise ProtocolDecodeError {
  match value {
    String("user") => User
    String("assistant") => Assistant
    String(value) => raise InvalidDiscriminator(path~, value~)
    _ => raise ExpectedString(path~)
  }
}