package engine

import (
	"fmt"
	"sort"

	"gitea.iliadenisov.ru/developer/scrabble-solver/scrabble"
)

// The no-repeat-words rule of Russian "Эрудит": a word laid on the board belongs to the game
// from then on and cannot be laid again. It is a rule of that variant only — official Scrabble
// places no such restriction — and it is pinned per game (Options.NoRepeatWords) rather than
// derived from the variant, so a game started before the rule existed keeps replaying and
// scoring exactly as it did when it was played.
//
// The rule reads the game's own move log, which is the record of every word ever formed, and
// applies in two different ways:
//
//   - the play's main word must not already be there — such a play is illegal, and is neither
//     accepted nor offered by the move generator;
//   - a perpendicular cross-word that is already there is allowed, because it is incidental to
//     laying the main word, but it scores nothing.
//
// The rule is deliberately kept out of the solver, which stays a stateless, standard-rules
// engine: only this package knows a game's history.

// playedWords returns the set of words already formed in this game, as the decoded strings the
// move log records — the main word and every cross-word of every play. Both this set and the
// words a candidate move is checked against come from the same decoder, so they compare exactly.
func (g *Game) playedWords() map[string]struct{} {
	played := make(map[string]struct{})
	for _, rec := range g.log {
		if rec.Action != ActionPlay {
			continue
		}
		for _, w := range rec.Words {
			played[w] = struct{}{}
		}
	}
	return played
}

// noRepeat applies the no-repeat-words rule to an already validated move, given the set of words
// already played (see playedWords). It returns ErrIllegalPlay when the move's main word is one of
// them, and otherwise the move with every already-played cross-word scored down to zero and the
// total reduced to match. A game without the rule gets its move back untouched.
func (g *Game) noRepeat(m scrabble.Move, played map[string]struct{}) (scrabble.Move, error) {
	if !g.noRepeatWords {
		return m, nil
	}
	if main := g.word(m.Main); isPlayed(played, main) {
		return scrabble.Move{}, fmt.Errorf("%w: word %q is already on the board", ErrIllegalPlay, main)
	}
	for i, cw := range m.Cross {
		if !isPlayed(played, g.word(cw)) {
			continue
		}
		m.Score -= cw.Score
		m.Cross[i].Score = 0
	}
	return m, nil
}

// isPlayed reports whether word is in the played set. The empty string is never played: it is
// what the decoder yields for a malformed letter index, and such a word must not silently match.
func isPlayed(played map[string]struct{}, word string) bool {
	if word == "" {
		return false
	}
	_, ok := played[word]
	return ok
}

// noRepeatFilter applies the rule across a generated move list: it drops the plays whose main
// word is already on the board, rescores the rest, and re-ranks them by the adjusted score. The
// sort is stable, so plays the solver ranked equally keep its order. A game without the rule
// gets its list back untouched.
func (g *Game) noRepeatFilter(moves []scrabble.Move) []scrabble.Move {
	if !g.noRepeatWords {
		return moves
	}
	played := g.playedWords()
	out := moves[:0]
	for _, m := range moves {
		adjusted, err := g.noRepeat(m, played)
		if err != nil {
			continue
		}
		out = append(out, adjusted)
	}
	sort.SliceStable(out, func(i, j int) bool { return out[i].Score > out[j].Score })
	return out
}
