///|
/// The JRD data model: `JsonResourceDescriptor`, `JrdLink`,
/// `PropertyValue`, and semantic-equality helpers.
///
/// The model follows RFC 7033 Section 4.4:
///
/// * `subject` — OPTIONAL per the RFC (SHOULD be present); a URI string.
/// * `aliases` — OPTIONAL; array of URI strings.
/// * `properties` — OPTIONAL; property URI mapped to a string or null.
/// * `links` — OPTIONAL; array of link relation objects; array order MAY
///   indicate preference (first link is the preferred one).
///
/// In addition, unknown members (top level and per link) are preserved in
/// `extensions` maps: RFC 7033 Section 4.4 requires that clients MUST
/// ignore unknown members and not treat them as errors, so the parser
/// keeps them for forward-compatible round-tripping instead of dropping
/// them. Unknown members never influence validation or queries.
///
/// `rel` is REQUIRED by RFC 7033 in every link relation object, so
/// `JrdLink.rel` is a plain `String` (not optional) — a parsed model can
/// only ever contain links that had a rel member.

///|
/// A JRD property value: RFC 7033 allows properties (both subject-level
/// and link-level) to hold either a string or JSON null.
pub enum PropertyValue {
  StringValue(String)
  NullValue
} derive(Eq)

///|
/// Construct a string property value. Provided because the enum variants
/// themselves are read-only for consumer packages.
pub fn PropertyValue::string(s : String) -> PropertyValue {
  StringValue(s)
}

///|
/// Construct a null property value. Provided because the enum variants
/// themselves are read-only for consumer packages.
pub fn PropertyValue::null() -> PropertyValue {
  NullValue
}

///|
/// Whether this property value is null.
pub fn PropertyValue::is_null(self : PropertyValue) -> Bool {
  match self {
    NullValue => true
    _ => false
  }
}

///|
/// The string payload, or `None` for a null value.
pub fn PropertyValue::as_string(self : PropertyValue) -> String? {
  match self {
    StringValue(s) => Some(s)
    NullValue => None
  }
}

///|
/// A stable display form for tests and CLI output.
pub fn PropertyValue::to_display(self : PropertyValue) -> String {
  match self {
    StringValue(s) => s
    NullValue => ""
  }
}

///|
/// A link relation object as defined by RFC 7033 Section 4.4.4.
///
/// Members:
///
/// * `rel` — REQUIRED; exactly one URI or registered relation type.
/// * `media_type` — OPTIONAL; the media type of the link target (`type`).
/// * `href` — OPTIONAL; the target URI.
/// * `titles` — OPTIONAL; language tag (or `"und"`) to human-readable
///   title. RFC 7033 says a JRD SHOULD NOT repeat a language tag, but
///   repetition MUST NOT be treated as an error; the underlying JSON
///   parser collapses duplicate object keys before the library sees them
///   (see `docs/limitations.md`).
/// * `properties` — OPTIONAL; property URI to string-or-null value.
/// * `extensions` — unknown link members, preserved for round-tripping.
pub struct JrdLink {
  rel : String
  media_type : String?
  href : String?
  titles : Map[String, String]
  properties : Map[String, PropertyValue]
  extensions : Map[String, Json]
}

///|
/// Create an empty link with the given (non-validated) relation type.
pub fn JrdLink::new(rel : String) -> JrdLink {
  {
    rel,
    media_type: None,
    href: None,
    titles: Map([]),
    properties: Map([]),
    extensions: Map([]),
  }
}

///|
/// Case-insensitive (ASCII) lookup of a title by language tag. Used after
/// an exact lookup missed; if several keys differ only by case, the first
/// one in map order wins.
fn find_title_ci(titles : Map[String, String], tag : String) -> String? {
  let wanted = tag.to_lower()
  for k, v in titles {
    if k.to_lower() == wanted {
      return Some(v)
    }
  }
  None
}

///|
/// Whether this link carries a title for exactly the given language tag.
/// The match is exact first, then ASCII case-insensitive; full RFC 4647
/// language negotiation is out of scope (see `docs/limitations.md`).
pub fn JrdLink::has_title(self : JrdLink, language : String) -> Bool {
  self.titles.contains(language) ||
  find_title_ci(self.titles, language) is Some(_)
}

///|
/// The title for exactly the given language tag (exact match preferred,
/// then ASCII case-insensitive), or `None`.
pub fn JrdLink::title(self : JrdLink, language : String) -> String? {
  match self.titles.get(language) {
    Some(t) => Some(t)
    None => find_title_ci(self.titles, language)
  }
}

///|
/// The first title whose language tag appears in `languages`, in the
/// caller-given priority order, or `None`. Exact, ASCII case-insensitive
/// matching; no RFC 4647 negotiation.
pub fn JrdLink::preferred_title(
  self : JrdLink,
  languages : Array[String],
) -> String? {
  for lang in languages {
    match self.title(lang) {
      Some(t) => return Some(t)
      None => continue
    }
  }
  None
}

