// Package tts is maven's text-to-speech seam.
//
// Mirrors internal/stt: one method, two implementations (Stub + Remote), one
// swap seam at the daemon. The output is audio.Audio — raw 16k mono int16
// PCM, headerless per the audio package; the voice sink + reference client
// wrap it in a WAV at the disk edge.
//
// Per docs/design.md § Voice pipeline (STT / TTS): piper is the production tts
// (subprocess + espeak-ng, CPU-only on the ryzen box, driven by cmd/mavttsd);
// the older silero pick is retired (docs/design.md § Superseded, "named STT/TTS
// model picks"). A different voice is a model-file swap, not a code change.
// The daemon wires one impl — Remote pointing at the worker socket if
// configured, Stub otherwise.
//
// The Stub returns a short deterministic tone (a 200ms mid-frequency sine
// burst) so the voice loop round-trips end-to-end without a model. The
// reference client can `aplay` the reply, hear a tone, and know the wire
// shape is right; the production swap replaces the bytes with model output.
package tts

import (
	"context"
	"encoding/binary"
	"fmt"
	"math"

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

// Synthesizer — the text-to-speech contract. Input is text the phraser has
// already rendered (Body for proactive nudges, reply text for reactive).
// Output is raw PCM audio the delivery / client surfaces ship.
type Synthesizer interface {
	Synthesize(ctx context.Context, text string) (audio.Audio, error)
}

// Stub — the deterministic, no-model floor. Returns a fixed-duration tone
// keyed by the input text's first byte so different replies produce
// slightly different tones (a test asserting "the voice reply was sent"
// can distinguish them; a human smoke-testing hears that SOMETHING came
// back, not silence). 200ms at 16k mono int16 ⇒ 6400 bytes — small frames,
// instant over the wire.
type Stub struct{}

// NewStub builds the floor synthesizer.
func NewStub() *Stub { return &Stub{} }

// Synthesize returns a 200ms tone derived from the first byte of text.
// Empty text ⇒ a low tone (so an empty reply is still audible, not a
// silent no-op a bug could hide behind).
func (s *Stub) Synthesize(_ context.Context, text string) (audio.Audio, error) {
	const durMs = 200
	// The rate is read off the canonical format rather than written again, so
	// the tone stays in tune with the shape the seam declares.
	rate := audio.PCM16kMono.SampleRate
	bytesPerSample := audio.PCM16kMono.SampleBits / 8
	samples := rate * durMs / 1000
	pcm := make([]byte, samples*bytesPerSample)
	freq := 220.0 // A3
	if len(text) > 0 {
		freq = 180.0 + float64(text[0]%6)*60 // 180..480 Hz band
	}
	for i := 0; i < samples; i++ {
		t := float64(i) / float64(rate)
		v := int16(12000 * math.Sin(2*math.Pi*freq*t))
		binary.LittleEndian.PutUint16(pcm[i*2:], uint16(v))
	}
	return audio.Audio{Format: audio.PCM16kMono, Bytes: pcm}, nil
}

// Remote — the worker-backed Synthesizer. Holds a worker.Client that dials
// the tts module's unix socket.
type Remote struct {
	c     *worker.Client
	lang  string
	voice string
}

// NewRemote builds a Remote Synthesizer. lang is the default voice language;
// voice is the named voice ("" ⇒ the worker's configured default).
func NewRemote(c *worker.Client, lang, voice string) *Remote {
	return &Remote{c: c, lang: lang, voice: voice}
}

// Synthesize forwards to the worker module. A worker-side fault returns an
// empty Audio + error; the dispatcher's voice path logs and skips (a
// transient TTS fault drops the voice channel for that one send; away
// channels like ntfy/telegram still fire because their sinks are
// independent).
func (r *Remote) Synthesize(ctx context.Context, text string) (audio.Audio, error) {
	resp, err := r.c.Synthesize(ctx, worker.SynthesizeReq{Text: text, Lang: r.lang, Voice: r.voice})
	if err != nil {
		return audio.Audio{}, fmt.Errorf("tts: synthesize: %w", err)
	}
	return resp.Audio, nil
}
