package phraser

// The act and smart-home replies — what she says when a capability ran, refused,
// or could not be reached.
//
// Fourth family on the shared deck (deck.go). They were literals in
// ecosystem_acts.go, actions_act.go and smarthome.go, where a reworded line was
// a rebuild of the daemon that executes his house.
//
// The four outcomes stay four entries. Reporting a refusal with the wording of
// a success is the one failure mode this family can have, and a shared variant
// set is how it would happen.

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

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

//go:embed acts_ru_v1.json
var actJSON []byte

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

// The entry keys.
const (
	ActDone          = "act_done"
	ActDoneOut       = "act_done_out"
	ActDoneEntity    = "act_done_entity"
	ActConfirm       = "act_confirm"
	ActConfirmEntity = "act_confirm_entity"
	ActWhich         = "act_which"
	ActFail          = "act_fail"
	ActFailOut       = "act_fail_out"
	ActFailEntity    = "act_fail_entity"
	ActServerDown    = "act_server_down"
	ActWithdrawn     = "act_withdrawn"
	ActNeedsArgs     = "act_needs_args"
	// ActNeedsAuthedSurface — the irreversible tier, local row or Hexis
	// capability alike. Not a failure and not a refusal to help: a spoken "да"
	// is the only authority the voice path can offer, and this is the one act
	// it is not enough for (Vikunja #449, #523).
	ActNeedsAuthedSurface = "act_needs_authed_surface"

	EcoDenied        = "eco_denied"
	EcoDown          = "eco_down"
	EcoAmbiguous     = "eco_ambiguous"
	EcoUnknownEntity = "eco_unknown_entity"
	EcoNoNexus       = "eco_no_nexus"
	EcoAboutWhat     = "eco_about_what"
	EcoRecall        = "eco_recall"

	AttentionNone       = "attention_none"
	AttentionList       = "attention_list"
	AttentionFail       = "attention_fail"
	AttentionNoneEntity = "attention_none_entity"
	AttentionListEntity = "attention_list_entity"
	AttentionFailEntity = "attention_fail_entity"
	// AttentionDegraded and AttentionNoSources — the two ways an empty
	// attention list is not an all-clear (ECOSYSTEM-SPEC §2.6, Vikunja #540).
	AttentionDegraded  = "attention_degraded"
	AttentionNoSources = "attention_no_sources"
	ChangesNone        = "changes_none"
	ChangesList        = "changes_list"
	ChangesFail        = "changes_fail"
	HomeUnreachable    = "home_unreachable"
	HomeEmpty          = "home_empty"
	HomeOn             = "home_on"
	HomeDark           = "home_dark"
)

var actKeys = []string{
	ActDone, ActDoneOut, ActDoneEntity, ActConfirm, ActConfirmEntity, ActWhich,
	ActFail, ActFailOut, ActFailEntity, ActServerDown, ActWithdrawn, ActNeedsArgs,
	ActNeedsAuthedSurface,
	EcoDenied, EcoDown, EcoAmbiguous, EcoUnknownEntity, EcoNoNexus, EcoAboutWhat, EcoRecall,
	AttentionNone, AttentionList, AttentionFail,
	AttentionNoneEntity, AttentionListEntity, AttentionFailEntity,
	AttentionDegraded, AttentionNoSources,
	ChangesNone, ChangesList, ChangesFail,
	HomeUnreachable, HomeEmpty, HomeOn, HomeDark,
}

