package account

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"

	"github.com/go-jet/jet/v2/postgres"
	"github.com/go-jet/jet/v2/qrm"
	"github.com/google/uuid"

	"scrabble/backend/internal/postgres/jet/backend/model"
	"scrabble/backend/internal/postgres/jet/backend/table"
)

// BestMoveTile is one letter cell of a best-move word: its concrete letter (the
// designated letter for a blank), its tile point value (0 for a blank) and whether it
// is a blank. It is the persisted/served shape: the game domain marshals a slice of
// these into account_best_move.tiles, and the statistics screen renders them as game
// tiles without consulting the variant's alphabet.
type BestMoveTile struct {
	Letter string `json:"letter"`
	Value  int    `json:"value"`
	Blank  bool   `json:"blank"`
}

// BestMove is an account's highest-scoring single play within one game variant: the
// move's total score (every word it formed plus the all-tiles bonus, matching
// MaxWordPoints) and its main word as an ordered slice of tiles.
type BestMove struct {
	Variant string
	Score   int
	Tiles   []BestMoveTile
}

// Stats is a durable account's lifetime record, written by the game domain on each
// finish and read for the player's statistics screen. MaxGamePoints is the best
// single game's total; MaxWordPoints is the best single move's score (which already
// includes every word it formed plus the all-tiles bonus). BestMoves holds the same
// best move broken down per variant, with the word itself — empty for an account with
// no recorded play yet, and never carrying a variant the account has not played.
type Stats struct {
	Wins          int
	Losses        int
	Draws         int
	MaxGamePoints int
	MaxWordPoints int
	// Moves is the lifetime count of the account's plays (tile placements); HintsUsed is the
	// lifetime count of hints taken. The statistics screen shows the hint share (HintsUsed / Moves).
	Moves     int
	HintsUsed int
	BestMoves []BestMove
}

// GetStats returns the lifetime statistics for id. An account with no account_stats
// row yet — a guest, or a player who has not finished a game — yields the zero
// Stats (all counters zero) rather than an error.
func (s *Store) GetStats(ctx context.Context, id uuid.UUID) (Stats, error) {
	stmt := postgres.SELECT(table.AccountStats.AllColumns).
		FROM(table.AccountStats).
		WHERE(table.AccountStats.AccountID.EQ(postgres.UUID(id))).
		LIMIT(1)
	var row model.AccountStats
	if err := stmt.QueryContext(ctx, s.db, &row); err != nil {
		if errors.Is(err, qrm.ErrNoRows) {
			return Stats{}, nil
		}
		return Stats{}, fmt.Errorf("account: get stats %s: %w", id, err)
	}
	best, err := s.bestMoves(ctx, id)
	if err != nil {
		return Stats{}, err
	}
	return Stats{
		Wins:          int(row.Wins),
		Losses:        int(row.Losses),
		Draws:         int(row.Draws),
		MaxGamePoints: int(row.MaxGamePoints),
		MaxWordPoints: int(row.MaxWordPoints),
		Moves:         int(row.Moves),
		HintsUsed:     int(row.HintsUsed),
		BestMoves:     best,
	}, nil
}

// bestMoves reads an account's per-variant best moves, ordered by variant for a stable
// response. Each row's tiles JSON is decoded into the served BestMoveTile slice. An
// account with no recorded play yields an empty (nil) slice rather than an error.
func (s *Store) bestMoves(ctx context.Context, id uuid.UUID) ([]BestMove, error) {
	stmt := postgres.SELECT(
		table.AccountBestMove.Variant,
		table.AccountBestMove.Score,
		table.AccountBestMove.Tiles,
	).
		FROM(table.AccountBestMove).
		WHERE(table.AccountBestMove.AccountID.EQ(postgres.UUID(id))).
		ORDER_BY(table.AccountBestMove.Variant.ASC())
	var rows []model.AccountBestMove
	if err := stmt.QueryContext(ctx, s.db, &rows); err != nil {
		if errors.Is(err, qrm.ErrNoRows) {
			return nil, nil
		}
		return nil, fmt.Errorf("account: best moves %s: %w", id, err)
	}
	out := make([]BestMove, 0, len(rows))
	for _, r := range rows {
		var tiles []BestMoveTile
		if err := json.Unmarshal([]byte(r.Tiles), &tiles); err != nil {
			return nil, fmt.Errorf("account: decode best-move tiles %s/%s: %w", id, r.Variant, err)
		}
		out = append(out, BestMove{Variant: r.Variant, Score: int(r.Score), Tiles: tiles})
	}
	return out, nil
}
