///|
/// Only construction after one matching final tagged OK is allowed. This does
/// not claim every untagged event belongs to the command: unsolicited events
/// remain in wire order and FETCH projections expose their optional UID.
pub struct CompletedResponse {
  priv tag : String
  priv events : Array[TypedResponse]
  priv code : Array[ImapValue]
}

///|
/// Complete an already collected serial-command response (including completion).
/// NO/BAD, mismatched tags, BYE, continuation, trailing data and oversized
/// collections fail before exposing a high-level result. No partial success.
pub fn completed_response(
  tag : String,
  responses : Array[Response],
) -> CompletedResponse raise ImapError {
  if tag.is_empty() ||
    tag == "*" ||
    tag == "+" ||
    tag.length() > 128 ||
    tag
    .iter()
    .any(c => c <= ' ' || c > '~' || "(){%*\"\\]+[".contains(c.to_string())) {
    raise Invalid("invalid command tag")
  }
  if responses.is_empty() || responses.length() > 4096 {
    raise Invalid("command response count")
  }
  let events = []
  let mut total = 0
  for i, response in responses {
    if response.line.length() > 65536 {
      raise Invalid("response syntax budget")
    }
    total += @utf8.encode(response.line).length()
    if total > 8388608 {
      raise Invalid("command response byte budget")
    }
    for literal in response.literals {
      if literal.length() > 8388608 - total {
        raise Invalid("command response byte budget")
      }
      total += literal.length()
    }
    let parsed = response.parse()
    match parsed {
      Status(received, status, code, _) if received != "*" => {
        if received != tag || status != "OK" || i != responses.length() - 1 {
          raise Invalid("command requires one matching final tagged OK")
        }
        return { tag, events, code, }
      }
      Status(_, "BYE" | "PREAUTH", _, _) =>
        raise Invalid("command interrupted by server status")
      Continuation(_) => raise Invalid("unresolved command continuation")
      _ => events.push(parsed)
    }
  }
  raise Invalid("missing command completion")
}

///|
pub(all) enum MailboxAccess {
  ReadOnly
  ReadWrite
} derive(Debug, Eq)

///|
pub struct SelectionInfo {
  uid_validity : Int64
  uid_next : Int64?
  exists : Int64?
  flags : Array[String]
  permanent_flags : Array[String]
  access : MailboxAccess
} derive(Debug, Eq)

///|
/// UIDNEXT is an optional server prediction, never a message count/last UID.
/// EXAMINE callers require ReadOnly. Session already deselects on any new
/// SELECT/EXAMINE attempt; this projection never restores a failed old selection.
pub fn CompletedResponse::selection(
  self : CompletedResponse,
  read_only? : Bool = false,
) -> SelectionInfo raise ImapError {
  let access = match self.code {
    [Atom(name)] =>
      match name.to_upper() {
        "READ-ONLY" => ReadOnly
        "READ-WRITE" => ReadWrite
        _ => raise Invalid("selection access not confirmed")
      }
    _ => raise Invalid("selection access not confirmed")
  }
  if read_only && access != ReadOnly {
    raise Invalid("EXAMINE not read-only")
  }
  let mut validity = None
  let mut next = None
  let mut exists = None
  let mut flags = None
  let mut permanent = None
  for event in self.events {
    match event {
      Status("*", "OK", [Atom(key), value], _) =>
        match key.to_upper() {
          "UIDVALIDITY" => {
            if validity != None {
              raise Invalid("duplicate UIDVALIDITY")
            }
            validity = Some(numeric_atom(value, 4294967295L, nonzero=true))
          }
          "UIDNEXT" => {
            if next != None {
              raise Invalid("duplicate UIDNEXT")
            }
            next = Some(numeric_atom(value, 4294967295L, nonzero=true))
          }
          "PERMANENTFLAGS" => {
            if permanent != None {
              raise Invalid("duplicate PERMANENTFLAGS")
            }
            permanent = Some(response_flags(value))
          }
          _ => ()
        }
      Status("*", "OK", [Atom(key), ..], _) if key.to_upper() == "UIDVALIDITY" ||
        key.to_upper() == "UIDNEXT" ||
        key.to_upper() == "PERMANENTFLAGS" =>
        raise Invalid("malformed selection response code")
      Data([Atom("*"), count, Atom(name)]) if name.to_upper() == "EXISTS" => {
        if exists != None {
          raise Invalid("duplicate EXISTS in selection")
        }
        exists = Some(numeric_atom(count, 4294967295L))
      }
      Data([Atom("*"), Atom(name), value]) if name.to_upper() == "FLAGS" => {
        if flags != None {
          raise Invalid("duplicate FLAGS in selection")
        }
        flags = Some(response_flags(value))
      }
      _ => ()
    }
  }
  let uid_validity = match validity {
    Some(v) => v
    None => raise Invalid("missing UIDVALIDITY")
  }
  {
    uid_validity,
    uid_next: next,
    exists,
    flags: flags.unwrap_or([]),
    permanent_flags: permanent.unwrap_or([]),
    access,
  }
}

