///|
/// An opaque, fully validated Client endpoint.  It owns immutable services
/// and derived initialization capabilities, never connection-scoped state.
pub struct ClientEndpoint {
  info : Implementation
  session : ClientSessionService
  file_system : ClientFileSystemService?
  terminal : ClientTerminalService?
  elicitation : ClientElicitationService?
  capabilities : ClientCapabilities
}

///|
fn client_endpoint_marker(enabled : Bool) -> ProtocolNullable[CapabilityMarker] {
  if enabled {
    Value({ meta: Omitted })
  } else {
    Omitted
  }
}

///|
fn client_endpoint_bool(enabled : Bool) -> ProtocolNullable[Bool] {
  if enabled {
    Value(true)
  } else {
    Omitted
  }
}

///|
fn client_endpoint_file_system_capabilities(
  service : ClientFileSystemService?,
) -> ProtocolNullable[ClientFileSystemCapabilities] {
  match service {
    None => Omitted
    Some(service) => {
      let read = client_endpoint_bool(service.read_handler is Some(_))
      let write = client_endpoint_bool(service.write_handler is Some(_))
      Value({ read_text_file: read, write_text_file: write, meta: Omitted })
    }
  }
}

///|
fn client_endpoint_elicitation_capabilities(
  service : ClientElicitationService?,
) -> ProtocolNullable[ClientElicitationCapabilities] {
  match service {
    None => Omitted
    Some(service) => {
      let form = client_endpoint_marker(service.form_handler is Some(_))
      let url = client_endpoint_marker(service.url_handler is Some(_))
      if service.form_handler is Some(_) || service.url_handler is Some(_) {
        Value({ form, url, meta: Omitted })
      } else {
        Omitted
      }
    }
  }
}

///|
fn client_endpoint_capabilities(spec : ClientSpec) -> ClientCapabilities {
  {
    fs: client_endpoint_file_system_capabilities(spec.file_system),
    terminal: if spec.terminal is Some(_) {
      Value(true)
    } else {
      Omitted
    },
    elicitation: client_endpoint_elicitation_capabilities(spec.elicitation),
    session: Omitted,
    plan: Omitted,
    auth: Omitted,
    meta: Omitted,
  }
}

///|
fn client_endpoint_from_spec(spec : ClientSpec) -> ClientEndpoint {
  {
    info: spec.info,
    session: spec.session,
    file_system: spec.file_system,
    terminal: spec.terminal,
    elicitation: spec.elicitation,
    capabilities: client_endpoint_capabilities(spec),
  }
}

///|
/// Return the immutable implementation information used for initialization.
pub fn ClientEndpoint::info(self : ClientEndpoint) -> Implementation {
  self.info
}

///|
/// Return capabilities derived exclusively from the supplied services.
pub fn ClientEndpoint::capabilities(
  self : ClientEndpoint,
) -> ClientCapabilities {
  self.capabilities
}

///|
fn client_handler_error(method_name : String, error : Error) -> HandlerError {
  if @async.is_cancellation_error(error) {
    Cancelled
  } else {
    match error {
      HandlerError::Application(_) =>
        Application(message=method_name + ": handler failure")
      HandlerError::Cancelled => Cancelled
      HandlerError::UnavailableOperation(method_name~) =>
        UnavailableOperation(method_name~)
      _ => Application(message=method_name + ": handler failure")
    }
  }
}

///|
/// Normalize any ordinary application Error at the endpoint boundary.
/// Callback authors do not need to manufacture a protocol-specific error.
async fn[T] client_handler_call(
  method_name : String,
  handler : async () -> T,
) -> T raise HandlerError {
  handler() catch {
    error => raise client_handler_error(method_name, error)
  }
}

///|
/// Deliver a `session/update` notification to the Client observer.
pub async fn ClientEndpoint::session_update(
  self : ClientEndpoint,
  params : SessionUpdateParams,
) -> Unit raise HandlerError {
  client_handler_call("session/update", async fn() {
    (self.session.session_update_handler)(params)
  })
}

///|
/// Deliver a permission request to the Client handler.
pub async fn ClientEndpoint::request_permission(
  self : ClientEndpoint,
  request : RequestPermissionRequest,
) -> RequestPermissionResponse raise HandlerError {
  client_handler_call("session/request_permission", async fn() {
    (self.session.request_permission_handler)(request)
  })
}

///|
/// Invoke the optional `fs/read_text_file` handler.
pub async fn ClientEndpoint::read_text_file(
  self : ClientEndpoint,
  params : ReadTextFileParams,
) -> ReadTextFileResult raise HandlerError {
  match self.file_system {
    Some(service) =>
      match service.read_handler {
        Some(handler) =>
          client_handler_call("fs/read_text_file", async fn() {
            handler(params)
          })
        None => raise UnavailableOperation(method_name="fs/read_text_file")
      }
    None => raise UnavailableOperation(method_name="fs/read_text_file")
  }
}

///|
/// Invoke the optional `fs/write_text_file` handler.
pub async fn ClientEndpoint::write_text_file(
  self : ClientEndpoint,
  params : WriteTextFileParams,
) -> WriteTextFileResult raise HandlerError {
  match self.file_system {
    Some(service) =>
      match service.write_handler {
        Some(handler) =>
          client_handler_call("fs/write_text_file", async fn() {
            handler(params)
          })
        None => raise UnavailableOperation(method_name="fs/write_text_file")
      }
    None => raise UnavailableOperation(method_name="fs/write_text_file")
  }
}

