///|
/// A simple sequential LDAP session. It allocates message IDs, sends
/// requests and matches the corresponding responses.
pub struct Session[T] {
  config : LdapConfig
  transport : T
  priv mut next_id : Int
  priv mut bound : Bool
  priv mut tls_active : Bool
  priv mut retry : RetryPolicy?
}

///|
pub fn[T] Session::new(config : LdapConfig, transport : T) -> Session[T] {
  {
    config,
    transport,
    next_id: 1,
    bound: false,
    tls_active: false,
    retry: None,
  }
}

///|
pub fn[T] Session::is_bound(self : Session[T]) -> Bool {
  self.bound
}

///|
/// Establish the underlying connection.
pub async fn[T : LdapTransport] Session::connect(
  self : Session[T],
) -> Result[Unit, LdapError] {
  self.transport.connect(self.config)
}

///|
/// Allocate and consume the next message ID.
fn[T] Session::next_message_id(self : Session[T]) -> Int {
  let id = self.next_id
  self.next_id = self.next_id + 1
  id
}

///|
/// Send a request and read one matching response, validating the message ID.
/// When a retry policy is configured, transport-level failures trigger a
/// reconnect and resend (up to `max_retries` times, waiting `backoff_ms`
/// between attempts).
async fn[T : LdapTransport] Session::round_trip(
  self : Session[T],
  request : ProtocolOp,
) -> Result[LdapMessage, LdapError] {
  let mut attempt = 0
  let id = self.next_message_id()
  while true {
    match self.round_trip_once(request, id) {
      Ok(m) => return Ok(m)
      Err(e) => {
        let retriable = match e {
          Transport(_) | PrematureClose => true
          _ => false
        }
        match self.retry {
          Some(policy) if retriable =>
            if attempt >= policy.max_retries {
              return Err(e)
            } else {
              attempt = attempt + 1
              if policy.backoff_ms > 0 {
                @async.sleep(policy.backoff_ms)
              }
              // Session state is gone with the broken connection: rebind
              // is the caller's concern, but the channel itself must be
              // re-established.
              self.bound = false
              self.tls_active = false
              let _ = self.transport.connect(self.config)
            }
          _ => return Err(e)
        }
      }
    }
  } nobreak {
    abort("unreachable")
  }
}

///|
async fn[T : LdapTransport] Session::round_trip_once(
  self : Session[T],
  request : ProtocolOp,
  id : Int,
) -> Result[LdapMessage, LdapError] {
  let message = LdapMessage::new(id, request)
  let bytes = match encode_message(message) {
    Ok(b) => b
    Err(e) => return Err(e)
  }
  match self.transport.write(bytes) {
    Ok(_) => ()
    Err(e) => return Err(e)
  }
  let response = match self.transport.read() {
    Ok(b) => b
    Err(e) => return Err(e)
  }
  let decoded = match decode_message(response, None) {
    Ok(m) => m
    Err(e) => return Err(e)
  }
  if decoded.message_id != id {
    return Err(LdapError::InvalidMessageId(decoded.message_id))
  }
  Ok(decoded)
}

///|
/// Perform a simple bind (RFC 4513 5.2.1). Marks the session as bound on
/// success.
pub async fn[T : LdapTransport] Session::bind_simple(
  self : Session[T],
  name : String,
  password : String,
) -> Result[BindResponse, LdapError] {
  self.bind_op(simple_bind_request(name, password))
}

///|
/// Perform a SASL PLAIN bind (RFC 4616). Marks the session as bound on
/// success.
pub async fn[T : LdapTransport] Session::bind_plain(
  self : Session[T],
  authzid : String,
  authcid : String,
  password : String,
) -> Result[BindResponse, LdapError] {
  self.bind_op(sasl_plain_bind_request("", authzid, authcid, password))
}

///|
/// Perform a bind with a custom bind request.
pub async fn[T : LdapTransport] Session::bind(
  self : Session[T],
  request : BindRequest,
) -> Result[BindResponse, LdapError] {
  self.bind_op(request)
}

///|
async fn[T : LdapTransport] Session::bind_op(
  self : Session[T],
  request : BindRequest,
) -> Result[BindResponse, LdapError] {
  if self.config.require_tls && !self.tls_active {
    return Err(LdapError::TlsRequired)
  }
  let message = match self.round_trip(BindRequest(request)) {
    Ok(m) => m
    Err(e) => return Err(e)
  }
  let response = match message.op {
    BindResponse(resp) => resp
    _ => return Err(LdapError::UnexpectedOp(protocol_op_tag(message.op)))
  }
  self.record_bind_result(response)
  Ok(response)
}

