///|
/// RFC 8866 validation and interoperability diagnostics.
pub enum IssueSeverity {
  Error
  Warning
  Info
}

///|
pub struct ValidationIssue {
  severity : IssueSeverity
  code : String
  line : Int
  scope : String
  message : String
  reference : String
}

///|
pub struct ValidationReport {
  issues : Array[ValidationIssue]
}

///|
fn validation_issue(
  severity : IssueSeverity,
  code : String,
  line : Int,
  scope : String,
  message : String,
  reference : String,
) -> ValidationIssue {
  { severity, code, line, scope, message, reference }
}

///|
pub fn ValidationIssue::severity(self : ValidationIssue) -> IssueSeverity {
  self.severity
}

///|
pub fn ValidationIssue::code(self : ValidationIssue) -> String {
  self.code
}

///|
pub fn ValidationIssue::line(self : ValidationIssue) -> Int {
  self.line
}

///|
pub fn ValidationIssue::scope(self : ValidationIssue) -> String {
  self.scope
}

///|
pub fn ValidationIssue::message(self : ValidationIssue) -> String {
  self.message
}

///|
pub fn ValidationIssue::reference(self : ValidationIssue) -> String {
  self.reference
}

///|
pub fn IssueSeverity::to_string(self : IssueSeverity) -> String {
  match self {
    Error => "error"
    Warning => "warning"
    Info => "info"
  }
}

///|
pub fn ValidationReport::issues(
  self : ValidationReport,
) -> Array[ValidationIssue] {
  self.issues
}

///|
pub fn ValidationReport::issue_count(self : ValidationReport) -> Int {
  self.issues.length()
}

///|
pub fn ValidationReport::error_count(self : ValidationReport) -> Int {
  let mut count = 0
  for issue in self.issues {
    if issue.severity() is Error {
      count = count + 1
    }
  }
  count
}

///|
pub fn ValidationReport::warning_count(self : ValidationReport) -> Int {
  let mut count = 0
  for issue in self.issues {
    if issue.severity() is Warning {
      count = count + 1
    }
  }
  count
}

///|
pub fn ValidationReport::info_count(self : ValidationReport) -> Int {
  let mut count = 0
  for issue in self.issues {
    if issue.severity() is Info {
      count = count + 1
    }
  }
  count
}

///|
pub fn ValidationReport::is_valid(self : ValidationReport) -> Bool {
  self.error_count() == 0
}

///|
pub fn ValidationReport::has_code(
  self : ValidationReport,
  code : String,
) -> Bool {
  for issue in self.issues {
    if issue.code() == code {
      return true
    }
  }
  false
}

///|
fn push_issue(
  issues : Array[ValidationIssue],
  severity : IssueSeverity,
  code : String,
  line : Int,
  scope : String,
  message : String,
  reference : String,
) -> Unit {
  issues.push(validation_issue(severity, code, line, scope, message, reference))
}

///|
fn count_lines(lines : Array[SdpLine], kind : String) -> Int {
  let mut count = 0
  for line in lines {
    if line.kind() == kind {
      count = count + 1
    }
  }
  count
}

///|
fn first_line(lines : Array[SdpLine], kind : String) -> Int {
  for line in lines {
    if line.kind() == kind {
      return line.line()
    }
  }
  1
}

