///|
/// Explicit codecs make distributed data and timestamp serialization independent
/// of the execution backend. Decode must reject malformed values.
pub struct WireCodec[T] {
  priv encode : (T) -> Json
  priv decode : (Json) -> T?
}

///|
pub fn[T] WireCodec::WireCodec(
  encode~ : (T) -> Json,
  decode~ : (Json) -> T?,
) -> WireCodec[T] {
  { encode, decode, }
}

///|
/// Core JSON encoding preserves 64-bit integers as decimal strings.
pub fn[T : ToJson + @json.FromJson] WireCodec::json() -> WireCodec[T] {
  {
    encode: fn(value) { Json(value) },
    decode: fn(value) { Some(@json.from_json(value)) catch { _ => None } },
  }
}

///|
pub fn[A, B] WireCodec::product(
  a : WireCodec[A],
  b : WireCodec[B],
) -> WireCodec[Product[A, B]] {
  WireCodec(
    encode=fn(value) { [(a.encode)(value.outer), (b.encode)(value.inner)] },
    decode=fn(value) {
      guard value is [x, y] &&
        (a.decode)(x) is Some(x) &&
        (b.decode)(y) is Some(y) else {
        return None
      }
      Some(Product(x, y))
    },
  )
}

///|
pub suberror ClusterError {
  ClusterError(String)
} derive(Debug)

///|
fn[T : @json.FromJson] wire_read(value : Json) -> T raise ClusterError {
  @json.from_json(value) catch {
    _ => raise ClusterError("invalid protocol value")
  }
}

///|
pub extend ClusterError with Debug::{to_repr}

///|
pub fn[T] WireCodec::to_json(self : WireCodec[T], value : T) -> Json {
  (self.encode)(value)
}

///|
pub fn[T] WireCodec::from_json(self : WireCodec[T], value : Json) -> T? {
  (self.decode)(value)
}