///|
/// A deterministic fallback title when the caller has no language
/// preference: the `"und"` title if present, otherwise the title with the
/// lexicographically smallest language tag, otherwise `None`.
pub fn JrdLink::fallback_title(self : JrdLink) -> String? {
  match self.titles.get("und") {
    Some(t) => return Some(t)
    None => ()
  }
  let tags : Array[String] = []
  for k in self.titles.keys() {
    tags.push(k)
  }
  if tags.length() == 0 {
    return None
  }
  tags.sort()
  self.titles.get(tags[0])
}

///|
/// Whether this link has a title in any language.
pub fn JrdLink::has_any_title(self : JrdLink) -> Bool {
  !self.titles.is_empty()
}

///|
/// A link property value, or `None` if absent.
pub fn JrdLink::property(self : JrdLink, uri : String) -> PropertyValue? {
  self.properties.get(uri)
}

///|
/// Whether this link has a property with the given URI.
pub fn JrdLink::has_property(self : JrdLink, uri : String) -> Bool {
  self.properties.contains(uri)
}

///|
/// The library version. Mirrors `version` in moon.mod; both must be
/// updated together (single source discipline documented in
/// `docs/reproduction.md`).
pub fn library_version() -> String {
  "0.1.0"
}

///|
/// A JSON Resource Descriptor as defined by RFC 7033 Section 4.4.
pub struct JsonResourceDescriptor {
  subject : String?
  aliases : Array[String]
  properties : Map[String, PropertyValue]
  links : Array[JrdLink]
  extensions : Map[String, Json]
}

///|
/// Create an empty JRD (no subject, no aliases, no properties, no links).
pub fn JsonResourceDescriptor::empty() -> JsonResourceDescriptor {
  {
    subject: None,
    aliases: [],
    properties: Map([]),
    links: [],
    extensions: Map([]),
  }
}

///|
/// The number of links in this JRD.
pub fn JsonResourceDescriptor::link_count(self : JsonResourceDescriptor) -> Int {
  self.links.length()
}

///|
/// A subject-level property value, or `None` if absent.
pub fn JsonResourceDescriptor::property(
  self : JsonResourceDescriptor,
  uri : String,
) -> PropertyValue? {
  self.properties.get(uri)
}

///|
/// Whether this JRD has a subject-level property with the given URI.
pub fn JsonResourceDescriptor::has_property(
  self : JsonResourceDescriptor,
  uri : String,
) -> Bool {
  self.properties.contains(uri)
}

///|
/// Whether this JRD carries any unknown (extension) top-level members.
pub fn JsonResourceDescriptor::has_extensions(
  self : JsonResourceDescriptor,
) -> Bool {
  !self.extensions.is_empty()
}

///|
/// Semantic equality of two property maps: same keys, and for each key
/// equal values. Key order is irrelevant.
fn property_maps_equal(
  a : Map[String, PropertyValue],
  b : Map[String, PropertyValue],
) -> Bool {
  if a.length() != b.length() {
    return false
  }
  for k, v in a {
    match b.get(k) {
      Some(w) => if v != w { return false }
      None => return false
    }
  }
  true
}

///|
/// Semantic equality of two title maps (key order irrelevant).
fn title_maps_equal(a : Map[String, String], b : Map[String, String]) -> Bool {
  if a.length() != b.length() {
    return false
  }
  for k, v in a {
    match b.get(k) {
      Some(w) => if v != w { return false }
      None => return false
    }
  }
  true
}

///|
/// Element-wise equality of string arrays (MoonBit `Array` has no
/// structural `==`, so this helper exists).
pub fn same_strings(a : Array[String], b : Array[String]) -> Bool {
  if a.length() != b.length() {
    return false
  }
  let mut i = 0
  while i < a.length() {
    if a[i] != b[i] {
      return false
    }
    i = i + 1
  }
  true
}

///|
/// Semantic equality of two links: all members compared structurally,
/// with map and array key/element order ignored.
pub fn jrd_link_semantic_equal(a : JrdLink, b : JrdLink) -> Bool {
  a.rel == b.rel &&
  a.media_type == b.media_type &&
  a.href == b.href &&
  title_maps_equal(a.titles, b.titles) &&
  property_maps_equal(a.properties, b.properties) &&
  a.extensions == b.extensions
}

///|
/// Semantic equality of two links arrays (order matters: RFC 7033 says
/// links array order MAY indicate preference, so it is semantic).
pub fn jrd_links_semantic_equal(a : Array[JrdLink], b : Array[JrdLink]) -> Bool {
  if a.length() != b.length() {
    return false
  }
  let mut i = 0
  while i < a.length() {
    if !jrd_link_semantic_equal(a[i], b[i]) {
      return false
    }
    i = i + 1
  }
  true
}

///|
/// Semantic equality of two JRDs. Used by the deterministic property
/// tests (model -> serialize -> parse -> compare). Member order inside
/// JSON objects is ignored; `links` array order is preserved because it
/// can carry preference information.
pub fn jrd_semantic_equal(
  a : JsonResourceDescriptor,
  b : JsonResourceDescriptor,
) -> Bool {
  a.subject == b.subject &&
  same_strings(a.aliases, b.aliases) &&
  property_maps_equal(a.properties, b.properties) &&
  jrd_links_semantic_equal(a.links, b.links) &&
  a.extensions == b.extensions
}