///|
fn validate_required(
  document : SessionDescription,
  issues : Array[ValidationIssue],
) -> Unit {
  if document.version() != 0 {
    push_issue(
      issues,
      Error,
      "SDP_VERSION_NOT_ZERO",
      first_line(document.source_lines(), "v"),
      "session",
      "RFC 8866 defines protocol version zero",
      "RFC 8866 §5.1",
    )
  }
  for kind in ["v", "o", "s"] {
    let count = count_lines(document.source_lines(), kind)
    if count != 1 {
      push_issue(
        issues,
        Error,
        "SDP_SINGLETON_\{kind.to_upper()}",
        1,
        "session",
        "field \{kind}= must appear exactly once",
        "RFC 8866 §5",
      )
    }
  }
  if document.timings().length() == 0 {
    push_issue(
      issues,
      Error,
      "SDP_MISSING_TIME",
      1,
      "session",
      "at least one time description is required",
      "RFC 8866 §5",
    )
  }
  if document.session_name().length() == 0 {
    push_issue(
      issues,
      Error,
      "SDP_EMPTY_SESSION_NAME",
      first_line(document.source_lines(), "s"),
      "session",
      "the session name must not be empty",
      "RFC 8866 §5.3",
    )
  }
  match document.origin() {
    None => ()
    Some(item) => {
      if !all_digits(item.session_id()) || !all_digits(item.session_version()) {
        push_issue(
          issues,
          Error,
          "SDP_ORIGIN_NUMERIC",
          first_line(document.source_lines(), "o"),
          "session",
          "origin session-id and session-version must be decimal integers",
          "RFC 8866 §5.2",
        )
      }
      if item.network_type() != "IN" {
        push_issue(
          issues,
          Warning,
          "SDP_UNKNOWN_NETTYPE",
          first_line(document.source_lines(), "o"),
          "session",
          "origin network type is not registered as IN",
          "IANA SDP Parameters",
        )
      }
      if item.address_type() != "IP4" && item.address_type() != "IP6" {
        push_issue(
          issues,
          Warning,
          "SDP_UNKNOWN_ADDRTYPE",
          first_line(document.source_lines(), "o"),
          "session",
          "origin address type is not a core registered value",
          "IANA SDP Parameters",
        )
      }
    }
  }
}

///|
fn rank_session_field(kind : String) -> Int {
  match kind {
    "v" => 0
    "o" => 1
    "s" => 2
    "i" => 3
    "u" => 4
    "e" => 5
    "p" => 6
    "c" => 7
    "b" => 8
    "t" => 9
    "r" => 10
    "z" => 11
    "k" => 12
    "a" => 13
    "m" => 14
    _ => 99
  }
}

///|
fn rank_media_field(kind : String) -> Int {
  match kind {
    "m" => 0
    "i" => 1
    "c" => 2
    "b" => 3
    "k" => 4
    "a" => 5
    _ => 99
  }
}

///|
fn validate_order(
  document : SessionDescription,
  issues : Array[ValidationIssue],
) -> Unit {
  let mut in_media = false
  let mut rank = -1
  for line in document.source_lines() {
    if line.kind() == "m" {
      in_media = true
      rank = 0
      continue
    }
    let current = if in_media {
      rank_media_field(line.kind())
    } else {
      rank_session_field(line.kind())
    }
    if current < rank && !(line.kind() == "r" && rank == 10) {
      push_issue(
        issues,
        Error,
        "SDP_FIELD_ORDER",
        line.line(),
        if in_media {
          "media"
        } else {
          "session"
        },
        "field appears outside RFC 8866 ordering",
        "RFC 8866 §5",
      )
    }
    if current > rank {
      rank = current
    }
  }
}

///|
fn validate_connection(
  item : Connection,
  line : Int,
  scope : String,
  issues : Array[ValidationIssue],
) -> Unit {
  if item.network_type() != "IN" {
    push_issue(
      issues,
      Warning,
      "SDP_UNKNOWN_NETTYPE",
      line,
      scope,
      "connection network type is not registered as IN",
      "IANA SDP Parameters",
    )
  }
  if item.address_type() != "IP4" && item.address_type() != "IP6" {
    push_issue(
      issues,
      Warning,
      "SDP_UNKNOWN_ADDRTYPE",
      line,
      scope,
      "connection address type is not IP4 or IP6",
      "IANA SDP Parameters",
    )
  }
  match item.ttl() {
    Some(value) =>
      if value > 255 {
        push_issue(
          issues,
          Error,
          "SDP_TTL_RANGE",
          line,
          scope,
          "IPv4 multicast TTL must be in the range 0..255",
          "RFC 8866 §5.7",
        )
      }
    None => ()
  }
  match item.address_count() {
    Some(value) =>
      if value < 1 {
        push_issue(
          issues,
          Error,
          "SDP_ADDRESS_COUNT",
          line,
          scope,
          "address count must be at least one",
          "RFC 8866 §5.7",
        )
      }
    None => ()
  }
}

