///|
/// A service contract and the shapes that its operations reach.
pub struct ServiceDescription {
  service : @smithy.ShapeId
  methods : Array[@rpc.MethodContract]
  shapes : Array[@smithy.ShapeId]
  values : Array[@validation.StructureContract]
  value_config : @validation.Config
} derive(Eq, Debug)

///|
/// The service that a peer receives. None means that the peer receives no calls.
pub struct PeerDescription {
  name : @rpc.PeerName
  accepts : ServiceDescription?
} derive(Eq, Debug)

///|
/// A language-neutral JSON-RPC contract. Each constructor uses RPC preparation.
pub struct Description {
  protocol : @smithy.ShapeId?
  version : @rpc.ProtocolVersion
  peers : Array[PeerDescription]
  session : @rpc.SessionContract?
} derive(Eq, Debug)

///|
/// Prepare a service with the implicit client and server peers.
pub fn Description::for_service(
  model : @smithy.Model,
  service : @smithy.ShapeId,
  version : @rpc.ProtocolVersion,
  config~ : @validation.Config = @validation.Config::new(
    unknown_members=@validation.Ignore,
  ),
) -> Description raise {
  let prepared = @rpc.PreparedService::prepare(model, service, version, config~)
  {
    protocol: None,
    session: None,
    version,
    peers: [
      { name: @rpc.PeerName::parse("client").unwrap(), accepts: None, },
      {
        name: @rpc.PeerName::parse("server").unwrap(),
        accepts: Some({
          service,
          methods: prepared.contracts(),
          values: prepared.value_contracts(),
          value_config: prepared.value_config(),
          shapes: @codegen.service_closure(model, service),
        }),
      },
    ],
  }
}

///|
/// Both directions for one peer: the service it accepts and the service it calls.
pub struct PeerSelection {
  name : @rpc.PeerName
  accepts : ServiceDescription?
  calls : ServiceDescription?
} derive(Eq, Debug)

///|
/// Prepare an explicit two-peer protocol with its session rules.
pub fn Description::for_protocol(
  model : @smithy.Model,
  protocol : @smithy.ShapeId,
  version : @rpc.ProtocolVersion,
  config~ : @validation.Config = @validation.Config::new(
    unknown_members=@validation.Ignore,
  ),
) -> Description raise {
  let prepared = @rpc.ProtocolDescription::from_smithy(
    model,
    protocol,
    version,
    config~,
  )
  let peers = prepared
    .peers()
    .map(name => {
      name,
      accepts: prepared
      .service_for(name)
      .map(service => {
        service: service.service_id(),
        methods: service.contracts(),
        values: service.value_contracts(),
        value_config: service.value_config(),
        shapes: @codegen.service_closure(model, service.service_id()),
      }),
    })
  peers.sort_by((a, b) => a.name.to_string().lexical_compare(b.name.to_string()))
  {
    protocol: Some(protocol),
    version,
    peers,
    session: prepared.session_contract(),
  }
}

///|
/// Select the two directions of a known peer.
pub fn Description::peer(
  self : Description,
  name : @rpc.PeerName,
) -> PeerSelection? {
  guard self.peers.iter().find_first(peer => peer.name == name) is Some(peer) else {
    return None
  }
  Some({
    name,
    accepts: peer.accepts,
    calls: self.peers
    .iter()
    .find_first(other => other.name != name)
    .bind(other => other.accepts),
  })
}