// Package voice is maven's client↔core network surface.
//
// It is the SECOND protocol surface (the first being internal/ipc, the
// module boundary). Three structural differences from ipc:
//
//   - TRAVERSAL: clients cross the network (spec: wg + mTLS / passkey). ipc
//     is local-only unix socket; this surface is a TCP listener, expected to
//     live inside the wg tunnel. The auth cascade (L0 wg / L1 mTLS / L2
//     passkey / L3 step-up) applies HERE; ipc's auth floor is "same unix
//     user" on the box. Today's floor is plaintext: same-wg-tunnel caller
//     trusted (the listener binds wg-egress only). mTLS / passkey layer in
//     when the auth machinery lands; the wire frames stay unchanged.
//
//   - DIRECTION: bidirectional. ipc is strict request/response (modules
//     pull). clients here ALSO receive server-initiated pushes (async TTS
//     audio back from reactive requests, proactive nudge audio from the
//     loop). One persistent conn per client; the server reads requests AND
//     writes pushes through it. The wire distinguishes a Response (matches
//     a client Request.ID) from a Push (server-initiated, no matching ID).
//
//   - PAYLOAD: audio bytes. Frames hold raw PCM (base64 in JSON for the
//     same debuggability instinct as ipc/worker). Cap is 64 MiB — same as
//     worker — to allow minutes-long transcription / synthesis payloads
//     without chunking complexity (chunked audio is post-MVP, only needed
//     by meeting-record mode).
//
// Session tracking: each connected client = one session (id, surface,
// last-active). Proactive voice delivery (voicesink) routes to the
// most-recently-active session by last-active ts. The spec's "proactive
// voice routing: most-recently-active client plays it; if no client
// reachable, reroute to ntfy/telegram" is enforced HERE not in delivery —
// delivery's voice sink asks Sessions.PickRecent() and either pushes audio
// or returns "no live session" to the dispatcher, which the dispatcher
// translates into rerouting per ChannelsFor's away path. (Today the
// routing-table already drops care-away; the rerouting for ops-when-
// no-client is a deferred plug point in delivery/voicesink, marked below.)
//
// The surface caps: a Push frame carries Audio; the client plays it. The
// reference client (cmd/mavenclient) writes the audio to stdout/-out for
// `aplay` / inspection. A real PWA / native client plays it directly.
package voice

import (
	"encoding/json"
	"time"

	"github.com/kami/maven/internal/audio"
	"github.com/kami/maven/internal/auth"
)

// Surface — the auth surface a client is on. Pulled from internal/auth so
// the voice surface maps 1:1 to the auth cascade's surface-caps table
// (SurfacePCClient / SurfaceAuthedPage / SurfaceVoice). The wire does NOT
// carry the surface — the auth handshake derives it (mTLS metadata,
// passkey enrollment); today's floor sets it to SurfacePCClient (the
// reference client's surface), capped at L3 per the table.
type Surface = auth.Surface

// Re-export the surface constants so the rest of the voice package and
// downstream clients (cmd/mavenclient, daemon wiring) don't need to import
// internal/auth directly. The auth package IS the source of truth; these
// aliases forward to it.
const (
	SurfaceVoice       = auth.SurfaceVoice
	SurfaceTelegram    = auth.SurfaceTelegram
	SurfacePCClient    = auth.SurfacePCClient
	SurfaceAuthedPage  = auth.SurfaceAuthedPage
	SurfaceCoreProcess = auth.SurfaceCoreProcess
	SurfaceUnknown     = auth.SurfaceUnknown
)

// maxFrame — 64 MiB. Same instinct as worker: a single utterance at 16k mono
// int16 ⇒ ~1.9 MiB/min; 64 MiB covers ~33 minutes of audio in one frame,
// which comfortably includes a long pushed reply (a few seconds) and a
// long upload (push-to-talk clips).
const maxFrame = 64 << 20

// Method — one client→server verb. Adding one is a voice API change (the
// client has to learn it). Today: PushToTalk (reactive round-trip) + Pong
// (liveness/last-active update). Proactive is server-initiated (a Push
// frame), not a Method.
type Method string

const (
	MethodPushToTalk Method = "push_to_talk" // client audio → reply audio (sync)
	MethodPong       Method = "pong"         // reply to a Ping push, refreshes last-active
)