///|
fn validate_bandwidths(
  items : Array[Bandwidth],
  line : Int,
  scope : String,
  issues : Array[ValidationIssue],
) -> Unit {
  let seen : Array[String] = Array::new()
  for item in items {
    let key = item.modifier().to_upper()
    if string_in(seen, key) {
      push_issue(
        issues,
        Warning,
        "SDP_DUPLICATE_BANDWIDTH",
        line,
        scope,
        "bandwidth modifier appears more than once",
        "RFC 8866 §5.8",
      )
    } else {
      seen.push(key)
    }
    if !is_registered_bandwidth(item.modifier()) {
      push_issue(
        issues,
        Info,
        "SDP_EXTENSION_BANDWIDTH",
        line,
        scope,
        "unregistered bandwidth modifier is treated as an extension",
        "IANA SDP Parameters",
      )
    }
  }
}

///|
fn validate_attributes(
  items : Array[Attribute],
  formats : Array[String],
  scope : String,
  issues : Array[ValidationIssue],
) -> Unit {
  let directions = count_attribute_named(items, "sendrecv") +
    count_attribute_named(items, "sendonly") +
    count_attribute_named(items, "recvonly") +
    count_attribute_named(items, "inactive")
  if directions > 1 {
    push_issue(
      issues,
      Error,
      "SDP_MULTIPLE_DIRECTIONS",
      1,
      scope,
      "only one direction attribute may apply at a level",
      "RFC 3264 §5",
    )
  }
  let rtp_payloads : Array[String] = Array::new()
  let mids : Array[String] = Array::new()
  for item in items {
    if !is_registered_attribute(item.name()) {
      push_issue(
        issues,
        Info,
        "SDP_EXTENSION_ATTRIBUTE",
        item.line(),
        scope,
        "unknown attribute is preserved as an extension",
        "RFC 8866 §5.13",
      )
    }
    if item.name().equal_ignore_ascii_case("rtcp-mux") &&
      item.value() is Some(_) {
      push_issue(
        issues,
        Error,
        "SDP_RTCP_MUX_VALUE",
        item.line(),
        scope,
        "rtcp-mux is a property attribute without a value",
        "RFC 5761 §5.1",
      )
    }
    if item.name().equal_ignore_ascii_case("rtpmap") {
      match parse_rtpmap(item) {
        Err(error) =>
          push_issue(
            issues,
            Error,
            "SDP_INVALID_RTPMAP",
            item.line(),
            scope,
            error.message(),
            "RFC 8866 §6.6",
          )
        Ok(mapping) => {
          let key = mapping.payload_type().to_string()
          if string_in(rtp_payloads, key) {
            push_issue(
              issues,
              Error,
              "SDP_DUPLICATE_RTPMAP",
              item.line(),
              scope,
              "payload type has more than one rtpmap",
              "RFC 8866 §6.6",
            )
          } else {
            rtp_payloads.push(key)
          }
          if formats.length() > 0 && !string_in(formats, key) {
            push_issue(
              issues,
              Error,
              "SDP_RTPMAP_NOT_ADVERTISED",
              item.line(),
              scope,
              "rtpmap payload is absent from the m= format list",
              "RFC 8866 §6.6",
            )
          }
          if mapping.payload_type() < 0 || mapping.payload_type() > 127 {
            push_issue(
              issues,
              Error,
              "SDP_RTP_PAYLOAD_RANGE",
              item.line(),
              scope,
              "RTP payload type must be in the range 0..127",
              "RFC 3551 §6",
            )
          }
          if mapping.clock_rate() == 0 {
            push_issue(
              issues,
              Error,
              "SDP_RTPMAP_CLOCK_ZERO",
              item.line(),
              scope,
              "rtpmap clock rate must be positive",
              "RFC 8866 §6.6",
            )
          }
        }
      }
    }
    if item.name().equal_ignore_ascii_case("fmtp") {
      match parse_fmtp(item) {
        Err(error) =>
          push_issue(
            issues,
            Error,
            "SDP_INVALID_FMTP",
            item.line(),
            scope,
            error.message(),
            "RFC 8866 §6.6",
          )
        Ok(mapping) =>
          if formats.length() > 0 && !string_in(formats, mapping.format()) {
            push_issue(
              issues,
              Error,
              "SDP_FMTP_NOT_ADVERTISED",
              item.line(),
              scope,
              "fmtp format is absent from the m= format list",
              "RFC 8866 §6.6",
            )
          }
      }
    }
    if item.name().equal_ignore_ascii_case("mid") {
      match item.value() {
        None =>
          push_issue(
            issues,
            Error,
            "SDP_MID_EMPTY",
            item.line(),
            scope,
            "mid requires a value",
            "RFC 5888 §4",
          )
        Some(value) =>
          if string_in(mids, value) {
            push_issue(
              issues,
              Error,
              "SDP_DUPLICATE_MID",
              item.line(),
              scope,
              "mid value is duplicated within this level",
              "RFC 5888 §4",
            )
          } else {
            mids.push(value)
          }
      }
    }
  }
}

