// Package persona builds the one shared context block that goes in front of
// every LLM system prompt: who the owner is, how to address him, and what
// time it is right now.
//
// Why one block and not a line pasted into each prompt: there are five
// prompts (nudges, action replies, chat, note queries, general knowledge) and
// the "address him as ты" rule had only reached two of them. Five copies drift.
// One block cannot.
//
// The rules here are defaults in code, not config. Maven is feminine and the
// owner is a man addressed informally — that is a hard constraint of the
// product, so it must hold with an empty config file. Config only ADDS
// optional facts (his name, his city).
package persona

import (
	"fmt"
	"strings"
	"time"

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

// Facts — the optional, deployment-specific half of the block. All fields may
// be empty; the block is still correct and useful without them.
type Facts struct {
	OwnerName string // his name, e.g. "Ками"
	City      string // where he is, e.g. "Москва"
	Static    string // the free-text `persona` config string, appended verbatim

	// The two config-gated capabilities. They are listed only when this
	// deployment actually has them, because a capability she names and cannot
	// do is worse than one she never mentions.
	Weather  bool // an open-meteo provider is configured
	Telegram bool // a telegram bot token + chat id are configured
	Tools    bool // at least one shell act is on the allowlist
}

// The weekday and month names are closed classes and live in internal/lexicon,
// which indexes weekdays from Sunday the way time.Weekday does and months from
// one. This file used to carry its own copies, making four copies of the twelve
// months in the tree after cmd/mavend/ruwords.go gave up its own (Vikunja #525).

// Block renders the context block for one turn. Russian even in front of the
// English prompts: the rules it states are Russian grammar (ты/тебя, feminine
// verbs), and a Russian rule reads best stated in Russian.
//
// Keep it short. It ships on every turn to a 0.8B on laptop CPU, so every
// line here is latency.
func (f Facts) Block(now time.Time) string {
	var b strings.Builder

	b.WriteString("Ты — Maven, домашняя ассистентка. О себе говоришь в женском роде: \"я записала\", \"я проверила\".\n")

	// The address form gets its own line. It is the thing that kept getting
	// lost when it was buried in prose.
	b.WriteString("ОБРАЩЕНИЕ: владелец — мужчина, всегда на \"ты\" (ты, тебя, тебе, твой) и в единственном числе (\"выпей\", \"посмотри\"). Никогда \"вы\"/\"вас\"/\"ваш\". Никогда \"он\"/\"его\" о нём — ты говоришь ему, а не о нём. Глаголы о нём — в мужском роде (\"ты забыл\").\n")

	if who := f.who(); who != "" {
		b.WriteString(who + "\n")
	}

	b.WriteString(fmt.Sprintf("Сейчас: %s, %d %s %d, %02d:%02d (местное время).\n",
		lexicon.Weekday(int(now.Weekday())), now.Day(), lexicon.MonthGenitive(int(now.Month())), now.Year(),
		now.Hour(), now.Minute()))

	b.WriteString("Умеешь: " + strings.Join(f.can(), "; ") +
		". Других ДЕЙСТВИЙ не умеешь — если просят такое, скажи прямо.\n")

	if s := strings.TrimSpace(f.Static); s != "" {
		b.WriteString(s + "\n")
	}
	return b.String()
}

// can lists what she can really do. Every entry here is a code path that
// exists in the daemon today:
//   - reminders: IntentReminder → CoreAPI.CreateReminder, fired by the tick.
//   - notes and facts: IntentNote/IntentFact write, IntentQuery reads them back.
//   - calendar: IntentQuery answers "что у меня сегодня" from CalendarEvents.
//   - weather / telegram / shell acts: only when configured (see Facts).
//
// Nothing speculative goes in this list. A capability she offers and cannot
// perform is worse than one she never mentions.
func (f Facts) can() []string {
	c := []string{
		// Talking comes first, and the closing line says "действий" rather than
		// "ничего", because this same block sits in front of the chat and
		// general-knowledge prompts. A flat "you can do nothing else" would
		// tell her to refuse the exact thing those two prompts are for.
		"разговаривать и отвечать на вопросы",
		"ставить напоминания",
		"записывать заметки и факты и отвечать по ним",
		"смотреть календарь",
	}
	if f.Weather {
		c = append(c, "говорить погоду")
	}
	if f.Telegram {
		c = append(c, "писать в телеграм")
	}
	if f.Tools {
		c = append(c, "запускать разрешённые команды на сервере")
	}
	return c
}

// who renders the optional name/city line, or "" when neither is configured.
//
// Written as labels ("Имя владельца: ..."), not as a sentence with pronouns:
// the block's own "ты" is Maven, so "тебя зовут" would read as her name and
// "его" would model the third-person form she must never use about him.
func (f Facts) who() string {
	name := strings.TrimSpace(f.OwnerName)
	city := strings.TrimSpace(f.City)
	switch {
	case name != "" && city != "":
		return "Имя владельца: " + name + ". Город: " + city + "."
	case name != "":
		return "Имя владельца: " + name + "."
	case city != "":
		return "Город: " + city + "."
	}
	return ""
}

// Prepend puts the block in front of a system prompt. Nil-safe: a nil renderer
// (tests, the stub paths) returns the prompt untouched.
func Prepend(block func() string, prompt string) string {
	if block == nil {
		return prompt
	}
	s := strings.TrimSpace(block())
	if s == "" {
		return prompt
	}
	return s + "\n\n" + prompt
}
