///|
/// Deterministic JRD serialization and canonicalization.
///
/// `serialize_jrd` emits a valid `application/jrd+json` document with a
/// stable, documented layout:
///
/// * top-level members in the order subject, aliases, properties,
///   links, then preserved extension members;
/// * object members (including inside extensions) in lexically sorted
///   key order;
/// * links arrays in model order (RFC 7033 says links array order MAY
///   indicate preference, so it is never reordered);
/// * minimal escaping (quotes, backslashes, control characters);
///   non-ASCII text is emitted as UTF-8, which RFC 8259 permits.
///
/// JSON object member order carries no semantics; the fixed order exists
/// only so that the same model always serializes to the same bytes, which
/// is what makes round-trip and canonicalization properties testable.
///
/// `canonicalize_jrd` applies exactly those rules (stable member order,
/// stable whitespace, stable escaping) and nothing else: it never
/// changes URI character case, drops aliases, merges links, reorders
/// link arrays, or rewrites titles or hrefs, because any of those could
/// change application-level semantics.

///|
/// Internal: write a JSON string with minimal escaping.
fn write_json_string(s : String, sb : StringBuilder) -> Unit {
  sb.write_char('"')
  for c in s {
    let cp = c.to_int()
    if c == '"' {
      sb.write_string("\\\"")
    } else if c == '\\' {
      sb.write_string("\\\\")
    } else if c == '\n' {
      sb.write_string("\\n")
    } else if c == '\r' {
      sb.write_string("\\r")
    } else if c == '\t' {
      sb.write_string("\\t")
    } else if c == '\b' {
      sb.write_string("\\b")
    } else if c == '\f' {
      sb.write_string("\\f")
    } else if cp < 0x20 {
      sb.write_string("\\u00")
      sb.write_char(hex_digit_upper(cp / 16))
      sb.write_char(hex_digit_upper(cp % 16))
    } else {
      sb.write_char(c)
    }
  }
  sb.write_char('"')
}

///|
/// Internal: keys of a JSON object in lexically sorted order.
fn sorted_keys(members : Map[String, Json]) -> Array[String] {
  let keys : Array[String] = []
  for k in members.keys() {
    keys.push(k)
  }
  keys.sort()
  keys
}

///|
/// Internal: render a number. Prefer the original literal representation
/// when the value was parsed (so round-trips keep e.g. `3.50`); otherwise
/// use the platform's shortest round-trip form. NaN and infinities have
/// no JSON representation and become `null`.
fn format_double(n : Double, repr : String?) -> String {
  match repr {
    Some(r) => r
    None => {
      if n != n {
        return "null"
      }
      if n > 1.7976931348623157e308 {
        return "null"
      }
      if n < -1.7976931348623157e308 {
        return "null"
      }
      n.to_string()
    }
  }
}

///|
/// Internal: compact JSON writer (no whitespace). Object keys are
/// written in sorted order.
fn write_json_compact(j : Json, sb : StringBuilder) -> Unit {
  match j {
    Null => sb.write_string("null")
    True => sb.write_string("true")
    False => sb.write_string("false")
    Number(n, repr~) => sb.write_string(format_double(n, repr))
    String(s) => write_json_string(s, sb)
    Array(arr) => {
      sb.write_char('[')
      let mut i = 0
      while i < arr.length() {
        if i > 0 {
          sb.write_char(',')
        }
        write_json_compact(arr[i], sb)
        i = i + 1
      }
      sb.write_char(']')
    }
    Object(members) => {
      sb.write_char('{')
      let keys = sorted_keys(members)
      let mut i = 0
      while i < keys.length() {
        if i > 0 {
          sb.write_char(',')
        }
        write_json_string(keys[i], sb)
        sb.write_char(':')
        match members.get(keys[i]) {
          Some(v) => write_json_compact(v, sb)
          None => abort("internal error: sorted key missing from object")
        }
        i = i + 1
      }
      sb.write_char('}')
    }
  }
}

///|
/// Internal: write indentation of `depth` levels, `width` spaces each.
fn write_pad(sb : StringBuilder, depth : Int, width : Int) -> Unit {
  let mut i = 0
  let total = depth * width
  while i < total {
    sb.write_char(' ')
    i = i + 1
  }
}

///|
/// Internal: indented JSON writer. Object keys are written in sorted
/// order.
fn write_json_indented(
  j : Json,
  sb : StringBuilder,
  width : Int,
  depth : Int,
) -> Unit {
  match j {
    Array(arr) =>
      if arr.length() == 0 {
        sb.write_string("[]")
      } else {
        sb.write_string("[\n")
        let mut i = 0
        while i < arr.length() {
          if i > 0 {
            sb.write_string(",\n")
          }
          write_pad(sb, depth + 1, width)
          write_json_indented(arr[i], sb, width, depth + 1)
          i = i + 1
        }
        sb.write_char('\n')
        write_pad(sb, depth, width)
        sb.write_char(']')
      }
    Object(members) =>
      if members.is_empty() {
        sb.write_string("{}")
      } else {
        sb.write_string("{\n")
        let keys = sorted_keys(members)
        let mut i = 0
        while i < keys.length() {
          if i > 0 {
            sb.write_string(",\n")
          }
          write_pad(sb, depth + 1, width)
          write_json_string(keys[i], sb)
          sb.write_string(": ")
          match members.get(keys[i]) {
            Some(v) => write_json_indented(v, sb, width, depth + 1)
            None => abort("internal error: sorted key missing from object")
          }
          i = i + 1
        }
        sb.write_char('\n')
        write_pad(sb, depth, width)
        sb.write_char('}')
      }
    other => write_json_compact(other, sb)
  }
}

///|
/// Serialize any core `Json` value to a compact JSON string with object
/// keys in sorted order. Used by the CLI for deterministic output.
pub fn stringify_json(j : Json) -> String {
  let sb = StringBuilder::new()
  write_json_compact(j, sb)
  sb.to_string()
}

///|
/// Serialize any core `Json` value to an indented JSON string with
/// object keys in sorted order. `indent` is the number of spaces per
/// nesting level.
pub fn stringify_json_with_indent(j : Json, indent : Int) -> String {
  if indent <= 0 {
    return stringify_json(j)
  }
  let sb = StringBuilder::new()
  write_json_indented(j, sb, indent, 0)
  sb.to_string()
}

///|
/// Serialize a JRD model to a compact, deterministic
/// `application/jrd+json` document.
pub fn serialize_jrd(jrd : JsonResourceDescriptor) -> String {
  stringify_json(jrd_to_json(jrd))
}

///|
/// Serialize a JRD model to an indented, deterministic
/// `application/jrd+json` document.
pub fn serialize_jrd_with_indent(
  jrd : JsonResourceDescriptor,
  indent : Int,
) -> String {
  stringify_json_with_indent(jrd_to_json(jrd), indent)
}

///|
/// Canonicalize a JRD model: stable member order, stable whitespace
/// (compact), stable escaping. Nothing else is changed — no URI case
/// folding, no alias dropping, no link merging or reordering.
pub fn canonicalize_jrd(jrd : JsonResourceDescriptor) -> String {
  serialize_jrd(jrd)
}

///|
/// Canonicalize a raw JRD document (parse, then re-serialize
/// canonically). Fails with a structured error on invalid JSON or on an
/// invalid JRD.
pub fn canonicalize_jrd_text(input : String) -> Result[String, WebFingerError] {
  match parse_jrd(input) {
    Ok(jrd) => Ok(canonicalize_jrd(jrd))
    Err(e) => Err(e)
  }
}