// Request — one frame from client to server. ID is chosen by the client,
// monotonically increasing per conn; the server echoes it back in the
// matching Response so the client can multiplex (today it doesn't, but the
// field is reserved for an async client library later).
type Request struct {
	ID     uint64          `json:"id"`
	Method Method          `json:"m"`
	Params json.RawMessage `json:"p,omitempty"`
}

// Response — one frame from server to client, matching a Request.ID. For
// today's synchronous PushToTalk, the server writes the Response on the
// same conn immediately after handling the Request (the client blocks
// reading). Asynchronous replies (server-initiated) come as Push frames,
// not Responses.
type Response struct {
	ID     uint64          `json:"id"`
	Result json.RawMessage `json:"r,omitempty"`
	Error  *RpcError       `json:"e,omitempty"`
}

// Push — one server-initiated frame. No matching Request.ID (the field is
// absent). Kind names the push type; today the only Push is the proactive
// voice nudge (KindAudioNudge), where the server has TTS'd a nudge to audio
// and wants the client to play it. A Push arriving on a conn that's
// awaiting a Request response is interleaved — the client distinguishes by
// the json shape (Response has `id`, Push has `kind`).
type Push struct {
	Kind   PushKind        `json:"kind"`
	Params json.RawMessage `json:"p,omitempty"`
}

// PushKind — one server→client push verb.
type PushKind string

const (
	// PushKindAudioNudge — proactive delivery: a loop rule fired and the
	// phraser rendered its Body; the TTS module synthesised audio; the
	// voicesink picked this session (most-recently-active) and is pushing
	// the bytes here for the client to play. Params: AudioNudgePush.
	PushKindAudioNudge PushKind = "audio_nudge"
	// PushKindPing — liveness probe. Server may send this to refresh
	// last-active; client may respond with MethodPong (today optional —
	// last-active is updated by ANY frame from the client, including the
	// next PushToTalk). Reserved for heartbeat wiring.
	PushKindPing PushKind = "ping"
)

// RpcError — typed wire error, same shape as ipc's. Sentinel codes mirror
// the package sentinels 1:1 (see errors.go). Errors don't carry internal
// text across the wire except for bad-params / internal codes (diagnostic
// only; not authority-bearing — auth refusals carry codeForbidden with no
// message).
type RpcError struct {
	Code    string `json:"c"`
	Message string `json:"m,omitempty"`
}

// PushToTalkReq — the push-to-talk payload. Audio is the captured PCM
// (format declared in the audio.Audio.Format). Lang is the requested
// recognition language for this utterance, overriding the daemon's default
// for mid-sentence code-switches ("ru" / "en" / "mixed"). Surface is the
// client's auth surface; today the floor sets it to SurfacePCClient for
// every conn, but the wire carries it so future mTLS / passkey handshakes
// can populate it without a protocol version bump.
type PushToTalkReq struct {
	Audio   audio.Audio `json:"audio"`
	Lang    string      `json:"lang,omitempty"`
	Surface Surface     `json:"surface,omitempty"`
}

// PushToTalkResp — the reply. ReplyAudio is TTS-synthesised; ReplyText is
// the same reply in text form (for clients that can't play audio, for
// logging, for tests asserting the round-trip shape). Routed channels list
// the away channels the dispatcher also delivered to (e.g. a low-severity
// nudge fired alongside the reply and was forwarded to ntfy); today the
// reactive handler doesn't dispatch nudges, so this is empty.
type PushToTalkResp struct {
	ReplyAudio     audio.Audio `json:"reply_audio"`
	ReplyText      string      `json:"reply_text"`
	Transcript     string      `json:"transcript,omitempty"`
	RoutedChannels []string    `json:"routed_channels,omitempty"`
}

// AudioNudgePush — the proactive nudge push payload. RuleName + Severity
// for the client to display; Audio is the synthesised Body; Text is the
// same in text form.
type AudioNudgePush struct {
	RuleName string      `json:"rule_name"`
	Severity int         `json:"severity"`
	Audio    audio.Audio `json:"audio"`
	Text     string      `json:"text"`
	Ts       time.Time   `json:"ts"`
}
