///|
/// Wire protocol version this build speaks. Bumped when peers need to
/// reason about a new protocol capability or when a breaking
/// Register/Connect/sys-channel handshake change lands. Optional
/// version fields default to 0 so peers built before versioning was
/// introduced decode as version 0 — fully backwards compatible.
pub const ProtoVersion : Int = 4

///|
/// 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,
    server_capabilities~ : CapabilitySet
  )
  Registered(task_uuid~ : @uuid.UUID)
  RegisterRejected(reason~ : RegisterRejectReason)
  TaskExited(exit_code~ : Int)
  /// Server -> relay: the task's current terminal title (OSC 0/2), sent on
  /// registration and whenever it changes. `None` means the child has no
  /// title set. The relay persists it for task listings; it is not
  /// forwarded to attached clients, which receive the title inside render
  /// frames instead.
  Title(title~ : String?)
  Connect(
    task_uuid~ : @uuid.UUID,
    last_seen_id~ : Int64?,
    proto_version~ : Int,
    client_capabilities~ : CapabilitySet
  )
  Connected(server_proto_version~ : Int, server_capabilities~ : CapabilitySet)
  TaskOnline(server_proto_version~ : Int, server_capabilities~ : CapabilitySet)
  NewClient(
    client_id~ : String,
    last_seen_id~ : Int64?,
    client_proto_version~ : Int,
    client_capabilities~ : CapabilitySet
  )
  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 fn to_repr(self) {
  match self {
    Register(task_uuid~, hostname~, cwd~, proto_version~, server_capabilities~) => {
      let fields = [
        (Some("task_uuid"), @debug.Repr::string(task_uuid.to_string())),
        (Some("hostname"), @debug.Repr(hostname)),
        (Some("cwd"), @debug.Repr(cwd)),
        (Some("proto_version"), @debug.Repr(proto_version)),
      ]
      if server_capabilities.length() != 0 {
        fields.push(
          (Some("server_capabilities"), @debug.Repr(server_capabilities)),
        )
      }
      @debug.Repr::ctor("Register", fields)
    }
    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.Repr(reason)),
      ])
    TaskExited(exit_code~) =>
      @debug.Repr::ctor("TaskExited", [
        (Some("exit_code"), @debug.Repr(exit_code)),
      ])
    Title(title~) =>
      @debug.Repr::ctor("Title", [(Some("title"), @debug.Repr(title))])
    Connect(task_uuid~, last_seen_id~, proto_version~, client_capabilities~) => {
      let fields = [
        (Some("task_uuid"), @debug.Repr::string(task_uuid.to_string())),
        (Some("last_seen_id"), @debug.Repr(last_seen_id)),
        (Some("proto_version"), @debug.Repr(proto_version)),
      ]
      if client_capabilities.length() != 0 {
        fields.push(
          (Some("client_capabilities"), @debug.Repr(client_capabilities)),
        )
      }
      @debug.Repr::ctor("Connect", fields)
    }
    Connected(server_proto_version~, server_capabilities~) => {
      let fields = [
        (Some("server_proto_version"), @debug.Repr(server_proto_version)),
      ]
      if server_capabilities.length() != 0 {
        fields.push(
          (Some("server_capabilities"), @debug.Repr(server_capabilities)),
        )
      }
      @debug.Repr::ctor("Connected", fields)
    }
    TaskOnline(server_proto_version~, server_capabilities~) => {
      let fields = [
        (Some("server_proto_version"), @debug.Repr(server_proto_version)),
      ]
      if server_capabilities.length() != 0 {
        fields.push(
          (Some("server_capabilities"), @debug.Repr(server_capabilities)),
        )
      }
      @debug.Repr::ctor("TaskOnline", fields)
    }
    NewClient(
      client_id~,
      last_seen_id~,
      client_proto_version~,
      client_capabilities~
    ) => {
      let fields = [
        (Some("client_id"), @debug.Repr(client_id)),
        (Some("last_seen_id"), @debug.Repr(last_seen_id)),
      ]
      if client_proto_version != 0 {
        fields.push(
          (Some("client_proto_version"), @debug.Repr(client_proto_version)),
        )
      }
      if client_capabilities.length() != 0 {
        fields.push(
          (Some("client_capabilities"), @debug.Repr(client_capabilities)),
        )
      }
      @debug.Repr::ctor("NewClient", fields)
    }
    ClientDisconnected(client_id~) =>
      @debug.Repr::ctor("ClientDisconnected", [
        (Some("client_id"), @debug.Repr(client_id)),
      ])
    Error(code) => @debug.Repr::ctor("Error", [(None, @debug.Repr(code))])
    AuthError(message~) =>
      @debug.Repr::ctor("AuthError", [(Some("message"), @debug.Repr(message))])
  }
}

