///|
/// The resolve half of go-zero's discovery (← `discov.Discovery`): a function from a
/// service name to its live endpoints. Any store — the in-memory `InMemoryRegistry`,
/// the persisted `PersistentRegistry`, or a future etcd/consul client — exposes one
/// via `resolver()`, so a balancer and the load-balanced channel are written once
/// against the interface and the backing store swaps by swapping the closure.
pub type Resolve = (String) -> Array[Endpoint]

///|
/// This registry as a `Resolve` interface value.
pub fn InMemoryRegistry::resolver(self : InMemoryRegistry) -> Resolve {
  service => self.resolve(service)
}

///|
/// This registry as a `Resolve` interface value.
pub fn PersistentRegistry::resolver(self : PersistentRegistry) -> Resolve {
  service => self.resolve(service)
}

///|
/// A change to the registry keyspace, in etcd v3's watch shape: a `Put` carries the
/// instance key and its endpoint, a `Delete` carries the key that went away, and
/// both carry the store revision the change produced. A watcher receives these in
/// revision order, so a client can rebuild the live set incrementally instead of
/// re-resolving the whole service.
pub(all) enum RegistryEvent {
  Put(key~ : String, endpoint~ : Endpoint, revision~ : Int64)
  Delete(key~ : String, revision~ : Int64)
} derive(Eq, Debug)

///|
/// The store revision a registry event was produced at.
pub fn RegistryEvent::revision(self : RegistryEvent) -> Int64 {
  match self {
    Put(revision~, ..) => revision
    Delete(revision~, ..) => revision
  }
}

///|
/// A persisted, watchable service registry (← go-zero's etcd `discov` publisher):
/// the same two-level `service -> instance-id -> endpoint` store as
/// `InMemoryRegistry`, plus a per-key mod-revision, an append-only event log for
/// catch-up watchers, live watcher callbacks fired on every mutation, and
/// `snapshot`/`restore` that round-trip the whole keyspace through an etcd v3
/// `RangeResponse`-shaped JSON document — the bytes a file- or etcd-backed
/// deployment persists and reloads without losing a revision.
pub struct PersistentRegistry {
  instances : Map[String, Map[String, Endpoint]]
  key_rev : Map[String, Int64]
  mut seq : Int
  mut revision : Int64
  events : Array[RegistryEvent]
  watchers : Array[(RegistryEvent) -> Unit]
}

///|
/// A fresh, empty persisted registry at revision `0`.
pub fn PersistentRegistry::new() -> PersistentRegistry {
  {
    instances: Map([]),
    key_rev: Map([]),
    seq: 0,
    revision: 0,
    events: [],
    watchers: [],
  }
}

///|
/// The store revision, bumped on each register/deregister.
pub fn PersistentRegistry::revision(self : PersistentRegistry) -> Int64 {
  self.revision
}

///|
/// Notify live watchers and append to the catch-up log.
fn PersistentRegistry::emit(
  self : PersistentRegistry,
  event : RegistryEvent,
) -> Unit {
  self.events.push(event)
  for w in self.watchers {
    w(event)
  }
}

///|
/// Register `endpoint` under `service`, mint a fresh instance key, bump the
/// revision, and emit a `Put`. Returns the instance key (`/`).
pub fn PersistentRegistry::register(
  self : PersistentRegistry,
  service : String,
  endpoint : Endpoint,
) -> String {
  let bucket = match self.instances.get(service) {
    Some(b) => b
    None => {
      let b : Map[String, Endpoint] = Map([])
      self.instances[service] = b
      b
    }
  }
  self.seq = self.seq + 1
  let key = service + "/" + self.seq.to_string()
  bucket[key] = endpoint
  self.revision = self.revision + 1L
  self.key_rev[key] = self.revision
  self.emit(Put(key~, endpoint~, revision=self.revision))
  key
}

