/**
 * Base-app WebSocket protocol shapes, as the browser bundle SPEAKS them end-to-end.
 *
 * The agent (P2) forwards each logical stream to the UNCHANGED web-terminal at `127.0.0.1:3000`,
 * which speaks this exact JSON protocol (TECH_DOC §4 / src/protocol.ts). relay-web is a legitimate
 * consumer of that contract, so it declares the minimal shapes locally — it does NOT import the
 * base `src/` (that stays byte-for-byte untouched; verification greps `src` clean).
 *
 * These frames are the PLAINTEXT that the E2E layer (T8) later seals; in v0.8 they ride the
 * passthrough transport unencrypted.
 */

/** client → server (agent → base app). `attach` MUST be the first message. */
export type ClientMessage =
  | { readonly type: 'attach'; readonly sessionId: string | null; readonly cwd?: string }
  | { readonly type: 'input'; readonly data: string }
  | { readonly type: 'resize'; readonly cols: number; readonly rows: number }

/** server → client. */
export type ServerMessage =
  | { readonly type: 'attached'; readonly sessionId: string }
  | { readonly type: 'output'; readonly data: string }
  | { readonly type: 'exit'; readonly code: number; readonly reason?: string }

const encoder = new TextEncoder()
const decoder = new TextDecoder()

/** Encode a client message as UTF-8 JSON bytes for `TerminalTransport.send`. */
export function encodeClientMessage(msg: ClientMessage): Uint8Array {
  return encoder.encode(JSON.stringify(msg))
}

/**
 * Decode UTF-8 JSON bytes into a ServerMessage. NEVER throws (mirrors base invariant #3): an
 * unparseable / unknown frame yields `null` so the caller drops it rather than crashing the view.
 */
export function decodeServerMessage(bytes: Uint8Array): ServerMessage | null {
  let parsed: unknown
  try {
    parsed = JSON.parse(decoder.decode(bytes))
  } catch {
    return null
  }
  if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return null
  const obj = parsed as Record<string, unknown>
  const type = obj['type']
  if (type === 'attached' && typeof obj['sessionId'] === 'string') {
    return { type: 'attached', sessionId: obj['sessionId'] }
  }
  if (type === 'output' && typeof obj['data'] === 'string') {
    return { type: 'output', data: obj['data'] }
  }
  if (type === 'exit' && typeof obj['code'] === 'number') {
    const reason = obj['reason']
    return typeof reason === 'string'
      ? { type: 'exit', code: obj['code'], reason }
      : { type: 'exit', code: obj['code'] }
  }
  return null
}
