package lobby

import (
	"context"
	"errors"
	"fmt"

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

// OnUserBlocked releases every lobby binding owned by the user under
// the `blocked` semantics: active memberships flip to `blocked`,
// pending applications get rejected, pending invites incoming get
// declined / outgoing get revoked, race-name entries are deleted, and
// owned games in non-running statuses are cancelled.
//
// Implements `internal/user.LobbyCascade.OnUserBlocked`. Errors during
// the cascade are joined and returned but never roll back the
// already-committed user write — the canonical state is the row in
// Postgres.
func (s *Service) OnUserBlocked(ctx context.Context, userID uuid.UUID) error {
	return s.runCascade(ctx, userID, MembershipStatusBlocked)
}

// OnUserDeleted runs the same cascade as OnUserBlocked but transitions
// memberships to `removed` instead of `blocked`. Implements
// `internal/user.LobbyCascade.OnUserDeleted`.
func (s *Service) OnUserDeleted(ctx context.Context, userID uuid.UUID) error {
	return s.runCascade(ctx, userID, MembershipStatusRemoved)
}

func (s *Service) runCascade(ctx context.Context, userID uuid.UUID, membershipStatus string) error {
	snap, err := s.deps.Store.LoadCascadeSnapshot(ctx, userID)
	if err != nil {
		return fmt.Errorf("lobby cascade: load snapshot: %w", err)
	}
	if snap.empty() {
		return nil
	}
	now := s.deps.Now().UTC()
	if err := s.deps.Store.CascadeUser(ctx, userID, snap, membershipStatus, now); err != nil {
		return fmt.Errorf("lobby cascade: write: %w", err)
	}
	s.deps.Cache.EvictUserMemberships(userID)
	s.deps.Cache.EvictUserRaceNames(userID)
	s.deps.Cache.EvictOwnerGames(userID)

	var notifyErrs []error
	for _, gameID := range snap.OwnedGameIDs {
		s.deps.Cache.RemoveGame(gameID)
	}
	if len(snap.ActiveMembershipIDs) > 0 {
		intent := LobbyNotification{
			Kind:           NotificationLobbyMembershipRemoved,
			IdempotencyKey: "user-cascade-membership:" + userID.String(),
			Recipients:     []uuid.UUID{userID},
			Payload: map[string]any{
				"reason": membershipStatus,
			},
		}
		if pubErr := s.deps.Notification.PublishLobbyEvent(ctx, intent); pubErr != nil {
			notifyErrs = append(notifyErrs, pubErr)
		}
	}
	if len(notifyErrs) > 0 {
		s.deps.Logger.Warn("lobby cascade notification failures",
			zap.String("user_id", userID.String()),
			zap.Int("notify_errors", len(notifyErrs)))
	}
	return errors.Join(notifyErrs...)
}

func (snap CascadeUserSnapshot) empty() bool {
	return len(snap.OwnedGameIDs) == 0 &&
		len(snap.ActiveMembershipIDs) == 0 &&
		len(snap.PendingApplications) == 0 &&
		len(snap.IncomingInvites) == 0 &&
		len(snap.OutgoingInvites) == 0 &&
		len(snap.RaceNameKeys) == 0
}
