///|
priv struct DiagnosticSink {
  items : Array[Diagnostic]
  limit : Int
  mut exhausted : Bool
}

///|
fn DiagnosticSink::add(self : DiagnosticSink, error : Diagnostic) -> Unit {
  if self.exhausted {
    return
  }
  if self.items.length() < self.limit {
    self.items.push(error)
  } else {
    self.items[self.limit - 1] = diagnostic(
      "R_DIAGNOSTICS",
      Resource,
      error.span.start,
      error.span.end,
      "Diagnostic budget exhausted; validation stopped.",
      segment_index?=error.segment_index,
    )
    self.exhausted = true
  }
}

///|
fn DiagnosticSink::problem(
  self : DiagnosticSink,
  code : String,
  segment : Segment,
  span : SourceSpan,
  hint : String,
) -> Unit {
  self.add(
    diagnostic(
      code,
      Envelope,
      span.start,
      span.end,
      hint,
      segment_index=segment.index,
    ),
  )
}

///|
fn field_span(s : Segment, element : Int) -> SourceSpan {
  if element < s.elements.length() {
    s.elements[element].span
  } else {
    { start: s.span.end - 1, end: s.span.end - 1, }
  }
}

///|
fn mandatory_component(
  s : Segment,
  element : Int,
  component : Int,
  sink : DiagnosticSink,
) -> Bytes? {
  if element < s.elements.length() &&
    component < s.elements[element].components.length() {
    let c = s.elements[element].components[component]
    if !c.value.is_empty() {
      return Some(c.value)
    }
  }
  sink.problem(
    "E_FIELD_REQUIRED",
    s,
    field_span(s, element),
    "A mandatory service field is missing or empty.",
  )
  None
}

///|
fn mandatory_simple(
  s : Segment,
  element : Int,
  sink : DiagnosticSink,
) -> Bytes? {
  let value = mandatory_component(s, element, 0, sink)
  if element < s.elements.length() &&
    s.elements[element].components.length() != 1 {
    sink.problem(
      "E_SERVICE_STRUCTURE",
      s,
      field_span(s, element),
      "This service field must be a simple data element.",
    )
    return None
  }
  value
}

///|
fn reference_value(s : Segment, element : Int, sink : DiagnosticSink) -> Bytes? {
  let value = mandatory_simple(s, element, sink)
  if value is Some(bytes) && bytes.length() > 14 {
    sink.problem(
      "E_REFERENCE_INVALID",
      s,
      field_span(s, element),
      "Syntax 3 references must contain 1 to 14 characters.",
    )
    return None
  }
  value
}

///|
fn control_count(s : Segment, sink : DiagnosticSink) -> Int? {
  let value = mandatory_simple(s, 0, sink)
  match value {
    None => None
    Some(bytes) => {
      if bytes.length() > 6 {
        sink.problem(
          "E_COUNT_INVALID",
          s,
          field_span(s, 0),
          "Syntax 3 counts require 1 to 6 decimal digits.",
        )
        return None
      }
      let mut count = 0
      for b in bytes.iter() {
        if b < b'0' || b > b'9' {
          sink.problem(
            "E_COUNT_INVALID",
            s,
            field_span(s, 0),
            "Service count must contain decimal digits only.",
          )
          return None
        }
        count = count * 10 + (b.to_int() - 48)
      }
      Some(count)
    }
  }
}

///|
fn check_unb(s : Segment, sink : DiagnosticSink) -> Bytes? {
  for (e, c) in [(1, 0), (2, 0), (3, 0), (3, 1)] {
    ignore(mandatory_component(s, e, c, sink))
  }
  reference_value(s, 4, sink)
}

///|
fn check_unh(s : Segment, sink : DiagnosticSink) -> Bytes? {
  let reference = reference_value(s, 0, sink)
  for c = 0; c < 4; c = c + 1 {
    ignore(mandatory_component(s, 1, c, sink))
  }
  reference
}

///|
fn check_trailer(s : Segment, sink : DiagnosticSink) -> (Int?, Bytes?) {
  if s.elements.length() != 2 {
    sink.problem(
      "E_SERVICE_STRUCTURE",
      s,
      s.span,
      "A trailer must contain exactly a count and reference.",
    )
  }
  (control_count(s, sink), reference_value(s, 1, sink))
}

