///|
/// Wire protocol version this build speaks. Bumped on every breaking
/// change to Register/Connect or the sys-channel handshake. Both
/// fields default to 0 so peers built before versioning was introduced
/// decode as version 0 — fully backwards compatible.
pub const ProtoVersion : Int = 0

///|
/// Lowest `proto_version` the relay accepts on Register/Connect. Bump
/// to match `ProtoVersion` when dropping support for a prior version.
pub const ProtoMinVersion : Int = 0

///|
pub(all) enum ErrorCode {
  TaskNotFound
  TaskOffline
  InternalError
  /// Relay refused the Connect because the client's `proto_version` is
  /// below `proto_min_version`. `min_version` tells the client which
  /// version to upgrade to before retrying.
  ProtoVersionTooOld(min_version~ : Int)
} derive(Debug, Eq)

///|
pub fn ErrorCode::to_wire(self : ErrorCode) -> String {
  match self {
    TaskNotFound => "server_not_found"
    TaskOffline => "server_offline"
    InternalError => "internal_error"
    ProtoVersionTooOld(_) => "proto_version_too_old"
  }
}

///|
pub(all) enum RegisterRejectReason {
  UuidAlreadyClaimedThisSession
  InvalidUuid
  /// Relay refused the Register because the server's `proto_version`
  /// is below `proto_min_version`. `min_version` tells the server
  /// operator which version to upgrade to before retrying.
  ProtoVersionTooOld(min_version~ : Int)
} derive(Eq, Debug)

///|
pub fn RegisterRejectReason::to_wire(self : RegisterRejectReason) -> String {
  match self {
    UuidAlreadyClaimedThisSession => "uuid_already_claimed_this_session"
    InvalidUuid => "invalid_uuid"
    ProtoVersionTooOld(_) => "proto_version_too_old"
  }
}

///|
pub(all) enum SysMessage {
  Register(
    task_uuid~ : @uuid.UUID,
    hostname~ : String,
    cwd~ : String,
    proto_version~ : Int
  )
  Registered(task_uuid~ : @uuid.UUID)
  RegisterRejected(reason~ : RegisterRejectReason)
  TaskExited(exit_code~ : Int)
  Connect(task_uuid~ : @uuid.UUID, last_seen_id~ : Int64?, proto_version~ : Int)
  Connected
  TaskOnline
  NewClient(client_id~ : String, last_seen_id~ : Int64?)
  ClientDisconnected(client_id~ : String)
  Error(ErrorCode)
  AuthError(message~ : String)
}

///|
/// Manual `Debug` impl because `@uuid.UUID` does not implement `Debug` and
/// the orphan rule prevents us from adding it from this package. We render
/// UUID fields as their canonical hex-string form via `Repr::string`, which
/// matches what `derive(Debug)` would produce if `UUID : Debug` were bridged
/// through its `Show` impl.
pub impl @debug.Debug for SysMessage with to_repr(self) {
  match self {
    Register(task_uuid~, hostname~, cwd~, proto_version~) =>
      @debug.Repr::ctor("Register", [
        (Some("task_uuid"), @debug.Repr::string(task_uuid.to_string())),
        (Some("hostname"), @debug.to_repr(hostname)),
        (Some("cwd"), @debug.to_repr(cwd)),
        (Some("proto_version"), @debug.to_repr(proto_version)),
      ])
    Registered(task_uuid~) =>
      @debug.Repr::ctor("Registered", [
        (Some("task_uuid"), @debug.Repr::string(task_uuid.to_string())),
      ])
    RegisterRejected(reason~) =>
      @debug.Repr::ctor("RegisterRejected", [
        (Some("reason"), @debug.to_repr(reason)),
      ])
    TaskExited(exit_code~) =>
      @debug.Repr::ctor("TaskExited", [
        (Some("exit_code"), @debug.to_repr(exit_code)),
      ])
    Connect(task_uuid~, last_seen_id~, proto_version~) =>
      @debug.Repr::ctor("Connect", [
        (Some("task_uuid"), @debug.Repr::string(task_uuid.to_string())),
        (Some("last_seen_id"), @debug.to_repr(last_seen_id)),
        (Some("proto_version"), @debug.to_repr(proto_version)),
      ])
    Connected => @debug.Repr::ctor("Connected", [])
    TaskOnline => @debug.Repr::ctor("TaskOnline", [])
    NewClient(client_id~, last_seen_id~) =>
      @debug.Repr::ctor("NewClient", [
        (Some("client_id"), @debug.to_repr(client_id)),
        (Some("last_seen_id"), @debug.to_repr(last_seen_id)),
      ])
    ClientDisconnected(client_id~) =>
      @debug.Repr::ctor("ClientDisconnected", [
        (Some("client_id"), @debug.to_repr(client_id)),
      ])
    Error(code) => @debug.Repr::ctor("Error", [(None, @debug.to_repr(code))])
    AuthError(message~) =>
      @debug.Repr::ctor("AuthError", [
        (Some("message"), @debug.to_repr(message)),
      ])
  }
}

///|
pub(all) enum TermMessage {
  // `id` is monotonic per task, dense from 1, assigned by the wrapper. A
  // client records the highest id it has written to its log; on reconnect
  // it sends that id as Connect.last_seen_id and the wrapper replays the
  // tail as ordinary Data frames — there is no separate scrollback variant.
  // `created` is ms since epoch, stamped by the wrapper on PTY output.
  // `None` means the sender did not stamp this frame (older peer, or a
  // client→server keystroke that is not the stamped direction).
  // `replay` explicitly marks frames emitted from the retained replay log.
  // `None` means legacy peer; clients should fall back to `created` age.
  Data(
    client_id~ : String,
    id~ : Int64,
    created~ : Int64?,
    replay~ : Bool?,
    data~ : String
  )
  // `id` is the next id after the last Data emitted, so a client can tell
  // whether its log is caught up at stream end.
  Exited(client_id~ : String, id~ : Int64)
} derive(Debug)

///|
pub(all) enum Message {
  Sys(SysMessage)
  Fs(FsMessage)
  Term(TermMessage)
  Codex(client_id~ : String, data~ : String)
} derive(Debug)

///|
pub(all) struct FileStat {
  file_type : Int
  size : Int64
  mtime : Int64
  ctime : Int64
} derive(Debug)

///|
pub(all) enum FsMessage {
  Stat(req_id~ : String, client_id~ : String, path~ : String)
  StatResult(req_id~ : String, client_id~ : String, stat~ : FileStat)
  Readdir(req_id~ : String, client_id~ : String, path~ : String)
  ReaddirResult(
    req_id~ : String,
    client_id~ : String,
    entries~ : Array[(String, Int)]
  )
  ReadFile(req_id~ : String, client_id~ : String, path~ : String)
  ReadFileResult(req_id~ : String, client_id~ : String, data~ : String)
  WriteFile(
    req_id~ : String,
    client_id~ : String,
    path~ : String,
    data~ : String,
    create~ : Bool,
    overwrite~ : Bool
  )
  Mkdir(req_id~ : String, client_id~ : String, path~ : String)
  FsDelete(
    req_id~ : String,
    client_id~ : String,
    path~ : String,
    recursive~ : Bool
  )
  FsRename(
    req_id~ : String,
    client_id~ : String,
    from~ : String,
    to~ : String,
    overwrite~ : Bool
  )
  FsOk(req_id~ : String, client_id~ : String)
  FsError(
    req_id~ : String,
    client_id~ : String,
    code~ : String,
    message~ : String
  )
} derive(Debug)