///|
/// RFC semantic validation of parsed JRDs.
///
/// The parser (`parser.mbt` / `json_adapter.mbt`) enforces JSON member
/// *types* and limits. This module enforces the *semantic* constraints
/// that RFC 7033 places on a JRD, separated into three severity levels:
///
/// * `Error` — violations of RFC MUSTs (e.g. `subject` and `aliases`
///   must be URIs; `properties` names must be URIs; `rel` must be a URI
///   or registered relation type; `href` must be a URI);
/// * `Warning` — violations of RFC SHOULDs that are explicitly not
///   hard errors (e.g. the subject SHOULD be present per Section 4.4.1);
/// * `Info` — permitted variations the caller may want to know about
///   (e.g. a subject that differs from the queried resource, which RFC
///   7033 Section 4.4.1 explicitly allows).
///
/// `validate_jrd` collects every issue; `validate_jrd_all` reports
/// whether any Error-severity issue exists. Validation never modifies
/// the model and never follows any link.

///|
/// Severity of a validation issue.
pub enum ValidationSeverity {
  Error
  Warning
  Info
}

///|
pub impl Show for ValidationSeverity with fn to_string(self) -> String {
  match self {
    Error => "Error"
    Warning => "Warning"
    Info => "Info"
  }
}

///|
/// A single validation issue. `code` is a stable machine-readable name
/// (e.g. `MissingSubject`), `message` a human-readable description.
pub struct ValidationIssue {
  severity : ValidationSeverity
  code : String
  message : String
}

///|
/// The severity name, for consumer packages that cannot match the enum.
pub fn ValidationIssue::severity_name(self : ValidationIssue) -> String {
  Show::to_string(self.severity)
}

///|
/// Internal: RFC 5646 basic-syntax check for a language tag: a primary
/// subtag of 1..8 ASCII letters followed by zero or more subtags of
/// 1..8 ASCII letters or digits, or the reserved tag `und`. This is a
/// syntactic check only; full RFC 5646 registry validation is out of
/// scope.
fn is_plausible_language_tag(tag : String) -> Bool {
  if tag == "und" {
    return true
  }
  let mut subtag_len = 0
  let mut subtag_index = 0
  let mut i = 0
  while i < tag.length() {
    let u = tag[i]
    if u == 45 {
      if subtag_len == 0 {
        return false
      }
      subtag_index = subtag_index + 1
      subtag_len = 0
    } else if is_ascii_alpha_u16(u) {
      subtag_len = subtag_len + 1
    } else if is_ascii_digit_u16(u) {
      if subtag_index == 0 {
        return false
      }
      subtag_len = subtag_len + 1
    } else {
      return false
    }
    if subtag_len > 8 {
      return false
    }
    i = i + 1
  }
  subtag_len > 0
}

///|
/// Whether a language tag (or `und`) passes the RFC 5646 basic syntax
/// check used by the validator and builders.
pub fn check_language_tag(tag : String) -> Bool {
  is_plausible_language_tag(tag)
}

///|
/// Internal: collect the top-level property names in sorted order.
fn sorted_property_names(m : Map[String, PropertyValue]) -> Array[String] {
  let keys : Array[String] = []
  for k in m.keys() {
    keys.push(k)
  }
  keys.sort()
  keys
}

