///|
/// A high-level LDAP client that wraps a `Session` and records a diagnostic
/// trace. It never logs passwords or SASL payloads.
pub struct LdapClient[T] {
  session : Session[T]
  trace : LdapTrace
}

///|
pub fn[T] LdapClient::new(config : LdapConfig, transport : T) -> LdapClient[T] {
  { session: Session::new(config, transport), trace: LdapTrace::new() }
}

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

///|
pub async fn[T : LdapTransport] LdapClient::connect(
  self : LdapClient[T],
) -> Result[Unit, LdapError] {
  self.session.connect()
}

///|
/// Connect, then perform a simple bind.
pub async fn[T : LdapTransport] LdapClient::connect_and_bind_simple(
  self : LdapClient[T],
  name : String,
  password : String,
) -> Result[BindResponse, LdapError] {
  match self.session.connect() {
    Ok(_) => ()
    Err(e) => return Err(e)
  }
  self.bind_simple(name, password)
}

///|
/// Connect, then perform a SASL PLAIN bind.
pub async fn[T : LdapTransport] LdapClient::connect_and_bind_plain(
  self : LdapClient[T],
  authzid : String,
  authcid : String,
  password : String,
) -> Result[BindResponse, LdapError] {
  match self.session.connect() {
    Ok(_) => ()
    Err(e) => return Err(e)
  }
  self.bind_plain(authzid, authcid, password)
}

///|
/// Perform a simple bind and record the trace.
pub async fn[T : LdapTransport] LdapClient::bind_simple(
  self : LdapClient[T],
  name : String,
  password : String,
) -> Result[BindResponse, LdapError] {
  let response = match self.session.bind_simple(name, password) {
    Ok(r) => r
    Err(e) => return Err(e)
  }
  self.record_bind(response)
  Ok(response)
}

///|
/// Perform a SASL PLAIN bind and record the trace.
pub async fn[T : LdapTransport] LdapClient::bind_plain(
  self : LdapClient[T],
  authzid : String,
  authcid : String,
  password : String,
) -> Result[BindResponse, LdapError] {
  let response = match self.session.bind_plain(authzid, authcid, password) {
    Ok(r) => r
    Err(e) => return Err(e)
  }
  self.record_bind(response)
  Ok(response)
}

///|
fn[T] LdapClient::record_bind(
  self : LdapClient[T],
  response : BindResponse,
) -> Unit {
  let step = LdapStep::new(
    "bind",
    response.result.result_code.to_int(),
    response.result.diagnostic_message,
  )
  self.trace.record_step(step)
  self.trace.record_result(response.result)
}

///|
/// Run a streaming search and record the trace.
pub async fn[T : LdapTransport] LdapClient::search(
  self : LdapClient[T],
  request : SearchRequest,
  on_entry? : (SearchResultEntry) -> Unit,
  on_reference? : (SearchResultReference) -> Unit,
) -> Result[LdapResult, LdapError] {
  let result = match (on_entry, on_reference) {
    (Some(e), Some(r)) =>
      self.session.search(request, on_entry=e, on_reference=r)
    (Some(e), None) => self.session.search(request, on_entry=e)
    (None, Some(r)) => self.session.search(request, on_reference=r)
    (None, None) => self.session.search(request)
  }
  match result {
    Ok(r) => {
      self.record_search_result(r)
      Ok(r)
    }
    Err(e) => Err(e)
  }
}

///|
/// Collect a search into an outcome and record the trace.
pub async fn[T : LdapTransport] LdapClient::search_to_array(
  self : LdapClient[T],
  request : SearchRequest,
) -> Result[SearchOutcome, LdapError] {
  let outcome = match self.session.search_to_array(request) {
    Ok(o) => o
    Err(e) => return Err(e)
  }
  self.trace.entries = outcome.entries.length()
  self.record_search_result(outcome.result)
  Ok(outcome)
}

///|
fn[T] LdapClient::record_search_result(
  self : LdapClient[T],
  result : LdapResult,
) -> Unit {
  let step = LdapStep::new(
    "search",
    result.result_code.to_int(),
    result.diagnostic_message,
  )
  self.trace.record_step(step)
  self.trace.record_result(result)
}

