package diplomail

import (
	"time"

	"github.com/google/uuid"
)

// Message mirrors a row in `backend.diplomail_messages` enriched with
// the per-message metadata captured at insert time.
//
// SenderUserID and SenderUsername are nullable in the DB so that the
// CHECK constraint can cover the three legal sender shapes:
//
//   - player: SenderUserID set, SenderUsername set
//   - admin:  SenderUserID nil, SenderUsername set
//   - system: SenderUserID nil, SenderUsername nil
type Message struct {
	MessageID      uuid.UUID
	GameID         uuid.UUID
	GameName       string
	Kind           string
	SenderKind     string
	SenderUserID   *uuid.UUID
	SenderUsername *string
	SenderIP       string
	Subject        string
	Body           string
	BodyLang       string
	BroadcastScope string
	CreatedAt      time.Time
}

// Recipient mirrors a row in `backend.diplomail_recipients`. The
// per-recipient state (read/deleted/delivered/notified) lives here.
// RecipientUserName, RecipientRaceName, and
// RecipientPreferredLanguage are snapshots taken at insert time so
// the inbox listing, admin search, and translation worker render
// correctly even after the source rows are renamed or revoked.
//
// AvailableAt encodes the async-translation contract introduced in
// Stage E:
//
//   - non-nil → message is visible to the recipient (in inbox /
//     unread counts / push events) starting from this timestamp;
//   - nil → recipient is waiting for the translation worker to fan
//     out the translated rendering. The translation_attempts counter
//     tracks the number of failed LibreTranslate calls; the worker
//     gives up after `MaxTranslationAttempts` and falls back to the
//     original body, flipping AvailableAt to now().
type Recipient struct {
	RecipientID                uuid.UUID
	MessageID                  uuid.UUID
	GameID                     uuid.UUID
	UserID                     uuid.UUID
	RecipientUserName          string
	RecipientRaceName          *string
	RecipientPreferredLanguage string
	AvailableAt                *time.Time
	TranslationAttempts        int32
	NextTranslationAttemptAt   *time.Time
	DeliveredAt                *time.Time
	ReadAt                     *time.Time
	DeletedAt                  *time.Time
	NotifiedAt                 *time.Time
}

// InboxEntry is the read-side projection composed of a Message and the
// caller's own Recipient row. The HTTP layer renders one of these per
// item in the inbox listing. Translation, when non-nil, carries the
// per-recipient rendering returned from
// `Service.GetMessage(ctx, …, targetLang)` and surfaced under the
// `body_translated` payload field; Stage D ships a noop translator,
// so this field stays nil until a real backend is wired.
type InboxEntry struct {
	Message
	Recipient   Recipient
	Translation *Translation
}

// Translation mirrors a row in `backend.diplomail_translations`. The
// engine identifier is preserved so an operator can see which
// backend produced the cached rendering.
type Translation struct {
	TranslationID    uuid.UUID
	MessageID        uuid.UUID
	TargetLang       string
	TranslatedSubject string
	TranslatedBody   string
	Translator       string
	TranslatedAt     time.Time
}

// SendPersonalInput is the request payload for SendPersonal: the
// caller sending a single-recipient personal message. Validation
// (active membership, body length, etc.) is performed inside the
// service.
type SendPersonalInput struct {
	GameID          uuid.UUID
	SenderUserID    uuid.UUID
	RecipientUserID uuid.UUID
	Subject         string
	Body            string
	SenderIP        string
}

// CallerKind enumerates the privileged sender roles for admin-kind
// messages. Owners (`CallerKindOwner`) are players who own a private
// game; admins (`CallerKindAdmin`) hit the dedicated admin route;
// `CallerKindSystem` is reserved for internal lifecycle hooks.
const (
	CallerKindOwner  = "owner"
	CallerKindAdmin  = "admin"
	CallerKindSystem = "system"
)

// SendAdminPersonalInput is the request payload for an owner /
// admin / system sending an admin-kind message to a single
// recipient. Authorization (owner-vs-admin distinction) is enforced
// by the HTTP layer; the service trusts the caller designation.
type SendAdminPersonalInput struct {
	GameID          uuid.UUID
	CallerKind      string
	CallerUserID    *uuid.UUID
	CallerUsername  string
	RecipientUserID uuid.UUID
	Subject         string
	Body            string
	SenderIP        string
}

