package wang.yaojia.webterm.wire

/**
 * Client -> server WS frames (FROZEN contract, plan §3.1). The Android analogue of
 * `src/types.ts:87-92` `ClientMessage` and iOS `WireProtocol.ClientMessage`.
 *
 * Immutable value types. Encoding to the wire is A4 (`MessageCodec`) — this file only
 * freezes the SHAPES so every other module reads stable names.
 *
 * Server-side validation the caller must respect (the server SILENTLY DISCARDS invalid
 * frames — `src/protocol.ts:45-86` — no error reply comes back):
 *  - `attach` MUST be the FIRST frame on a connection (`src/server.ts:766-773`).
 *  - `resize` cols/rows must be integers in [WireConstants.RESIZE_RANGE] (1..1000).
 *  - `attach.cwd`, when present, must be an absolute path (leading '/').
 *  - `input.data` is raw keyboard bytes, passed through VERBATIM — never filtered
 *    (ARCHITECTURE invariant #9).
 *
 * DEVIATION NOTE (A2): the task brief listed `Attach(sessionId, cols, rows, cwd)`, but the
 * server `attach` frame (`src/protocol.ts` validateAttach, `src/types.ts:88`) and the iOS
 * `ClientMessage.attach(sessionId:cwd:)` carry NO cols/rows — the server hardcodes
 * DEFAULT_COLS/ROWS on attach (`src/server.ts:774-781`) and takes real dimensions from a
 * separate `resize` frame sent right after attach. Adding cols/rows here would make the
 * encoded attach frame differ from the web client and break A4's byte-equality vectors, so
 * the faithful wire contract (sessionId + optional cwd) is frozen instead.
 */
public sealed interface ClientMessage {
    /**
     * First frame. `sessionId == null` spawns a NEW session (A4 encodes this as an explicit
     * JSON `"sessionId":null` — the key is REQUIRED by the server, `src/protocol.ts:132-134`).
     * `cwd` = "new tab here" spawn directory (M6); appended only when [sessionId] is null.
     */
    public data class Attach(val sessionId: String?, val cwd: String? = null) : ClientMessage

    /** Raw keyboard bytes, verbatim (invariant #9 — no content filtering). */
    public data class Input(val data: String) : ClientMessage

    /** Own message type so the server can `ioctl(TIOCSWINSZ)` -> SIGWINCH for TUIs. */
    public data class Resize(val cols: Int, val rows: Int) : ClientMessage

    /**
     * Resolve a held permission gate with allow. [mode] is only meaningful for a `plan` gate
     * and is encoded as a TOP-LEVEL `mode` key — the server's WS wiring re-parses the raw
     * frame for it (`src/server.ts:91-102`). No [mode] = a plain approve.
     */
    public data class Approve(val mode: ApproveMode? = null) : ClientMessage

    /** Resolve a held permission gate with deny. */
    public data object Reject : ClientMessage
}

/**
 * Permission mode written back when resolving a `plan` gate. Wire values mirror the server
 * whitelist `PermissionMode` (`src/types.ts:371`). The Kotlin analogue of Swift's
 * `enum ApproveMode: String` RawRepresentable — [wire] is the JSON string, [fromWire] the
 * reverse lookup.
 *
 * Note: the plan-gate three-way UI only ever sends [ACCEPT_EDITS] / [DEFAULT]; raw [AUTO] is
 * gated server-side by ALLOW_AUTO_MODE (default false) and downgraded to `default` otherwise.
 */
public enum class ApproveMode(public val wire: String) {
    DEFAULT("default"),
    ACCEPT_EDITS("acceptEdits"),
    PLAN("plan"),
    AUTO("auto");

    public companion object {
        public fun fromWire(value: String): ApproveMode? = entries.firstOrNull { it.wire == value }
    }
}

/** The `type` discriminator whitelist for client frames (`src/protocol.ts:27` ALLOWED_TYPES). */
public enum class ClientMessageType(public val wire: String) {
    ATTACH("attach"),
    INPUT("input"),
    RESIZE("resize"),
    APPROVE("approve"),
    REJECT("reject");

    public companion object {
        public fun fromWire(value: String): ClientMessageType? = entries.firstOrNull { it.wire == value }
    }
}
