package main

import (
	"context"
	"fmt"
	"log"
	"strings"
	"unicode"

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

// Ordinal selection over a list she just read (Vikunja #448).
//
// The dialogue session already carried the intent, the slots and the history.
// What it did not carry was the list: she recited five tasks, he said "второй",
// and there was nothing for that word to point at, so it routed as a fresh
// utterance and meant nothing.
//
// Candidates are bound when she speaks the list, in the order she spoke it (see
// tasks.Spoken). Binding afterwards would resolve "второй" against a fresh
// query, and the list can change between two turns.
//
// An ordinal with no verb is read back, not acted on: "второй" names a task, it
// does not say what to do with it. Acting on the bare word would guess, and a
// wrong guess here closes work he never finished.

// The position words come from the lexicon, which lists every form with its
// position and "последний" as -1 (V-522). They used to be stem prefixes here —
// {"перв", 1}, {"втор", 2} — which is the shape that sweep removed: a stem
// decides meaning by guessing where a word ends, and "трет" also opens
// "third-party". The lexicon runs to twelve rather than five, so he can pick
// past the fifth of a longer list; resolveCandidate already answers a position
// she did not read.

// candidateDigits — "второй" said as a number. Matched whole, never by prefix:
// "15" starts with "1" and is a time, not a position. Digits are not a Russian
// word list, so they stay here rather than in the lexicon.
var candidateDigits = map[string]int{"1": 1, "2": 2, "3": 3, "4": 4, "5": 5}

// parseOrdinal reads which position he named. 0 and false when he named none.
// A negative result means the last one.
func parseOrdinal(text string) (int, bool) {
	// Token by token, not substring: " 1" would otherwise match inside
	// "напомни в 15:00" and turn a reminder into a selection.
	toks := strings.FieldsFunc(strings.ToLower(text), func(r rune) bool {
		return !unicode.IsLetter(r) && !unicode.IsDigit(r)
	})
	for i, tok := range toks {
		if n, ok := candidateDigits[tok]; ok {
			return n, true
		}
		// A spoken half hour names the hour it is entering with the same
		// genitive ordinal: "в половине восьмого" is 07:30, not the eighth
		// thing she read out. She reads a list and he answers with a time
		// often enough that this has to be declined here, or the reminder
		// becomes a selection.
		if i > 0 && lexicon.IsHalfHour(toks[i-1]) {
			continue
		}
		if n, ok := lexicon.Ordinal(tok); ok {
			return n, true
		}
	}
	return 0, false
}

// candidateVerbs — what he wants done with the one he picked. Nothing here is
// destructive: a task moves forward or is dropped, and both are recorded with a
// provenance the /tasks page shows.
var candidateVerbs = []struct {
	words  []string
	status string
	say    string
}{
	{[]string{"готов", "сделал", "выполнил", "закрыл", "done"}, store.TaskDone, "закрыла"},
	{[]string{"не надо", "убери", "отмени", "не буду", "drop"}, store.TaskDropped, "убрала"},
	{[]string{"подтвержда", "беру", "да,", "буду делать"}, store.TaskOpen, "взяла в работу"},
}

func parseCandidateVerb(text string) (status, say string, ok bool) {
	s := strings.ToLower(strings.TrimSpace(text))
	for _, v := range candidateVerbs {
		for _, w := range v.words {
			if strings.Contains(s, w) {
				return v.status, v.say, true
			}
		}
	}
	return "", "", false
}

// offerCandidates records the list she just read, so his next words can pick
// from it. Best effort: no session store, or a session that expired between the
// question and the answer, means the words route normally.
func (h *reactiveHandler) offerCandidates(ctx context.Context, cands []dialogue.Candidate) {
	if h.dialogueSessions == nil || len(cands) == 0 {
		return
	}
	h.dialogueSessions.SetCandidates(dialogueIDOf(ctx), h.now(), cands)
}

// resolveCandidate handles "второй", "первую сделал", "последнюю убери" against
// the list she just read.
func (h *reactiveHandler) resolveCandidate(ctx context.Context, text string, src turnSource) (string, bool) {
	if h.dialogueSessions == nil {
		return "", false
	}
	sess := h.dialogueSessions.Get(dialogueIDOf(ctx), h.now())
	if sess == nil || len(sess.Candidates) == 0 {
		return "", false
	}
	nth, ok := parseOrdinal(text)
	if !ok {
		return "", false
	}
	if nth < 0 {
		nth = len(sess.Candidates)
	}
	if nth > len(sess.Candidates) {
		// Claim the turn: he is picking from her list and named a position she
		// did not read. Routing it fresh would answer something else entirely.
		return fmt.Sprintf("я назвала только %d.", len(sess.Candidates)), true
	}
	pick := sess.Candidates[nth-1]
	status, say, hasVerb := parseCandidateVerb(text)
	if !hasVerb || pick.Kind != "task" {
		// Read it back and keep the list: naming one is often the first half of
		// a sentence, and the second half is the next turn.
		return pick.Label, true
	}
	if err := h.api.SetTaskStatus(ctx, pick.Ref, status, h.now(), string(src)); err != nil {
		log.Printf("voice: candidate %d → %s: %v", pick.Ref, status, err)
		return "не получилось изменить задачу.", true
	}
	// Spent: the list she read is no longer the list, and a second ordinal
	// against it would close the wrong task.
	h.dialogueSessions.SetCandidates(dialogueIDOf(ctx), h.now(), nil)
	log.Printf("voice: candidate %d (%q) → %s", pick.Ref, pick.Label, status)
	return say + ": " + pick.Label, true
}
