// Package delivery is maven's channel-routing + dispatch layer.
//
// Spec contract (from docs/design.md § Delivery / channel routing):
//
//   - routing = f(severity, presence). presence decides REACHABILITY; severity
//     decides INSISTENCE. need both.
//
//     | | present | away |
//     | sev1–2 (care)     | voice          | drop                         |
//     | sev3   (ops soft) | voice          | ntfy, once                   |
//     | sev4   (ops hard) | voice + ntfy   | telegram, repeat til ack     |
//
//   - sev ≤ 2 drops on away, sev ≥ 3 holds. "a missed water nudge is noise;
//     a missed backup-failure isn't."
//
//   - away channels (ntfy/telegram) leave the box — the one path that crosses
//     "never phones home," through your relay. MINIMAL BODY — "disk low on
//     homesrv," not detail. don't make notifications a shoulder-surf exfil
//     surface.
//
//   - reminders are a SEPARATE class — two delivery paths. reminders bypass
//     the restraint gate ("wake me 7" fires in quiet hours; that's the point).
//     snooze still applies. voice when present; when away, try ntfy then
//     telegram as alternatives and stop after the first success. fire once.
//
// Architecture mirrors the loop's gather/pure split: the routing table is a
// PURE function of (severity, presence); the Dispatcher holds the impure Sinks
// (one per channel transport) and the recorder seams. ShouldRepeat is pure —
// the daemon's tick loop calls it each cycle for un-acked sev4 telegram sends.
package delivery

import (
	"errors"

	"github.com/kami/maven/internal/loop"
	"github.com/kami/maven/internal/store"
)

// ErrVoiceNoSession — sentinel returned by the voice sink when no live
// client session exists at push time. The dispatcher treats this as a
// skip-continue: the voice channel is unavailable for this delivery, but
// remaining channels (ntfy, telegram) should still fire. The voicesink
// maps voice.ErrNoSession to this sentinel so the dispatcher doesn't need
// to import the voice package.
var ErrVoiceNoSession = errors.New("delivery: voice: no live session")

// ErrPermanent is the class of transport failures that waiting cannot repair:
// a revoked credential or an endpoint that refuses this sender. Dispatchers
// may still try a different reach for the same message, but the failed reach
// must not be put on an automatic retry clock until its configuration changes.
var ErrPermanent = errors.New("delivery: permanent failure")

// Channel — one delivery transport. Drop is an explicit no-op (the routing
// table chose to suppress, which is a decision, not a failure — "a missed
// water nudge is noise"). a nil Sink for a wired channel is a daemon config
// gap, not a Channel value.
type Channel string

const (
	ChannelVoice    Channel = "voice"
	ChannelNtfy     Channel = "ntfy"
	ChannelTelegram Channel = "telegram"
	ChannelDrop     Channel = "drop"
)

// ChannelsFor — the PURE routing table: f(severity, presence).
//
// presence decides reachability; severity decides insistence. the loop's gate
// already suppressed care nudges (sev ≤ 2) on away — this table is the
// delivery-side authority for ALL severities, including the ops nudges the
// gate lets through. double authority is intentional: the gate decides whether
// a rule EMITS; delivery decides where it LANDS. they agree on care-away
// (both drop) and diverge only where they must (ops survives away here, not
// because the gate let it through, but because delivery insists).
//
// sev4 present → voice + ntfy: the disk-fire alarm gets voice AND a push —
// you're here, but this is loud enough to also surface on the watch.
// sev4 away → telegram, repeat til ack: the one channel that crosses the relay
// and insists until you respond.
func ChannelsFor(sev loop.Severity, presence store.Bucket) []Channel {
	if presence == store.Away {
		switch {
		case sev <= loop.Sev2:
			return []Channel{ChannelDrop}
		case sev == loop.Sev3:
			return []Channel{ChannelNtfy}
		case sev >= loop.Sev4:
			return []Channel{ChannelTelegram}
		}
		return []Channel{ChannelDrop} // unknown sev → fail-closed
	}
	// present
	if sev >= loop.Sev4 {
		return []Channel{ChannelVoice, ChannelNtfy}
	}
	return []Channel{ChannelVoice}
}

// ChannelsForReminder — reminders are a SEPARATE class that bypasses the gate.
// "wake me 7" fires in quiet hours; that's the point. presence still routes
// reachability: voice when present, then an ordered ntfy→telegram alternative
// chain when away. The dispatcher stops after the first successful alternative,
// so a reminder still fires once rather than being broadcast on both channels.
// There is no repeat (repeat-til-ack is a sev4 ops-hard behavior, not a reminder
// behavior).
//
// reminders don't carry a Severity — they're user-stated future intent, not
// loop-derived insistence. the routing is presence-only: reachability without
// the insistence axis. a reminder that must be louder (an alarm) is a future
// per-reminder override, not a table entry.
func ChannelsForReminder(presence store.Bucket) []Channel {
	if presence == store.Away {
		return []Channel{ChannelNtfy, ChannelTelegram}
	}
	return []Channel{ChannelVoice}
}
