package game

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"slices"

	"github.com/google/uuid"

	"scrabble/backend/internal/engine"
)

// DraftTile is one tile a player has laid on the board but not yet submitted.
type DraftTile struct {
	Row    int    `json:"row"`
	Col    int    `json:"col"`
	Letter string `json:"letter"`
	Blank  bool   `json:"blank"`
}

// Draft is a player's persisted client-side composition for a game: the
// preferred rack tile order and the board tiles laid but not yet submitted. The server
// keeps it so a reload or a second device resumes the same arrangement.
type Draft struct {
	RackOrder  string
	BoardTiles []DraftTile
}

// GetDraft returns the player's draft for a game, or a zero Draft when none is stored.
func (svc *Service) GetDraft(ctx context.Context, gameID, accountID uuid.UUID) (Draft, error) {
	return svc.store.getDraft(ctx, gameID, accountID)
}

// SaveDraft upserts the player's draft; the account must be seated in the game.
func (svc *Service) SaveDraft(ctx context.Context, gameID, accountID uuid.UUID, d Draft) error {
	seats, _, _, err := svc.Participants(ctx, gameID)
	if err != nil {
		return err
	}
	if !slices.Contains(seats, accountID) {
		return ErrNotAPlayer
	}
	return svc.store.saveDraft(ctx, gameID, accountID, d)
}

// getDraft reads one draft row, returning a zero Draft when absent.
func (s *Store) getDraft(ctx context.Context, gameID, accountID uuid.UUID) (Draft, error) {
	var rackOrder string
	var boardJSON []byte
	err := s.db.QueryRowContext(ctx,
		`SELECT rack_order, board_tiles FROM backend.game_drafts WHERE game_id = $1 AND account_id = $2`,
		gameID, accountID).Scan(&rackOrder, &boardJSON)
	if errors.Is(err, sql.ErrNoRows) {
		return Draft{}, nil
	}
	if err != nil {
		return Draft{}, fmt.Errorf("game: get draft %s: %w", gameID, err)
	}
	d := Draft{RackOrder: rackOrder}
	if len(boardJSON) > 0 {
		if err := json.Unmarshal(boardJSON, &d.BoardTiles); err != nil {
			return Draft{}, fmt.Errorf("game: decode draft tiles: %w", err)
		}
	}
	return d, nil
}

// saveDraft upserts the player's draft.
func (s *Store) saveDraft(ctx context.Context, gameID, accountID uuid.UUID, d Draft) error {
	tiles := d.BoardTiles
	if tiles == nil {
		tiles = []DraftTile{}
	}
	boardJSON, err := json.Marshal(tiles)
	if err != nil {
		return fmt.Errorf("game: encode draft tiles: %w", err)
	}
	if _, err := s.db.ExecContext(ctx,
		`INSERT INTO backend.game_drafts (game_id, account_id, rack_order, board_tiles, updated_at)
		 VALUES ($1, $2, $3, $4, now())
		 ON CONFLICT (game_id, account_id)
		 DO UPDATE SET rack_order = $3, board_tiles = $4, updated_at = now()`,
		gameID, accountID, d.RackOrder, boardJSON); err != nil {
		return fmt.Errorf("game: save draft: %w", err)
	}
	return nil
}

// clearDraft drops a player's draft row (their composition is consumed or discarded).
func (s *Store) clearDraft(ctx context.Context, gameID, accountID uuid.UUID) error {
	if _, err := s.db.ExecContext(ctx,
		`DELETE FROM backend.game_drafts WHERE game_id = $1 AND account_id = $2`,
		gameID, accountID); err != nil {
		return fmt.Errorf("game: clear draft: %w", err)
	}
	return nil
}

// resetConflictingBoardDrafts clears the board_tiles of every OTHER player's draft that has
// a tile on one of the just-committed cells, since that draft can no longer be placed; the
// rack order is kept.
func (s *Store) resetConflictingBoardDrafts(ctx context.Context, gameID, actorID uuid.UUID, cells []DraftTile) error {
	if len(cells) == 0 {
		return nil
	}
	occupied := make(map[[2]int]bool, len(cells))
	for _, c := range cells {
		occupied[[2]int{c.Row, c.Col}] = true
	}
	rows, err := s.db.QueryContext(ctx,
		`SELECT account_id, board_tiles FROM backend.game_drafts
		 WHERE game_id = $1 AND account_id <> $2 AND board_tiles <> '[]'::jsonb`,
		gameID, actorID)
	if err != nil {
		return fmt.Errorf("game: scan drafts for conflict: %w", err)
	}
	var toClear []uuid.UUID
	func() {
		defer func() { _ = rows.Close() }()
		for rows.Next() {
			var acc uuid.UUID
			var boardJSON []byte
			if err = rows.Scan(&acc, &boardJSON); err != nil {
				return
			}
			var tiles []DraftTile
			if json.Unmarshal(boardJSON, &tiles) != nil {
				continue // skip a malformed draft
			}
			for _, t := range tiles {
				if occupied[[2]int{t.Row, t.Col}] {
					toClear = append(toClear, acc)
					break
				}
			}
		}
		err = rows.Err()
	}()
	if err != nil {
		return fmt.Errorf("game: read drafts for conflict: %w", err)
	}
	for _, acc := range toClear {
		if _, err := s.db.ExecContext(ctx,
			`UPDATE backend.game_drafts SET board_tiles = '[]'::jsonb, updated_at = now()
			 WHERE game_id = $1 AND account_id = $2`,
			gameID, acc); err != nil {
			return fmt.Errorf("game: clear conflicting draft: %w", err)
		}
	}
	return nil
}

// draftTilesFrom projects a play's committed tiles into draft cells, for the conflict scan.
func draftTilesFrom(rec engine.MoveRecord) []DraftTile {
	out := make([]DraftTile, 0, len(rec.Tiles))
	for _, t := range rec.Tiles {
		out = append(out, DraftTile{Row: t.Row, Col: t.Col, Letter: t.Letter, Blank: t.Blank})
	}
	return out
}
