package main

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/kami/maven/internal/dialogue"
	"github.com/kami/maven/internal/router"
)

// A reminder commits only when three things are answered: what to say, what
// time to say it, and what day (owner's rule, 2026-08-06, V-579). Anything
// missing is asked about, and nothing missing is filled from the clock.
//
// "напомни завтра в 3 заказать цветы" has the what and the day and an hour that
// could be either half of the day, so she asks which 3. "напомни в 9 вечера
// разгрузить стиралку" has the what and an unambiguous hour and no day, so she
// asks which day. Today being a valid reading is not the same as him saying it.
//
// Two things are already whole and are not asked about. A time that admits one
// reading is not queried for its half of the day, so "завтра в 15:00" commits.
// And an interval is an instant, so "через час" carries all three by itself.
type whenGap string

const (
	whenComplete      whenGap = ""
	whenNoHour        whenGap = "hour"
	whenAmbiguousHour whenGap = "part_of_day"
	whenNoDay         whenGap = "day"
)

// whenGapOf reads the request and names the first thing about its time that he
// has not said. hasTime is whether a parser could read an instant out of it,
// which is necessary and not sufficient: the parser answers a dayless "в 9"
// with a day it picked.
func whenGapOf(text string, hasTime bool) whenGap {
	if !router.NamesAnHour(text) {
		return whenNoHour
	}
	if router.NamesAnInterval(text) {
		return whenComplete
	}
	if !hasTime {
		return whenNoHour
	}
	if router.HourIsAmbiguous(text) {
		return whenAmbiguousHour
	}
	if !router.NamesADay(text) {
		return whenNoDay
	}
	return whenComplete
}

// whenQuestion is what she asks for each gap. Every one of them opens with the
// current time, because she is reasoning from it and he cannot check that
// reasoning unless he hears it. The hour deck varies with the attempt, like
// every other slot; the other two say one thing and there is only one way to
// say it.
//
// taken is what his last turn added, in his own words, and it goes between the
// clock and the question (V-593). It is empty whenever his turn moved nothing
// forward, which is the case where repeating the question verbatim is honest.
func whenQuestion(gap whenGap, attempt int, now time.Time, taken string) (string, bool) {
	clock := fmt.Sprintf("Сейчас %s.", now.Format("15:04"))
	if taken != "" {
		clock += " " + taken
	}
	switch gap {
	case whenNoHour:
		q, ok := clarifyQuestionFor(dialogue.SlotTime, attempt)
		if !ok {
			return "", false
		}
		return clock + " " + q, true
	case whenAmbiguousHour:
		return clock + " Это утра или вечера?", true
	case whenNoDay:
		return clock + " В какой день?", true
	}
	return "", false
}

// whenKnown — the three things he has to say about the time, and whether the
// words so far say them. Read off the same predicates whenGapOf reads, so the
// two cannot disagree about what is still open.
type whenKnown struct{ hour, part, day bool }

func whenKnownOf(text string, hasTime bool) whenKnown {
	if !router.NamesAnHour(text) {
		return whenKnown{}
	}
	if router.NamesAnInterval(text) {
		return whenKnown{hour: true, part: true, day: true}
	}
	if !hasTime {
		return whenKnown{}
	}
	return whenKnown{
		hour: true,
		part: !router.HourIsAmbiguous(text),
		day:  router.NamesADay(text),
	}
}

// movedForward reports whether b says something a did not.
func (a whenKnown) movedForward(b whenKnown) bool {
	return (!a.hour && b.hour) || (!a.part && b.part) || (!a.day && b.day)
}

// whenTakenLine — the acknowledgement in front of a re-ask, in the words he
// just used (V-593).
//
// It is an echo and never a restatement, for the same reason the fact
// confirmation is (V-592): a 1.7B asked to say a Russian sentence back invents.
// Its only job is evidence that the turn between two asks was heard, so after
// "на 9" and then "на завтра" she does not ask "утра или вечера?" twice
// byte-identically while he wonders whether the microphone is on.
func whenTakenLine(text string) string {
	text = strings.TrimSpace(text)
	text = strings.TrimRight(text, " \t.,!?;:")
	if text == "" {
		return ""
	}
	return "Поняла: " + text + "."
}

// whenTextOf is everything he has said about when, the original request plus
// every answer he has given to a question about it.
//
// The answers are kept apart from the utterance on purpose. The utterance is
// the reminder's payload, so folding "завтра" into it would have her read the
// day back to him at the time she says it. And a time answer has to be read
// against the request rather than alone: "завтра" names no hour, and the hour
// it belongs to is the one she is already holding.
func whenTextOf(q *dialogue.PendingQuestion) string {
	if q.WhenText == "" {
		return q.Utterance
	}
	return strings.TrimSpace(q.Utterance + " " + q.WhenText)
}

// slotStillMissing reports whether a slot is still open. Every slot but the
// reminder's time is open when it is empty; the time is open until all three of
// what he must say about it are said.
func slotStillMissing(slot dialogue.Slot, utterance string, s dialogue.Slots) bool {
	if len(dialogue.StillMissing([]dialogue.Slot{slot}, s)) > 0 {
		return true
	}
	return slot == dialogue.SlotTime && whenGapOf(utterance, s.HasTime) != whenComplete
}

// readWhen reads the instant out of what he has said about the time, newest
// statement first.
//
// The request plus his latest answer is tried before the whole history, and
// that order is what makes a correction win: "нет, сегодня в 15:00" after "в
// 11:00" must land on 15:00, and a parser reading left to right off the joined
// history would find the 11 he just took back. The history is the fallback,
// because an answer often completes an earlier one rather than replacing it -
// "вечера" says which 9, and alone it names no hour at all.
func (h *reactiveHandler) readWhen(ctx context.Context, intent router.Intent, q *dialogue.PendingQuestion, text string) (time.Time, bool) {
	latest := strings.TrimSpace(q.Utterance + " " + text)
	if router.NamesAnHour(text) {
		if w := h.extractor.Extract(ctx, intent, latest, h.now()); w.HasTime {
			return w.Time, true
		}
	}
	if w := h.extractor.Extract(ctx, intent, whenTextOf(q), h.now()); w.HasTime {
		return w.Time, true
	}
	return time.Time{}, false
}

// asksAboutTime reports whether the parked question is one about when.
func asksAboutTime(missing []dialogue.Slot) bool {
	for _, s := range missing {
		if s == dialogue.SlotTime {
			return true
		}
	}
	return false
}

// stillOpen reports whether any of the slots she asked about is still unsaid.
func stillOpen(missing []dialogue.Slot, utterance string, s dialogue.Slots) bool {
	for _, slot := range missing {
		if slotStillMissing(slot, utterance, s) {
			return true
		}
	}
	return false
}

// stillMissingFor is missingFor's engine, in wantedSlots order. It reads the
// utterance as well as the slots, which plain StillMissing cannot: whether an
// hour is ambiguous is a fact about the words, not about the instant they
// parsed to.
func stillMissingFor(intent router.Intent, utterance string, s dialogue.Slots) []dialogue.Slot {
	var out []dialogue.Slot
	for _, want := range wantedSlots[intent] {
		if slotStillMissing(want, utterance, s) {
			out = append(out, want)
		}
	}
	return out
}
