///|
priv struct Settings {
  module_name : String
  package_name : String
  peer : @rpc.PeerName
  protocol : @smithy.ShapeId?
}

///|
fn go_identifier(text : String) -> Bool {
  if text.is_empty() {
    return false
  }
  for i, c in text.to_array() {
    if !(c == '_' ||
      (c >= 'A' && c <= 'Z') ||
      (c >= 'a' && c <= 'z') ||
      (i > 0 && c >= '0' && c <= '9')) {
      return false
    }
  }
  ![
    "break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough",
    "for", "func", "go", "goto", "if", "import", "interface", "map", "package", "range",
    "return", "select", "struct", "switch", "type", "var", "_",
  ].contains(text)
}

///|
fn check_output_names(
  module_name : String,
  package_name : String,
) -> Unit raise @codegen.CodegenError {
  if module_name.is_empty() ||
    module_name.has_prefix("/") ||
    module_name.has_suffix("/") ||
    module_name
    .split("/")
    .any(part => part == ".." || part == "." || part.is_empty()) ||
    module_name
    .to_array()
    .any(c => {
      !((c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') ||
      c == '/' ||
      c == '.' ||
      c == '-' ||
      c == '_')
    }) {
    raise @codegen.InvalidSettings(
      "I expected a Go module path in setting `module`; I found `\{module_name}`.",
    )
  }
  if !go_identifier(package_name) {
    raise @codegen.InvalidSettings(
      "I expected a Go package name in setting `package`; I found `\{package_name}`.",
    )
  }
}

///|
fn settings(
  plugin : @codegen.PluginContext,
) -> Settings raise @codegen.CodegenError {
  plugin.check_settings([
    "module", "package", "peer", "protocol", "version", "transports",
  ])
  let module_name = plugin
    .setting_string("module")
    .unwrap_or("example/generated")
  let package_name = plugin.setting_string("package").unwrap_or("generated")
  check_output_names(module_name, package_name)
  let peer_text = plugin.setting_string("peer").unwrap_or("client")
  guard @rpc.PeerName::parse(peer_text) is Some(peer) else {
    raise @codegen.InvalidSettings(
      "I expected a peer name in setting `peer`; I found `\{peer_text}`.",
    )
  }
  let protocol = match plugin.setting_string("protocol") {
    None => None
    Some(text) =>
      match @smithy.ShapeId::parse(text) {
        Some(id) => Some(id)
        None =>
          raise @codegen.InvalidSettings(
            "I expected a shape ID in setting `protocol`; I found `\{text}`.",
          )
      }
  }
  let version = plugin.setting_string("version").unwrap_or("2.0")
  if version != "2.0" {
    raise @codegen.InvalidSettings(
      "I support JSON-RPC version `2.0` in the Go generator; I found `\{version}`.",
    )
  }
  match plugin.settings.get("transports") {
    None => ()
    Some(Array(items)) =>
      for item in items {
        guard item is String(name) &&
          ["stdio", "http", "websocket"].contains(name) else {
          raise @codegen.InvalidSettings(
            "I expected `stdio`, `http`, or `websocket` in setting `transports`.",
          )
        }
      }
    Some(_) =>
      raise @codegen.InvalidSettings(
        "I expected an array in setting `transports`.",
      )
  }
  { module_name, package_name, peer, protocol, }
}

///|
/// Register this plugin as `go-jsonrpc-codegen` in the build runner.
pub fn plugin(
  model : @smithy.Model,
  raw : Json,
) -> Map[String, String] raise @build.BuildError {
  @codegen.build_plugin(run)(model, raw)
}

///|
/// Prepare RPC settings once, then generate one peer.
pub fn run(
  plugin : @codegen.PluginContext,
) -> Map[String, String] raise @codegen.CodegenError {
  let config = settings(plugin)
  let description = try {
    match config.protocol {
      Some(id) =>
        @jsonrpc.Description::for_protocol(plugin.model, id, @rpc.V2_0)
      None =>
        @jsonrpc.Description::for_service(
          plugin.model,
          plugin.service.id,
          @rpc.V2_0,
        )
    }
  } catch {
    @rpc.InvalidContract(id, message) =>
      raise @codegen.InvalidSettings(
        "I could not prepare RPC construct `\{id}`: \{message}",
      )
    error =>
      raise @codegen.InvalidSettings(
        "I could not prepare the Go RPC contract: \{error}",
      )
  }
  generate(
    plugin.model,
    description,
    config.peer,
    module_name=config.module_name,
    package_name=config.package_name,
  )
}