// Package morph answers questions about Russian grammar from a dictionary.
//
// The second of the three mechanisms replacing hand-written Russian patterns
// (Vikunja #522, owner's call 2026-08-04). internal/lexicon holds the sets that
// can be finished; the embedder recognises an open set of phrasings; and this
// package answers the questions that are about grammar rather than meaning:
//
//   - is this word a form of a verb, so it carries its own subject?
//   - are these two tokens the same word in different cases?
//
// Three places used to answer those from a list of letter endings, and each list
// was wrong in a way its own comment admitted. "канал" read as a past-tense verb
// because it ends in -ал. A list of nineteen nouns ending in л existed only to
// suppress the false positives of "ends in л means masculine past tense", which
// is a pattern conceding it is wrong. Grammar is what a dictionary is for.
//
// Not the resident model. This has to be right every time, offline, in
// microseconds, and a 1.7B is neither reliable enough nor fast enough to ask.
//
// The dictionary is github.com/aaaton/golem's Russian data, vendored. It is
// embedded in the module, so a load failure is not a network problem and not a
// config problem — it is corrupt data that got past the build. Every function
// answers conservatively in that case rather than failing the turn, and says so
// in its own doc comment.
package morph

import (
	"log"
	"strings"
	"sync"

	"github.com/aaaton/golem/v4"
	"github.com/aaaton/golem/v4/dicts/ru"
)

var (
	once    sync.Once
	lemma   *golem.Lemmatizer
	loadErr error
)

// dict loads the lemmatizer on first use. Loading costs a few megabytes of maps,
// which is why it is not done at init: a daemon that never sees Russian never
// pays for it.
func dict() *golem.Lemmatizer {
	once.Do(func() {
		lemma, loadErr = golem.New(ru.New())
		if loadErr != nil {
			// Once, not per call: this is a permanent condition and a voice loop
			// would otherwise fill the log with it at speech rate.
			log.Printf("morph: russian dictionary unavailable, answering conservatively: %v", loadErr)
		}
	})
	return lemma
}

// Available reports whether the dictionary loaded. Callers do not need it to be
// correct — every function below has a defined answer without it — but a test
// that means to measure the dictionary should skip rather than pass vacuously.
func Available() bool {
	dict()
	return loadErr == nil
}

// Lemma returns the dictionary form of a word, or the word itself when the
// dictionary does not know it or could not load. An unknown word is its own
// lemma: "бэкап" is not in the dictionary and there is nothing better to say
// about it than what he said.
func Lemma(word string) string {
	w := strings.ToLower(strings.TrimSpace(word))
	if w == "" {
		return ""
	}
	l := dict()
	if l == nil {
		return w
	}
	if got := l.Lemma(w); got != "" {
		return got
	}
	return w
}

// infinitiveEndings — how a Russian infinitive ends. This is not a stem pattern:
// it is applied to a LEMMA the dictionary returned, where the infinitive is the
// dictionary form of every verb by definition, so the test is about the
// dictionary's own output and not about the word he said.
//
// The reflexive forms are listed because a reflexive lemma keeps its particle:
// "тренировался" lemmatises to "тренироваться", which ends in "ся" rather than
// "ть".
var infinitiveEndings = []string{"ться", "тись", "чься", "ть", "ти", "чь"}

// IsVerbForm reports whether a word is some form of a verb — past tense, present,
// imperative, reflexive, participle. A verb carries its own subject and tense, so
// in Russian one verb is a whole sentence, which is what the callers care about.
//
// Without the dictionary this answers false: not knowing is not evidence that a
// word IS a verb, and the callers all treat false as the cautious direction.
func IsVerbForm(word string) bool {
	if dict() == nil {
		return false
	}
	l := Lemma(word)
	for _, e := range infinitiveEndings {
		if strings.HasSuffix(l, e) {
			return true
		}
	}
	return false
}

// SameWord reports whether two tokens are the same word in different cases —
// "режим" and "режиме", "тихий" and "тихо". It is the test a stem-plus-endings
// comparison was approximating, and it draws the line the ending list could not:
// "тихонько" and "потихоньку" are different words, and the dictionary says so
// because it has never heard of either.
//
// Without the dictionary this falls back to exact equality, which is the
// narrowest honest answer.
func SameWord(a, b string) bool {
	la, lb := Lemma(a), Lemma(b)
	if la == "" || lb == "" {
		return false
	}
	return la == lb
}
