package zenmoney

import (
	"encoding/json"
	"fmt"
	"strings"
	"time"
)

// Fact keys the poller writes, all under source "poll:zenmoney". Two windows,
// because they are the two questions he actually asks; a per-category
// breakdown would mean storing what he bought, and the store is not a ledger.
const (
	KeySpentToday = "money_today"
	KeySpentMonth = "money_month"
)

// Source — the provenance every money fact carries. The loop's rules trust
// source, and nothing in Maven has a rule on these keys: they are read when he
// asks, never a reason to speak. Maven is not a nag, least of all about money.
const Source = "poll:zenmoney"

// FactValue — the JSON stored in a money fact.
//
// From and AsOf are both here because the fact's own Ts can express neither.
//
//   - From is the first instant of the window the figure covers. The day fact
//     is only true for the day it was read on, and after midnight the poller
//     has nothing new to write until the first spend of the new day, so the
//     previous day's total sits there as the latest money_today looking
//     perfectly fresh. Without From, "сколько я потратил сегодня?" at 09:00
//     answered with yesterday's spending.
//   - AsOf is when the figure was last READ, not when it last changed. The
//     poller used to skip a write when the value was byte-identical, so a quiet
//     stretch left Ts pointing at the last time the number moved and the answer
//     came back prefixed "данные от 30.07" while being current and correct.
type FactValue struct {
	Spent  []Money   `json:"spent"`
	Earned []Money   `json:"earned"`
	Count  int       `json:"count"`
	From   time.Time `json:"from,omitempty"`
	AsOf   time.Time `json:"as_of,omitempty"`
}

// Value encodes the summary for the facts table, stamped with the instant it
// was read. Returns ok=false for an empty summary: no transactions read means
// no fact written, so that a failed or empty poll can never be recited back to
// him as a zero.
func (s Summary) Value(asOf time.Time) (string, bool) {
	if s.Empty() {
		return "", false
	}
	b, err := json.Marshal(FactValue{
		Spent: s.Spent, Earned: s.Earned, Count: s.Count,
		From: s.From, AsOf: asOf,
	})
	if err != nil {
		return "", false
	}
	return string(b), true
}

// CoversDay reports whether this value's window starts at now's midnight, in
// now's location. A day total whose window has rolled over is not a stale
// figure to be prefixed with a date, it is an answer to a different question,
// and it must not be spoken as today's.
//
// A value written before From existed has a zero From and fails the check,
// which is the safe direction: the next poll rewrites it.
func (v FactValue) CoversDay(now time.Time) bool {
	if v.From.IsZero() {
		return false
	}
	f := v.From.In(now.Location())
	return f.Year() == now.Year() && f.Month() == now.Month() && f.Day() == now.Day()
}

// ParseFactValue decodes a stored money fact.
func ParseFactValue(raw string) (FactValue, error) {
	var v FactValue
	if err := json.Unmarshal([]byte(raw), &v); err != nil {
		return FactValue{}, err
	}
	return v, nil
}

// FormatRU renders a money fact the way Maven says it — feminine, informal,
// and only about numbers that came from ZenMoney. window is the Russian phrase
// for the period ("сегодня", "в этом месяце").
//
// No commentary. She reports the figure and stops: an opinion about his
// spending is exactly the nagging Maven is not for.
func (v FactValue) FormatRU(window string) string {
	return v.formatRU(window, false)
}

// FormatIncomeRU is FormatRU with the income read first, for a question that
// asked about income ("сколько я заработал в этом месяце?"). Same figures, same
// refusal to comment; only the order of the two halves differs, so the number
// he asked for is the number she says first.
func (v FactValue) FormatIncomeRU(window string) string {
	return v.formatRU(window, true)
}

func (v FactValue) formatRU(window string, incomeFirst bool) string {
	if v.Count == 0 {
		return ""
	}
	spent, earned := "", ""
	if len(v.Spent) > 0 {
		if s := joinMoney(v.Spent); s != "" {
			spent = "потратил " + s
		}
	}
	if len(v.Earned) > 0 {
		if s := joinMoney(v.Earned); s != "" {
			earned = "получил " + s
		}
	}
	order := []string{spent, earned}
	if incomeFirst {
		order = []string{earned, spent}
	}
	var parts []string
	for _, p := range order {
		if p != "" {
			parts = append(parts, p)
		}
	}
	if len(parts) == 0 {
		return ""
	}
	return window + " ты " + strings.Join(parts, ", ") + "."
}

// joinMoney renders the amounts, DROPPING any whose currency could not be
// named. "сегодня ты потратил 1749.5 ?." is not something to read aloud, and a
// figure with no currency on it is not a figure he can check against his bank.
// An amount silently missing is the lesser wrong: the alternative is speaking a
// number whose units Maven does not know.
func joinMoney(ms []Money) string {
	parts := make([]string, 0, len(ms))
	for _, m := range ms {
		if m.Currency == UnknownCurrency || m.Currency == "" {
			continue
		}
		parts = append(parts, fmt.Sprintf("%s %s", formatAmount(m.Amount), m.Currency))
	}
	return strings.Join(parts, " и ")
}

// formatAmount — whole units when the amount is whole, two decimals otherwise.
// Never rounded to something prettier than the truth.
func formatAmount(a float64) string {
	if a == float64(int64(a)) {
		return fmt.Sprintf("%d", int64(a))
	}
	return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", a), "0"), ".")
}

// StaleAfter — how old a money fact may be and still be worth reciting. The
// poller is off unless configured and can be down; answering with last week's
// total as if it were today's would be a lie by omission, so a stale fact is
// reported as stale.
const StaleAfter = 26 * time.Hour
