///|
/// Errors returned before a wire message reaches domain logic.
pub(all) suberror WireError {
  TooLarge
  InvalidData
  UnsupportedVersion
} derive(Debug, Eq)

///|
pub extend WireError with @debug.Debug::{to_repr}

///|
pub extend WireError with Eq::{not_equal, equal}

///|
/// Receiver acknowledgement status; only Accepted or Duplicate permits a
/// sender to remove a pending event.
pub(all) enum WireAckStatus {
  Accepted
  Duplicate
  Rejected(String)
} derive(Eq, ToJson, FromJson)

///|
pub extend WireAckStatus with ToJson::{to_json}

///|
pub extend WireAckStatus with @json.FromJson::{from_json}

///|
pub extend WireAckStatus with Eq::{not_equal, equal}

///|
/// Version 1 request/response kinds. Strings are values, never shell text.
/// Semantic validation follows JSON decoding before dispatch.
pub(all) enum WireMessage {
  Handshake(Array[TransportCapability])
  Enroll(String, Array[NodeCapability], Timestamp)
  Heartbeat(String, NodeHealth, Timestamp)
  ConfigUpdate(String, ConfigSnapshot)
  Command(String, String, String, Timestamp)
  EventReplay(MessageEnvelope)
  Receipt(String, WireAckStatus)
  ErrorResponse(String, String)
} derive(ToJson, FromJson)

///|
pub extend WireMessage with ToJson::{to_json}

///|
pub extend WireMessage with @json.FromJson::{from_json}

///|
/// One versioned JSON frame. The transport supplies an external length prefix.
pub(all) struct WireFrame {
  version : ProtocolVersion
  message : WireMessage
} derive(ToJson, FromJson)

///|
pub extend WireFrame with ToJson::{to_json}

///|
pub extend WireFrame with @json.FromJson::{from_json}

///|
/// The maximum UTF-8 JSON payload accepted by the native transport.
pub fn wire_max_bytes() -> Int32 {
  65536
}

///|
/// Encodes a validated version 1 frame for length-delimited transport.
pub fn encode_wire(frame : WireFrame) -> String raise WireError {
  validate_wire(frame)
  let encoded = @json.to_json(frame).stringify()
  if @utf8.encode(encoded).length() > wire_max_bytes() {
    raise WireError::TooLarge
  }
  encoded
}

///|
/// Decodes one complete UTF-8 JSON frame and rejects invalid semantics.
pub fn decode_wire(content : String) -> WireFrame raise WireError {
  if @utf8.encode(content).length() > wire_max_bytes() {
    raise WireError::TooLarge
  }
  let json = @json.parse(content) catch { _ => raise WireError::InvalidData }
  let frame : WireFrame = @json.from_json(json) catch {
    _ => raise WireError::InvalidData
  }
  validate_wire(frame)
  frame
}

///|
fn validate_wire(frame : WireFrame) -> Unit raise WireError {
  if frame.version != ProtocolVersion::current() {
    raise WireError::UnsupportedVersion
  }
  match frame.message {
    WireMessage::Handshake(capabilities) =>
      if capabilities.length() == 0 {
        raise WireError::InvalidData
      }
    WireMessage::Enroll(id, _, at) =>
      if id == "" || at.unix_seconds < 0 {
        raise WireError::InvalidData
      }
    WireMessage::Heartbeat(id, _, at) =>
      if id == "" || at.unix_seconds < 0 {
        raise WireError::InvalidData
      }
    WireMessage::ConfigUpdate(id, snapshot) =>
      if id == "" ||
        snapshot.version.value <= 0 ||
        snapshot.content == "" ||
        snapshot.created_at.unix_seconds < 0 {
        raise WireError::InvalidData
      }
    WireMessage::Command(node_id, command_id, action, expires_at) =>
      if node_id == "" ||
        command_id == "" ||
        action == "" ||
        expires_at.unix_seconds < 0 {
        raise WireError::InvalidData
      }
    WireMessage::EventReplay(envelope) => {
      if envelope.node_id.value == "" || envelope.created_at.unix_seconds < 0 {
        raise WireError::InvalidData
      }
      let _ = MessageEnvelope::new(
        version=envelope.version,
        message_id=envelope.message_id,
        node_id=envelope.node_id,
        created_at=envelope.created_at,
        correlation_id=envelope.correlation_id,
        payload=envelope.payload,
      ) catch {
        _ => raise WireError::InvalidData
      }
    }
    WireMessage::Receipt(message_id, status) => {
      if message_id == "" {
        raise WireError::InvalidData
      }
      match status {
        WireAckStatus::Rejected(reason) if reason == "" =>
          raise WireError::InvalidData
        _ => ()
      }
    }
    WireMessage::ErrorResponse(code, reason) =>
      if code == "" || reason == "" {
        raise WireError::InvalidData
      }
  }
}