///|
/// Invoke `terminal/create` on the complete terminal service.
pub async fn ClientEndpoint::terminal_create(
  self : ClientEndpoint,
  params : TerminalCreateParams,
) -> TerminalCreateResult raise HandlerError {
  match self.terminal {
    Some(service) =>
      client_handler_call("terminal/create", async fn() {
        (service.create_handler)(params)
      })
    None => raise UnavailableOperation(method_name="terminal/create")
  }
}

///|
/// Invoke `terminal/output` on the complete terminal service.
pub async fn ClientEndpoint::terminal_output(
  self : ClientEndpoint,
  params : TerminalOutputParams,
) -> TerminalOutputResult raise HandlerError {
  match self.terminal {
    Some(service) =>
      client_handler_call("terminal/output", async fn() {
        (service.output_handler)(params)
      })
    None => raise UnavailableOperation(method_name="terminal/output")
  }
}

///|
/// Invoke `terminal/wait_for_exit` on the complete terminal service.
pub async fn ClientEndpoint::terminal_wait_for_exit(
  self : ClientEndpoint,
  params : TerminalWaitForExitParams,
) -> TerminalWaitForExitResult raise HandlerError {
  match self.terminal {
    Some(service) =>
      client_handler_call("terminal/wait_for_exit", async fn() {
        (service.wait_for_exit_handler)(params)
      })
    None => raise UnavailableOperation(method_name="terminal/wait_for_exit")
  }
}

///|
/// Invoke `terminal/kill` on the complete terminal service.
pub async fn ClientEndpoint::terminal_kill(
  self : ClientEndpoint,
  params : TerminalKillParams,
) -> TerminalKillResult raise HandlerError {
  match self.terminal {
    Some(service) =>
      client_handler_call("terminal/kill", async fn() {
        (service.kill_handler)(params)
      })
    None => raise UnavailableOperation(method_name="terminal/kill")
  }
}

///|
/// Invoke `terminal/release` on the complete terminal service.
pub async fn ClientEndpoint::terminal_release(
  self : ClientEndpoint,
  params : TerminalReleaseParams,
) -> TerminalReleaseResult raise HandlerError {
  match self.terminal {
    Some(service) =>
      client_handler_call("terminal/release", async fn() {
        (service.release_handler)(params)
      })
    None => raise UnavailableOperation(method_name="terminal/release")
  }
}

///|
/// Invoke form-mode `elicitation/create`.
pub async fn ClientEndpoint::elicitation_form(
  self : ClientEndpoint,
  params : ElicitationFormParams,
) -> ElicitationCreateResult raise HandlerError {
  match self.elicitation {
    Some(service) =>
      match service.form_handler {
        Some(handler) =>
          client_handler_call("elicitation/create", async fn() {
            handler(params)
          })
        None => raise UnavailableOperation(method_name="elicitation/create")
      }
    None => raise UnavailableOperation(method_name="elicitation/create")
  }
}

///|
/// Invoke URL-mode `elicitation/create`.
pub async fn ClientEndpoint::elicitation_url(
  self : ClientEndpoint,
  params : ElicitationUrlParams,
) -> ElicitationCreateResult raise HandlerError {
  match self.elicitation {
    Some(service) =>
      match service.url_handler {
        Some(handler) =>
          client_handler_call("elicitation/create", async fn() {
            handler(params)
          })
        None => raise UnavailableOperation(method_name="elicitation/create")
      }
    None => raise UnavailableOperation(method_name="elicitation/create")
  }
}

///|
/// Observe `elicitation/complete` notifications.
pub async fn ClientEndpoint::elicitation_complete(
  self : ClientEndpoint,
  params : ElicitationCompleteParams,
) -> Unit raise HandlerError {
  match self.elicitation {
    Some(service) =>
      match service.complete_handler {
        Some(handler) =>
          client_handler_call("elicitation/complete", async fn() {
            handler(params)
          })
        None => raise UnavailableOperation(method_name="elicitation/complete")
      }
    None => raise UnavailableOperation(method_name="elicitation/complete")
  }
}

///|
/// A pure Reader composition program for one caller-owned environment.
pub type ClientProgram[Env] = @reader.Reader[
  Env,
  Result[ClientEndpoint, ClientCompositionError],
]

///|
/// Turn a Reader of a validated Client specification into a one-shot endpoint
/// program.  Each `Reader::run` derives an isolated endpoint from its Env.
pub fn[Env] client_program(
  spec_program : @reader.Reader[Env, Result[ClientSpec, ClientCompositionError]],
) -> ClientProgram[Env] {
  @reader.Reader::map(spec_program, fn(result) {
    Result::map(result, fn(spec) { client_endpoint_from_spec(spec) })
  })
}

///|
/// Compose a Client endpoint directly from a caller-owned environment
/// projection without storing that environment globally.
pub fn[Env] client_program_from(
  project : (Env) -> Result[ClientSpec, ClientCompositionError],
) -> ClientProgram[Env] {
  client_program(@reader.asks(project))
}