// Package devsandbox provisions a ready-to-play game on backend boot
// for the `tools/local-dev` stack.
//
// Bootstrap is invoked from `backend/cmd/backend/main.go` after the
// admin bootstrap and before the HTTP listener starts. It reads
// `cfg.DevSandbox`; when `Email` is empty (the production posture)
// the function logs "skipped" and returns nil. When set, it
// idempotently:
//
//   1. registers the configured engine version and image;
//   2. find-or-creates the real dev user with the configured email;
//   3. find-or-creates `cfg.PlayerCount - 1` deterministic dummy
//      users so the engine's minimum-players constraint is met;
//   4. find-or-creates a private "Dev Sandbox" game owned by the
//      real user with min/max_players = cfg.PlayerCount and a
//      year-out turn schedule (effectively frozen at turn 1);
//   5. inserts memberships for all participants bypassing the
//      application/approval flow;
//   6. drives the lifecycle to `running` (or as far as possible if
//      the runtime is busy).
//
// The function is a no-op on subsequent boots once the game is
// running; partial states from earlier crashes are recovered.
package devsandbox

import (
	"context"
	"errors"
	"fmt"
	"time"

	"galaxy/backend/internal/config"
	"galaxy/backend/internal/lobby"
	"galaxy/backend/internal/runtime"

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

// SandboxGameName is the display name used to identify the
// auto-provisioned game on subsequent reboots. The combination of
// game_name and owner_user_id is unique enough in practice — only
// the dev sandbox bootstrap creates a game owned by the configured
// real user with this exact name.
const SandboxGameName = "Dev Sandbox"

// SandboxTurnSchedule keeps the game on turn 1 by scheduling the
// next turn a year out. The runtime scheduler still parses this and
// will tick once a year — long enough to never interfere with
// solo UI development.
const SandboxTurnSchedule = "0 0 1 1 *"

// UserEnsurer matches `auth.UserEnsurer`. We define a local
// interface to avoid importing the auth package and circular
// dependencies — the production wiring passes the same `*user.Service`
// instance used by auth.
type UserEnsurer interface {
	EnsureByEmail(ctx context.Context, email, preferredLanguage, timeZone, declaredCountry string) (uuid.UUID, error)
}

// Deps aggregates the collaborators Bootstrap needs.
type Deps struct {
	Users          UserEnsurer
	Lobby          *lobby.Service
	EngineVersions *runtime.EngineVersionService
}

// Bootstrap runs the seven-step provisioning flow described on the
// package doc comment. Errors are returned to the caller; the boot
// path in `cmd/backend/main.go` aborts startup if Bootstrap fails so
// a misconfigured dev environment surfaces immediately rather than
// silently leaving the lobby empty.
func Bootstrap(ctx context.Context, deps Deps, cfg config.DevSandboxConfig, logger *zap.Logger) error {
	if logger == nil {
		logger = zap.NewNop()
	}
	logger = logger.Named("dev_sandbox")

	if cfg.Email == "" {
		logger.Info("skipped (no email)")
		return nil
	}
	if deps.Users == nil || deps.Lobby == nil || deps.EngineVersions == nil {
		return errors.New("dev_sandbox: deps.Users, deps.Lobby and deps.EngineVersions are required")
	}
	if cfg.PlayerCount <= 0 {
		return fmt.Errorf("dev_sandbox: PlayerCount must be positive, got %d", cfg.PlayerCount)
	}

	if err := ensureEngineVersion(ctx, deps.EngineVersions, cfg, logger); err != nil {
		return err
	}

	realID, err := deps.Users.EnsureByEmail(ctx, cfg.Email, "en", "UTC", "")
	if err != nil {
		return fmt.Errorf("dev_sandbox: ensure real user: %w", err)
	}

	dummyIDs := make([]uuid.UUID, 0, cfg.PlayerCount-1)
	for i := 1; i < cfg.PlayerCount; i++ {
		email := fmt.Sprintf("dev-dummy-%02d@local.test", i)
		id, err := deps.Users.EnsureByEmail(ctx, email, "en", "UTC", "")
		if err != nil {
			return fmt.Errorf("dev_sandbox: ensure dummy %d: %w", i, err)
		}
		dummyIDs = append(dummyIDs, id)
	}

	if err := purgeTerminalSandboxGames(ctx, deps.Lobby, realID, logger); err != nil {
		return err
	}

	game, err := findOrCreateSandboxGame(ctx, deps.Lobby, realID, cfg)
	if err != nil {
		return err
	}

	game, err = ensureMembershipsAndDrive(ctx, deps.Lobby, game, realID, dummyIDs, logger)
	if err != nil {
		return err
	}

	logger.Info("bootstrap complete",
		zap.String("user_id", realID.String()),
		zap.String("game_id", game.GameID.String()),
		zap.String("status", game.Status),
	)
	return nil
}

func ensureEngineVersion(ctx context.Context, svc *runtime.EngineVersionService, cfg config.DevSandboxConfig, logger *zap.Logger) error {
	_, err := svc.Register(ctx, runtime.RegisterInput{
		Version:  cfg.EngineVersion,
		ImageRef: cfg.EngineImage,
	})
	switch {
	case err == nil:
		logger.Info("engine version registered",
			zap.String("version", cfg.EngineVersion),
			zap.String("image", cfg.EngineImage),
		)
		return nil
	case errors.Is(err, runtime.ErrEngineVersionTaken):
		logger.Debug("engine version already registered",
			zap.String("version", cfg.EngineVersion),
		)
		return nil
	default:
		return fmt.Errorf("dev_sandbox: register engine version: %w", err)
	}
}

// terminalSandboxStatus reports whether a sandbox game has reached a
// state from which it can no longer be driven back to running. We
// treat such games as "absent" so the next bootstrap creates a fresh
// one rather than handing the developer a dead lobby tile.
func terminalSandboxStatus(status string) bool {
	switch status {
	case lobby.GameStatusCancelled, lobby.GameStatusFinished, lobby.GameStatusStartFailed:
		return true
	}
	return false
}

// purgeTerminalSandboxGames deletes every previous "Dev Sandbox" game
// the dev user owns that has reached a terminal state
// (cancelled / finished / start_failed). The cascade declared in
// `00001_init.sql` removes the matching memberships, applications,
// invites, runtime records, and player mappings in the same write,
// so the developer's lobby never piles up dead tiles between
// `make rebuild` cycles. Non-terminal games are left untouched —
// a `running` sandbox from a previous boot is the happy path.
func purgeTerminalSandboxGames(ctx context.Context, svc *lobby.Service, ownerID uuid.UUID, logger *zap.Logger) error {
	games, err := svc.ListMyGames(ctx, ownerID)
	if err != nil {
		return fmt.Errorf("dev_sandbox: list my games: %w", err)
	}
	for _, g := range games {
		if g.GameName != SandboxGameName || g.OwnerUserID == nil || *g.OwnerUserID != ownerID {
			continue
		}
		if !terminalSandboxStatus(g.Status) {
			continue
		}
		if err := svc.DeleteGame(ctx, g.GameID); err != nil {
			return fmt.Errorf("dev_sandbox: delete terminal sandbox %s: %w", g.GameID, err)
		}
		logger.Info("purged terminal sandbox game",
			zap.String("game_id", g.GameID.String()),
			zap.String("status", g.Status),
		)
	}
	return nil
}

func findOrCreateSandboxGame(ctx context.Context, svc *lobby.Service, ownerID uuid.UUID, cfg config.DevSandboxConfig) (lobby.GameRecord, error) {
	games, err := svc.ListMyGames(ctx, ownerID)
	if err != nil {
		return lobby.GameRecord{}, fmt.Errorf("dev_sandbox: list my games: %w", err)
	}
	for _, g := range games {
		if g.GameName != SandboxGameName || g.OwnerUserID == nil || *g.OwnerUserID != ownerID {
			continue
		}
		// `purgeTerminalSandboxGames` ran before us, so any sandbox
		// game still in the list is either a live one we should
		// reuse or a transient state we can drive forward.
		return g, nil
	}
	rec, err := svc.CreateGame(ctx, lobby.CreateGameInput{
		OwnerUserID:         &ownerID,
		Visibility:          lobby.VisibilityPrivate,
		GameName:            SandboxGameName,
		Description:         "Auto-provisioned by backend/internal/devsandbox for solo UI development.",
		MinPlayers:          int32(cfg.PlayerCount),
		MaxPlayers:          int32(cfg.PlayerCount),
		StartGapHours:       0,
		StartGapPlayers:     0,
		EnrollmentEndsAt:    time.Now().Add(365 * 24 * time.Hour),
		TurnSchedule:        SandboxTurnSchedule,
		TargetEngineVersion: cfg.EngineVersion,
	})
	if err != nil {
		return lobby.GameRecord{}, fmt.Errorf("dev_sandbox: create game: %w", err)
	}
	return rec, nil
}

func ensureMembershipsAndDrive(ctx context.Context, svc *lobby.Service, game lobby.GameRecord, realID uuid.UUID, dummyIDs []uuid.UUID, logger *zap.Logger) (lobby.GameRecord, error) {
	caller := realID
	if game.Status == lobby.GameStatusDraft {
		next, err := svc.OpenEnrollment(ctx, &caller, false, game.GameID)
		if err != nil {
			return game, fmt.Errorf("dev_sandbox: open enrollment: %w", err)
		}
		game = next
	}

	if game.Status == lobby.GameStatusEnrollmentOpen {
		users := append([]uuid.UUID{realID}, dummyIDs...)
		for i, uid := range users {
			raceName := fmt.Sprintf("Sandbox-%02d", i+1)
			if _, err := svc.InsertMembershipDirect(ctx, lobby.InsertMembershipDirectInput{
				GameID:   game.GameID,
				UserID:   uid,
				RaceName: raceName,
			}); err != nil {
				return game, fmt.Errorf("dev_sandbox: insert membership %d: %w", i+1, err)
			}
		}
		logger.Info("memberships ensured",
			zap.Int("count", len(users)),
			zap.String("game_id", game.GameID.String()),
		)
		next, err := svc.ReadyToStart(ctx, &caller, false, game.GameID)
		if err != nil {
			return game, fmt.Errorf("dev_sandbox: ready to start: %w", err)
		}
		game = next
	}

	if game.Status == lobby.GameStatusReadyToStart {
		next, err := svc.Start(ctx, &caller, false, game.GameID)
		if err != nil {
			return game, fmt.Errorf("dev_sandbox: start: %w", err)
		}
		game = next
	}

	if game.Status == lobby.GameStatusStartFailed {
		next, err := svc.RetryStart(ctx, &caller, false, game.GameID)
		if err != nil {
			logger.Warn("retry start failed", zap.Error(err))
			return game, nil
		}
		game = next
		if game.Status == lobby.GameStatusReadyToStart {
			next, err := svc.Start(ctx, &caller, false, game.GameID)
			if err != nil {
				return game, fmt.Errorf("dev_sandbox: start after retry: %w", err)
			}
			game = next
		}
	}

	return game, nil
}