///|
/// Send an unbind and close the connection.
pub async fn[T : LdapTransport] LdapClient::unbind(
  self : LdapClient[T],
) -> Result[Unit, LdapError] {
  self.session.unbind()
}

///|
/// Abandon an outstanding request.
pub async fn[T : LdapTransport] LdapClient::abandon(
  self : LdapClient[T],
  message_id : Int,
) -> Result[Unit, LdapError] {
  self.session.abandon(message_id)
}

///|
fn[T] LdapClient::record_op(
  self : LdapClient[T],
  op_name : String,
  result : LdapResult,
) -> Unit {
  let step = LdapStep::new(
    op_name,
    result.result_code.to_int(),
    result.diagnostic_message,
  )
  self.trace.record_step(step)
  self.trace.record_result(result)
}

///|
/// Add an entry and record the trace.
pub async fn[T : LdapTransport] LdapClient::add(
  self : LdapClient[T],
  request : AddRequest,
) -> Result[AddResponse, LdapError] {
  let response = match self.session.add(request) {
    Ok(r) => r
    Err(e) => return Err(e)
  }
  self.record_op("add", response.result)
  Ok(response)
}

///|
/// Modify an entry and record the trace.
pub async fn[T : LdapTransport] LdapClient::modify(
  self : LdapClient[T],
  request : ModifyRequest,
) -> Result[ModifyResponse, LdapError] {
  let response = match self.session.modify(request) {
    Ok(r) => r
    Err(e) => return Err(e)
  }
  self.record_op("modify", response.result)
  Ok(response)
}

///|
/// Delete an entry and record the trace.
pub async fn[T : LdapTransport] LdapClient::delete(
  self : LdapClient[T],
  entry : String,
) -> Result[DelResponse, LdapError] {
  let response = match self.session.delete(entry) {
    Ok(r) => r
    Err(e) => return Err(e)
  }
  self.record_op("delete", response.result)
  Ok(response)
}

///|
/// Rename or move an entry and record the trace.
pub async fn[T : LdapTransport] LdapClient::modify_dn(
  self : LdapClient[T],
  request : ModifyDnRequest,
) -> Result[ModifyDnResponse, LdapError] {
  let response = match self.session.modify_dn(request) {
    Ok(r) => r
    Err(e) => return Err(e)
  }
  self.record_op("modifyDN", response.result)
  Ok(response)
}

///|
/// Compare an attribute value; compareTrue / compareFalse are recorded as
/// successful steps in the trace.
pub async fn[T : LdapTransport] LdapClient::compare(
  self : LdapClient[T],
  request : CompareRequest,
) -> Result[CompareResponse, LdapError] {
  let response = match self.session.compare(request) {
    Ok(r) => r
    Err(e) => return Err(e)
  }
  self.record_op("compare", response.result)
  Ok(response)
}

///|
/// Run a search with automatic paged-results iteration (RFC 2696): each page
/// is requested with `page_size` entries and the server-issued cookie is
/// followed until it comes back empty. All pages are merged into a single
/// outcome and one trace step per page is recorded.
pub async fn[T : LdapTransport] LdapClient::search_paged(
  self : LdapClient[T],
  request : SearchRequest,
  page_size : Int,
) -> Result[SearchOutcome, LdapError] {
  let acc = SearchOutcome::new()
  let mut cookie = Bytes::new(0)
  while true {
    let control = paged_results_control(page_size, cookie)
    let done = match
      self.session.search_done(
        request,
        controls=[control],
        on_entry=fn(entry) { acc.entries.push(entry) },
        on_reference=fn(refs) { acc.references.push(refs) },
      ) {
      Ok(d) => d
      Err(e) => return Err(e)
    }
    self.trace.entries = acc.entries.length()
    self.record_op("search", done.result)
    match parse_paged_results_response(done.controls) {
      Some(resp) =>
        if resp.cookie.length() == 0 {
          acc.result = done.result
          break
        } else {
          cookie = resp.cookie
        }
      None => {
        acc.result = done.result
        break
      }
    }
  }
  Ok(acc)
}