///|
/// Validate direct UNB/UNZ and UNH/UNT envelopes. Unknown business tags are preserved.
/// Groups, interactive envelopes and multiple interchanges are outside the 0.1.0 scope.
pub fn validate_envelope(
  document : Document,
  max_diagnostics? : Int = 100,
) -> EnvelopeResult {
  if max_diagnostics <= 0 {
    return {
      interchange: None,
      diagnostics: [
        diagnostic(
          "R_LIMITS_INVALID",
          Resource,
          0,
          0,
          "Diagnostic limit must be positive.",
        ),
      ],
      status: Failed,
    }
  }
  let sink : DiagnosticSink = {
    items: [],
    limit: max_diagnostics,
    exhausted: false,
  }
  let segments = document.segments
  if segments.is_empty() || segments[0].tag != "UNB" {
    sink.add(
      diagnostic(
        "E_UNB_MISSING",
        Envelope,
        document.span.start,
        document.span.start,
        "Interchange header UNB is missing.",
      ),
    )
    return { interchange: None, diagnostics: sink.items, status: Failed, }
  }
  let interchange_reference = check_unb(segments[0], sink)
  let messages : Array[Message] = []
  let seen : Map[Bytes, Bool] = Map([])
  let mut active : Int? = None
  let mut active_reference : Bytes? = None
  let mut message_headers = 0
  let mut closed = false
  for i = 1; i < segments.length(); i = i + 1 {
    if sink.exhausted {
      break
    }
    let s = segments[i]
    if closed {
      sink.problem(
        "E_TRAILING_SEGMENT",
        s,
        s.span,
        "Only one interchange is supported; segments after UNZ are rejected.",
      )
      continue
    }
    match s.tag {
      "UNH" => {
        if active is Some(_) {
          sink.problem(
            "E_UNT_MISSING",
            s,
            s.span,
            "A message header occurred before the previous message trailer.",
          )
        }
        active = Some(i)
        message_headers = message_headers + 1
        active_reference = check_unh(s, sink)
        if active_reference is Some(reference) {
          if seen.contains(reference) {
            sink.problem(
              "E_MESSAGE_REFERENCE_DUPLICATE",
              s,
              field_span(s, 0),
              "Message references must be unique within the interchange.",
            )
          }
          seen[reference] = true
        }
      }
      "UNT" => {
        let (count, reference) = check_trailer(s, sink)
        match active {
          None =>
            sink.problem(
              "E_UNH_MISSING",
              s,
              s.span,
              "Message trailer has no matching UNH.",
            )
          Some(first) => {
            if count is Some(n) && n != i - first + 1 {
              sink.problem(
                "E_MESSAGE_COUNT",
                s,
                field_span(s, 0),
                "UNT count must include UNH, business segments and UNT.",
              )
            }
            if reference is Some(r) && active_reference is Some(h) && r != h {
              sink.problem(
                "E_MESSAGE_REFERENCE",
                s,
                field_span(s, 1),
                "UNT reference does not match UNH.",
              )
            }
            messages.push({
              reference: active_reference.unwrap_or(b""),
              first_segment: first + 1,
              last_segment: i + 1,
              span: { start: segments[first].span.start, end: s.span.end, },
            })
            active = None
            active_reference = None
          }
        }
      }
      "UNZ" => {
        if active is Some(_) {
          sink.problem(
            "E_UNT_MISSING",
            s,
            s.span,
            "UNZ occurred before the message trailer.",
          )
          active = None
        }
        let (count, reference) = check_trailer(s, sink)
        if count is Some(n) && n != message_headers {
          sink.problem(
            "E_INTERCHANGE_COUNT",
            s,
            field_span(s, 0),
            "UNZ count must equal the number of direct message headers.",
          )
        }
        if reference is Some(r) && interchange_reference is Some(h) && r != h {
          sink.problem(
            "E_INTERCHANGE_REFERENCE",
            s,
            field_span(s, 1),
            "UNZ reference does not match UNB.",
          )
        }
        closed = true
      }
      "UNB" =>
        sink.problem(
          "E_UNB_NESTED",
          s,
          s.span,
          "Nested or repeated UNB is unsupported.",
        )
      "UNA" =>
        sink.problem(
          "E_UNA_POSITION",
          s,
          s.span,
          "UNA is only allowed before UNB.",
        )
      "UNG" | "UNE" =>
        sink.problem(
          "E_GROUP_UNSUPPORTED",
          s,
          s.span,
          "Functional groups are not supported in 0.1.0.",
        )
      "UIB" | "UIH" | "UIT" | "UIZ" =>
        sink.problem(
          "E_INTERACTIVE_UNSUPPORTED",
          s,
          s.span,
          "Interactive envelopes are not supported in 0.1.0.",
        )
      _ =>
        if active is None {
          sink.problem(
            "E_SEGMENT_OUTSIDE_MESSAGE",
            s,
            s.span,
            "Business segments must occur inside UNH/UNT.",
          )
        }
    }
  }
  if !sink.exhausted {
    if active is Some(first) {
      sink.problem(
        "E_UNT_MISSING",
        segments[first],
        { start: document.span.end, end: document.span.end, },
        "The message trailer UNT is missing.",
      )
    }
    if !closed {
      sink.add(
        diagnostic(
          "E_UNZ_MISSING",
          Envelope,
          document.span.end,
          document.span.end,
          "The interchange trailer UNZ is missing.",
        ),
      )
    }
    if message_headers == 0 {
      sink.add(
        diagnostic(
          "E_MESSAGE_MISSING",
          Envelope,
          document.span.start,
          document.span.end,
          "At least one direct message is required.",
        ),
      )
    }
  }
  {
    interchange: Some({ messages, span: document.span, }),
    diagnostics: sink.items,
    status: if sink.items.is_empty() {
      Passed
    } else {
      Failed
    },
  }
}