///|
/// The `STATUS` aggregate OFX attaches to a response.
///
/// `code` is the numeric result: zero means success, and a negative or positive
/// value is the server's own error code. `severity` is the document's own
/// `INFO`/`WARN`/`ERROR` label, kept as written so a caller can compare it with
/// the code instead of trusting either one alone.
pub struct Status {
  code : Int
  severity : String
  message : String
}

///|
/// Whether the status code means success.
pub fn Status::is_success(self : Status) -> Bool {
  self.code == 0
}

///|
/// Whether the document labelled the status as an error, without regard to
/// case.
pub fn Status::is_error(self : Status) -> Bool {
  self.severity.to_upper() == "ERROR"
}

///|
/// The status as `severity:code: message`, the form a log line wants.
pub fn Status::to_string(self : Status) -> String {
  let label = if self.severity == "" { "INFO" } else { self.severity }
  let detail = if self.message == "" { "" } else { ": " + self.message }
  label + ":" + self.code.to_string() + detail
}

///|
/// The `SONRS` sign-on response: when the file was produced, which financial
/// institution produced it, and whether that server considered the response a
/// success.
///
/// A statement whose sign-on reports an error is still parsed — the
/// transactions are usually there — but the caller should know that the server
/// itself said something went wrong, so the error is also raised as a
/// diagnostic.
pub struct Signon {
  server_date : String
  language : String
  organisation : String
  institution_id : String
  session_cookie : String
  status : Status?
}

///|
/// Whether the document carried a `SONRS` aggregate at all.
pub fn Signon::is_present(self : Signon) -> Bool {
  self.server_date != "" ||
  self.language != "" ||
  self.organisation != "" ||
  self.institution_id != "" ||
  self.session_cookie != "" ||
  self.status is Some(_)
}

///|
/// The `DTSERVER` instant, or `None` when it is absent or not a real date.
pub fn Signon::server_time(self : Signon) -> PostedDate? {
  parse_posted_date(self.server_date)
}

///|
/// The sign-on as one line for a report, or the empty string when the document
/// declared none.
pub fn Signon::to_string(self : Signon) -> String {
  if !self.is_present() {
    return ""
  }
  let parts : Array[String] = []
  if self.server_date != "" {
    parts.push("server=" + self.server_date)
  }
  if self.organisation != "" {
    parts.push("fi=" + self.organisation)
  }
  if self.institution_id != "" {
    parts.push("fid=" + self.institution_id)
  }
  if self.language != "" {
    parts.push("language=" + self.language)
  }
  match self.status {
    Some(status) => parts.push("status=" + status.to_string())
    None => ()
  }
  parts.join(" ")
}

///|
/// Read the sign-on response out of a token stream.
///
/// This runs as its own pass over the tokens rather than inside the statement
/// loop, because the aggregates it cares about (`SONRS`, `FI`, `STATUS`) nest
/// and have nothing to do with accounts or transactions. Both dialects are
/// handled: an XML document closes its aggregates, and an SGML document is
/// scoped by the next aggregate that starts.
fn parse_signon(
  tokens : Array[TagToken],
  diagnostics : Array[Diagnostic],
) -> Signon {
  let mut server_date = ""
  let mut server_date_line = 0
  let mut language = ""
  let mut session_cookie = ""
  let mut organisation = ""
  let mut institution_id = ""
  let mut code_text = ""
  let mut code_line = 0
  let mut severity = ""
  let mut message = ""
  let mut in_fi = false
  let mut in_status = false
  for token in tokens {
    let tag = token.name
    match close_target(tag) {
      Some(target) =>
        if target == "FI" {
          in_fi = false
        } else if target == "STATUS" {
          in_status = false
        } else if target == "SONRS" {
          in_fi = false
          in_status = false
        }
      None =>
        match tag {
          "SONRS" => {
            in_fi = false
            in_status = false
          }
          "FI" => {
            in_fi = true
            in_status = false
          }
          "STATUS" => {
            in_status = true
            in_fi = false
          }
          "DTSERVER" => {
            server_date = token.value
            server_date_line = token.line
          }
          "LANGUAGE" => language = token.value
          "SESSCOOKIE" => session_cookie = token.value
          "ORG" => if in_fi { organisation = token.value }
          "FID" => if in_fi { institution_id = token.value }
          "CODE" =>
            if in_status {
              code_text = token.value
              code_line = token.line
            }
          "SEVERITY" => if in_status { severity = token.value }
          "MESSAGE" => if in_status { message = token.value }
          _ => ()
        }
    }
  }
  let mut status : Status? = None
  if code_text != "" || severity != "" || message != "" {
    match parse_int(code_text) {
      Some(code) => status = Some({ code, severity, message, })
      None =>
        diagnostics.push(
          warning_at("invalid-status-code", at_text(code_text, code_line)),
        )
    }
  }
  if server_date != "" && !valid_date(server_date) {
    diagnostics.push(
      warning_at("invalid-server-date", at_line("DTSERVER", server_date_line)),
    )
  }
  match status {
    Some(value) =>
      if value.is_error() {
        diagnostics.push(warning_at("signon-status-error", value.to_string()))
      }
    None => ()
  }
  {
    server_date,
    language,
    organisation,
    institution_id,
    session_cookie,
    status,
  }
}