///|
/// A transport that a generated peer supports.
pub(all) enum Transport {
  Stdio
  Http
  WebSocket
} derive(Eq, Debug)

///|
/// The setting text of a transport.
pub fn Transport::to_string(self : Transport) -> String {
  match self {
    Stdio => "stdio"
    Http => "http"
    WebSocket => "websocket"
  }
}

///|
/// The settings that every JSON-RPC language plugin shares, after I checked
/// them against the model: the prepared description, the selected peer and
/// the transports.
pub struct PeerSettings {
  description : Description
  peer : PeerSelection
  transports : Array[Transport]
}

///|
fn peer_settings_error(error : Error) -> @codegen.CodegenError {
  match error {
    @rpc.InvalidContract(id, text)
    | @validation.InvalidTarget(id, text)
    | @validation.InvalidModel(id, text)
    | @validation.UnsupportedConstraint(id, text) =>
      @codegen.InvalidSettings(
        "I could not prepare the JSON-RPC contract of `\{id}`: \{text}",
      )
    @validation.InvalidConfiguration(text) =>
      @codegen.InvalidSettings(
        "I could not prepare the JSON-RPC contract: \{text}",
      )
    error =>
      @codegen.InvalidSettings(
        "I could not prepare the JSON-RPC contract: \{error.to_string()}",
      )
  }
}

///|
/// Read the shared settings of a JSON-RPC plugin. `service` is the service
/// shape of the plugin context. `protocol` (optional) is a `jsonRpcProtocol`
/// structure; without it, the service has the implicit peers `client` and
/// `server`. `peer` (required) selects the peer to generate. `transports`
/// (optional, all by default) lists `stdio`, `http` and `websocket`. `extra`
/// lists the settings of the language plugin. The generators target JSON-RPC
/// 2.0.
pub fn PeerSettings::from_context(
  ctx : @codegen.PluginContext,
  extra~ : Array[String] = [],
) -> PeerSettings raise @codegen.CodegenError {
  ctx.check_settings(["protocol", "peer", "transports", ..extra])
  guard ctx.setting_string("peer") is Some(peer_text) else {
    raise @codegen.InvalidSettings(
      "I expected setting `peer`, the name of the peer to generate.",
    )
  }
  let service = ctx.service.id
  let description = match ctx.setting_string("protocol") {
    Some(text) => {
      guard @smithy.ShapeId::parse(text) is Some(protocol) else {
        raise @codegen.InvalidSettings(
          "I expected a shape ID in setting `protocol`, found `\{text}`.",
        )
      }
      let description = Description::for_protocol(
        ctx.model,
        protocol,
        @rpc.V2_0,
      ) catch {
        error => raise peer_settings_error(error)
      }
      let services = description.peers.filter_map(peer => {
        peer.accepts.map(accepts => accepts.service)
      })
      services.sort_by((a, b) => a.to_string().lexical_compare(b.to_string()))
      if !services.contains(service) {
        let names = services.map(id => id.to_string()).join(", ")
        raise @codegen.InvalidSettings(
          "I expected setting `service` to name a service of protocol `\{protocol}` (\{names}), found `\{service}`.",
        )
      }
      description
    }
    None =>
      Description::for_service(ctx.model, service, @rpc.V2_0) catch {
        error => raise peer_settings_error(error)
      }
  }
  let selection = match @rpc.PeerName::parse(peer_text) {
    Some(name) => description.peer(name)
    None => None
  }
  guard selection is Some(peer) else {
    let names = description.peers.map(peer => peer.name.to_string())
    match description.protocol {
      Some(protocol) =>
        raise @codegen.InvalidSettings(
          "I expected setting `peer` to name a peer of protocol `\{protocol}` (\{names.join(", ")}), found `\{peer_text}`.",
        )
      None =>
        raise @codegen.InvalidSettings(
          "I expected setting `peer` to be `client` or `server` for service `\{service}`, found `\{peer_text}`.",
        )
    }
  }
  { description, peer, transports: read_transports(ctx), }
}

///|
fn read_transports(
  ctx : @codegen.PluginContext,
) -> Array[Transport] raise @codegen.CodegenError {
  guard ctx.settings.get("transports") is Some(value) else {
    return [Stdio, Http, WebSocket]
  }
  guard value is Array(items) else {
    raise @codegen.InvalidSettings(
      "I expected an array of transports in setting `transports`.",
    )
  }
  let transports : Array[Transport] = []
  for item in items {
    let transport = match item {
      String("stdio") => Stdio
      String("http") => Http
      String("websocket") => WebSocket
      String(text) =>
        raise @codegen.InvalidSettings(
          "I expected each transport in setting `transports` to be `stdio`, `http` or `websocket`, found `\{text}`.",
        )
      _ =>
        raise @codegen.InvalidSettings(
          "I expected each transport in setting `transports` to be a string.",
        )
    }
    if transports.contains(transport) {
      raise @codegen.InvalidSettings(
        "I found transport `\{transport.to_string()}` twice in setting `transports`.",
      )
    }
    transports.push(transport)
  }
  transports
}

///|
/// The services of the peer: the service it accepts and the service it calls,
/// without duplicates, in lexical shape ID order.
pub fn PeerSettings::services(self : PeerSettings) -> Array[@smithy.ShapeId] {
  let out : Array[@smithy.ShapeId] = []
  for service in [self.peer.accepts, self.peer.calls] {
    if service is Some(description) && !out.contains(description.service) {
      out.push(description.service)
    }
  }
  out.sort_by((a, b) => a.to_string().lexical_compare(b.to_string()))
  out
}

///|
/// The contextual names of the shapes of the peer's services.
pub fn PeerSettings::names(
  self : PeerSettings,
  model : @smithy.Model,
) -> @codegen.ServiceNames raise @codegen.CodegenError {
  @codegen.ServiceNames::for_services(model, self.services())
}