package lobby

import (
	"context"
	"fmt"

	"github.com/google/uuid"
)

// InsertMembershipDirectInput is the parameter struct for
// Service.InsertMembershipDirect.
type InsertMembershipDirectInput struct {
	GameID   uuid.UUID
	UserID   uuid.UUID
	RaceName string
}

// InsertMembershipDirect grants a membership to userID inside gameID
// bypassing the application/approval flow. It performs the same DB
// writes as ApproveApplication: the per-game race-name reservation
// row plus the membership row, and refreshes the in-memory caches.
//
// The method is intended for boot-time provisioning by
// `backend/internal/devsandbox` and similar trusted callers. It is
// not exposed through any HTTP handler. The caller must guarantee
// game.Status == GameStatusEnrollmentOpen — the function returns
// ErrConflict otherwise — and that the race-name policy and
// canonical-key invariants are honoured (the implementation reuses
// the lobby's own Policy and assertRaceNameAvailable so a duplicate
// or unsuitable name still fails).
//
// Idempotency: if a membership for (GameID, UserID) already exists
// the function returns the existing row without modifying state.
// This makes the helper safe to call on every backend boot from
// devsandbox.Bootstrap.
func (s *Service) InsertMembershipDirect(ctx context.Context, in InsertMembershipDirectInput) (Membership, error) {
	displayName, err := ValidateDisplayName(in.RaceName)
	if err != nil {
		return Membership{}, err
	}
	game, err := s.GetGame(ctx, in.GameID)
	if err != nil {
		return Membership{}, err
	}
	if game.Status != GameStatusEnrollmentOpen {
		return Membership{}, fmt.Errorf("%w: game status is %q, want enrollment_open", ErrConflict, game.Status)
	}
	canonical, err := s.deps.Policy.Canonical(displayName)
	if err != nil {
		return Membership{}, err
	}
	existing, err := s.deps.Store.ListMembershipsForGame(ctx, in.GameID)
	if err != nil {
		return Membership{}, err
	}
	for _, m := range existing {
		if m.UserID == in.UserID && m.Status == MembershipStatusActive {
			return m, nil
		}
	}
	if err := s.assertRaceNameAvailable(ctx, canonical, in.UserID, in.GameID); err != nil {
		return Membership{}, err
	}
	now := s.deps.Now().UTC()
	if _, err := s.deps.Store.InsertRaceName(ctx, raceNameInsert{
		Name:        displayName,
		Canonical:   canonical,
		Status:      RaceNameStatusReservation,
		OwnerUserID: in.UserID,
		GameID:      in.GameID,
		ReservedAt:  &now,
	}); err != nil {
		return Membership{}, err
	}
	membership, err := s.deps.Store.InsertMembership(ctx, membershipInsert{
		MembershipID: uuid.New(),
		GameID:       in.GameID,
		UserID:       in.UserID,
		RaceName:     displayName,
		CanonicalKey: canonical,
	})
	if err != nil {
		_ = s.deps.Store.DeleteRaceName(ctx, canonical, in.GameID)
		return Membership{}, err
	}
	s.deps.Cache.PutMembership(membership)
	s.deps.Cache.PutRaceName(RaceNameEntry{
		Name:        displayName,
		Canonical:   canonical,
		Status:      RaceNameStatusReservation,
		OwnerUserID: in.UserID,
		GameID:      in.GameID,
		ReservedAt:  &now,
	})
	return membership, nil
}
