///|
pub struct Session {
  priv mut phase : Phase
  priv mut user_ok : Bool
  priv mut pending : Command?
  priv line : Array[Byte]
  priv body : Array[Byte]
  priv mut status : String?
  priv mut failed : Bool
  priv mut tls_active : Bool
  priv mut auth_ready : Bool
  priv mut auth_sent : Bool
  priv mut auth_cancelled : Bool
} derive(Debug)

///|
pub fn Session::new() -> Session {
  {
    phase: Greeting,
    user_ok: false,
    pending: None,
    line: [],
    body: [],
    status: None,
    failed: false,
    tls_active: false,
    auth_ready: false,
    auth_sent: false,
    auth_cancelled: false,
  }
}

///|
pub fn Session::phase(self : Session) -> Phase {
  self.phase
}

///|
pub fn Session::issue(
  self : Session,
  command : Command,
) -> String raise PopError {
  if self.failed ||
    self.pending is Some(_) ||
    self.phase == Greeting ||
    self.phase == TlsHandshake ||
    self.phase == Closed {
    raise Invalid("session not ready for command")
  }
  match command {
    User(_) | Apop(_, _) | Auth(_) =>
      if self.phase != Authorization {
        raise Invalid("USER outside authorization")
      }
    Stls =>
      if self.phase != Authorization || self.tls_active {
        raise Invalid("STLS requires unencrypted authorization state")
      }
    Pass(_) =>
      if self.phase != Authorization || !self.user_ok {
        raise Invalid("PASS requires successful USER")
      }
    Quit | Capa => ()
    _ =>
      if self.phase != Transaction {
        raise Invalid("command requires authentication")
      }
  }
  let wire = command.encode()
  self.pending = Some(command)
  wire
}

///|
fn Session::complete(self : Session, ok : Bool, message : String) -> Reply {
  if self.phase == Greeting {
    self.phase = if ok { Authorization } else { Closed }
  } else if self.pending is Some(command) {
    match command {
      User(_) => self.user_ok = ok
      Pass(_) | Apop(_, _) | Auth(_) => if ok { self.phase = Transaction }
      Stls =>
        if ok {
          self.phase = TlsHandshake
          self.user_ok = false
        }
      Quit => self.phase = Closed
      _ => ()
    }
  }
  self.pending = None
  self.status = None
  self.auth_ready = false
  self.auth_sent = false
  self.auth_cancelled = false
  let reply : Reply = {
    ok,
    message,
    body: Bytes::from_array(self.body),
    continuation: false,
  }
  self.body.clear()
  reply
}

///|
pub fn Session::feed(
  self : Session,
  input : Bytes,
) -> Array[Reply] raise PopError {
  if self.failed || self.phase == Closed {
    raise Invalid("closed or failed session")
  }
  errdefer {
    self.failed = true
  }
  let replies = []
  for b in input {
    if self.phase == TlsHandshake {
      raise Invalid("plaintext after STLS completion")
    }
    if b != 10 {
      self.line.push(b)
      let limit = if self.status is Some(_) {
        65536
      } else if self.pending is Some(Auth(_)) {
        16384
      } else {
        511
      }
      if self.line.length() > limit {
        raise Invalid("reply line too long")
      }
      continue
    }
    if self.line.is_empty() || self.line[self.line.length() - 1] != 13 {
      raise Invalid("expected CRLF")
    }
    ignore(self.line.pop())
    if self.status is Some(message) {
      if self.line.length() == 1 && self.line[0] == 46 {
        replies.push(self.complete(true, message))
      } else {
        let start = if self.line.length() > 0 && self.line[0] == 46 {
          1
        } else {
          0
        }
        if start == 1 && (self.line.length() < 2 || self.line[1] != 46) {
          raise Invalid("invalid dot-stuffed body line")
        }
        if self.body.length() + self.line.length() + 2 > 8388608 {
          raise Invalid("message too large")
        }
        for i in start.. raise Invalid("invalid status encoding")
      }
      if line == "+" || line.has_prefix("+ ") {
        if !(self.pending is Some(Auth(_))) ||
          self.auth_ready ||
          self.auth_sent ||
          self.auth_cancelled {
          raise Invalid("unexpected AUTH continuation")
        }
        let challenge = if line.length() <= 2 {
          ""
        } else {
          line[2:].to_owned()
        }
        validate_sasl_base64(challenge, allow_empty=true)
        self.auth_ready = true
        replies.push({
          ok: true,
          message: challenge,
          body: b"",
          continuation: true,
        })
        self.line.clear()
        continue
      }
      let ok = line == "+OK" || line.has_prefix("+OK ")
      let bad = line == "-ERR" || line.has_prefix("-ERR ")
      if !ok && !bad {
        raise Invalid("invalid status indicator")
      }
      if ok && self.pending is Some(Auth(_)) && !self.auth_sent {
        raise Invalid("AUTH success before response")
      }
      let message = line[if ok { 3 } else { 4 }:].trim_start().to_owned()
      if ok &&
        (match self.pending {
          Some(c) => c.multiline()
          None => false
        }) {
        self.status = Some(message)
      } else {
        replies.push(self.complete(ok, message))
      }
    }
    self.line.clear()
  }
  replies
}

///|
pub fn Session::finish(self : Session) -> Unit raise PopError {
  if self.failed ||
    !self.line.is_empty() ||
    self.status is Some(_) ||
    self.pending is Some(_) ||
    self.phase == TlsHandshake ||
    self.phase == Greeting {
    raise Invalid("truncated or failed exchange")
  }
}

///|
/// Called by the transport only after a verified TLS handshake, never on STLS +OK alone.
pub fn Session::tls_established(self : Session) -> Unit raise PopError {
  if self.failed || self.phase != TlsHandshake || !self.line.is_empty() {
    raise Invalid("TLS handshake state")
  }
  self.tls_active = true
  self.user_ok = false
  self.phase = Authorization
}

///|
pub fn Session::authenticate_response(
  self : Session,
  response : String,
) -> String raise PopError {
  if self.failed || !self.auth_ready || !(self.pending is Some(Auth(_))) {
    raise Invalid("AUTH continuation state")
  }
  if response != "*" {
    validate_sasl_base64(response, allow_empty=false)
  }
  self.auth_ready = false
  // Cancellation cannot turn a server's erroneous +OK into successful authentication.
  self.auth_sent = response != "*"
  self.auth_cancelled = response == "*"
  response + "\r\n"
}