// Package voicesink implements delivery.Sink for the voice channel.
//
// It is the LAST mile of proactive voice delivery: the dispatcher calls
// voicesink.Send for a PhrasedNudge whose routing landed on ChannelVoice.
// The sink:
//
//  1. renders the Sendable.Body (the full phrased message) to PCM via the
//     tts.Synthesizer seam. The phraser already produced the text; the
//     voice sink synthesises AUDIO for it. Body, not Summary — voice is the
//     local channel; no shoulder-surf exfil concern (no relay); the user
//     hears the full message.
//
//  2. finds the most-recently-active voice client session via the
//     voice.Sessions registry and pushes the audio as a voice.AudioNudgePush
//     on that session's conn. The user's most-recently-active client plays
//     it; other clients stay silent (no dogpile — the spec's "play it on
//     most-recently-active, reroute to ntfy/telegram if none reachable").
//
//  3. if no live session exists, Send returns voice.ErrNoSession
//     (wrapped). The daemon logs the partial dispatch; an OPEN deferred
//     question is whether the dispatcher should reroute to away-channels
//     instead of returning partial.
//
// Import direction: voicesink imports internal/tts (synth seam) and
// internal/voice (Sessions registry). Both are siblings of delivery; the
// dispatcher holds `delivery.Sink` and doesn't import voicesink, so the
// import cycle is broken. voicesink is wired at the daemon.
package voicesink

import (
	"context"
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/kami/maven/internal/delivery"
	"github.com/kami/maven/internal/tts"
	"github.com/kami/maven/internal/ttsnorm"
	"github.com/kami/maven/internal/voice"
)

// Sink — implements delivery.Sink for the voice channel via TTS synthesis +
// per-session push.
type Sink struct {
	tts  tts.Synthesizer
	sess *voice.Sessions
}

// New builds a Sink wired to a Synthesizer and the Sessions registry shared
// with the voice.Server. The daemon constructs one when its config enables
// voice; otherwise the dispatcher's voice slot stays nil and the routing
// table's ChannelVoice selections drop silently (today's behaviour).
func New(synth tts.Synthesizer, sess *voice.Sessions) *Sink {
	return &Sink{tts: synth, sess: sess}
}

// Send — delivery.Sink. Synthesises Body text, picks the most-recently-
// active session, pushes the audio. Errors ⇒ the dispatcher surfaces
// partial dispatch; a deferred question is rerouting to away channels when
// no live voice session exists (today the dispatcher errors and stops;
// tomorrow: reroute on voice.ErrNoSession within the dispatcher or here).
func (s *Sink) Send(ctx context.Context, send delivery.Sendable) error {
	if s.tts == nil {
		return fmt.Errorf("voicesink: tts synthesizer not wired")
	}
	if s.sess == nil {
		return fmt.Errorf("voicesink: sessions registry not wired")
	}
	text := send.Body
	if text == "" {
		// voice gets Body; if the phraser didn't produce one, fall back to
		// Summary (terse full beats silence — the routing table insisted
		// on voice for this severity, so a silent no-op would hide a bug
		// in the phraser behind routing).
		text = send.Summary
	}
	out, err := s.tts.Synthesize(ctx, ttsnorm.Speakable(text))
	if err != nil {
		return fmt.Errorf("voicesink: synthesize: %w", err)
	}
	if !out.Format.IsValid() {
		// A synthesizer returning an off-spec format is a model bug; refuse
		// to ship bytes the client can't play rather than misrouting to a
		// decoder that doesn't exist.
		return fmt.Errorf("voicesink: tts returned %v, want PCM16kMono", out.Format)
	}
	push := voice.AudioNudgePush{
		RuleName: send.RuleName,
		Severity: int(send.Severity),
		Audio:    out,
		Text:     text,
		Ts:       time.Now(),
	}
	if err := s.sess.PushToMostRecent(ctx, push); err != nil {
		if errors.Is(err, voice.ErrNoSession) {
			log.Printf("voicesink: no live voice session for %s, falling through to away channels", send.RuleName)
			return delivery.ErrVoiceNoSession
		}
		return fmt.Errorf("voicesink: push: %w", err)
	}
	return nil
}