///|
fn validate_media(
  document : SessionDescription,
  media : MediaDescription,
  index : Int,
  issues : Array[ValidationIssue],
) -> Unit {
  let scope = "media[\{index}]"
  if media.port() < 0 || media.port() > 65535 {
    push_issue(
      issues,
      Error,
      "SDP_PORT_RANGE",
      1,
      scope,
      "media port must be in the range 0..65535",
      "RFC 8866 §5.14",
    )
  }
  if media.port_count() < 1 {
    push_issue(
      issues,
      Error,
      "SDP_PORT_COUNT",
      1,
      scope,
      "media port count must be positive",
      "RFC 8866 §5.14",
    )
  }
  if !is_registered_media(media.media()) {
    push_issue(
      issues,
      Info,
      "SDP_EXTENSION_MEDIA",
      1,
      scope,
      "media token is not in the IANA core registry",
      "IANA SDP Parameters",
    )
  }
  if !is_registered_protocol(media.protocol()) {
    push_issue(
      issues,
      Warning,
      "SDP_UNKNOWN_PROTOCOL",
      1,
      scope,
      "transport protocol is not in the bundled IANA snapshot",
      "IANA SDP Parameters",
    )
  }
  if document.connection() is None && media.connections().length() == 0 {
    push_issue(
      issues,
      Error,
      "SDP_MISSING_CONNECTION",
      1,
      scope,
      "connection information is required at session or media level",
      "RFC 8866 §5.7",
    )
  }
  for item in media.connections() {
    validate_connection(item, 1, scope, issues)
  }
  validate_bandwidths(media.bandwidths(), 1, scope, issues)
  validate_attributes(media.attributes(), media.formats(), scope, issues)
  if is_rtp_protocol(media.protocol()) {
    for format in media.formats() {
      match parse_non_negative_int(format) {
        None =>
          push_issue(
            issues,
            Error,
            "SDP_NON_NUMERIC_RTP_FORMAT",
            1,
            scope,
            "RTP media formats must be numeric payload types",
            "RFC 8866 §5.14",
          )
        Some(payload) =>
          if payload > 127 {
            push_issue(
              issues,
              Error,
              "SDP_RTP_PAYLOAD_RANGE",
              1,
              scope,
              "RTP payload type must be in the range 0..127",
              "RFC 3551 §6",
            )
          }
      }
    }
  }
}

///|
/// Performs syntax-independent RFC and registry validation.
pub fn validate(document : SessionDescription) -> ValidationReport {
  let issues : Array[ValidationIssue] = Array::new()
  validate_required(document, issues)
  validate_order(document, issues)
  match document.connection() {
    Some(item) => validate_connection(item, 1, "session", issues)
    None => ()
  }
  validate_bandwidths(document.bandwidths(), 1, "session", issues)
  validate_attributes(document.attributes(), Array::new(), "session", issues)
  for index, media in document.media_descriptions() {
    validate_media(document, media, index, issues)
  }
  { issues, }
}

///|
/// Parses and validates, returning parse errors separately from diagnostics.
pub fn parse_and_validate(input : String) -> Result[ValidationReport, SdpError] {
  match parse(input) {
    Err(error) => Err(error)
    Ok(document) => Ok(validate(document))
  }
}

///|
/// A deterministic human-readable validation report.
pub fn ValidationReport::to_text(self : ValidationReport) -> String {
  let sb = StringBuilder()
  sb.write_string("validation: errors=")
  sb.write_string(self.error_count().to_string())
  sb.write_string(" warnings=")
  sb.write_string(self.warning_count().to_string())
  sb.write_string(" info=")
  sb.write_string(self.info_count().to_string())
  for issue in self.issues {
    sb.write_string("\n[")
    sb.write_string(issue.severity().to_string())
    sb.write_string("] ")
    sb.write_string(issue.code())
    sb.write_string(" ")
    sb.write_string(issue.scope())
    sb.write_string(": ")
    sb.write_string(issue.message())
  }
  sb.to_string()
}