///|
/// Remove the instance at `key` from `service`. On success bumps the revision and
/// emits a `Delete`; an unknown service or key is a no-op returning `false`.
pub fn PersistentRegistry::deregister(
  self : PersistentRegistry,
  service : String,
  key : String,
) -> Bool {
  match self.instances.get(service) {
    Some(bucket) =>
      if bucket.contains(key) {
        bucket.remove(key)
        self.key_rev.remove(key)
        self.revision = self.revision + 1L
        self.emit(Delete(key~, revision=self.revision))
        true
      } else {
        false
      }
    None => false
  }
}

///|
/// The endpoints registered for `service`, in registration order.
pub fn PersistentRegistry::resolve(
  self : PersistentRegistry,
  service : String,
) -> Array[Endpoint] {
  match self.instances.get(service) {
    Some(bucket) => bucket.values().collect()
    None => []
  }
}

///|
/// Every service name with at least one live instance.
pub fn PersistentRegistry::services(self : PersistentRegistry) -> Array[String] {
  self.instances.keys().collect()
}

///|
/// Every registered instance as `(instance-key, endpoint, mod-revision)`, across
/// all services — the flat keyspace a file- or etcd-backed reader diffs one load
/// against the next to compute the `Put`/`Delete` events a change produced.
pub fn PersistentRegistry::keyed_entries(
  self : PersistentRegistry,
) -> Array[(String, Endpoint, Int64)] {
  let out : Array[(String, Endpoint, Int64)] = []
  for _service, bucket in self.instances {
    for key, ep in bucket {
      let rev = match self.key_rev.get(key) {
        Some(r) => r
        None => 0L
      }
      out.push((key, ep, rev))
    }
  }
  out
}

///|
/// Register a live watcher fired on every subsequent mutation, in revision order
/// (← etcd's `Watch` with no start revision). To also see changes already applied,
/// replay `events_since` first.
pub fn PersistentRegistry::watch(
  self : PersistentRegistry,
  on_event : (RegistryEvent) -> Unit,
) -> Unit {
  self.watchers.push(on_event)
}

///|
/// Every event with a revision greater than `revision` (← etcd's watch
/// `start_revision`): the catch-up a client replays to reach the current state
/// before switching to live `watch` callbacks.
pub fn PersistentRegistry::events_since(
  self : PersistentRegistry,
  revision : Int64,
) -> Array[RegistryEvent] {
  let out : Array[RegistryEvent] = []
  for e in self.events {
    if e.revision() > revision {
      out.push(e)
    }
  }
  out
}

///|
/// Serialize the whole keyspace as an etcd v3 `RangeResponse`-shaped JSON document:
/// a header carrying the store revision and the id counter, and one key/value entry
/// per instance carrying its endpoint and mod-revision. This is the exact payload a
/// file- or etcd-backed deployment persists; `restore` rebuilds an identical
/// registry from it, revisions intact.
pub fn PersistentRegistry::snapshot(self : PersistentRegistry) -> String {
  let kvs : Array[Json] = []
  for service, bucket in self.instances {
    for key, ep in bucket {
      let rev = match self.key_rev.get(key) {
        Some(r) => r
        None => 0L
      }
      let entry : Map[String, Json] = Map([
        ("key", key.to_json()),
        ("service", service.to_json()),
        ("host", ep.host.to_json()),
        ("port", ep.port.to_json()),
        ("weight", ep.weight.to_json()),
        ("mod_revision", rev.to_string().to_json()),
      ])
      kvs.push(entry.to_json())
    }
  }
  let header : Map[String, Json] = Map([
    ("revision", self.revision.to_string().to_json()),
    ("seq", self.seq.to_json()),
  ])
  let root : Map[String, Json] = Map([
    ("header", header.to_json()),
    ("kvs", kvs.to_json()),
  ])
  root.to_json().stringify(indent=2)
}

