package diplomail

import (
	"context"
	"time"

	"galaxy/backend/internal/config"
	"galaxy/backend/internal/diplomail/detector"
	"galaxy/backend/internal/diplomail/translator"

	"github.com/google/uuid"
	"go.uber.org/zap"
)

// Deps aggregates every collaborator the diplomail Service depends on.
//
// Store and Memberships are required. Logger and Now default to
// zap.NewNop / time.Now when nil. Notification falls back to a no-op
// publisher so unit tests can construct a Service with only the
// required collaborators populated. Entitlements and Games are
// optional — they are used by Stage C surfaces (paid-tier player
// broadcast, multi-game admin broadcast, bulk cleanup). Wiring may
// pass nil for tests that do not exercise those paths.
type Deps struct {
	Store        *Store
	Memberships  MembershipLookup
	Notification NotificationPublisher
	Entitlements EntitlementReader
	Games        GameLookup
	Detector     detector.LanguageDetector
	Translator   translator.Translator
	Config       config.DiplomailConfig
	Logger       *zap.Logger
	Now          func() time.Time
}

// EntitlementReader is the read-only surface diplomail uses to gate
// the paid-tier player broadcast. The canonical implementation in
// `cmd/backend/main` reads
// `*user.Service.GetEntitlementSnapshot(userID).IsPaid`.
type EntitlementReader interface {
	IsPaidTier(ctx context.Context, userID uuid.UUID) (bool, error)
}

// GameLookup exposes the slim view of `games` the multi-game admin
// broadcast and bulk-cleanup paths consume. The canonical
// implementation walks the lobby cache plus an explicit store call
// for finished-game pruning.
type GameLookup interface {
	// ListRunningGames returns every game whose `status` is one of
	// the still-active values (running, paused, starting, …). The
	// admin `all_running` broadcast scope iterates over the result.
	ListRunningGames(ctx context.Context) ([]GameSnapshot, error)

	// ListFinishedGamesBefore returns every game whose `finished_at`
	// is older than `cutoff`. The bulk-purge admin endpoint reads
	// this to compose the cascade-delete IN list.
	ListFinishedGamesBefore(ctx context.Context, cutoff time.Time) ([]GameSnapshot, error)

	// GetGame returns one game snapshot identified by id, or
	// ErrNotFound. Used by the multi-game broadcast to verify the
	// caller-supplied id list before enqueuing fan-out work.
	GetGame(ctx context.Context, gameID uuid.UUID) (GameSnapshot, error)
}

// GameSnapshot is the trim view of `games` consumed by the multi-game
// admin broadcast and the cleanup paths. The struct intentionally
// avoids the full `lobby.GameRecord` so the diplomail package stays
// decoupled from the lobby domain.
type GameSnapshot struct {
	GameID      uuid.UUID
	GameName    string
	Status      string
	FinishedAt  *time.Time
}

// ActiveMembership is the slim view of a single (user, game) roster
// row the diplomail package needs at send time: it confirms the
// participant is active in the game and captures the snapshot fields
// (`game_name`, `user_name`, `race_name`, `preferred_language`) that
// we persist on each new message / recipient row.
type ActiveMembership struct {
	UserID            uuid.UUID
	GameID            uuid.UUID
	GameName          string
	UserName          string
	RaceName          string
	PreferredLanguage string
}

// MembershipLookup is the read-only surface diplomail uses to verify
// "is this user an active member of this game" and to snapshot the
// roster metadata. The canonical implementation in `cmd/backend/main`
// adapts the `*lobby.Service` membership cache to this interface.
//
// GetActiveMembership returns ErrNotFound (the diplomail sentinel)
// when the user is not an active member of the game; the service
// boundary maps that to 403 forbidden.
//
// GetMembershipAnyStatus returns the same shape regardless of
// membership status (`active`, `removed`, `blocked`). Used by the
// inbox read path to check whether a kicked recipient still belongs
// to the game's roster; ErrNotFound is surfaced when the user has
// never been a member.
//
// ListMembers returns every roster row matching scope, in stable
// order. Scope values are `active`, `active_and_removed`, and
// `all_members` (the spec calls these out by name). Used by the
// broadcast composition step in admin / owner sends.
type MembershipLookup interface {
	GetActiveMembership(ctx context.Context, gameID, userID uuid.UUID) (ActiveMembership, error)
	GetMembershipAnyStatus(ctx context.Context, gameID, userID uuid.UUID) (MemberSnapshot, error)
	ListMembers(ctx context.Context, gameID uuid.UUID, scope string) ([]MemberSnapshot, error)
}

// Recipient scope values accepted by ListMembers and by the
// `recipients` request field on admin / owner broadcasts.
const (
	RecipientScopeActive            = "active"
	RecipientScopeActiveAndRemoved  = "active_and_removed"
	RecipientScopeAllMembers        = "all_members"
)

// MemberSnapshot is the slim view of a membership row that survives
// all three status values. RaceName is the immutable string captured
// at registration time; an empty value is legal for rare cases where
// the row was inserted without one. PreferredLanguage is included so
// the broadcast and lifecycle paths can decide whether the recipient
// needs to wait for a translation before delivery.
type MemberSnapshot struct {
	UserID            uuid.UUID
	GameID            uuid.UUID
	GameName          string
	UserName          string
	RaceName          string
	PreferredLanguage string
	Status            string
}

// NotificationPublisher is the outbound surface diplomail uses to
// emit the `diplomail.message.received` push event. The canonical
// implementation in `cmd/backend/main` adapts the notification.Service
// the same way it adapts `lobby.NotificationPublisher`; tests pass
// the no-op publisher below to avoid wiring the dispatcher.
type NotificationPublisher interface {
	PublishDiplomailEvent(ctx context.Context, ev DiplomailNotification) error
}

// DiplomailNotification is the open shape carried by a per-recipient
// push intent. The struct lives in the diplomail package so the
// producer vocabulary stays here; the publisher adapter translates it
// into a `notification.Intent` at the wiring boundary.
type DiplomailNotification struct {
	Kind           string
	IdempotencyKey string
	Recipient      uuid.UUID
	Payload        map[string]any
}

// NewNoopNotificationPublisher returns a publisher that logs every
// call at debug level and returns nil. Used by unit tests and as the
// fallback inside NewService when callers leave Deps.Notification nil.
func NewNoopNotificationPublisher(logger *zap.Logger) NotificationPublisher {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &noopNotificationPublisher{logger: logger.Named("diplomail.notify.noop")}
}

type noopNotificationPublisher struct {
	logger *zap.Logger
}

func (p *noopNotificationPublisher) PublishDiplomailEvent(_ context.Context, ev DiplomailNotification) error {
	p.logger.Debug("noop notification",
		zap.String("kind", ev.Kind),
		zap.String("idempotency_key", ev.IdempotencyKey),
		zap.String("recipient", ev.Recipient.String()),
	)
	return nil
}