///|
pub struct UidIdentity {
  mailbox : String
  uid_validity : Int64
  uid : Int64
} derive(Debug, Eq)

///|
pub fn SelectionInfo::identity(
  self : SelectionInfo,
  mailbox : String,
  uid : Int64,
) -> UidIdentity raise ImapError {
  if mailbox.is_empty() || uid < 1L || uid > 4294967295L {
    raise Invalid("UID identity range/mailbox")
  }
  { mailbox, uid_validity: self.uid_validity, uid, }
}

///|
pub(all) enum FetchBody {
  Missing
  NilBody
  Body(Bytes)
} derive(Debug, Eq)

///|
pub struct FetchInfo {
  sequence : Int64
  uid : Int64?
  size : Int64?
  flags : Array[String]?
  body : FetchBody
  unknown : Array[(String, ImapValue)]
} derive(Debug, Eq)

///|
/// FETCH data, including unsolicited flag-only updates. Unknown attributes are
/// retained with structural values. BODY[] alone is the full-body profile;
/// sections, partials and BODYSTRUCTURE stay unknown rather than being guessed.
pub fn CompletedResponse::fetch(
  self : CompletedResponse,
) -> Array[FetchInfo] raise ImapError {
  let records = []
  for event in self.events {
    match event {
      Data([Atom("*"), count, Atom(name), List(items)]) if name.to_upper() ==
        "FETCH" => {
        let sequence = numeric_atom(count, 4294967295L, nonzero=true)
        if items.length() % 2 != 0 {
          raise Invalid("FETCH attributes require values")
        }
        let mut uid = None
        let mut size = None
        let mut flags = None
        let mut body : FetchBody = Missing
        let unknown = []
        let seen : Map[String, Bool] = Map([])
        for i = 0; i < items.length(); i = i + 2 {
          let key = atom_name(items[i])
          if seen.contains(key) {
            raise Invalid("duplicate FETCH attribute")
          }
          seen[key] = true
          let value = items[i + 1]
          match key {
            "UID" => uid = Some(numeric_atom(value, 4294967295L, nonzero=true))
            "RFC822.SIZE" =>
              size = Some(numeric_atom(value, 9223372036854775807L))
            "FLAGS" => flags = Some(response_flags(value))
            "BODY[]" =>
              body = match value {
                Literal(bytes) => Body(bytes)
                Nil => NilBody
                _ => raise Invalid("BODY[] requires literal or NIL")
              }
            _ => unknown.push((key, value))
          }
        }
        if body != Missing && uid == None {
          raise Invalid("full body without UID identity")
        }
        records.push({ sequence, uid, size, flags, body, unknown, })
      }
      Data([Atom("*"), _, Atom(name), ..]) if name.to_upper() == "FETCH" =>
        raise Invalid("malformed FETCH response")
      _ => ()
    }
  }
  records
}