package lobby

import (
	"context"
	"fmt"

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

// ListMembershipsForGame returns every membership row for gameID
// ordered by joined_at ASC. Reads always go to the store (the cache
// holds only active rows and would skip removed/blocked entries).
func (s *Service) ListMembershipsForGame(ctx context.Context, gameID uuid.UUID) ([]Membership, error) {
	if _, err := s.GetGame(ctx, gameID); err != nil {
		return nil, err
	}
	return s.deps.Store.ListMembershipsForGame(ctx, gameID)
}

// RemoveMembership transitions an active membership to `removed`. The
// caller must be the membership's user (self-leave) or the owner of
// the game (owner removal). Removing a membership releases its race
// name reservation in the same flow.
func (s *Service) RemoveMembership(ctx context.Context, callerUserID *uuid.UUID, callerIsAdmin bool, gameID, membershipID uuid.UUID) (Membership, error) {
	return s.changeMembershipStatus(ctx, callerUserID, callerIsAdmin, gameID, membershipID, MembershipStatusRemoved, NotificationLobbyMembershipRemoved, true)
}

// BlockMembership transitions an active membership to `blocked`. Only
// the owner of the game (or admin) may block.
func (s *Service) BlockMembership(ctx context.Context, callerUserID *uuid.UUID, callerIsAdmin bool, gameID, membershipID uuid.UUID) (Membership, error) {
	return s.changeMembershipStatus(ctx, callerUserID, callerIsAdmin, gameID, membershipID, MembershipStatusBlocked, NotificationLobbyMembershipBlocked, false)
}

// AdminBanMember is the admin-only variant of BlockMembership: targets
// a user_id directly (the request body carries it instead of a
// membership_id) and emits the same intent as BlockMembership.
func (s *Service) AdminBanMember(ctx context.Context, gameID, userID uuid.UUID, reason string) (Membership, error) {
	game, err := s.GetGame(ctx, gameID)
	if err != nil {
		return Membership{}, err
	}
	memberships, err := s.deps.Store.ListMembershipsForGame(ctx, gameID)
	if err != nil {
		return Membership{}, err
	}
	var target Membership
	found := false
	for _, m := range memberships {
		if m.UserID == userID && m.Status == MembershipStatusActive {
			target = m
			found = true
			break
		}
	}
	if !found {
		return Membership{}, ErrNotFound
	}
	now := s.deps.Now().UTC()
	updated, err := s.deps.Store.UpdateMembershipStatus(ctx, target.MembershipID, MembershipStatusBlocked, now)
	if err != nil {
		return Membership{}, err
	}
	s.deps.Cache.PutMembership(updated)
	intent := LobbyNotification{
		Kind:           NotificationLobbyMembershipBlocked,
		IdempotencyKey: "membership-blocked:" + updated.MembershipID.String(),
		Recipients:     []uuid.UUID{userID},
		Payload: map[string]any{
			"game_id": gameID.String(),
			"reason":  reason,
		},
	}
	if pubErr := s.deps.Notification.PublishLobbyEvent(ctx, intent); pubErr != nil {
		s.deps.Logger.Warn("admin ban notification failed",
			zap.String("membership_id", updated.MembershipID.String()),
			zap.Error(pubErr))
	}
	s.emitMembershipLifecycleMail(ctx, updated, MembershipStatusBlocked, true, reason)
	_ = game
	return updated, nil
}

// changeMembershipStatus is the shared implementation for Remove /
// Block. allowSelf controls whether the caller's own membership_id is
// an authorised target (true for Remove → "leave the game"; false for
// Block → owner-only).
func (s *Service) changeMembershipStatus(
	ctx context.Context,
	callerUserID *uuid.UUID,
	callerIsAdmin bool,
	gameID, membershipID uuid.UUID,
	newStatus, notificationKind string,
	allowSelf bool,
) (Membership, error) {
	membership, err := s.deps.Store.LoadMembership(ctx, membershipID)
	if err != nil {
		return Membership{}, err
	}
	if membership.GameID != gameID {
		return Membership{}, ErrNotFound
	}
	if membership.Status != MembershipStatusActive {
		return Membership{}, fmt.Errorf("%w: membership is %q", ErrConflict, membership.Status)
	}
	game, err := s.GetGame(ctx, gameID)
	if err != nil {
		return Membership{}, err
	}
	if !callerIsAdmin {
		if !s.canManageMembership(game, membership, callerUserID, allowSelf) {
			return Membership{}, fmt.Errorf("%w: caller is not authorised to manage this membership", ErrForbidden)
		}
	}
	now := s.deps.Now().UTC()
	updated, err := s.deps.Store.UpdateMembershipStatus(ctx, membershipID, newStatus, now)
	if err != nil {
		return Membership{}, err
	}
	s.deps.Cache.PutMembership(updated)
	if newStatus != MembershipStatusActive {
		// Release the race-name reservation tied to this game.
		if err := s.deps.Store.DeleteRaceName(ctx, CanonicalKey(membership.CanonicalKey), gameID); err != nil {
			s.deps.Logger.Warn("release race name on membership change failed",
				zap.String("membership_id", membershipID.String()),
				zap.String("canonical_key", membership.CanonicalKey),
				zap.Error(err))
		} else {
			s.deps.Cache.RemoveRaceName(CanonicalKey(membership.CanonicalKey))
		}
	}
	intent := LobbyNotification{
		Kind:           notificationKind,
		IdempotencyKey: notificationKind + ":" + updated.MembershipID.String(),
		Recipients:     []uuid.UUID{updated.UserID},
		Payload: map[string]any{
			"game_id": gameID.String(),
		},
	}
	if pubErr := s.deps.Notification.PublishLobbyEvent(ctx, intent); pubErr != nil {
		s.deps.Logger.Warn("membership notification failed",
			zap.String("membership_id", updated.MembershipID.String()),
			zap.String("kind", notificationKind),
			zap.Error(pubErr))
	}
	s.emitMembershipLifecycleMail(ctx, updated, newStatus, callerIsAdmin, "")
	return updated, nil
}

// emitMembershipLifecycleMail asks the diplomail publisher to drop a
// durable explanation into the kicked player's inbox. The mail
// survives the membership row going to `removed` / `blocked` so the
// player keeps read access to it (soft-access rule, item 8).
func (s *Service) emitMembershipLifecycleMail(ctx context.Context, membership Membership, newStatus string, callerIsAdmin bool, reason string) {
	var kind string
	switch newStatus {
	case MembershipStatusRemoved:
		kind = LifecycleKindMembershipRemoved
	case MembershipStatusBlocked:
		kind = LifecycleKindMembershipBlocked
	default:
		return
	}
	actor := "the game owner"
	if callerIsAdmin {
		actor = "an administrator"
	}
	target := membership.UserID
	ev := LifecycleEvent{
		GameID:     membership.GameID,
		Kind:       kind,
		Actor:      actor,
		Reason:     reason,
		TargetUser: &target,
	}
	if err := s.deps.Diplomail.PublishLifecycle(ctx, ev); err != nil {
		s.deps.Logger.Warn("publish membership lifecycle mail failed",
			zap.String("membership_id", membership.MembershipID.String()),
			zap.String("kind", kind),
			zap.Error(err))
	}
}

func (s *Service) canManageMembership(game GameRecord, membership Membership, callerUserID *uuid.UUID, allowSelf bool) bool {
	if game.Visibility == VisibilityPublic {
		// Public-game membership management is admin-only.
		return false
	}
	if game.OwnerUserID != nil && callerUserID != nil && *game.OwnerUserID == *callerUserID {
		return true
	}
	if allowSelf && callerUserID != nil && membership.UserID == *callerUserID {
		return true
	}
	return false
}
