package game

import (
	"crypto/rand"
	"fmt"
	"math/big"

	"github.com/google/uuid"

	"scrabble/backend/internal/engine"
)

// maxSeedingRounds caps the first-move draw's tie re-draws. With a real bag a tie
// breaks with positive probability each round, so this is reached only by a
// degenerate (e.g. test) entropy source that ties forever; the cap turns that into
// an error instead of an infinite loop.
const maxSeedingRounds = 1000

// SetupDraw is one recorded tile draw of the first-move seeding — one row of
// game_setup_draws (docs/ARCHITECTURE.md §6, §9): the round, the draw order within
// it, the seated account that drew, and the decoded tile with its rank. It is
// dictionary-independent: the letter, the blank flag and the numeric rank describe
// the draw without any alphabet table.
type SetupDraw struct {
	Round   int
	PickNo  int
	Account uuid.UUID
	Letter  string
	Blank   bool
	Rank    int
}

// seedingResult is the outcome of the first-move seeding: the winning account, the
// seated accounts rotated so the winner leads (seat 0), and the full draw log to
// persist.
type seedingResult struct {
	winner uuid.UUID
	order  []uuid.UUID
	draws  []SetupDraw
}

// drawIntn returns a uniformly random integer in [0, n) for n > 0. It is the
// entropy seam of the first-move seeding: production uses crypto/rand (cryptoIntn),
// so every draw is honestly random with no single seed; tests inject a
// deterministic source.
type drawIntn func(n int) (int, error)

// cryptoIntn draws a uniform integer in [0, n) from crypto/rand — the honest,
// seedless entropy the first-move draw requires.
func cryptoIntn(n int) (int, error) {
	v, err := rand.Int(rand.Reader, big.NewInt(int64(n)))
	if err != nil {
		return 0, fmt.Errorf("game: first-move draw entropy: %w", err)
	}
	return int(v.Int64()), nil
}

// seedFirstMove runs the official first-move draw over accounts for variant v,
// drawing tiles with the entropy source intn. Each round every contender draws one
// tile (without replacement) from a fresh full bag; the tile closest to "A" wins, a
// blank beating every letter; contenders tied for the best tile re-draw in the next
// round until a single leader remains. It returns the leader, the rotation that
// seats the leader first (preserving the others' seating order), and every draw for
// the record. It performs no I/O beyond calling intn.
func seedFirstMove(v engine.Variant, accounts []uuid.UUID, intn drawIntn) (seedingResult, error) {
	if len(accounts) < 2 {
		return seedingResult{}, fmt.Errorf("game: first-move seeding needs at least 2 accounts, got %d", len(accounts))
	}
	full, err := engine.SetupBag(v)
	if err != nil {
		return seedingResult{}, err
	}
	contenders := append([]uuid.UUID(nil), accounts...)
	var draws []SetupDraw
	for round := 1; ; round++ {
		if round > maxSeedingRounds {
			return seedingResult{}, fmt.Errorf("game: first-move seeding unresolved after %d rounds", maxSeedingRounds)
		}
		bag := append([]engine.SetupTile(nil), full...)
		picks := make([]engine.SetupTile, len(contenders))
		for i, acc := range contenders {
			tile, rest, err := drawSetupTile(bag, intn)
			if err != nil {
				return seedingResult{}, err
			}
			bag = rest
			picks[i] = tile
			draws = append(draws, SetupDraw{
				Round: round, PickNo: i, Account: acc,
				Letter: tile.Letter, Blank: tile.Blank, Rank: tile.Rank,
			})
		}
		winners := bestContenders(contenders, picks)
		if len(winners) == 1 {
			return seedingResult{winner: winners[0], order: rotateToFirst(accounts, winners[0]), draws: draws}, nil
		}
		contenders = winners
	}
}

// drawSetupTile removes one uniformly random tile from bag using the entropy source
// intn and returns it with the shrunk bag (a fresh slice, leaving bag untouched).
// It is the per-tile draw primitive — the seam a future manual "player N draws a
// tile" tournament API will drive, one call per external request.
func drawSetupTile(bag []engine.SetupTile, intn drawIntn) (engine.SetupTile, []engine.SetupTile, error) {
	if len(bag) == 0 {
		return engine.SetupTile{}, nil, fmt.Errorf("game: first-move draw from an empty bag")
	}
	i, err := intn(len(bag))
	if err != nil {
		return engine.SetupTile{}, nil, err
	}
	if i < 0 || i >= len(bag) {
		return engine.SetupTile{}, nil, fmt.Errorf("game: first-move draw index %d out of range %d", i, len(bag))
	}
	tile := bag[i]
	rest := make([]engine.SetupTile, 0, len(bag)-1)
	rest = append(rest, bag[:i]...)
	rest = append(rest, bag[i+1:]...)
	return tile, rest, nil
}

// bestContenders returns the contenders whose drawn tile has the lowest (best)
// rank — the sole winner if one, else the tied set that re-draws. picks is aligned
// with contenders by index.
func bestContenders(contenders []uuid.UUID, picks []engine.SetupTile) []uuid.UUID {
	best := picks[0].Rank
	for _, p := range picks[1:] {
		if p.Rank < best {
			best = p.Rank
		}
	}
	var winners []uuid.UUID
	for i, p := range picks {
		if p.Rank == best {
			winners = append(winners, contenders[i])
		}
	}
	return winners
}

// rotateToFirst returns accounts rotated cyclically so winner sits first (seat 0),
// preserving the seating order of the rest (docs/ARCHITECTURE.md §6). winner must be
// present in accounts.
func rotateToFirst(accounts []uuid.UUID, winner uuid.UUID) []uuid.UUID {
	i := 0
	for ; i < len(accounts); i++ {
		if accounts[i] == winner {
			break
		}
	}
	out := make([]uuid.UUID, 0, len(accounts))
	out = append(out, accounts[i:]...)
	out = append(out, accounts[:i]...)
	return out
}
