///|
/// Advisory audit of parsed JRDs and response contexts.
///
/// Audit findings are advisory observations for humans and tooling; they
/// are deliberately NOT protocol errors. A JRD can carry every audit
/// finding listed here and still be a conforming document, and a
/// response can raise a finding and still be a valid exchange — the
/// validator (`validator.mbt`) owns the hard RFC constraints.
///
/// Findings are produced in a deterministic order and never trigger any
/// network activity. Every finding has a stable `code`, a severity
/// (`Warning` or `Info`), and a one-line `message`.

///|
/// Severity of an audit finding.
pub enum AuditSeverity {
  Warning
  Info
}

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

///|
/// A single advisory finding.
pub struct AuditFinding {
  code : String
  severity : AuditSeverity
  message : String
}

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

///|
/// Internal: append a finding.
fn push_finding(
  findings : Array[AuditFinding],
  severity : AuditSeverity,
  code : String,
  message : String,
) -> Unit {
  findings.push({ severity, code, message })
}

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

///|
/// Internal: sorted keys of a property map.
fn sorted_prop_keys(m : Map[String, PropertyValue]) -> Array[String] {
  let keys : Array[String] = []
  for k in m.keys() {
    keys.push(k)
  }
  keys.sort()
  keys
}

///|
/// Internal: whether a URI-shaped string uses a risky scheme. Advisory
/// only: RFC 7033 imposes no scheme policy on hrefs or property URIs.
fn has_risky_scheme(s : String) -> Bool {
  match uri_scheme(s) {
    Some(scheme) =>
      scheme == "file" ||
      scheme == "data" ||
      scheme == "javascript" ||
      scheme == "vbscript" ||
      scheme == "ftp"
    None => false
  }
}

///|
/// Internal: duplicate detection helpers.
fn alias_is_duplicate(aliases : Array[String], index : Int) -> Bool {
  let mut i = 0
  while i < index {
    if aliases[i] == aliases[index] {
      return true
    }
    i = i + 1
  }
  false
}

///|
/// Internal: whether two links share rel + href.
fn same_rel_href(a : JrdLink, b : JrdLink) -> Bool {
  a.rel == b.rel && a.href == b.href
}

///|
/// Internal: whether two links share rel + type + href.
fn same_rel_type_href(a : JrdLink, b : JrdLink) -> Bool {
  a.rel == b.rel && a.media_type == b.media_type && a.href == b.href
}

///|
/// Internal: whether a link duplicates an earlier one (same rel + href,
/// and later same rel + type + href).
fn link_duplicates(
  links : Array[JrdLink],
  index : Int,
  with_type : Bool,
) -> Bool {
  let mut i = 0
  while i < index {
    if with_type {
      if same_rel_type_href(links[i], links[index]) {
        return true
      }
    } else if same_rel_href(links[i], links[index]) {
      return true
    }
    i = i + 1
  }
  false
}

