///|
/// Audit advice and heuristics; these are not parser errors.
pub(all) enum AuditIssueKind {
  RelativeTypeUri
  RelativeInstanceUri
  SuspiciousExtensionName
  OverlongTitle
  OverlongDetail
  AboutBlankTitleMismatch
  MissingTitle
  MissingStatus
  SensitiveLookingExtension
  DebugInformationRisk
} derive(Eq, Debug)

///|
pub(all) enum AuditSeverity {
  Info
  Warning
} derive(Eq, Debug)

///|
pub struct AuditIssue {
  kind : AuditIssueKind
  severity : AuditSeverity
  context : String
} derive(Eq, Debug)

///|
pub struct AuditReport {
  issues : Array[AuditIssue]
} derive(Eq, Debug)

///|
pub fn AuditIssue::kind(self : AuditIssue) -> AuditIssueKind {
  self.kind
}

///|
pub fn AuditIssue::severity(self : AuditIssue) -> AuditSeverity {
  self.severity
}

///|
pub fn AuditIssue::context(self : AuditIssue) -> String {
  self.context
}

///|
pub fn AuditReport::issues(self : AuditReport) -> Array[AuditIssue] {
  self.issues.copy()
}

///|
pub fn AuditReport::is_clean(self : AuditReport) -> Bool {
  self.issues.is_empty()
}

///|
pub fn AuditIssueKind::name(self : AuditIssueKind) -> String {
  match self {
    RelativeTypeUri => "RelativeTypeUri"
    RelativeInstanceUri => "RelativeInstanceUri"
    SuspiciousExtensionName => "SuspiciousExtensionName"
    OverlongTitle => "OverlongTitle"
    OverlongDetail => "OverlongDetail"
    AboutBlankTitleMismatch => "AboutBlankTitleMismatch"
    MissingTitle => "MissingTitle"
    MissingStatus => "MissingStatus"
    SensitiveLookingExtension => "SensitiveLookingExtension"
    DebugInformationRisk => "DebugInformationRisk"
  }
}

///|
/// Heuristic security and interoperability audit. It makes no leak-detection guarantee.
pub fn audit_problem(problem : ProblemDetails) -> AuditReport {
  let issues : Array[AuditIssue] = []
  match problem.type_raw() {
    Some(uri) =>
      match validate_uri_reference(uri) {
        Ok(info) if info.is_relative() =>
          push_audit(
            issues,
            RelativeTypeUri,
            "type is a relative URI-reference",
          )
        _ => ()
      }
    None => ()
  }
  match problem.instance() {
    Some(uri) =>
      match validate_uri_reference(uri) {
        Ok(info) if info.is_relative() =>
          push_audit(
            issues,
            RelativeInstanceUri,
            "instance is a relative URI-reference",
          )
        _ => ()
      }
    None => ()
  }
  match problem.title() {
    None => push_audit(issues, MissingTitle, "title is absent")
    Some(title) => {
      if utf8_length(title) > 256 {
        push_audit(issues, OverlongTitle, "title exceeds 256 UTF-8 bytes")
      }
      if problem.is_about_blank() {
        match problem.status() {
          Some(code) =>
            match status_reason(code) {
              Some(reason) if title != reason =>
                push_audit(
                  issues,
                  AboutBlankTitleMismatch,
                  "about:blank title differs from the common reason phrase; localization may explain this",
                )
              _ => ()
            }
          None => ()
        }
      }
    }
  }
  match problem.detail() {
    Some(detail) => {
      if utf8_length(detail) > 4_096 {
        push_audit(issues, OverlongDetail, "detail exceeds 4096 UTF-8 bytes")
      }
      let lowered = ascii_lower(detail)
      if lowered.contains("stack trace") ||
        lowered.contains("select * from") ||
        lowered.contains("exception at") {
        push_audit(
          issues,
          DebugInformationRisk,
          "detail resembles debug information",
        )
      }
    }
    None => ()
  }
  if problem.status() is None {
    push_audit(issues, MissingStatus, "status is absent")
  }
  for extension in problem.extensions() {
    if suspicious_extension_name(extension.name()) {
      push_audit(
        issues,
        SuspiciousExtensionName,
        "extension name is outside the recommended portable form: \{extension.name()}",
      )
    }
    if sensitive_name(extension.name()) {
      push_audit(
        issues,
        SensitiveLookingExtension,
        "extension name may indicate sensitive or debug data: \{extension.name()}",
      )
    }
  }
  { issues, }
}

///|
fn push_audit(
  issues : Array[AuditIssue],
  kind : AuditIssueKind,
  context : String,
) -> Unit {
  issues.push({ kind, severity: Warning, context })
}

///|
fn suspicious_extension_name(name : String) -> Bool {
  let chars = name.to_array()
  if chars.length() < 3 || !is_alpha(chars[0]) {
    return true
  }
  for c in chars {
    if !is_alpha(c) && !is_digit(c) && c != '_' {
      return true
    }
  }
  false
}

///|
fn sensitive_name(name : String) -> Bool {
  let value = ascii_lower(name)
  value == "stack" ||
  value == "stacktrace" ||
  value == "exception" ||
  value == "sql" ||
  value == "query" ||
  value == "internal_error" ||
  value == "debug" ||
  value == "trace" ||
  value == "token" ||
  value == "secret" ||
  value == "password"
}