///|
pub(all) enum TermMessage {
  // Optional client -> server replay cursor. Relay treats this as opaque
  // user data and only injects `client_id`; server wrappers that
  // understand it can update the per-client replay cursor without relay
  // owning terminal semantics.
  ClientReplayCursor(client_id~ : String, last_seen_id~ : Int64?)
  // Client -> server visible viewport metadata. `active = false`
  // keeps the client attached but removes it from resize negotiation.
  Viewport(client_id~ : String, cols~ : Int, rows~ : Int, active~ : Bool)
  // Server -> client full VT framebuffer. `data` is base64-encoded VT
  // bytes that represent the complete visible viewport at `cols` x `rows`.
  RenderFrame(
    client_id~ : String,
    seq~ : Int64,
    cols~ : Int,
    rows~ : Int,
    scroll_offset~ : Int64,
    scrollback_rows~ : Int64,
    // Optional for wire compatibility with render-frame peers built before
    // title propagation existed. New servers send `Some("")` to clear title.
    title~ : String?,
    // Whether the child application currently consumes mouse input. Optional
    // for wire compatibility with older peers; render clients treat `None`
    // as `true` so a stale server keeps its current mouse routing.
    mouse~ : Bool?,
    data~ : String
  )
  // Server -> client structured render state for `render_frame_state`
  // clients: dirty-line styled runs instead of a VT byte stream. Frame-level
  // metadata matches `RenderFrame`; `frame` carries the run payload.
  RenderState(
    client_id~ : String,
    seq~ : Int64,
    cols~ : Int,
    rows~ : Int,
    scroll_offset~ : Int64,
    scrollback_rows~ : Int64,
    title~ : String?,
    mouse~ : Bool?,
    frame~ : Frame
  )
  // Render-frame clients send semantic terminal input instead of
  // pre-encoded VT bytes. The server owns keyboard, paste, mouse, and
  // scroll encoding because it owns the authoritative terminal modes.
  InputText(client_id~ : String, text~ : String)
  InputKey(
    client_id~ : String,
    key~ : String,
    mods~ : Array[String],
    // Printable text produced by the browser keyboard layout. Render-frame
    // servers use this to preserve shifted/locale-specific printable chords.
    text~ : String?
  )
  InputPaste(client_id~ : String, text~ : String)
  // `button` is absent when no button participates (plain motion, or a
  // release whose press was never tracked).
  InputMouse(
    client_id~ : String,
    x~ : Int,
    y~ : Int,
    button~ : String?,
    action~ : String,
    mods~ : Array[String]
  )
  InputScroll(
    client_id~ : String,
    x~ : Int,
    y~ : Int,
    delta_rows~ : Int,
    mods~ : Array[String]
  )
  // Client -> server request to reset the server-owned render scrollback view
  // to live output. This is intentionally separate from InputScroll so UI
  // jump actions cannot be forwarded to the child as mouse-wheel input.
  ScrollToLive(client_id~ : String)
  // Client -> server report of the client's default colors (`#rrggbb`). The
  // server installs them as the terminal's default foreground/background so
  // child applications probing OSC 10/11 see the colors the user is looking
  // at instead of the agent's host terminal. Theme follows the active
  // viewport: a report installs immediately only from a client whose
  // viewport is active, and a viewport activation installs that client's
  // stored theme.
  Theme(client_id~ : String, background~ : String, foreground~ : String)
  // `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)
  Ide(IdeMessage)
  Term(TermMessage)
  Codex(client_id~ : String, data~ : String)
} derive(Debug)

///|
/// Fixed error classifications for the Moon IDE hover channel.
pub(all) enum IdeErrorCode {
  InvalidRequest
  MoonUnavailable
  MoonIdeFailed
  IdeTimeout
} derive(Debug, Eq)

///|
pub fn IdeErrorCode::to_wire(self : IdeErrorCode) -> String {
  match self {
    InvalidRequest => "invalid_request"
    MoonUnavailable => "moon_unavailable"
    MoonIdeFailed => "moon_ide_failed"
    IdeTimeout => "timeout"
  }
}

///|
pub fn IdeErrorCode::from_wire(value : String) -> IdeErrorCode? {
  match value {
    "invalid_request" => Some(InvalidRequest)
    "moon_unavailable" => Some(MoonUnavailable)
    "moon_ide_failed" => Some(MoonIdeFailed)
    "timeout" => Some(IdeTimeout)
    _ => None
  }
}

///|
/// Moon IDE hover messages. Positions and compact ranges use one-based
/// Unicode code-point coordinates, matching `moon ide hover`.
pub(all) enum IdeMessage {
  Hover(
    req_id~ : String,
    client_id~ : String,
    path~ : String,
    line~ : Int,
    column~ : Int
  )
  CancelHover(req_id~ : String, client_id~ : String)
  HoverResult(
    req_id~ : String,
    client_id~ : String,
    path~ : String,
    range~ : String?,
    contents~ : Array[String]
  )
  IdeError(
    req_id~ : String,
    client_id~ : String,
    path~ : String,
    code~ : IdeErrorCode,
    message~ : String
  )
} derive(Debug)

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

///|
pub(all) enum FsMessage {
  /// Start watching one editor file. The relay injects `client_id`; `req_id`
  /// identifies this watch until replacement, explicit unwatch, or disconnect.
  WatchFile(req_id~ : String, client_id~ : String, path~ : String)
  /// Cancel the matching watch owned by `client_id`.
  UnwatchFile(req_id~ : String, client_id~ : String)
  /// Watcher construction completed. `path` is the requested identity and
  /// `resolved_path` is the file the server actually watches and reads.
  WatchFileReady(
    req_id~ : String,
    client_id~ : String,
    path~ : String,
    resolved_path~ : String
  )
  /// At least one filesystem event affected `resolved_path`.
  FileChanged(
    req_id~ : String,
    client_id~ : String,
    path~ : String,
    resolved_path~ : String
  )
  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)