package say

// The summary sentences — what she says around aggregated data: the morning
// plan, the ranked task list, and the habits read back out of behaviour records.
//
// Fifth family on the deck, and the first one outside internal/phraser. It
// lives here because its three callers — internal/morning, internal/tasks and
// internal/memory — sit under phraser in the import graph and cannot reach it.
//
// The "I have not seen enough yet" sentences are the load-bearing ones. Three
// days of taps and a year of them produce the same "обычно ты ...", and only one
// of those is worth believing, so the empty cases say she has not seen a
// pattern rather than that he has none.

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

//go:embed summary_ru_v1.json
var summaryJSON []byte

// SummarySchemaVersion — this family's own version.
const SummarySchemaVersion = 1

// The entry keys.
const (
	PlanRestEmpty = "plan_rest_empty"
	PlanDayEmpty  = "plan_day_empty"
	PlanDay       = "plan_day"
	PlanUncertain = "plan_uncertain"

	// "что дальше?" — the next few entries, not the day. PlanNextMore is the
	// same sentence when the cap hid something, so the count it states is the
	// only signal that the day is not over after the last line read.
	PlanNext     = "plan_next"
	PlanNextMore = "plan_next_more"

	TasksNone       = "tasks_none"
	TasksFirst      = "tasks_first"
	TasksCandidates = "tasks_candidates"

	// The counted stall shapes on /tasks and in the spoken list (V-512). Each
	// one states a count and nothing about what it means: "лежит дольше десяти
	// дней" is arithmetic, "стоит бросить" would be a judgement she may not make.
	StallOverdue     = "stall_overdue"
	StallSitting     = "stall_sitting"
	StallUnconfirmed = "stall_unconfirmed"

	ReasonOverdue     = "reason_overdue"
	ReasonOverdueDays = "reason_overdue_days"
	ReasonToday       = "reason_today"
	ReasonTomorrow    = "reason_tomorrow"
	ReasonInDays      = "reason_in_days"
	ReasonImportant   = "reason_important"
	ReasonUrgent      = "reason_urgent"
	ReasonStale       = "reason_stale"

	HabitWeekday     = "habit_weekday"
	HabitWeekdaySame = "habit_weekday_same"
	HabitWeekdayNone = "habit_weekday_none"
	HabitWeekendBoth = "habit_weekend_both"
	HabitWeekendSat  = "habit_weekend_sat"
	HabitWeekendSun  = "habit_weekend_sun"
	HabitWeekendSame = "habit_weekend_same"
	HabitWeekendNone = "habit_weekend_none"
	HabitOverall     = "habit_overall"
	HabitOverallNone = "habit_overall_none"
	HabitSpanToday   = "habit_span_today"
	HabitSpanDays    = "habit_span_days"
	HabitUnglossed   = "habit_unglossed"
	HabitAt          = "habit_at"
)

var summaryKeys = []string{
	PlanRestEmpty, PlanDayEmpty, PlanDay, PlanUncertain,
	PlanNext, PlanNextMore,
	TasksNone, TasksFirst, TasksCandidates,
	StallOverdue, StallSitting, StallUnconfirmed,
	ReasonOverdue, ReasonOverdueDays, ReasonToday, ReasonTomorrow,
	ReasonInDays, ReasonImportant, ReasonUrgent, ReasonStale,
	HabitWeekday, HabitWeekdaySame, HabitWeekdayNone,
	HabitWeekendBoth, HabitWeekendSat, HabitWeekendSun, HabitWeekendSame, HabitWeekendNone,
	HabitOverall, HabitOverallNone, HabitSpanToday, HabitSpanDays,
	HabitUnglossed, HabitAt,
}

