///|
fn string_list(values : Array[String], sort~ : Bool = false) -> String {
  let items = values.copy()
  if sort {
    items.sort_by((a, b) => a.lexical_compare(b))
  }
  "[]string{" + items.map(quote).join(", ") + "}"
}

///|
fn write_reference(
  ctx : GoContext,
  w : @codegen.CodeWriter,
  rule : @rpc.CallReferenceContract?,
) -> Unit raise @codegen.CodegenError {
  match rule {
    None => w.write_inline("nil")
    Some(rule) => {
      w.write_inline("&$T{WireName: $S", args=[
        ctx.runtime("CallReference"),
        Text(rule.wire_name),
      ])
      if rule.position is Some(position) {
        w.write_inline(", Position: pointer(int($L))", args=[Int(position)])
      }
      if rule.code is Some(code) {
        w.write_inline(", Code: pointer(int64($L))", args=[
          Text(code_text(code, rule.wire_name)),
        ])
      }
      w.write_inline("}")
    }
  }
}

///|
fn result_shape(method_ : @rpc.MethodContract) -> @smithy.ShapeId? {
  match method_.result {
    NullResult => None
    OutputStructure(id) => Some(id)
    OutputMember(id) => Some(id)
  }
}

///|
fn write_validator(
  ctx : GoContext,
  w : @codegen.CodeWriter,
  id : @smithy.ShapeId,
) -> Unit raise @codegen.CodegenError {
  w.write_inline("func(data $T) error { _, err := ", args=[
    ctx.foreign("RawMessage", "encoding/json"),
  ])
  decode_expression(ctx, w, id, "data")
  w.write_inline("; return err }")
}

///|
fn generate_contract(ctx : GoContext) -> String raise @codegen.CodegenError {
  let w = ctx.writer()
  w.write("func pointer[T any](value T) *T { return &value }\n")
  w.write(
    "// Contract returns a fresh prepared runtime contract.\nfunc Contract() $T {\n\treturn $T{\n\t\tPeers: []$T{",
    args=[
      ctx.runtime("Contract"),
      ctx.runtime("Contract"),
      ctx.runtime("PeerContract"),
    ],
  )
  let peers = ctx.description.peers.copy()
  peers.sort_by((a, b) => a.name.to_string().lexical_compare(b.name.to_string()))
  for peer in peers {
    w.write("\t\t\t{Name: $S, Methods: []$T{", args=[
      Text(peer.name.to_string()),
      ctx.runtime("MethodContract"),
    ])
    if peer.accepts is Some(service) {
      for method_ in service.methods {
        let kind = match method_.kind {
          RequestOnly => "request"
          NotificationOnly => "notification"
          RequestOrNotification => "either"
        }
        let mode = match method_.mode {
          Named => "named"
          Positional => "positional"
          Either => "either"
        }
        w.write(
          "\t\t\t\t{Name: $S, Kind: $S, Params: $S, Positional: $L, NullResult: $L,",
          args=[
            Text(method_.name.to_string()),
            Text(kind),
            Text(mode),
            Text(string_list(method_.positional.map(id => ctx.wire_name(id)))),
            Bool(method_.result == @rpc.NullResult),
          ],
        )
        w.write_inline("\t\t\t\t\tRefersToCall: ")
        write_reference(ctx, w, method_.refers_to_call)
        w.write(",")
        w.write_inline("\t\t\t\t\tValidateParams: ")
        write_validator(ctx, w, method_.input)
        w.write(",")
        if result_shape(method_) is Some(id) {
          w.write_inline("\t\t\t\t\tValidateResult: ")
          write_validator(ctx, w, id)
          w.write(",")
        }
        w.write("\t\t\t\t\tErrors: []$T{", args=[ctx.runtime("ErrorContract")])
        for error in method_.errors {
          w.write_inline(
            "\t\t\t\t\t\t{Code: $L, DataRequired: $L, DataAbsent: $L",
            args=[
              Text(code_text(error.code, error.shape.to_string())),
              Bool(error.data_required),
              Bool(error.data == None),
            ],
          )
          if error.data is Some(data) {
            w.write_inline(", ValidateData: ")
            write_validator(ctx, w, data)
          }
          w.write("},")
        }
        w.write("\t\t\t\t\t},\n\t\t\t\t},")
      }
    }
    w.write("\t\t\t}},")
  }
  w.write("\t\t},")
  if ctx.description.session is Some(session) {
    w.write("\t\tSession: &$T{\n\t\t\tStates: $L,\n\t\t\tCapabilities: []$T{", args=[
      ctx.runtime("SessionContract"),
      Text(string_list(session.states.map(s => s.to_string()))),
      ctx.runtime("CapabilityContract"),
    ])
    for capability in session.capabilities {
      w.write("\t\t\t\t{Name: $S, Method: $S, Pointer: $S},", args=[
        Text(capability.name),
        Text(capability.method_name.to_string()),
        Text(capability.pointer.to_string()),
      ])
    }
    w.write("\t\t\t},\n\t\t\tMethods: []$T{", args=[ctx.runtime("MethodRule")])
    for rule in session.methods {
      w.write_inline("\t\t\t\t{Receiver: $S, Method: $S", args=[
        Text(rule.receiver.to_string()),
        Text(rule.method_name.to_string()),
      ])
      if rule.allowed is Some(states) {
        w.write_inline(", Allowed: $L", args=[
          Text(string_list(states.map(s => s.to_string()), sort=true)),
        ])
      }
      for
        (field, state) in [
          ("OnCall", rule.on_call),
          ("OnSuccess", rule.on_success),
          ("OnError", rule.on_error),
        ] {
        if state is Some(state) {
          w.write_inline(", $L: $S", args=[Text(field), Text(state.to_string())])
        }
      }
      w.write_inline(", Requires: $L, Cancels: ", args=[
        Text(string_list(rule.requires, sort=true)),
      ])
      write_reference(ctx, w, rule.cancels)
      w.write("},")
    }
    w.write("\t\t\t},\n\t\t\tRefusals: []$T{", args=[
      ctx.runtime("StateRefusal"),
    ])
    for refusal in session.refusals {
      w.write("\t\t\t\t{State: $S, Code: $L},", args=[
        Text(refusal.state.to_string()),
        Text(code_text(refusal.code, refusal.state.to_string())),
      ])
    }
    w.write("\t\t\t},")
    if session.capability_refusal is Some(code) {
      w.write("\t\t\tCapabilityRefusal: pointer(int64($L)),", args=[
        Text(code_text(code, "capability refusal")),
      ])
    }
    w.write("\t\t},")
  }
  w.write("\t}\n}\n")
  ctx.render(w)
}