// SendAdminBroadcastInput is the request payload for an owner /
// admin / system broadcasting an admin-kind message inside a single
// game. RecipientScope selects the address book; the sender's own
// recipient row is never created (a broadcast author does not get a
// copy of their own message).
type SendAdminBroadcastInput struct {
	GameID         uuid.UUID
	CallerKind     string
	CallerUserID   *uuid.UUID
	CallerUsername string
	RecipientScope string
	Subject        string
	Body           string
	SenderIP       string
}

// LifecycleEventKind enumerates the producer-side intents the lobby
// emits when a game-state or membership-state transition lands.
const (
	LifecycleKindGamePaused          = "game.paused"
	LifecycleKindGameCancelled       = "game.cancelled"
	LifecycleKindMembershipRemoved   = "membership.removed"
	LifecycleKindMembershipBlocked   = "membership.blocked"
)

// SendPlayerBroadcastInput is the request payload for the paid-tier
// player broadcast. The sender is a player; recipients are the
// active members of the game minus the sender. The resulting message
// is `kind="personal"`, `sender_kind="player"`,
// `broadcast_scope="game_broadcast"` — recipients may reply as if it
// were a personal send, but the reply goes back to the broadcaster
// only.
type SendPlayerBroadcastInput struct {
	GameID       uuid.UUID
	SenderUserID uuid.UUID
	Subject      string
	Body         string
	SenderIP     string
}

// MultiGameBroadcastScope enumerates the admin multi-game broadcast
// modes. `selected` requires `GameIDs`; `all_running` enumerates
// every game whose status is non-terminal through GameLookup.
const (
	MultiGameScopeSelected   = "selected"
	MultiGameScopeAllRunning = "all_running"
)

// SendMultiGameBroadcastInput is the request payload for the admin
// multi-game broadcast. The service materialises one message row per
// addressed game (so a recipient who plays in two games receives two
// independently-deletable inbox entries), then fan-outs the push
// events.
type SendMultiGameBroadcastInput struct {
	CallerUsername string
	Scope          string
	GameIDs        []uuid.UUID
	RecipientScope string
	Subject        string
	Body           string
	SenderIP       string
}

// BulkCleanupInput selects messages eligible for purge. OlderThanYears
// must be >= 1; the service translates the value into a cutoff
// expressed in years and walks `GameLookup.ListFinishedGamesBefore`.
type BulkCleanupInput struct {
	OlderThanYears int
}

// CleanupResult summarises a bulk-cleanup run for the admin response
// envelope.
type CleanupResult struct {
	GameIDs        []uuid.UUID
	MessagesDeleted int
}

// AdminMessageListing is the filter passed to ListMessagesForAdmin.
// Pagination uses (Page, PageSize) consistent with the rest of the
// admin surface. Filters are AND-combined; the empty filter returns
// every persisted row.
type AdminMessageListing struct {
	Page     int
	PageSize int
	GameID   *uuid.UUID
	Kind     string
	SenderKind string
}

// AdminMessagePage is the canonical pagination envelope.
type AdminMessagePage struct {
	Items    []Message
	Total    int
	Page     int
	PageSize int
}

// LifecycleEvent is the payload lobby hands to PublishLifecycle when
// a transition needs to be reflected as durable system mail. The
// recipient set is derived by the service:
//
//   - For game.* events the message fans out to every active member
//     of the game except the actor (the actor sees the action in
//     their own UI through other channels).
//   - For membership.* events the message addresses exactly
//     `TargetUser` (the kicked player), regardless of their current
//     membership status — this is how a kicked player retains read
//     access to the explanation of the kick.
type LifecycleEvent struct {
	GameID     uuid.UUID
	Kind       string
	Actor      string
	Reason     string
	TargetUser *uuid.UUID
}

// UnreadCount carries a per-game unread-count row returned by
// UnreadCountsForUser. The lobby badge UI consumes the slice plus the
// derived total.
type UnreadCount struct {
	GameID   uuid.UUID
	GameName string
	Unread   int
}
