// Package worker is the audio-job boundary between core and its stt/tts
// modules.
//
// It is a SIBLING to internal/ipc, not a reuse of it:
//
//   - internal/ipc is the core↔module *state* boundary. Modules call INTO
//     core to write facts / read presence. core is the server; the module
//     is the client. The verbs (WriteFact, Since, ...) are authority-bound;
//     the auth layer scopes them per Caller.
//   - internal/worker is the core↔module *job* boundary. Core calls OUT to
//     the stt/tts module: "transcribe these bytes", "synthesize this text."
//     Core is the client here; the worker module is the server. The verbs
//     are not authority-bound — they are pure compute over the payload core
//     already owns (audio bytes / reply text). Nothing about authority moves
//     across this boundary because the worker has no key, no facts, nothing
//     stateful to read or write.
//
// The reversal matters: a module that ships audio work to core would have
// to hold a Caller identity, an enrolled daemon surface, etc.; core that
// ships work to a module ships only the work. Same unix-socket floor (0600
// dir + socket perms carry "same user") but the flow is reversed, and the
// surface stays out of the auth cascade — the worker never reaches into
// core's state, so it has nothing to be capped against.
//
// Transport: unix domain socket, local-only, same-host as core. Same wire
// shape as ipc (4-byte big-endian length + JSON body) so `socat`/`nc` can
// debug it identically. Frame cap is 64 MiB (vs ipc's 4) so audio blobs fit
// — a minute of 16k mono int16 is ~1.9 MiB, comfortably under; an hour's
// sleep-clip transcription is implausible to send as one frame but the cap
// permits long-enough live clips without framing the worker into chunks.
package worker

import (
	"encoding/json"
	"errors"
	"fmt"
)

// maxFrame — 64 MiB. Audio up to ~3 minutes @ 16k mono int16 fits one frame
// with headroom; longer audio is chunked by the caller (meeting-record mode
// is post-MVP) or rejected with ErrFrameTooLarge. Sized for stt inputs and
// tts outputs at single-utterance scale.
const maxFrame = 64 << 20

// Method — one RPC verb. Adding one is a worker-API change, not an authority
// change (worker has no authority surface — see package doc). The two verbs
// mirror the stt/tts interfaces the daemon wires; new verbs go with a new
// module kind (e.g. translation), not a new feature of an existing one.
type Method string

const (
	MethodTranscribe Method = "transcribe"
	MethodSynthesize Method = "synthesize"
)

// Request — one frame from core to a worker module.
type Request struct {
	Method Method          `json:"m"`
	Params json.RawMessage `json:"p,omitempty"`
}

// Response — one frame back from the worker. Exactly one of Result/Error is set.
type Response struct {
	Result json.RawMessage `json:"r,omitempty"`
	Error  *RpcError       `json:"e,omitempty"`
}

// RpcError — a typed wire error. Mirrors internal/ipc's shape for tooling
// parity (same args to socat, same `errors.Is` rehydration pattern); worker
// sentinels are independent since this boundary is independent.
type RpcError struct {
	Code    string `json:"c"`
	Message string `json:"m,omitempty"`
}

func (e *RpcError) Error() string {
	if e.Message != "" {
		return fmt.Sprintf("worker: %s: %s", e.Code, e.Message)
	}
	return fmt.Sprintf("worker: %s", e.Code)
}

// Sentinel codes. Stable over the wire — do not rename. Mirror the package
// sentinels 1:1.
const (
	codeUnknownMethod = "unknown_method"
	codeBadParams     = "bad_params"
	codeInternal      = "internal"
)

// Sentinel errors. The client rehydrates a wire RpcError into one of these so
// callers use errors.Is the same way they would in-process.
var (
	ErrUnknownMethod = errors.New("worker: unknown method")
	ErrBadParams     = errors.New("worker: bad params")
)

// codeOf maps a server-side sentinel to its wire code. Anything not matched
// is codeInternal — internal Go error text never ships to the caller; the
// server logs the real text and the client sees a generic code.
func codeOf(err error) string {
	switch {
	case err == nil:
		return ""
	case errors.Is(err, ErrUnknownMethod):
		return codeUnknownMethod
	case errors.Is(err, ErrBadParams):
		return codeBadParams
	default:
		return codeInternal
	}
}

// rpcErr builds the wire error for a server-side error. message is omitted
// for sentinel codes (the Code carries the meaning). internal errors carry
// the message text — it's not authority-bearing text, just a diagnostic.
func rpcErr(err error) *RpcError {
	c := codeOf(err)
	if c == codeInternal || c == codeBadParams {
		return &RpcError{Code: c, Message: err.Error()}
	}
	return &RpcError{Code: c}
}
