package say

// Package say holds the mechanics every family of hand-written Russian lines
// shares. The families themselves live next to the code that speaks them.
//
// Deck — the mechanics every family of hand-written Russian lines shares.
//
// A family is one embedded JSON file: schema-versioned, several variants per
// entry, never the same variant twice running, and a hard floor of Go literals
// under it so a broken file cannot take her words away. fallbacks.go was the
// first family (Vikunja #501) and acks.go the second, at which point copying
// eighty lines of loader per family stopped being defensible.
//
// What stays per family: the file, the keys, the floor literals, the accessor
// names, and any validation only that family can state.

import (
	"encoding/json"
	"fmt"
	"math/rand"
	"strings"
	"sync"
	"time"
)

// Entry — one line she can say, in as many wordings as the file gives.
type Entry struct {
	// Fixed — one variant, never picked between. For wording that must not
	// drift from turn to turn, like a phrase naming one specific gap.
	Fixed    bool     `json:"fixed"`
	Variants []string `json:"variants"`
}

type file struct {
	SchemaVersion int              `json:"schema_version"`
	Name          string           `json:"name"`
	Notes         []string         `json:"notes"`
	Entries       map[string]Entry `json:"entries"`
}

// Deck picks a line. Safe for concurrent use. A Deck with no entries answers
// from the floor, which is what an unloadable file leaves behind (FloorDeck).
type Deck struct {
	mu    sync.Mutex
	rnd   *rand.Rand
	last  map[string]string
	file  file
	keys  []string
	floor map[string]string
}

// loadDeck parses raw, checks the version and every required key, and seeds the
// picker. Pass a source to make the picking reproducible in tests; nil seeds
// from the clock.
func Load(raw []byte, version int, keys []string, floor map[string]string, src rand.Source) (*Deck, error) {
	var f file
	if err := json.Unmarshal(raw, &f); err != nil {
		return nil, fmt.Errorf("parse: %w", err)
	}
	if f.SchemaVersion != version {
		return nil, fmt.Errorf("schema_version %d, want %d", f.SchemaVersion, version)
	}
	for _, k := range keys {
		e, ok := f.Entries[k]
		if !ok || len(e.Variants) == 0 {
			return nil, fmt.Errorf("entry %q is missing or empty", k)
		}
		if e.Fixed && len(e.Variants) != 1 {
			return nil, fmt.Errorf("entry %q is fixed but has %d variants", k, len(e.Variants))
		}
	}
	if src == nil {
		src = rand.NewSource(time.Now().UnixNano())
	}
	return &Deck{rnd: rand.New(src), last: map[string]string{}, file: f, keys: keys, floor: floor}, nil
}

// requirePlaceholder fails the load when a variant of key does not use ph. For
// an entry whose whole job is to read something back, a variant without the
// placeholder silently drops it.
func (d *Deck) RequirePlaceholder(key, ph string) error {
	for _, v := range d.file.Entries[key].Variants {
		if !strings.Contains(v, ph) {
			return fmt.Errorf("%q variant %q does not use %s", key, v, ph)
		}
	}
	return nil
}

// text returns one variant for key with the placeholders filled in. A nil
// receiver answers from the floor, so no caller checks whether the file loaded.
func (d *Deck) Text(key string, vars map[string]string) string {
	tmpl := ""
	if d != nil {
		if e, ok := d.file.Entries[key]; ok && len(e.Variants) > 0 {
			tmpl = d.pick(key, fillable(e.Variants, vars))
		}
	}
	if tmpl == "" {
		tmpl = floorOf(d, key)
	}
	return fill(tmpl, vars)
}

// matches reports whether text is a line key could have produced. A caller that
// has to recognise one of these lines cannot compare against a literal any more.
func (d *Deck) Matches(key string, vars map[string]string, text string) bool {
	if fill(floorOf(d, key), vars) == text {
		return true
	}
	if d == nil {
		return false
	}
	for _, v := range d.file.Entries[key].Variants {
		if fill(v, vars) == text {
			return true
		}
	}
	return false
}

