// The calling side: the methods a server answers, invoked over a transport that carries JSON-RPC.
// `moona2a/http` supplies one over HTTP; a test supplies one that answers from a table.

///|
/// How a client reaches an agent. A transport carries JSON-RPC messages and finds the card; it
/// knows nothing about tasks.
pub(open) trait Transport {
  /// Send one message and return the answer.
  async fn exchange(Self, @moonjrpc.Message) -> @moonjrpc.Message
  /// Send one message and hand every reply to `out` as it arrives, for a method whose answer is a
  /// stream.
  async fn stream(Self, @moonjrpc.Message, &ReplySink) -> Unit
  /// Read the agent's card from its well-known address.
  async fn discover(Self) -> Json
}

///|
/// A caller's handle on a remote agent.
pub struct Client {
  transport : &Transport
  mut next : Int64
}

///|
/// A client talking to the agent behind `transport`.
pub fn Client::new(transport : &Transport) -> Client {
  { transport, next: 0L, }
}

///|
/// The next request identifier. Ids only have to be unique to this client, since the transport
/// pairs each answer with the request it sent.
fn Client::take_id(self : Client) -> @moonjrpc.Id {
  self.next = self.next + 1L
  @moonjrpc.Id::int(self.next)
}

///|
/// Read what an agent sent. A shape this library cannot read is the agent's fault, not the
/// caller's, so it comes back as `InvalidAgentResponse` naming the path that would not read.
fn[T : @json.FromJson] read_reply(json : Json) -> T raise {
  @json.from_json(json) catch {
    @json.JsonDecodeError((path, reason)) =>
      raise InvalidAgentResponse("\{path}: \{reason}")
  }
}

///|
/// Send `message` and return what the agent answered with: a task, or a message.
pub async fn Client::send_message(
  self : Client,
  message : Message,
  configuration? : SendMessageConfig,
  metadata? : Json,
  tenant? : String,
) -> StreamEvent {
  let request : SendMessageRequest = {
    message,
    configuration,
    metadata,
    tenant,
  }
  let result = self.call(Method::SendMessage, request.to_json())
  let event : StreamEvent = read_reply(result)
  match event {
    TaskSnapshot(_) | TaskMessage(_) => event
    StatusChange(update) =>
      raise InvalidAgentResponse(
        "SendMessage answered with a \{update.status.state.wire()} event",
      )
    ArtifactChunk(update) =>
      raise InvalidAgentResponse(
        "SendMessage answered with an artifact \{update.artifact.artifact_id}",
      )
  }
}

///|
/// Send `message` and hand every event to `sink` as it arrives.
pub async fn Client::send_streaming_message(
  self : Client,
  message : Message,
  sink : &ReplySink,
  configuration? : SendMessageConfig,
  metadata? : Json,
  tenant? : String,
) -> Unit {
  let request : SendMessageRequest = {
    message,
    configuration,
    metadata,
    tenant,
  }
  let call = @moonjrpc.Message::Call(
    id=self.take_id(),
    name=Method::SendStreamingMessage.name(),
    params=Some(request.to_json()),
  )
  self.transport.stream(call, sink)
}

///|
/// Read a task as it stands.
pub async fn Client::get_task(
  self : Client,
  id : String,
  history_length? : Int32,
) -> Task {
  let params = object()
  params["id"] = Json::string(id)
  put_int(params, "historyLength", history_length)
  read_reply(self.call(Method::GetTask, Json::object(params)))
}

///|
/// Read the tasks the caller may see.
pub async fn Client::list_tasks(
  self : Client,
  query : ListTasksQuery,
) -> TaskPage {
  read_reply(self.call(Method::ListTasks, query.to_json()))
}

///|
/// Ask the agent to stop a task.
pub async fn Client::cancel_task(self : Client, id : String) -> Task {
  let params = object()
  params["id"] = Json::string(id)
  read_reply(self.call(Method::CancelTask, Json::object(params)))
}

///|
/// Join a task that is already running, and follow it to its end.
pub async fn Client::subscribe_to_task(
  self : Client,
  id : String,
  sink : &ReplySink,
) -> Unit {
  let params = object()
  params["id"] = Json::string(id)
  let call = @moonjrpc.Message::Call(
    id=self.take_id(),
    name=Method::SubscribeToTask.name(),
    params=Some(Json::object(params)),
  )
  self.transport.stream(call, sink)
}

///|
/// Register a webhook for a task's updates.
pub async fn Client::create_push_config(
  self : Client,
  config : TaskPushConfig,
) -> TaskPushConfig {
  read_reply(self.call(Method::CreatePushConfig, config.to_json()))
}

///|
/// Read one of a task's webhook registrations.
pub async fn Client::get_push_config(
  self : Client,
  task_id : String,
  id : String,
) -> TaskPushConfig {
  let params = object()
  params["taskId"] = Json::string(task_id)
  params["id"] = Json::string(id)
  read_reply(self.call(Method::GetPushConfig, Json::object(params)))
}

///|
/// Read every webhook registration a task has.
pub async fn Client::list_push_configs(
  self : Client,
  task_id : String,
) -> Array[TaskPushConfig] {
  let params = object()
  params["taskId"] = Json::string(task_id)
  let page : PushConfigPage = read_reply(
    self.call(Method::ListPushConfigs, Json::object(params)),
  )
  page.configs
}

///|
/// Remove a webhook registration. Removing one that is already gone is not an error.
pub async fn Client::delete_push_config(
  self : Client,
  task_id : String,
  id : String,
) -> Unit {
  let params = object()
  params["taskId"] = Json::string(task_id)
  params["id"] = Json::string(id)
  self.call(Method::DeletePushConfig, Json::object(params)) |> ignore
}

///|
/// Read the card the agent shows only to authenticated callers.
pub async fn Client::extended_card(self : Client) -> AgentCard {
  read_reply(self.call(Method::GetExtendedCard, Json::object(Map([]))))
}

///|
/// Read the agent's public card.
pub async fn Client::discover(self : Client) -> AgentCard {
  read_reply(self.transport.discover())
}

///|
/// Make one call and return its result, turning a refusal into the error it names.
async fn Client::call(self : Client, rpc : Method, params : Json) -> Json {
  let call = @moonjrpc.Message::Call(
    id=self.take_id(),
    name=rpc.name(),
    params=Some(params),
  )
  let answer = self.transport.exchange(call)
  match answer {
    @moonjrpc.Message::Reply(result~, ..) => result
    @moonjrpc.Message::Refusal(failure~, ..) =>
      raise Refused::from_failure(failure)
    _ =>
      raise InvalidAgentResponse(
        "the transport answered a call with something that is not a reply",
      )
  }
}