///|
/// Rebuild a registry from a `snapshot` document, preserving instance keys, their
/// endpoints and mod-revisions, the id counter, and the store revision — so a
/// reloaded registry mints the next key exactly where the persisted one left off
/// and a watcher's `events_since(old_revision)` still lines up.
pub fn PersistentRegistry::restore(
  src : String,
) -> PersistentRegistry raise ConfigError {
  let root = @json.parse(src) catch {
    err => raise ConfigError("invalid registry snapshot: " + err.to_string())
  }
  let obj = match root {
    Object(m) => m
    _ => raise ConfigError("registry snapshot root must be a JSON object")
  }
  let reg = PersistentRegistry::new()
  match obj.get("header") {
    Some(Object(h)) => {
      reg.revision = int64_field(h, "revision")
      reg.seq = int_field_json(h, "seq")
    }
    _ => ()
  }
  match obj.get("kvs") {
    Some(Array(kvs)) =>
      for kv in kvs {
        match kv {
          Object(e) => {
            let service = string_field_json(e, "service")
            let key = string_field_json(e, "key")
            let ep = Endpoint::new(
              string_field_json(e, "host"),
              int_field_json(e, "port"),
              weight=int_field_json(e, "weight"),
            )
            let bucket = match reg.instances.get(service) {
              Some(b) => b
              None => {
                let b : Map[String, Endpoint] = Map([])
                reg.instances[service] = b
                b
              }
            }
            bucket[key] = ep
            reg.key_rev[key] = int64_field(e, "mod_revision")
          }
          _ => ()
        }
      }
    _ => ()
  }
  reg
}

// -- snapshot field accessors ----------------------------------------------

///|
/// The string value of `field` in `obj`, or `""` if absent or not a string.
fn string_field_json(obj : Map[String, Json], field : String) -> String {
  match obj.get(field) {
    Some(String(s)) => s
    _ => ""
  }
}

///|
/// The integer value of a JSON `Number` `field`, truncated toward zero, or `0`.
fn int_field_json(obj : Map[String, Json], field : String) -> Int {
  match obj.get(field) {
    Some(Number(n, ..)) => n.to_int()
    _ => 0
  }
}

///|
/// The `Int64` value of a decimal-string `field` (revisions are carried as strings
/// so no precision is lost), or `0`.
fn int64_field(obj : Map[String, Json], field : String) -> Int64 {
  match obj.get(field) {
    Some(String(s)) => int64_of_decimal(s)
    _ => 0L
  }
}

///|
/// Parse an unsigned decimal string as an `Int64`. Non-digit octets are skipped, so
/// a well-formed revision string parses exactly.
fn int64_of_decimal(s : String) -> Int64 {
  let mut n = 0L
  for i = 0; i < s.length(); i = i + 1 {
    let d = s[i].to_int() - 0x30
    if d >= 0 && d <= 9 {
      n = n * 10L + d.to_int64()
    }
  }
  n
}

// -- load-balanced client ---------------------------------------------------

///|
/// The choice of balancer for a load-balanced channel: round-robin cycles through
/// the resolved instances (spreading load), `PickFirst` pins the head instance
/// (← gRPC's `pick_first`).
pub(all) enum Balancer {
  RoundRobinBalancer(RoundRobin)
  PickFirst
}

///|
/// A round-robin balancer, cursor at the first instance.
pub fn Balancer::round_robin() -> Balancer {
  RoundRobinBalancer(RoundRobin::new())
}

///|
/// Pick one endpoint from a resolved set, or `None` if it is empty.
pub fn Balancer::pick(
  self : Balancer,
  endpoints : Array[Endpoint],
) -> Endpoint? {
  match self {
    RoundRobinBalancer(rr) => rr.pick(endpoints)
    PickFirst => pick_first(endpoints)
  }
}

///|
/// The in-process dial table: an endpoint address maps to the `RpcServer` listening
/// there. It stands in for DNS resolution plus a socket dial in the in-process h2c
/// transport — a real deployment opens a connection to the address instead of
/// looking the server up here, but the resolve→balance→call path above it is the
/// same.
pub struct RpcCluster {
  servers : Map[String, RpcServer]
}

