// The client's side of the wire: outbound calls to the agent, and the handlers that answer the
// agent's own calls. Run the connection in one task and call from any other.

///|
/// A connection to an agent, from the editor's side. `run` takes messages in; the methods here send
/// them out and wait for their answers.
pub struct Conn {
  peer : @peer.Peer
  client : &Client
}

///|
/// Wires a client to a transport. The caller runs the returned connection, which is what delivers
/// updates and answers the agent's calls.
pub fn connect(client : &Client, frames : &@peer.Frames) -> Conn {
  let methods = @moonjrpc.Methods::new()
  let peer = @peer.Peer::new(frames, methods~, timeout_ms=None, batch=false)
  connect_methods(client, methods)
  { peer, client, }
}

///|
fn connect_methods(client : &Client, methods : @moonjrpc.Methods) -> Unit {
  methods.notice("session/update", async fn(params) {
    let notification : SessionNotification = params_of(params) catch {
      _ => return
    }
    client.update(notification.session_id, notification.update)
  })
  on_call(methods, "session/request_permission", async fn(params : Json) {
    let ask : Permission = read_root(params)
    write(client.request_permission(ask))
  })
  on_call(methods, "fs/read_text_file", async fn(request : ReadTextFile) {
    let content = client.read_text_file(request)
    write(FileText::{ content, meta: None, })
  })
  on_call(methods, "fs/write_text_file", async fn(request : WriteTextFile) {
    client.write_text_file(request)
    Json::empty_object()
  })
  on_call(methods, "terminal/create", async fn(request : TerminalCommand) {
    let terminal_id = client.create_terminal(request)
    write(TerminalOpened::{ terminal_id, meta: None, })
  })
  on_call(methods, "terminal/output", async fn(request : TerminalRequest) {
    write(client.terminal_output(request))
  })
  on_call(methods, "terminal/wait_for_exit", async fn(
    request : TerminalRequest,
  ) {
    write(client.wait_for_terminal_exit(request))
  })
  on_call(methods, "terminal/kill", async fn(request : TerminalRequest) {
    client.kill_terminal(request)
    Json::empty_object()
  })
  on_call(methods, "terminal/release", async fn(request : TerminalRequest) {
    client.release_terminal(request)
    Json::empty_object()
  })
  on_call(methods, "elicitation/create", async fn(request : Elicitation) {
    write(client.elicit(request))
  })
}

///|
/// Takes messages in until the other side goes away. Updates reach the client from here, so it has
/// to run in a task of its own while the other tasks call the agent.
pub async fn Conn::run(self : Conn) -> Unit {
  self.peer.run()
}

///|
/// A call the protocol does not name: send it raw and read the answer raw.
pub async fn Conn::call(
  self : Conn,
  name : String,
  params : Json,
) -> Json raise Refused {
  self.peer.call(name, params~) catch {
    @moonjrpc.Failed(failure) => raise refused_of(failure)
    @moonjrpc.TimedOut(name~, ..) =>
      raise Refused::Closed(reason="\{name} timed out")
    _ => raise Refused::Closed(reason="\{name}: the agent is gone")
  }
}

///|
/// A notice the protocol does not name.
pub async fn Conn::notify(
  self : Conn,
  name : String,
  params : Json,
) -> Unit raise Refused {
  self.peer.notify(name, params~) catch {
    @moonjrpc.Failed(failure) => raise refused_of(failure)
    _ => raise Refused::Closed(reason="\{name}: the agent is gone")
  }
}

///|
/// Negotiates the version and trades capabilities. Call it once, before anything else.
pub async fn Conn::initialize(
  self : Conn,
  request : Initialize,
) -> Initialized raise Refused {
  read_root(self.call("initialize", write(request)))
}

///|
/// Runs one of the agent's authentication methods.
pub async fn Conn::authenticate(
  self : Conn,
  request : Authenticate,
) -> Unit raise Refused {
  ignore(self.call("authenticate", write(request)))
}

///|
/// Drops whatever `authenticate` established.
pub async fn Conn::logout(self : Conn) -> Unit raise Refused {
  ignore(self.call("logout", json_of([])))
}

///|
/// Starts a session and returns what the agent says about it: identifier, modes, options.
pub async fn Conn::new_session(
  self : Conn,
  request : NewSession,
) -> SessionStarted raise Refused {
  read_root(self.call("session/new", write(request)))
}

///|
/// Reopens a session; the updates that replay its history arrive on `run`.
pub async fn Conn::load_session(
  self : Conn,
  request : LoadSession,
) -> SessionStarted raise Refused {
  read_root(self.call("session/load", write(request)))
}

///|
/// Reattaches to a live session without replaying anything.
pub async fn Conn::resume_session(
  self : Conn,
  request : ResumeSession,
) -> SessionStarted raise Refused {
  read_root(self.call("session/resume", write(request)))
}

///|
/// The sessions the agent remembers, one page at a time.
pub async fn Conn::list_sessions(
  self : Conn,
  request : ListSessions,
) -> SessionList raise Refused {
  read_root(self.call("session/list", write(request)))
}

///|
/// Forgets a session.
pub async fn Conn::delete_session(
  self : Conn,
  request : DeleteSession,
) -> Unit raise Refused {
  ignore(self.call("session/delete", write(request)))
}

///|
/// Ends a running session, as if `session/cancel` had been sent.
pub async fn Conn::close_session(
  self : Conn,
  request : CloseSession,
) -> Unit raise Refused {
  ignore(self.call("session/close", write(request)))
}

///|
/// Switches the session's mode.
pub async fn Conn::set_mode(
  self : Conn,
  request : SetMode,
) -> Unit raise Refused {
  ignore(self.call("session/set_mode", write(request)))
}

///|
/// Sets one configuration option and returns the whole list, as the protocol requires.
pub async fn Conn::set_config_option(
  self : Conn,
  request : SetConfigOption,
) -> Array[ConfigOption] raise Refused {
  let answer = self.call("session/set_config_option", write(request))
  let reply : SetConfigOptions = read_root(answer)
  reply.config_options
}

///|
/// Sends one turn and waits for its answer. Updates arrive through the client's `update` while this
/// waits, because the connection is being run in another task.
pub async fn Conn::prompt(
  self : Conn,
  request : Prompt,
) -> PromptResult raise Refused {
  read_root(self.call("session/prompt", write(request)))
}

///|
/// Asks the agent to stop the turn running in a session. A notice: nothing comes back, and the turn
/// answers `Cancelled` on its own.
pub async fn Conn::cancel(
  self : Conn,
  session_id : String,
) -> Unit raise Refused {
  self.notify("session/cancel", write(Cancel::{ session_id, meta: None, }))
}

///|
/// The client this connection feeds.
pub fn Conn::client(self : Conn) -> &Client {
  self.client
}