///|
fn[T] Session::record_bind_result(
  self : Session[T],
  response : BindResponse,
) -> Unit {
  if response.result.result_code.is_success() {
    self.bound = true
  }
}

///|
/// Send an unbind request and close the transport. The server does not reply
/// to unbind (RFC 4511 4.3).
pub async fn[T : LdapTransport] Session::unbind(
  self : Session[T],
) -> Result[Unit, LdapError] {
  let id = self.next_message_id()
  let bytes = match encode_message(LdapMessage::new(id, UnbindRequest)) {
    Ok(b) => b
    Err(e) => return Err(e)
  }
  match self.transport.write(bytes) {
    Ok(_) => ()
    Err(e) => return Err(e)
  }
  self.transport.close()
  self.bound = false
  Ok(())
}

///|
/// Abandon an outstanding request (RFC 4511 4.11). No response is expected.
pub async fn[T : LdapTransport] Session::abandon(
  self : Session[T],
  message_id : Int,
) -> Result[Unit, LdapError] {
  let id = self.next_message_id()
  let bytes = match
    encode_message(LdapMessage::new(id, AbandonRequest(message_id))) {
    Ok(b) => b
    Err(e) => return Err(e)
  }
  match self.transport.write(bytes) {
    Ok(_) => Ok(())
    Err(e) => Err(e)
  }
}

///|
/// Send one request op and expect a specific response op back, validating
/// the message ID.
async fn[T : LdapTransport] Session::op_round_trip(
  self : Session[T],
  request : ProtocolOp,
  response_tag : Int,
) -> Result[LdapMessage, LdapError] {
  let message = match self.round_trip(request) {
    Ok(m) => m
    Err(e) => return Err(e)
  }
  if protocol_op_tag(message.op) != response_tag {
    return Err(LdapError::UnexpectedOp(protocol_op_tag(message.op)))
  }
  Ok(message)
}

///|
/// Modify an entry (RFC 4511 4.6).
pub async fn[T : LdapTransport] Session::modify(
  self : Session[T],
  request : ModifyRequest,
) -> Result[ModifyResponse, LdapError] {
  let message = match self.op_round_trip(ModifyRequest(request), 7) {
    Ok(m) => m
    Err(e) => return Err(e)
  }
  match message.op {
    ModifyResponse(resp) => Ok(resp)
    _ => Err(LdapError::UnexpectedOp(protocol_op_tag(message.op)))
  }
}

///|
/// Add an entry (RFC 4511 4.8).
pub async fn[T : LdapTransport] Session::add(
  self : Session[T],
  request : AddRequest,
) -> Result[AddResponse, LdapError] {
  let message = match self.op_round_trip(AddRequest(request), 9) {
    Ok(m) => m
    Err(e) => return Err(e)
  }
  match message.op {
    AddResponse(resp) => Ok(resp)
    _ => Err(LdapError::UnexpectedOp(protocol_op_tag(message.op)))
  }
}

///|
/// Delete an entry (RFC 4511 4.10).
pub async fn[T : LdapTransport] Session::delete(
  self : Session[T],
  entry : String,
) -> Result[DelResponse, LdapError] {
  let message = match
    self.op_round_trip(DelRequest(DelRequest::new(entry)), 11) {
    Ok(m) => m
    Err(e) => return Err(e)
  }
  match message.op {
    DelResponse(resp) => Ok(resp)
    _ => Err(LdapError::UnexpectedOp(protocol_op_tag(message.op)))
  }
}

///|
/// Rename or move an entry (RFC 4511 4.9).
pub async fn[T : LdapTransport] Session::modify_dn(
  self : Session[T],
  request : ModifyDnRequest,
) -> Result[ModifyDnResponse, LdapError] {
  let message = match self.op_round_trip(ModifyDnRequest(request), 13) {
    Ok(m) => m
    Err(e) => return Err(e)
  }
  match message.op {
    ModifyDnResponse(resp) => Ok(resp)
    _ => Err(LdapError::UnexpectedOp(protocol_op_tag(message.op)))
  }
}

///|
/// Compare an attribute value on an entry (RFC 4511 4.11). The assertion
/// outcome is carried in the result code (compareTrue / compareFalse).
pub async fn[T : LdapTransport] Session::compare(
  self : Session[T],
  request : CompareRequest,
) -> Result[CompareResponse, LdapError] {
  let message = match self.op_round_trip(CompareRequest(request), 15) {
    Ok(m) => m
    Err(e) => return Err(e)
  }
  match message.op {
    CompareResponse(resp) => Ok(resp)
    _ => Err(LdapError::UnexpectedOp(protocol_op_tag(message.op)))
  }
}