///|
/// Validate a parsed JRD and collect all issues (Error, Warning and
/// Info) in a deterministic order.
pub fn validate_jrd(jrd : JsonResourceDescriptor) -> Array[ValidationIssue] {
  let issues : Array[ValidationIssue] = []
  match jrd.subject {
    None =>
      issues.push({
        severity: Warning,
        code: "MissingSubject",
        message: "the JRD has no subject member (RFC 7033 Section 4.4.1: SHOULD be present)",
      })
    Some(subject) =>
      match check_absolute_uri(subject) {
        Ok(_) => ()
        Err(_) =>
          issues.push({
            severity: Error,
            code: "InvalidSubjectUri",
            message: "subject is not an absolute URI: \{subject}",
          })
      }
  }
  for alias in jrd.aliases {
    match check_absolute_uri(alias) {
      Ok(_) => ()
      Err(_) =>
        issues.push({
          severity: Error,
          code: "InvalidAliasUri",
          message: "alias is not an absolute URI: \{alias}",
        })
    }
  }
  for name in sorted_property_names(jrd.properties) {
    match check_absolute_uri(name) {
      Ok(_) => ()
      Err(_) =>
        issues.push({
          severity: Error,
          code: "InvalidPropertyUri",
          message: "property name is not a URI: \{name}",
        })
    }
  }
  for link in jrd.links {
    match check_rel_value(link.rel) {
      Ok(_) => ()
      Err(_) =>
        issues.push({
          severity: Error,
          code: "InvalidLinkRelValue",
          message: "link rel is neither a URI nor a registered relation type: \{link.rel}",
        })
    }
    match link.href {
      None => ()
      Some(href) =>
        match check_absolute_uri(href) {
          Ok(_) => ()
          Err(_) =>
            issues.push({
              severity: Error,
              code: "InvalidLinkHrefUri",
              message: "link href is not an absolute URI: \{href}",
            })
        }
    }
    for tag in sorted_title_names(link.titles) {
      if !is_plausible_language_tag(tag) {
        issues.push({
          severity: Warning,
          code: "InvalidTitleLanguageTag",
          message: "title key is not a plausible language tag (RFC 5646) or 'und': \{tag}",
        })
      }
    }
    for name in sorted_property_names(link.properties) {
      match check_absolute_uri(name) {
        Ok(_) => ()
        Err(_) =>
          issues.push({
            severity: Error,
            code: "InvalidPropertyUri",
            message: "link property name is not a URI: \{name}",
          })
      }
    }
  }
  issues
}

///|
/// Internal: sorted title language tags (for deterministic validation
/// output).
fn sorted_title_names(m : Map[String, String]) -> Array[String] {
  let keys : Array[String] = []
  for k in m.keys() {
    keys.push(k)
  }
  keys.sort()
  keys
}

///|
/// Whether a parsed JRD passes validation with no Error-severity issue.
/// Warnings (e.g. a missing subject) and Info issues do not fail this
/// check, matching the RFC's own SHOULD-level requirements.
pub fn validate_jrd_all(jrd : JsonResourceDescriptor) -> Bool {
  for issue in validate_jrd(jrd) {
    match issue.severity {
      Error => return false
      _ => ()
    }
  }
  true
}

///|
/// The first Error-severity issue, or `None` when the JRD is valid.
pub fn first_validation_error(jrd : JsonResourceDescriptor) -> ValidationIssue? {
  for issue in validate_jrd(jrd) {
    match issue.severity {
      Error => return Some(issue)
      _ => ()
    }
  }
  None
}

///|
/// Check a parsed JRD against the resource it was queried with.
///
/// RFC 7033 Section 4.4.1: the subject MAY differ from the queried
/// resource (identity changes, canonical forms), so a differing subject
/// is reported as Info, not as an error. A subject (or alias) equal to
/// the requested resource is also reported as Info for callers that
/// want to confirm the match.
pub fn validate_for_resource(
  jrd : JsonResourceDescriptor,
  requested_resource : String,
) -> Array[ValidationIssue] {
  let issues : Array[ValidationIssue] = []
  match check_absolute_uri(requested_resource) {
    Ok(_) => ()
    Err(_) =>
      issues.push({
        severity: Error,
        code: "InvalidRequestedResource",
        message: "requested resource is not an absolute URI: \{requested_resource}",
      })
  }
  match jrd.subject {
    None =>
      issues.push({
        severity: Warning,
        code: "MissingSubject",
        message: "the JRD has no subject member (RFC 7033 Section 4.4.1: SHOULD be present)",
      })
    Some(subject) =>
      if subject == requested_resource {
        issues.push({
          severity: Info,
          code: "SubjectMatch",
          message: "subject equals the requested resource",
        })
      } else {
        issues.push({
          severity: Info,
          code: "SubjectMismatch",
          message: "subject differs from the requested resource; RFC 7033 Section 4.4.1 permits this",
        })
      }
  }
  for alias in jrd.aliases {
    if alias == requested_resource {
      issues.push({
        severity: Info,
        code: "AliasMatch",
        message: "an alias equals the requested resource",
      })
    }
  }
  issues
}