// Package social owns the player-facing social fabric around games: the friend
// graph (request/accept), per-user blocks, and per-game chat with nudges folded
// in as a message kind. It owns the friendships, blocks and chat_messages tables,
// reads the account-level block toggles through account.Store, and gates chat and
// nudge on game state through a GameReader so it never imports the engine. The
// live delivery of chat and nudges (push / in-app stream) belongs to the gateway;
// this package only persists and reads them.
package social

import (
	"context"
	"errors"
	"time"

	"github.com/google/uuid"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/trace"

	"scrabble/backend/internal/account"
	"scrabble/backend/internal/notify"
)

// tracerName scopes the social domain's OpenTelemetry spans.
const tracerName = "scrabble/backend/social"

// GameReader is the slice of the game domain the social package needs: the seated
// accounts in seat order, the seat index whose turn it is, and the game status, plus
// a shared-game test. game.Service satisfies it, so chat, nudge and the
// befriend-an-opponent gate work without a dependency on the engine or the game's
// private state.
type GameReader interface {
	Participants(ctx context.Context, gameID uuid.UUID) (seats []uuid.UUID, toMove int, status string, err error)
	// SeatName is the display name of the account seated in the game (its per-game snapshot,
	// falling back to the account's current name), or "" when it holds no seat; it names a
	// nudge's sender in the toast and the out-of-app push by the same identity the game uses.
	SeatName(ctx context.Context, gameID, accountID uuid.UUID) (string, error)
	// SharedGame reports whether two accounts are seated together in any game
	// (active or finished); it gates the "befriend an opponent" request path.
	SharedGame(ctx context.Context, a, b uuid.UUID) (bool, error)
	// LastMoveAt is the time of an account's most recent move in a game (and whether it
	// has moved); the nudge cooldown resets once the player has taken a turn.
	LastMoveAt(ctx context.Context, gameID, accountID uuid.UUID) (time.Time, bool, error)
	// TurnStartedAt is the start time of the game's current turn; it bounds the
	// one-chat-message-per-turn limit (a message counts toward the current turn when it
	// was posted at or after this time).
	TurnStartedAt(ctx context.Context, gameID uuid.UUID) (time.Time, error)
	// VsAI reports whether the game is an honest-AI game, in which chat and nudge are
	// both disabled (the game still reports status 'active').
	VsAI(ctx context.Context, gameID uuid.UUID) (bool, error)
}

// Sentinel errors returned by the service.
var (
	// ErrSelfRelation is returned when an account targets itself.
	ErrSelfRelation = errors.New("social: cannot target yourself")
	// ErrRequestExists is returned when a friend request or friendship already
	// exists between the two accounts (in either direction).
	ErrRequestExists = errors.New("social: a friend request or friendship already exists")
	// ErrRequestBlocked is returned when the addressee does not accept friend
	// requests (their global toggle) or a block stands between the two accounts.
	ErrRequestBlocked = errors.New("social: the addressee is not accepting friend requests")
	// ErrGuestForbidden is returned when a guest attempts a durable-only action (send a friend
	// request, redeem a friend code); friends are a durable-account feature.
	ErrGuestForbidden = errors.New("social: guests cannot use friends")
	// ErrAccountDeleted is returned when a social action targets a deleted (tombstoned)
	// account: nobody is behind it any more, so it can be neither befriended nor blocked.
	// The client already hides those controls on a deleted seat; this is the server source
	// of truth, and the guard an older client hits.
	ErrAccountDeleted = errors.New("social: this account has been deleted")
	// ErrRequestNotFound is returned when no pending friend request matches.
	ErrRequestNotFound = errors.New("social: no pending friend request")
	// ErrNoSharedGame is returned when a friend request targets someone the
	// requester has never shared a game with (the befriend-an-opponent gate).
	ErrNoSharedGame = errors.New("social: you can only request someone you have played with")
	// ErrRequestDeclined is returned when the addressee has previously declined a
	// request from this requester; a re-send is refused (a one-time friend code
	// from the addressee bypasses this).
	ErrRequestDeclined = errors.New("social: this person has declined your friend request")
	// ErrFriendCodeInvalid is returned when a redeemed friend code is unknown,
	// already used, or expired.
	ErrFriendCodeInvalid = errors.New("social: friend code is invalid or expired")
	// ErrNotParticipant is returned when an account is not seated in the game.
	ErrNotParticipant = errors.New("social: account is not a player in this game")
	// ErrMessageNotFound is returned when an admin message lookup finds no such message.
	ErrMessageNotFound = errors.New("social: message not found")
	// ErrChatBlocked is returned when the sender has disabled chat for themselves.
	ErrChatBlocked = errors.New("social: chat is disabled for this account")
	// ErrRecipientBlocked is returned when a player tries to chat to, or nudge, someone
	// they have blocked — the blocker-side guard behind the hidden composer (a blocked
	// player's own sends are never refused, only silently suppressed, so they never notice).
	ErrRecipientBlocked = errors.New("social: cannot message a blocked player")
	// ErrMessageTooLong is returned when a chat message exceeds the rune limit.
	ErrMessageTooLong = errors.New("social: message exceeds the length limit")
	// ErrEmptyMessage is returned when a chat message is blank after trimming.
	ErrEmptyMessage = errors.New("social: message is empty")
	// ErrNudgeOnOwnTurn is returned when a player tries to nudge while it is their
	// own turn (there is no awaited opponent to nudge).
	ErrNudgeOnOwnTurn = errors.New("social: cannot nudge while it is your turn")
	// ErrNudgeTooSoon is returned when the per-game once-per-hour nudge limit is hit.
	ErrNudgeTooSoon = errors.New("social: a nudge was already sent in the last hour")
	// ErrGameNotActive is returned when a nudge is attempted on a finished game.
	ErrGameNotActive = errors.New("social: game is not active")
	// ErrGameVsAI is returned when chat or nudge is attempted in an honest-AI game,
	// where both are disabled (the UI also disables the controls).
	ErrGameVsAI = errors.New("social: chat and nudge are disabled in AI games")
	// ErrChatNotYourTurn is returned when a chat message is sent while it is not the
	// sender's turn — chat is allowed only on your own turn (the opponent's-turn control
	// is the nudge).
	ErrChatNotYourTurn = errors.New("social: cannot chat while it is not your turn")
	// ErrChatAlreadySentThisTurn is returned when the sender has already posted a chat
	// message during the current turn — at most one message is allowed per turn.
	ErrChatAlreadySentThisTurn = errors.New("social: already sent a chat message this turn")
)

// Service is the social domain. It is the only writer of the friendships, blocks
// and chat_messages tables and is safe for concurrent use.
type Service struct {
	store    *Store
	accounts *account.Store
	games    GameReader
	pub      notify.Publisher
	metrics  *socialMetrics
	tracer   trace.Tracer
	now      func() time.Time
}

// NewService constructs a Service. store owns the social tables; accounts supplies
// the block toggles; games gates chat and nudge on game state.
func NewService(store *Store, accounts *account.Store, games GameReader) *Service {
	return &Service{
		store:    store,
		accounts: accounts,
		games:    games,
		pub:      notify.Nop{},
		metrics:  defaultSocialMetrics(),
		tracer:   otel.Tracer(tracerName),
		now:      func() time.Time { return time.Now().UTC() },
	}
}

// SetNotifier installs the live-event publisher used to push chat messages and
// nudges to their recipients. It must be called during startup wiring, before
// the service serves traffic; the default is notify.Nop (no live events).
func (svc *Service) SetNotifier(p notify.Publisher) {
	if p != nil {
		svc.pub = p
	}
}
