package scenario

import (
	"context"
	"fmt"
	"math/rand"
	"time"

	"scrabble/loadtest/internal/edge"
	"scrabble/loadtest/internal/moves"
	"scrabble/loadtest/internal/seed"
)

// Game is one assembled match: its id, variant and members in seat order (Members[0]
// is the inviter, seat 0).
type Game struct {
	ID      string
	Variant string
	Members []seed.Account
}

// seatOf returns the seat index of accountID in the game, or -1.
func (g *Game) seatOf(accountID string) int {
	for i, m := range g.Members {
		if m.ID.String() == accountID {
			return i
		}
	}
	return -1
}

// assembleCohort forms games among a cohort of active players via the invitation
// flow, aiming for gamesPerPlayer (3-5) concurrent games per player with 2-4 players
// each. It returns the games it managed to start. Failures are logged and skipped so
// a partial assembly still drives load.
func (d *Driver) assembleCohort(ctx context.Context, cohort []seed.Account, gamesPerPlayer int, rng *rand.Rand) []*Game {
	if len(cohort) < 2 {
		return nil
	}
	c := edge.New(d.gateway) // one client for the assembly burst; players play on their own
	gamesOf := make(map[string]int, len(cohort))
	var games []*Game
	for i := range cohort {
		inviter := cohort[i]
		target := 3 + rng.Intn(3) // 3..5
		if gamesPerPlayer > 0 {
			target = gamesPerPlayer
		}
		for gamesOf[inviter.ID.String()] < target {
			members := pickMembers(cohort, inviter, rng)
			if len(members) < 2 {
				break
			}
			variant := moves.Variants()[rng.Intn(len(moves.Variants()))]
			g, err := d.assemble(ctx, c, members, variant)
			if err != nil {
				d.log.Debug("assemble game", "err", err)
				break
			}
			games = append(games, g)
			for _, m := range members {
				gamesOf[m.ID.String()]++
			}
		}
	}
	return games
}

// pickMembers builds a 2-4 player group led by inviter, drawing distinct others from
// the cohort at random.
func pickMembers(cohort []seed.Account, inviter seed.Account, rng *rand.Rand) []seed.Account {
	size := 2 + rng.Intn(3) // 2..4
	members := []seed.Account{inviter}
	seen := map[string]bool{inviter.ID.String(): true}
	for attempts := 0; len(members) < size && attempts < 4*size; attempts++ {
		cand := cohort[rng.Intn(len(cohort))]
		if seen[cand.ID.String()] {
			continue
		}
		seen[cand.ID.String()] = true
		members = append(members, cand)
	}
	return members
}

// assemble runs the invitation flow for one game: the inviter (members[0]) invites
// the rest, each invitee accepts the pending invitation, and the completing accept
// starts the game, which is then located in the inviter's game list.
func (d *Driver) assemble(ctx context.Context, c *edge.Client, members []seed.Account, variant string) (*Game, error) {
	inviter := members[0]
	inviteeIDs := make([]string, len(members)-1)
	for i, m := range members[1:] {
		inviteeIDs[i] = m.ID.String()
	}

	t0 := time.Now()
	code, err := c.CreateInvitation(ctx, inviter.Token, inviteeIDs, variant)
	d.rec.Record("invitation.create", code, time.Since(t0))
	if err != nil || code != "ok" {
		return nil, fmt.Errorf("invitation.create: %s", code)
	}

	for _, invitee := range members[1:] {
		t0 = time.Now()
		list, lc, err := c.ListInvitations(ctx, invitee.Token)
		d.rec.Record("invitation.list", lc, time.Since(t0))
		if err != nil || lc != "ok" {
			return nil, fmt.Errorf("invitation.list: %s", lc)
		}
		invID := findPending(list, inviter.ID.String())
		if invID == "" {
			return nil, fmt.Errorf("no pending invitation from %s", inviter.ID)
		}
		t0 = time.Now()
		ac, err := c.AcceptInvitation(ctx, invitee.Token, invID)
		d.rec.Record("invitation.accept", ac, time.Since(t0))
		if err != nil || ac != "ok" {
			return nil, fmt.Errorf("invitation.accept: %s", ac)
		}
	}

	t0 = time.Now()
	games, gc, err := c.GamesList(ctx, inviter.Token)
	d.rec.Record("games.list", gc, time.Since(t0))
	if err != nil || gc != "ok" {
		return nil, fmt.Errorf("games.list: %s", gc)
	}
	ids := make([]string, len(members))
	for i, m := range members {
		ids[i] = m.ID.String()
	}
	gameID := findGame(games, ids)
	if gameID == "" {
		return nil, fmt.Errorf("started game not found for %d members", len(members))
	}
	return &Game{ID: gameID, Variant: variant, Members: members}, nil
}

// findPending returns the id of a pending invitation from inviterID, or "".
func findPending(list []edge.Invitation, inviterID string) string {
	for _, inv := range list {
		if inv.InviterID == inviterID && inv.Status == "pending" {
			return inv.ID
		}
	}
	return ""
}

// findGame returns the id of the active game whose seat set equals memberIDs, or "".
func findGame(games []edge.Game, memberIDs []string) string {
	want := make(map[string]bool, len(memberIDs))
	for _, id := range memberIDs {
		want[id] = true
	}
	for _, g := range games {
		if !g.Active() || len(g.Seats) != len(memberIDs) {
			continue
		}
		match := true
		for _, s := range g.Seats {
			if !want[s] {
				match = false
				break
			}
		}
		if match {
			return g.ID
		}
	}
	return ""
}