// summaryFloor — the literal each key falls back to when the file is unusable.
// It started as the exact strings that lived in Go before this file existed and
// now tracks the file's first variant instead, because a floor that keeps the
// wording review threw out would say it back on the one turn nobody is watching.
var summaryFloor = map[string]string{
	PlanRestEmpty: "на сегодня больше ничего не запланировано.",
	PlanDayEmpty:  "на {date} ничего не запланировано.",
	PlanDay:       "план на {date}: {items}",
	PlanUncertain: "похоже, {line}",
	PlanNext:      "дальше: {items}",
	PlanNextMore:  "дальше: {items}. и ещё {n} {word} до конца дня.",

	TasksNone:       "задач нет.",
	TasksFirst:      "сначала: {items}",
	TasksCandidates: "нашла ещё, но ты не подтверждал: {items}",

	StallOverdue:     "{n} {word} просрочено",
	StallSitting:     "{n} {word} лежит дольше {days} {dayword}",
	StallUnconfirmed: "{n} {word} ждёт подтверждения",

	ReasonOverdue:     "просрочено",
	ReasonOverdueDays: "просрочено на {n} {word}",
	ReasonToday:       "сегодня",
	ReasonTomorrow:    "завтра",
	ReasonInDays:      "через {n} {word}",
	ReasonImportant:   "важно",
	ReasonUrgent:      "срочно",
	ReasonStale:       "давно в списке",

	HabitWeekday:     "по {day} ты обычно {items}.",
	HabitWeekdaySame: "по {day} всё как обычно — то же, что и в остальные дни: {items}.",
	HabitWeekdayNone: "по {day} я пока не вижу ничего постоянного.",
	HabitWeekendBoth: "по субботам ты обычно {items_sat}, по воскресеньям — {items_sun}.",
	HabitWeekendSat:  "по субботам ты обычно {items}, а по воскресеньям постоянного нет.",
	HabitWeekendSun:  "по воскресеньям ты обычно {items}, а по субботам постоянного нет.",
	HabitWeekendSame: "по выходным всё как обычно — то же, что и в остальные дни: {items}.",
	HabitWeekendNone: "по выходным я пока не вижу ничего постоянного.",
	HabitOverall:     "обычно ты {items} — {span}.",
	HabitOverallNone: "записей пока мало, про привычки не скажу.",
	HabitSpanToday:   "по записям за сегодня",
	HabitSpanDays:    "по записям за последние {n} {word}",
	HabitUnglossed:   "отмечаешь «{key}»",
	HabitAt:          "{gloss} около {time}",
}

// Summaries picks a hand-written Russian summary sentence. Safe for concurrent
// use.
type Summaries struct{ d *Deck }

// LoadSummaries reads the embedded file. Pass a source to make the picking
// reproducible in tests; nil seeds from the clock.
func LoadSummaries(src rand.Source) (*Summaries, error) {
	d, err := Load(summaryJSON, SummarySchemaVersion, summaryKeys, summaryFloor, src)
	if err != nil {
		return nil, err
	}
	// The entries that exist to read the aggregate back. A variant without the
	// placeholder would summarise the data by dropping it.
	for _, req := range []struct{ key, ph string }{
		{PlanDayEmpty, "{date}"}, {PlanDay, "{date}"}, {PlanDay, "{items}"},
		{PlanUncertain, "{line}"},
		{PlanNext, "{items}"},
		{PlanNextMore, "{items}"}, {PlanNextMore, "{n}"}, {PlanNextMore, "{word}"},
		{TasksFirst, "{items}"}, {TasksCandidates, "{items}"},
		{StallOverdue, "{n}"}, {StallOverdue, "{word}"},
		{StallSitting, "{n}"}, {StallSitting, "{word}"},
		{StallSitting, "{days}"}, {StallSitting, "{dayword}"},
		{StallUnconfirmed, "{n}"}, {StallUnconfirmed, "{word}"},
		{ReasonOverdueDays, "{n}"}, {ReasonOverdueDays, "{word}"},
		{ReasonInDays, "{n}"}, {ReasonInDays, "{word}"},
		{HabitWeekday, "{day}"}, {HabitWeekday, "{items}"},
		{HabitWeekdaySame, "{day}"}, {HabitWeekdaySame, "{items}"},
		{HabitWeekdayNone, "{day}"},
		{HabitWeekendBoth, "{items_sat}"}, {HabitWeekendBoth, "{items_sun}"},
		{HabitWeekendSat, "{items}"}, {HabitWeekendSun, "{items}"},
		{HabitWeekendSame, "{items}"},
		{HabitOverall, "{items}"}, {HabitOverall, "{span}"},
		{HabitSpanDays, "{n}"}, {HabitSpanDays, "{word}"},
		{HabitUnglossed, "{key}"}, {HabitAt, "{gloss}"}, {HabitAt, "{time}"},
	} {
		if err := d.RequirePlaceholder(req.key, req.ph); err != nil {
			return nil, err
		}
	}
	return &Summaries{d: d}, nil
}

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

// Say returns one line for key, with the values filled into the frame.
func (s *Summaries) Say(key string, vars map[string]string) string {
	return s.deck().Text(key, vars)
}

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

var (
	summaryOnce sync.Once
	summaries   *Summaries
)

// DefaultSummaries returns the shared instance, loading it on first use. A
// broken file logs once and leaves a nil *Summaries, which still answers from
// summaryFloor.
func DefaultSummaries() *Summaries {
	summaryOnce.Do(func() {
		s, err := LoadSummaries(nil)
		if err != nil {
			log.Printf("say: summary lines unavailable, using the built-in ones: %v", err)
			return
		}
		summaries = s
	})
	return summaries
}

// S — one summary sentence, the way every caller says it.
func S(key string, vars map[string]string) string { return DefaultSummaries().Say(key, vars) }

// IsS reports whether text is a line key could have produced, for the tests.
func IsS(key string, vars map[string]string, text string) bool {
	return DefaultSummaries().deck().Matches(key, vars, text)
}
