import Foundation

/// Wire constants for the host ⇄ agent protocol (docs/MACOS_VM_NATIVE_AGENT.md §3–§4).
///
/// The transport is NDJSON over vsock: one JSON request object per line, one JSON reply per line.
/// Requests carry an `op`; replies carry `ok` (plus op-specific fields, or `error` when `ok:false`).
///
/// The port constant is mirrored on the host side in `MacVMAgentWire`
/// (Sources/NucleicCore/MacVM/MacVMAgentClient.swift) — the two packages are deliberately separate
/// (different platform floors), so keep the values in lockstep.
public enum AgentWire {
    /// The Nucleic-reserved vsock port the agent listens on and the host connects to.
    public static let port: UInt32 = 2035
    /// Protocol version reported in the `ping` reply, bumped on incompatible wire changes.
    /// v2 adds the streaming ``Exec`` op — the vsock replacement for the retired SSH exec path.
    public static let version = 2

    /// The streaming **`exec`** sub-protocol (docs/MACOS_VM_NATIVE_AGENT.md §4.4). Unlike every other
    /// op (one request line → one reply line), `exec` takes over its vsock connection for the life of
    /// the command: after the single request line the connection carries a bidirectional stream of
    /// one-JSON-object-per-line *frames*, each tagged by ``tagKey``. The host opens a **dedicated**
    /// connection per exec, so a multi-minute build never blocks the control channel and several
    /// execs run concurrently (the listener is one thread per connection).
    ///
    /// Payload bytes (`stdout`/`stderr`/`stdin`) are **base64** in ``dataKey`` so arbitrary binary
    /// output can't break the NDJSON line framing. The connection closes after the ``exit`` (or
    /// ``spawnError``) frame.
    public enum Exec {
        /// The request op: `{"op":"exec","command":<shell script>}`. The host composes any working
        /// directory + env exports into `command` itself, so the guest just runs one script.
        public static let op = "exec"
        /// Field on the request line carrying the `/bin/zsh -c` script to run.
        public static let commandKey = "command"

        /// Common frame discriminator key.
        public static let tagKey = "t"
        // guest → host
        public static let stdout = "o"       // {"t":"o","d":<base64 chunk>}
        public static let stderr = "e"       // {"t":"e","d":<base64 chunk>}
        public static let exit = "x"         // {"t":"x","code":<int>}
        public static let spawnError = "err" // {"t":"err","m":<string>}
        // host → guest
        public static let stdin = "i"        // {"t":"i","d":<base64 chunk>}
        public static let stdinEOF = "eof"   // {"t":"eof"}
        public static let signal = "sig"     // {"t":"sig","n":<int>}

        /// Frame field keys.
        public static let dataKey = "d"      // base64 payload (stdout/stderr/stdin frames)
        public static let codeKey = "code"   // exit status (exit frame)
        public static let messageKey = "m"   // human message (spawnError frame)
        public static let signalKey = "n"    // signal number (signal frame)

        // There is deliberately NO shell-selection request field: nash is the only shell the exec
        // surface runs (docs/NASH.md §7.5 — locked), so the wire offers nothing to override it
        // with. A legacy host that still sends `"shell"` is ignored. The lone exception is the
        // in-guest operator break-glass (§4.1) — `disable=1` in the root-owned ``NashPolicy``
        // file, which is an operator's `sudo` inside the guest, not wire and not environment
        // (§4.3: an agent-writable channel is not a channel for an operator lever).
    }

    /// The **`shell-events`** drain op (docs/NASH.md §6.2, §7.5) — the macOS twin of the Linux agent's
    /// op and mirror of the host's `MacVMAgentWire.ShellEvents`. Inside a VM nash can't POST to the host
    /// socket, so it spools each `shell-batch` as a line of JSONL under ``ShellSpool/defaultDir``; the
    /// host polls this op after each `mac_vm_exec` to collect them. One request → one reply (not the
    /// streaming exec sub-protocol); the reply carries the concatenated JSONL base64 in ``eventsKey``.
    public enum ShellEvents {
        public static let op = "shell-events"
        // request (all optional)
        public static let dirKey = "dir"           // spool dir override (default ShellSpool.defaultDir)
        public static let maxBytesKey = "maxBytes" // cap on bytes returned this poll
        public static let truncateKey = "truncate" // nonzero → delete fully-drained files after the reply
        // reply
        public static let eventsKey = "events"     // base64 of the concatenated JSONL batches
        public static let filesKey = "files"       // count of files drained
        public static let bytesKey = "bytes"       // total JSONL bytes returned
        public static let truncatedKey = "truncated" // more remains for the next poll
    }
}

/// Accumulates raw bytes and yields complete newline-terminated lines — the NDJSON framing both
/// sides use (one request/reply object per line). Pure and unit-tested; the connection loop feeds
/// it whatever `read(2)` returns.
public struct LineSplitBuffer {
    private var data = Data()

    public init() {}

    public mutating func append(_ chunk: Data) {
        data.append(chunk)
    }

    /// The next complete line (without its trailing `\n`), or `nil` when no full line is buffered.
    public mutating func nextLine() -> Data? {
        guard let nl = data.firstIndex(of: 0x0A) else { return nil }
        let line = data.subdata(in: data.startIndex..<nl)
        data.removeSubrange(data.startIndex...nl)
        // Tolerate CRLF framing from a debugging client.
        if line.last == 0x0D { return line.dropLast() }
        return line
    }

    /// Bytes currently buffered without a terminating newline — the runaway-line guard reads this.
    public var pendingBytes: Int { data.count }
}
