package phraser

// The phrasing fallbacks — what she says when the model gave her nothing usable.
//
// They were four string literals spread across phraser.go, llmphraser.go and
// cmd/mavend/worldmodel.go. Every one of them is a line he hears out loud, so
// rewording one was a Go edit, a rebuild and a redeploy for what is product copy.
//
// The floor under the floor is deliberate. These strings exist because something
// already failed, so a broken template file must not be able to take the last
// words she has: every accessor falls back to the literal it replaced.

import (
	_ "embed"
	"log"
	"math/rand"
	"sync"

	"github.com/kami/maven/internal/say"
)

//go:embed fallbacks_ru_v1.json
var fallbackJSON []byte

// FallbackSchemaVersion — the version this code understands. Its own constant,
// not shared with the nudge templates or the eval fixtures: two files that change
// on different days cannot be versioned by one number (Vikunja #397).
const FallbackSchemaVersion = 1

// The entry keys. Every one of them is read by a method below, so a typo in the
// file is caught at load rather than at the moment she needs the words.
const (
	fbChat         = "chat"
	fbQueryUnknown = "query_unknown"
	fbQuerySources = "query_sources"
	fbWorldGap     = "world_gap"
)

// fbKeys — every key the code requires the file to define.
var fbKeys = []string{fbChat, fbQueryUnknown, fbQuerySources, fbWorldGap}

// hardFloor — the literal each key falls back to when the file is unusable.
// These are the exact strings that lived in Go before this file existed.
var hardFloor = map[string]string{
	fbChat:         "даже не знаю, что сказать.",
	fbQueryUnknown: "не знаю.",
	fbQuerySources: "вот что я нашла: {sources}",
	fbWorldGap:     "сейчас не могу ответить — большая модель недоступна, а придумывать не хочу.",
}

// Fallbacks picks a hand-written Russian fallback line. Safe for concurrent use.
type Fallbacks struct{ d *say.Deck }

// LoadFallbacks reads the embedded file. Pass a source to make the picking
// reproducible in tests; nil seeds from the clock.
func LoadFallbacks(src rand.Source) (*Fallbacks, error) {
	d, err := say.Load(fallbackJSON, FallbackSchemaVersion, fbKeys, hardFloor, src)
	if err != nil {
		return nil, err
	}
	// query_sources is the one entry whose whole job is to read something back.
	if err := d.RequirePlaceholder(fbQuerySources, "{sources}"); err != nil {
		return nil, err
	}
	return &Fallbacks{d: d}, nil
}

// deck reads through a nil *Fallbacks, which is the unloadable-file case.
func (f *Fallbacks) deck() *say.Deck {
	if f == nil {
		return say.FloorDeck(hardFloor)
	}
	return f.d
}

// Chat — nothing usable came back on the chat path.
func (f *Fallbacks) Chat() string { return f.deck().Text(fbChat, nil) }

// Unknown — a question she cannot answer and will not guess at.
func (f *Fallbacks) Unknown() string { return f.deck().Text(fbQueryUnknown, nil) }

// FromSources — read back what she was handed, because phrasing it failed.
func (f *Fallbacks) FromSources(sources string) string {
	return f.deck().Text(fbQuerySources, map[string]string{"sources": sources})
}

// WorldGap — the world model is the one configured to answer and it is not
// answering. Fixed wording: it names a specific gap, and a variant set here
// would let "the big model is asleep" drift into "I don't know".
func (f *Fallbacks) WorldGap() string { return f.deck().Text(fbWorldGap, nil) }

// Variants returns every line the file can produce, for the persona scorer.
func (f *Fallbacks) Variants() []string { return f.deck().Variants() }

// The process-wide instance. Package-level because these lines are needed on
// paths that have no phraser to hand — cmd/mavend names the world gap without
// one — and because a template file that is embedded and validated at load has
// nothing per-instance to configure.
var (
	fallbackOnce sync.Once
	fallbacks    *Fallbacks
)

// DefaultFallbacks returns the shared instance, loading it on first use. A
// broken file logs once and leaves a nil *Fallbacks, which still answers from
// hardFloor — a daemon must not fail to boot over its own copy deck.
func DefaultFallbacks() *Fallbacks {
	fallbackOnce.Do(func() {
		fb, err := LoadFallbacks(nil)
		if err != nil {
			log.Printf("phraser: fallbacks unavailable, using the built-in lines: %v", err)
			return
		}
		fallbacks = fb
	})
	return fallbacks
}

// ChatFallback — what she says when the chat path produced nothing.
func ChatFallback() string { return DefaultFallbacks().Chat() }

// UnknownFallback — what she says when she has no answer and will not invent one.
func UnknownFallback() string { return DefaultFallbacks().Unknown() }

// SourcesFallback — read the sources back rather than ship a broken fragment.
func SourcesFallback(sources string) string { return DefaultFallbacks().FromSources(sources) }

// WorldGap — what he hears when the world model is configured and unreachable.
func WorldGap() string { return DefaultFallbacks().WorldGap() }

// IsUnknownFallback reports whether text is one of her "I do not know" lines.
// The daemon tests read it to tell an answer from a shrug.
func IsUnknownFallback(text string) bool {
	return DefaultFallbacks().deck().Matches(fbQueryUnknown, nil, text)
}

// IsSourcesFallback reports whether text is sources read back verbatim.
func IsSourcesFallback(text, sources string) bool {
	return DefaultFallbacks().deck().Matches(fbQuerySources, map[string]string{"sources": sources}, text)
}