// variants returns every line the file can produce, in key order, for the
// persona scorer. Stable order so a failure names the same variant twice.
func (d *Deck) Variants() []string {
	if d == nil {
		return nil
	}
	var out []string
	for _, k := range d.keys {
		out = append(out, d.file.Entries[k].Variants...)
	}
	return out
}

// VariantsOf returns the wordings the file gives for one key, for a test that
// has something to say about every one of them.
func (d *Deck) VariantsOf(key string) []string {
	if d == nil {
		return nil
	}
	return d.file.Entries[key].Variants
}

// UnfixedSingles lists the keys with exactly one variant that are not marked
// fixed. Nothing breaks on one — the picker has nothing to pick either way — but
// the flag is what a reader goes by, and parallel entries disagreeing about it
// is how a family stops being readable. Load already rejects the other half of
// the rule, fixed with more than one variant, so this is the pair to it.
func (d *Deck) UnfixedSingles() []string {
	if d == nil {
		return nil
	}
	var out []string
	for _, k := range d.keys {
		if e := d.file.Entries[k]; len(e.Variants) == 1 && !e.Fixed {
			out = append(out, k)
		}
	}
	return out
}

// fillable narrows variants to the ones this call can actually say, which is
// the rule an optional placeholder needs: a caller with nothing to put in
// {tail} must not be handed a variant that has one. Two passes, because both
// halves matter. The first keeps only variants whose every placeholder has a
// non-empty value, so an absent optional never reaches him as braces. The
// second prefers, among those, the variants using the most of what the caller
// supplied, so a caveat he was given is not dropped for a shorter wording.
// Nothing fillable leaves the list alone, and the unfilled placeholder shows
// up in the answer rather than turning it into silence.
func fillable(variants []string, vars map[string]string) []string {
	if len(variants) < 2 {
		return variants
	}
	best, bestUsed := make([]string, 0, len(variants)), -1
	for _, v := range variants {
		used := 0
		ok := true
		for _, ph := range placeholders(v) {
			if vars[ph] == "" {
				ok = false
				break
			}
			used++
		}
		if !ok || used < bestUsed {
			continue
		}
		if used > bestUsed {
			best, bestUsed = best[:0], used
		}
		best = append(best, v)
	}
	if len(best) == 0 {
		return variants
	}
	return best
}

// placeholders lists the {name}s in tmpl, in order.
func placeholders(tmpl string) []string {
	var out []string
	for {
		i := strings.IndexByte(tmpl, '{')
		if i < 0 {
			return out
		}
		j := strings.IndexByte(tmpl[i:], '}')
		if j < 0 {
			return out
		}
		out = append(out, tmpl[i+1:i+j])
		tmpl = tmpl[i+j+1:]
	}
}

// pick chooses at random, skipping whatever this entry said last time.
func (d *Deck) pick(key string, variants []string) string {
	d.mu.Lock()
	defer d.mu.Unlock()

	choices := variants
	if len(choices) > 1 {
		fresh := make([]string, 0, len(choices))
		for _, v := range choices {
			if v != d.last[key] {
				fresh = append(fresh, v)
			}
		}
		if len(fresh) > 0 {
			choices = fresh
		}
	}
	got := choices[d.rnd.Intn(len(choices))]
	d.last[key] = got
	return got
}

// floorOf reads the Go literal behind key. Every deck carries its own family's
// map, including the floor-only deck an unloadable file leaves behind, so no
// lookup ever crosses families. It used to go through one global map keyed by
// bare entry name, which two families both calling an entry query_unknown
// silently shared: whichever registered last answered for both (Vikunja #521).
func floorOf(d *Deck, key string) string {
	if d == nil {
		return ""
	}
	return d.floor[key]
}

// FloorDeck — the deck a family falls back to when its file will not load. It
// has no entries, so every read drops through to the floor literals, and it is
// a real *Deck so no accessor has to know which case it is in.
func FloorDeck(floor map[string]string) *Deck {
	return &Deck{last: map[string]string{}, floor: floor}
}

// fill substitutes {name} for each var. A placeholder with no value is left
// alone rather than blanked, so a missing value is visible instead of silent.
func fill(tmpl string, vars map[string]string) string {
	for k, v := range vars {
		tmpl = strings.ReplaceAll(tmpl, "{"+k+"}", v)
	}
	return tmpl
}
