///|
/// Invokes a typed command through the bridge installed on the active page.
pub async fn[Request : ToJson, Response : @json.FromJson] invoke(
  command : @proton_contract.Command[Request, Response],
  request : Request,
) -> Response raise ClientFailure {
  desktop.invoke(command, request)
}

///|
/// Invokes a typed command using this client's boundary. Task cancellation
/// cancels the pending transport request.
pub async fn[Request : ToJson, Response : @json.FromJson] Client::invoke(
  self : Client,
  command : @proton_contract.Command[Request, Response],
  request : Request,
) -> Response raise ClientFailure {
  let outcome : Ref[Result[Response, ClientFailure]?] = Ref(None)
  let ready = @async.CondVar::Cond()
  let cancel = self.invoke_with_callbacks(
    command,
    request,
    value => {
      outcome.val = Some(Ok(value))
      ready.signal()
    },
    error => {
      outcome.val = Some(Err(error))
      ready.signal()
    },
  )
  defer cancel()
  while outcome.val is None {
    ready.wait() catch {
      _ => raise RequestCancelled
    }
  }
  match outcome.val {
    Some(Ok(value)) => value
    Some(Err(error)) => raise error
    None => abort("request completed without a result")
  }
}

///|
/// Starts a typed request on the desktop client. The returned function cancels
/// observation and requests transport cancellation exactly once.
pub fn[Request : ToJson, Response : @json.FromJson] invoke_with_callbacks(
  command : @proton_contract.Command[Request, Response],
  request : Request,
  success : (Response) -> Unit,
  failure : (ClientFailure) -> Unit,
) -> () -> Unit {
  desktop.invoke_with_callbacks(command, request, success, failure)
}

///|
/// Starts a typed request without entering an async scheduler. Late responses
/// after cancellation and repeated completions are ignored.
pub fn[Request : ToJson, Response : @json.FromJson] Client::invoke_with_callbacks(
  self : Client,
  command : @proton_contract.Command[Request, Response],
  request : Request,
  success : (Response) -> Unit,
  failure : (ClientFailure) -> Unit,
) -> () -> Unit {
  command.validate() catch {
    error => {
      failure(InvalidContract(message=error.message()))
      return () => ()
    }
  }
  let active = Ref(true)
  let cancel = (self.start_request)(
    command.contract_route().operation_name(),
    ToJson::to_json(request).stringify(),
    response_json => {
      guard active.val else { return }
      active.val = false
      let response : Response = @json.from_json(@json.parse(response_json)) catch {
        error => {
          failure(ResponseDecode(message=error.to_string()))
          return
        }
      }
      success(response)
    },
    error => {
      guard active.val else { return }
      active.val = false
      failure(error)
    },
  )
  () => {
    if active.val {
      active.val = false
      cancel()
    }
  }
}

///|
/// A live event subscription installed on the active renderer page.
pub struct Subscription {
  mut closed : Bool
  close_listener : () -> Unit
}

///|
/// Removes this subscription. Repeated calls have no effect.
pub fn Subscription::close(self : Subscription) -> Unit {
  if !self.closed {
    self.closed = true
    (self.close_listener)()
  }
}

///|
/// Subscribes to a typed live event on the active renderer page.
///
/// A malformed payload reports `EventDecode` through `failure` without
/// removing the subscription.
pub fn[Payload : @json.FromJson] subscribe(
  event : @proton_contract.Event[Payload],
  received : (Payload) -> Unit,
  failure : (ClientFailure) -> Unit,
) -> Subscription raise ClientFailure {
  desktop.subscribe(event, received, failure)
}

///|
/// Installs an independent listener. Closing it also suppresses queued events.
pub fn[Payload : @json.FromJson] Client::subscribe(
  self : Client,
  event : @proton_contract.Event[Payload],
  received : (Payload) -> Unit,
  failure : (ClientFailure) -> Unit,
) -> Subscription raise ClientFailure {
  event.validate() catch {
    error => raise InvalidContract(message=error.message())
  }
  self.listen_json(event.contract_route().operation_name(), payload_json => {
    let payload : Payload = @json.from_json(@json.parse(payload_json)) catch {
      error => {
        failure(EventDecode(message=error.to_string()))
        return
      }
    }
    received(payload)
  })
}

///|
/// Framework integration boundary. Prefer typed subscriptions in application code.
#doc(hidden)
pub fn Client::listen_json(
  self : Client,
  route : String,
  received : (String) -> Unit,
) -> Subscription raise ClientFailure {
  let active = Ref(true)
  let close_listener = (self.start_listener)(route, value => {
    if active.val {
      received(value)
    }
  })
  Subscription::{
    closed: false,
    close_listener: () => {
      active.val = false
      close_listener()
    },
  }
}