///|
/// An empty cluster.
pub fn RpcCluster::new() -> RpcCluster {
  { servers: Map([]) }
}

///|
/// Bind the server reachable at `endpoint`'s address.
pub fn RpcCluster::add(
  self : RpcCluster,
  endpoint : Endpoint,
  server : RpcServer,
) -> Unit {
  self.servers[endpoint.address()] = server
}

///|
/// Open a channel to the server bound at `endpoint`, or `None` if nothing is
/// reachable there (a stale registry entry pointing at a gone instance).
pub fn RpcCluster::dial(
  self : RpcCluster,
  endpoint : Endpoint,
) -> RpcChannel? raise {
  match self.servers.get(endpoint.address()) {
    Some(server) =>
      Some(RpcChannel::connect(server, authority=endpoint.address()))
    None => None
  }
}

///|
/// A load-balanced zRPC client (← go-zero's `zrpc.Client` over a discovery target):
/// it resolves a service through the `Resolver`, picks a live instance with the
/// `Balancer`, dials it on the `RpcCluster`, and makes the call. The whole
/// resolve→balance→dial→call path runs per call, so instances registering or
/// deregistering between calls take effect on the next one.
pub struct LoadBalancedChannel {
  resolve : Resolve
  cluster : RpcCluster
  balancer : Balancer
  service : String
}

///|
/// Build a load-balanced channel for `service` over a `Resolve` interface, dialing
/// through `cluster` with `balancer` (round-robin by default).
pub fn LoadBalancedChannel::new(
  resolve : Resolve,
  cluster : RpcCluster,
  service : String,
  balancer? : Balancer = Balancer::round_robin(),
) -> LoadBalancedChannel {
  { resolve, cluster, balancer, service }
}

///|
/// Resolve the service, pick a live instance, and dial it — the shared prelude of
/// every load-balanced call. `Unavailable` when the service has no instances or the
/// picked one is unreachable, matching the `grpc-status` a real client surfaces when
/// no subchannel is ready.
fn LoadBalancedChannel::pick_channel(
  self : LoadBalancedChannel,
) -> Result[RpcChannel, @moonrpc.Status] raise {
  let endpoints = (self.resolve)(self.service)
  match self.balancer.pick(endpoints) {
    None => Err(@moonrpc.Status::Unavailable)
    Some(endpoint) =>
      match self.cluster.dial(endpoint) {
        Some(channel) => Ok(channel)
        None => Err(@moonrpc.Status::Unavailable)
      }
  }
}

///|
/// Make a unary call to `path`, resolving and balancing to a live instance first.
pub fn LoadBalancedChannel::call(
  self : LoadBalancedChannel,
  path : String,
  request : Bytes,
) -> Result[Bytes, @moonrpc.Status] raise {
  match self.pick_channel() {
    Ok(channel) => channel.call(path, request)
    Err(status) => Err(status)
  }
}

///|
/// Make a server-streaming call to `path`, resolving and balancing to a live
/// instance first.
pub fn LoadBalancedChannel::call_server_streaming(
  self : LoadBalancedChannel,
  path : String,
  request : Bytes,
) -> Result[Array[Bytes], @moonrpc.Status] raise {
  match self.pick_channel() {
    Ok(channel) => channel.call_server_streaming(path, request)
    Err(status) => Err(status)
  }
}

///|
/// Make a bidirectional-streaming call to `path`, resolving and balancing to a
/// live instance first, then driving the whole `requests` exchange to completion.
pub fn LoadBalancedChannel::call_bidi_streaming(
  self : LoadBalancedChannel,
  path : String,
  requests : Array[Bytes],
) -> Result[Array[Bytes], @moonrpc.Status] raise {
  match self.pick_channel() {
    Ok(channel) => channel.call_bidi_streaming(path, requests)
    Err(status) => Err(status)
  }
}