package game

import (
	"context"
	"errors"
	"fmt"

	"github.com/google/uuid"

	"scrabble/backend/internal/engine"
)

// ReplayStep is one step of an admin game replay: the move that produced it (nil for the
// initial dealt state, step 0) and the resulting position — every seat's rack, the running
// scores, whose turn it is and the bag remainder. Step k's board is the union of every
// play's placements through step k, which the renderer accumulates onto an empty grid
// (docs/ARCHITECTURE.md §9.1 visual replay).
type ReplayStep struct {
	// Move is the journalled move that produced this state, or nil for the initial deal.
	Move *HistoryMove
	// Drawn lists the tiles the mover drew from the bag after this move ("?" for a blank);
	// empty for the initial deal, a pass or a resignation.
	Drawn []string
	// Racks holds every seat's rack at this step, indexed by seat ("?" for a blank).
	Racks [][]string
	// Scores holds every seat's running score, indexed by seat.
	Scores []int
	// ToMove is the seat to move at this step.
	ToMove int
	// BagLen is the number of tiles left in the bag at this step.
	BagLen int
}

// ReplayTimelineView is the admin replay of a game: the persisted game plus the ordered
// replay steps (the initial deal followed by one step per journalled move).
type ReplayTimelineView struct {
	Game  Game
	Steps []ReplayStep
}

// ReplayTimeline rebuilds a game from its pinned seed and journal and returns the ordered
// replay steps for the admin console: the initial deal (step 0) then one step per
// journalled move, each carrying the resulting racks, scores, turn cursor, bag size and the
// tiles the mover drew. The deterministic bag makes the reconstruction exact. It needs no
// dictionary beyond the engine the seed deals, and — like the live replay — stops early if a
// committed move became illegal under tightened rules rather than failing.
func (svc *Service) ReplayTimeline(ctx context.Context, gameID uuid.UUID) (ReplayTimelineView, error) {
	pre, err := svc.store.GetGame(ctx, gameID)
	if err != nil {
		return ReplayTimelineView{}, err
	}
	seed, err := svc.store.GameSeed(ctx, gameID)
	if err != nil {
		return ReplayTimelineView{}, err
	}
	g, err := engine.New(svc.registry, engine.Options{
		Variant:              pre.Variant,
		Version:              pre.DictVersion,
		Players:              pre.Players,
		Seed:                 seed,
		DropoutTiles:         pre.DropoutTiles,
		MultipleWordsPerTurn: pre.MultipleWordsPerTurn,
		NoRepeatWords:        pre.NoRepeatWords,
	})
	if err != nil {
		return ReplayTimelineView{}, err
	}
	moves, err := svc.store.GetJournal(ctx, gameID)
	if err != nil {
		return ReplayTimelineView{}, err
	}
	steps := make([]ReplayStep, 0, len(moves)+1)
	steps = append(steps, snapshotStep(g, nil, nil))
	for i := range moves {
		mv := moves[i]
		before := g.Hand(mv.Seat)
		if err := replayMove(g, mv); err != nil {
			if errors.Is(err, engine.ErrIllegalPlay) {
				g.Abort()
				break
			}
			return ReplayTimelineView{}, fmt.Errorf("game: replay-timeline %s move %d: %w", gameID, mv.Seq, err)
		}
		moveCopy := mv
		steps = append(steps, snapshotStep(g, &moveCopy, drawnTiles(before, g.Hand(mv.Seat), usedTiles(mv))))
	}
	return ReplayTimelineView{Game: pre, Steps: steps}, nil
}

// snapshotStep captures the position after applying move (nil for the initial deal): every
// seat's rack and score, the turn cursor and the bag size, with the supplied drawn tiles.
func snapshotStep(g *engine.Game, move *HistoryMove, drawn []string) ReplayStep {
	n := g.Players()
	racks := make([][]string, n)
	scores := make([]int, n)
	for i := 0; i < n; i++ {
		racks[i] = g.Hand(i)
		scores[i] = g.Score(i)
	}
	return ReplayStep{Move: move, Drawn: drawn, Racks: racks, Scores: scores, ToMove: g.ToMove(), BagLen: g.BagLen()}
}

// usedTiles returns the rack tiles a move consumed ("?" for a blank): the placed tiles of a
// play or the swapped tiles of an exchange; a pass or resignation consumes none.
func usedTiles(mv HistoryMove) []string {
	switch mv.Action {
	case "play":
		used := make([]string, len(mv.Tiles))
		for i, t := range mv.Tiles {
			if t.Blank {
				used[i] = "?" // a placed blank leaves the rack as the blank marker
			} else {
				used[i] = t.Letter
			}
		}
		return used
	case "exchange":
		return mv.Exchanged
	}
	return nil
}

// drawnTiles returns the tiles the mover drew from the bag: the post-move rack (after) minus
// the tiles kept (before minus used). It compares the racks as multisets, so duplicate
// letters are counted correctly.
func drawnTiles(before, after, used []string) []string {
	kept := make(map[string]int, len(before))
	for _, t := range before {
		kept[t]++
	}
	for _, t := range used {
		if kept[t] > 0 {
			kept[t]--
		}
	}
	var drawn []string
	for _, t := range after {
		if kept[t] > 0 {
			kept[t]--
			continue
		}
		drawn = append(drawn, t)
	}
	return drawn
}