// actFloor — 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 actFloor = map[string]string{
	ActDone:               "готово.",
	ActDoneOut:            "готово: {out}",
	ActDoneEntity:         "готово: {name}.",
	ActConfirm:            "выполнить «{name}»? да или нет.",
	ActConfirmEntity:      "выполнить «{name}» для {name_entity}? да или нет.",
	ActWhich:              "какую команду для {name}: {items}?",
	ActFail:               "не получилось выполнить команду.",
	ActFailOut:            "не получилось выполнить команду: {out}",
	ActFailEntity:         "не получилось выполнить команду для {name}.",
	ActServerDown:         "инструмент есть, но сервер не подключён.",
	ActWithdrawn:          "сервер больше не отдаёт этот инструмент — сняла его с разрешённых, посмотри /tools.",
	ActNeedsArgs:          "тут нужны аргументы, из голоса не соберу. угадывать не буду.",
	ActNeedsAuthedSurface: "это из голоса не выполню — после него ничего не вернуть. запусти сам.",

	EcoDenied:        "{name} отклоняет доступ, проверь токен.",
	EcoDown:          "{name} не отвечает, попробуй ещё раз.",
	EcoAmbiguous:     "что именно: {items}?",
	EcoUnknownEntity: "не знаю, что это.",
	EcoNoNexus:       "не с чем связать — Nexus не настроен.",
	EcoAboutWhat:     "про что именно?",
	EcoRecall:        "я помню: {items}",

	AttentionNone:       "ничего не требует внимания.",
	AttentionList:       "требует внимания: {items}",
	AttentionFail:       "не могу сейчас узнать, что требует внимания.",
	AttentionNoneEntity: "по «{name}» ничего нет.",
	AttentionListEntity: "по «{name}»: {items}",
	AttentionFailEntity: "не могу сейчас узнать, что требует внимания по «{name}».",
	AttentionDegraded:   "за всё не отвечу — источники молчат: {items}.",
	AttentionNoSources:  "мне пока нечего смотреть — у Praxis нет источников.",
	ChangesNone:         "изменений нет.",
	ChangesList:         "изменения: {items}",
	ChangesFail:         "не могу сейчас узнать об изменениях.",
	HomeUnreachable:     "дом не отвечает.",
	HomeEmpty:           "дом ничего не отдаёт.",
	HomeOn:              "включено: {items}",
	HomeDark:            "не отвечают: {count} {word}.",
}

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

// LoadActs reads the embedded file. Pass a source to make the picking
// reproducible in tests; nil seeds from the clock.
func LoadActs(src rand.Source) (*Acts, error) {
	d, err := say.Load(actJSON, ActSchemaVersion, actKeys, actFloor, src)
	if err != nil {
		return nil, err
	}
	// The entries that name what ran or what he has to choose between. A
	// variant that dropped the name would confirm an act without saying which.
	for _, req := range []struct{ key, ph string }{
		{ActDoneOut, "{out}"}, {ActDoneEntity, "{name}"}, {ActFailOut, "{out}"},
		{ActFailEntity, "{name}"}, {ActConfirm, "{name}"},
		{ActConfirmEntity, "{name}"}, {ActConfirmEntity, "{name_entity}"},
		{ActWhich, "{name}"}, {ActWhich, "{items}"},
		{EcoAmbiguous, "{items}"}, {EcoRecall, "{items}"},
		{AttentionList, "{items}"}, {ChangesList, "{items}"}, {HomeOn, "{items}"},
		{EcoDenied, "{name}"}, {EcoDown, "{name}"},
		{HomeDark, "{count}"}, {HomeDark, "{word}"},
		{AttentionNoneEntity, "{name}"}, {AttentionListEntity, "{name}"},
		{AttentionListEntity, "{items}"}, {AttentionFailEntity, "{name}"},
	} {
		if err := d.RequirePlaceholder(req.key, req.ph); err != nil {
			return nil, err
		}
	}
	return &Acts{d: d}, nil
}

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

// Say returns one line for key, with the names filled into the frame.
func (a *Acts) 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 *Acts) Variants() []string { return a.deck().Variants() }

var (
	actOnce  sync.Once
	actsDeck *Acts
)

// DefaultActs returns the shared instance, loading it on first use. A broken
// file logs once and leaves a nil *Acts, which still answers from actFloor.
func DefaultActs() *Acts {
	actOnce.Do(func() {
		a, err := LoadActs(nil)
		if err != nil {
			log.Printf("phraser: act replies unavailable, using the built-in lines: %v", err)
			return
		}
		actsDeck = a
	})
	return actsDeck
}

// A — one act reply, the way every caller says it.
func A(key string, vars map[string]string) string { return DefaultActs().Say(key, vars) }

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