// Package notification implements the in-process notification pipeline
// described in `backend/PLAN.md` §5.7, `ARCHITECTURE.md` §12, and the
// catalog in `backend/README.md` §10. Producers (lobby, runtime) submit
// intents via Submit; the service persists each intent into
// `backend.notifications`, materialises one row per (recipient, channel)
// in `backend.notification_routes`, and attempts a synchronous best-effort
// dispatch. Failed routes are picked up by a background Worker that retries
// with exponential backoff and dead-letters past the configured maximum.
//
// Push routes are emitted via PushPublisher (the canonical
// `push.Service` over gRPC; the package also ships a
// NoopPushPublisher for tests). Email routes call
// mail.EnqueueTemplate, which feeds the durable mail outbox.
package notification

import (
	"time"

	"galaxy/backend/internal/config"

	"go.uber.org/zap"
)

// Status values stored in `notification_routes.status`. Mirrored by the
// CHECK constraint in migration 00001.
const (
	RouteStatusPending = "pending"
	RouteStatusRetrying = "retrying"
	RouteStatusPublished = "published"
	RouteStatusSkipped = "skipped"
	RouteStatusDeadLettered = "dead_lettered"
)

// Channel values stored in `notification_routes.channel`. The catalog in
// `backend/README.md` §10 documents the per-kind set.
const (
	ChannelPush = "push"
	ChannelEmail = "email"
)

// Backoff parameters for the route worker. Mirrors the trade-off captured
// for the mail outbox in `backend/README.md`: exponential
// growth from a 10 second base, capped at 10 minutes, with ±25% jitter.
const (
	backoffBase = 10 * time.Second
	backoffFactor = 2.0
	backoffMax = 10 * time.Minute
	backoffJitter = 0.25

	// claimBatchSize bounds the number of routes pulled out of Postgres
	// per worker tick. Same logic as `mail.claimBatchSize`: each row is
	// processed in its own short transaction so a slow channel does not
	// block its peers.
	claimBatchSize = 16
)

// Service is the notification entry point. It composes the persistence
// store, the push and mail dispatchers, the account resolver used for
// recipient email lookups, runtime configuration, and a structured
// logger.
type Service struct {
	deps Deps
}

// NewService constructs a Service from deps. Nil Logger defaults to
// zap.NewNop; nil Now defaults to time.Now. Store, Mail, and Accounts
// must be non-nil — calling Service methods with either nil panics on
// first use, matching how the rest of `internal/*` signals missing
// wiring. A nil Push defaults to the no-op publisher used by tests
// that do not exercise the gRPC stream.
func NewService(deps Deps) *Service {
	if deps.Now == nil {
		deps.Now = time.Now
	}
	if deps.Logger == nil {
		deps.Logger = zap.NewNop()
	}
	deps.Logger = deps.Logger.Named("notification")
	if deps.Push == nil {
		deps.Push = NewNoopPushPublisher(deps.Logger)
	}
	return &Service{deps: deps}
}

// Config returns the runtime notification configuration. Worker uses it
// to schedule the scan loop and bound retries.
func (s *Service) Config() config.NotificationConfig {
	return s.deps.Config
}

// Logger returns the package-named structured logger. Worker and the
// admin handlers reuse it so scoped fields stay consistent.
func (s *Service) Logger() *zap.Logger {
	return s.deps.Logger
}

// now returns the package-configured clock; the helper keeps the rest
// of the code free from `if s.deps.Now == nil` checks.
func (s *Service) now() time.Time {
	if s.deps.Now == nil {
		return time.Now()
	}
	return s.deps.Now()
}

// nowUTC returns the configured clock normalised to UTC, matching the
// convention used by `time.Time` columns elsewhere in `backend`.
func (s *Service) nowUTC() time.Time {
	return s.now().UTC()
}

// adminEmail returns the configured admin recipient address with
// surrounding whitespace removed; the empty string indicates no admin
// recipient is configured.
func (s *Service) adminEmail() string {
	return trimSpace(s.deps.Config.AdminEmail)
}