///|
/// Audit a parsed JRD and return all advisory findings in a
/// deterministic order.
pub fn audit_jrd(jrd : JsonResourceDescriptor) -> Array[AuditFinding] {
  let findings : Array[AuditFinding] = []
  match jrd.subject {
    None =>
      push_finding(
        findings,
        Warning,
        "MissingSubject",
        "the JRD has no subject member (RFC 7033 Section 4.4.1: SHOULD be present)",
      )
    Some(_) => ()
  }
  if jrd.aliases.length() == 0 {
    push_finding(findings, Info, "NoAliases", "the JRD carries no aliases")
  } else {
    let mut i = 0
    while i < jrd.aliases.length() {
      if alias_is_duplicate(jrd.aliases, i) {
        push_finding(
          findings,
          Warning,
          "DuplicateAlias",
          "alias appears more than once: \{jrd.aliases[i]}",
        )
      }
      i = i + 1
    }
  }
  if jrd.links.length() == 0 {
    push_finding(
      findings,
      Info,
      "NoLinks",
      "the JRD carries no links (servers may return an empty or absent links array)",
    )
  } else {
    let mut i = 0
    while i < jrd.links.length() {
      let link = jrd.links[i]
      if link_duplicates(jrd.links, i, true) {
        push_finding(
          findings,
          Warning,
          "DuplicateLink",
          "link \{i} repeats an earlier link's rel, type and href (rel: \{link.rel})",
        )
      } else if link_duplicates(jrd.links, i, false) {
        push_finding(
          findings,
          Warning,
          "DuplicateRelHref",
          "link \{i} repeats an earlier link's rel and href (rel: \{link.rel})",
        )
      }
      match link.href {
        None => ()
        Some(href) => {
          match check_absolute_uri(href) {
            Ok(_) => ()
            Err(_) =>
              push_finding(
                findings,
                Warning,
                "RelativeHref",
                "link href is not an absolute URI (RFC 7033 Section 2: relative references are not used with WebFinger): \{href}",
              )
          }
          if scheme_is(href, "http") {
            push_finding(
              findings,
              Warning,
              "InsecureHttpHref",
              "link href uses the http scheme; RFC 7033 does not forbid this, but consider https: \{href}",
            )
          }
          if has_risky_scheme(href) {
            push_finding(
              findings,
              Warning,
              "SuspiciousHrefScheme",
              "link href uses a potentially risky URI scheme: \{href}",
            )
          }
        }
      }
      i = i + 1
    }
  }
  for name in sorted_prop_keys(jrd.properties) {
    if has_risky_scheme(name) {
      push_finding(
        findings,
        Warning,
        "SuspiciousPropertyUri",
        "property identifier uses a potentially risky URI scheme: \{name}",
      )
    }
  }
  if serialize_jrd(jrd).length() > 65536 {
    push_finding(
      findings,
      Info,
      "VeryLargeDescriptor",
      "the serialized JRD exceeds 64 KiB; consider tighter limits for hostile inputs",
    )
  }
  for name in sorted_json_keys(jrd.extensions) {
    push_finding(
      findings,
      Info,
      "UnknownTopLevelMember",
      "unknown top-level member '\{name}' ignored per RFC 7033 Section 4.4 and preserved for round-tripping",
    )
  }
  findings
}

///|
/// Audit a response context and return advisory findings about the
/// transport facts the caller observed. Mirrors
/// `validate_response_context`, but everything here is advisory.
pub fn audit_response(ctx : WebFingerResponseContext) -> Array[AuditFinding] {
  let findings : Array[AuditFinding] = []
  if !scheme_is(ctx.final_url, "https") {
    push_finding(
      findings,
      Warning,
      "NonHttpsContext",
      "final_url does not use the https scheme (RFC 7033 Section 4.2 requires HTTPS)",
    )
  }
  if ctx.status < 200 || ctx.status > 299 {
    push_finding(
      findings,
      Warning,
      "NonSuccessStatus",
      "status \{ctx.status} is outside the 2xx success range",
    )
  }
  match ctx.content_type {
    None =>
      push_finding(
        findings,
        Warning,
        "MissingContentType",
        "response carried no Content-Type; JRD responses must use \{JRD_MEDIA_TYPE}",
      )
    Some(ct) =>
      if !is_jrd_content_type(ct) {
        push_finding(
          findings,
          Warning,
          "UnexpectedContentType",
          "Content-Type \{ct} is not \{JRD_MEDIA_TYPE}",
        )
      }
  }
  match ctx.request_url {
    None => ()
    Some(request_url) =>
      match (origin_of(request_url), origin_of(ctx.final_url)) {
        (Some(request_origin), Some(final_origin)) =>
          if request_origin != final_origin {
            push_finding(
              findings,
              Info,
              "RedirectDetected",
              "final URL origin differs from the request origin (redirects are permitted when they stay on https)",
            )
          }
        _ => ()
      }
  }
  match ctx.body_bytes {
    Some(bytes) =>
      if bytes > 1048576 {
        push_finding(
          findings,
          Info,
          "LargeBody",
          "response body is \{bytes} bytes; consider the max_input_bytes limit when parsing",
        )
      }
    None => ()
  }
  findings
}