// Rich errors: a handler ends a call with a non-OK status through `RpcContext::fail`,
// and attaches structured detail with `add_error_detail`. The server sends the base
// `grpc-status` / `grpc-message` trailer and packs the code, message, and details into
// a `google.rpc.Status` in the `grpc-status-details-bin` trailer. The client surfaces
// the code and percent-decoded message; this decodes the packed status back too.
//
//     moon run src/examples/14-rich-errors

///|
fn drive(
  server : @moonrpc.H2Server,
  client : @moonrpc.H2Client,
  id : Int32,
  initial : Array[@http2.Frame],
) -> Unit raise {
  let to_server : Array[@http2.Frame] = []
  let to_client : Array[@http2.Frame] = []
  for f in client.preface() {
    to_server.push(f)
  }
  for f in server.preface() {
    to_client.push(f)
  }
  for f in initial {
    to_server.push(f)
  }
  let mut si = 0
  let mut ci = 0
  let mut steps = 0
  while !client.is_done(id) && steps < 100000 {
    steps = steps + 1
    if si < to_server.length() {
      let f = to_server[si]
      si = si + 1
      for r in server.feed(f) {
        to_client.push(r)
      }
    } else if ci < to_client.length() {
      let f = to_client[ci]
      ci = ci + 1
      for r in client.feed(f) {
        to_server.push(r)
      }
    } else {
      break
    }
  }
}

///|
/// Look for a header by name across both metadata blocks. A failing unary call sends a
/// trailers-only response, so its packed status rides the single header block the client
/// surfaces as initial metadata rather than as separate trailers.
fn find_meta(reply : @moonrpc.CallReply, name : Bytes) -> Bytes {
  for h in reply.headers {
    if h.name == name {
      return h.value
    }
  }
  for h in reply.trailers {
    if h.name == name {
      return h.value
    }
  }
  b""
}

///|
/// A `google.protobuf.Any`-style detail: a type URL (field 1) and an opaque value
/// (field 2). Any bytes work — the transport treats details as opaque.
fn make_detail(type_url : String, value : String) -> Bytes {
  let w = @moonrpc.PbWriter::new()
  w.string_(1, type_url)
  w.string_(2, value)
  w.to_bytes()
}

///|
/// Decode the `google.rpc.Status` in a `grpc-status-details-bin` trailer: `code = 1`,
/// `message = 2`, repeated `details = 3`.
fn decode_status_details(body : Bytes) -> (Int32, String, Int32) raise {
  let r = @moonrpc.PbReader::new(body)
  let mut code = 0
  let mut message = ""
  let mut details = 0
  while !r.eof() {
    let (field, wire) = r.read_tag()
    match field {
      1 => code = r.read_int32()
      2 => message = r.read_string()
      3 => {
        let _ = r.read_bytes()
        details = details + 1
      }
      _ => r.skip(wire)
    }
  }
  (code, message, details)
}

///|
fn run() -> Unit raise {
  // `grpc-message` rides the wire percent-encoded; the client decodes it. The codec on
  // its own:
  let decoded = @moonrpc.percent_decode(b"user%20not%20found%3A%2042")
  println("percent_decode: \{decoded}")
  let server = @moonrpc.H2Server::new()
  server.register_unary("/users.Users/Get", (ctx, _req) => {
    ctx.add_error_detail(
      make_detail("type.googleapis.com/google.rpc.ErrorInfo", "reason=ABSENT"),
    )
    ctx.add_error_detail(
      make_detail("type.googleapis.com/google.rpc.ResourceInfo", "users/42"),
    )
    ctx.fail(@moonrpc.Status::code(NotFound), "user 42 does not exist") // discarded once the call fails
    b""
  })
  let client = @moonrpc.H2Client::new()
  let (id, initial) = client.unary("/users.Users/Get", b"id=42")
  drive(server, client, id, initial)
  let reply = client.reply(id)
  let not_found : @moonrpc.Status = NotFound
  println(
    "grpc-status \{reply.grpc_status} (\{not_found.name()}), \{reply.messages.length()} messages",
  )
  println("grpc-message: \{reply.grpc_message}")
  let packed = find_meta(reply, b"grpc-status-details-bin")
  let (code, message, details) = decode_status_details(packed)
  println(
    "google.rpc.Status: code=\{code}, message=\{message}, \{details} detail(s) attached",
  )
}

///|
#coverage.skip
fn main {
  run() catch {
    e => println("error: \{e}")
  }
}