package phraser

// The capture acknowledgements — what she says after storing something he said,
// and what she says when storing it failed.
//
// They were string literals in eight files under cmd/mavend plus the stub
// replier in internal/voice. He hears them many times a day, which is exactly
// why they need variants and exactly why rewording one must not be a rebuild.
// Same shape as fallbacks_ru_v1.json, on the shared deck (deck.go).
//
// His data stays Go-side. The file holds "отметила: {key} = {value}"; the key
// and the value are interpolated here, so nothing he said lives in the copy.

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

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

//go:embed ack_ru_v1.json
var ackJSON []byte

// AckSchemaVersion — this family's own version. A file that changes on a
// different day than the fallbacks cannot share their number (Vikunja #397).
const AckSchemaVersion = 1

// The entry keys. Namespaced by family, because the floor map behind a nil deck
// is process-wide.
const (
	AckFact          = "ack_fact"
	AckFactKey       = "ack_fact_key"
	AckFactValue     = "ack_fact_kv"
	AckNote          = "ack_note"
	AckReminder      = "ack_reminder"
	AckAct           = "ack_act"
	AckTask          = "ack_task"
	AckTaskUrgent    = "ack_task_urgent"
	AckTaskDuplicate = "ack_task_duplicate"
	AckNudge         = "ack_nudge"
	AckSnooze        = "ack_snooze"
	AckGeneric       = "ack_generic"
	AckQuietOn       = "quiet_on"
	AckQuietOff      = "quiet_off"

	FailFact         = "fail_fact"
	FailFactUnparsed = "fail_fact_unparsed"
	FailNote         = "fail_note"
	FailReminder     = "fail_reminder"
	FailReminderTime = "fail_reminder_time"
	FailTask         = "fail_task"
	FailAck          = "fail_ack"
	FailSnooze       = "fail_snooze"
	FailQuiet        = "fail_quiet"
)

// ackKeys — every key the code requires the file to define.
var ackKeys = []string{
	AckFact, AckFactKey, AckFactValue, AckNote, AckReminder, AckAct,
	AckTask, AckTaskUrgent, AckTaskDuplicate, AckNudge, AckSnooze, AckGeneric,
	AckQuietOn, AckQuietOff,
	FailFact, FailFactUnparsed, FailNote, FailReminder, FailReminderTime,
	FailTask, FailAck, FailSnooze, FailQuiet,
}

// ackFloor — 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 ackFloor = map[string]string{
	AckFact:          "записала факт.",
	AckFactKey:       "отметила: {key}",
	AckFactValue:     "отметила: {key} = {value}",
	AckNote:          "сохранила заметку.",
	AckReminder:      "напомню.",
	AckAct:           "ок, записала действие: {fn}",
	AckTask:          "записала: {text}",
	AckTaskUrgent:    "поняла, беру в работу: {text}",
	AckTaskDuplicate: "это уже в списке.",
	AckNudge:         "отлично, отметила.",
	AckSnooze:        "хорошо, вернусь к этому позже.",
	AckGeneric:       "приняла.",
	AckQuietOn:       "тихий режим включён. буду реже напоминать.",
	AckQuietOff:      "тихий режим выключен.",

	FailFact:         "не получилось сохранить факт.",
	FailFactUnparsed: "не разобрала, что записать — попробуй иначе.",
	FailNote:         "не получилось сохранить заметку.",
	FailReminder:     "не получилось поставить напоминание.",
	FailReminderTime: "не получилось разобрать время напоминания.",
	FailTask:         "не получилось записать задачу.",
	FailAck:          "не получилось отметить.",
	FailSnooze:       "не получилось отложить.",
	FailQuiet:        "не получилось переключить тихий режим.",
}

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

// LoadAcks reads the embedded file. Pass a source to make the picking
// reproducible in tests; nil seeds from the clock.
func LoadAcks(src rand.Source) (*Acks, error) {
	d, err := say.Load(ackJSON, AckSchemaVersion, ackKeys, ackFloor, src)
	if err != nil {
		return nil, err
	}
	// The three entries that exist to read his own words back. A variant
	// without the placeholder would confirm the capture and drop what was
	// captured, which reads as a successful save of nothing.
	for _, req := range []struct{ key, ph string }{
		{AckFactKey, "{key}"}, {AckFactValue, "{key}"}, {AckFactValue, "{value}"},
		{AckAct, "{fn}"}, {AckTask, "{text}"}, {AckTaskUrgent, "{text}"},
	} {
		if err := d.RequirePlaceholder(req.key, req.ph); err != nil {
			return nil, err
		}
	}
	return &Acks{d: d}, nil
}

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

// Say returns one line for key, with his data filled into the frame. Pass nil
// when the entry takes none.
func (a *Acks) Say(key string, vars map[string]string) string {
	return a.deck().Text(key, vars)
}

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

var (
	ackOnce sync.Once
	acks    *Acks
)

// DefaultAcks returns the shared instance, loading it on first use. A broken
// file logs once and leaves a nil *Acks, which still answers from ackFloor.
func DefaultAcks() *Acks {
	ackOnce.Do(func() {
		a, err := LoadAcks(nil)
		if err != nil {
			log.Printf("phraser: acknowledgements unavailable, using the built-in lines: %v", err)
			return
		}
		acks = a
	})
	return acks
}

// Ack — one acknowledgement line, the way every caller says it.
func Ack(key string, vars map[string]string) string { return DefaultAcks().Say(key, vars) }

// IsAck reports whether text is a line key could have produced. For the daemon
// tests, which can no longer compare against one literal.
func IsAck(key string, vars map[string]string, text string) bool {
	return DefaultAcks().deck().Matches(key, vars, text)
}
