package dialogue

import "time"

// Capability names the thing being assembled across a clarify exchange —
// "reminder.create", not "reminder". A router intent says what she heard; a
// capability says what she is about to do, and those are not the same word:
// three intents currently reach exactly one capability each, but a fact key
// that turns out to be a Hexis target does not. Named in the ecosystem's
// dotted form because that is what a confirmation binds (cmd/mavend/confirm.go)
// and what Hexis registers.
//
// This package must stay free of internal/router (the cycle rule that makes
// Slots a hand-kept copy), so the mapping from an intent lives here and reads
// off dialogue.Intent only.
type Capability string

const (
	CapReminderCreate Capability = "reminder.create"
	CapFactWrite      Capability = "fact.write"
	CapNoteWrite      Capability = "note.write"
	CapActRun         Capability = "act.run"
	CapQueryAnswer    Capability = "query.answer"
	CapChatReply      Capability = "chat.reply"
	CapSystemControl  Capability = "system.control"
)

// intentCapability — the one place an intent becomes a capability. Every intent
// is listed, including the four that are never worth a clarifying question, so a
// parked action always knows what it is even when nothing asks it.
var intentCapability = map[Intent]Capability{
	IntentReminder: CapReminderCreate,
	IntentFact:     CapFactWrite,
	IntentNote:     CapNoteWrite,
	IntentAct:      CapActRun,
	IntentQuery:    CapQueryAnswer,
	IntentChat:     CapChatReply,
	IntentSystem:   CapSystemControl,
}

// CapabilityFor maps a router intent (already narrowed to dialogue.Intent by
// the caller) to the capability being assembled. "" for an intent she does not
// recognise — an unknown intent must not silently become a real capability.
func CapabilityFor(in Intent) Capability {
	return intentCapability[in]
}

// PendingAction is the action Maven is assembling, as an object rather than as
// conversational history: which capability, the slots it already has, the slots
// it is still missing, when she asked, how many questions that has cost and how
// long the answer stays welcome.
//
// It exists because the resolver used to have to infer all of that from a
// parked question plus the previous turn (Vikunja #558): "is this his answer or
// a new request" is answerable against an object and guessy against a
// transcript. PendingQuestion carries one of these and keeps its own flat
// fields, so this is a widening — nothing reads the capability yet.
type PendingAction struct {
	Capability Capability
	Slots      Slots  // what is filled so far
	Missing    []Slot // what she is waiting for, in the order to ask about
	Utterance  string // his original raw words, as the action's provenance
	Asked      time.Time
	TTL        time.Duration
	Attempts   int // questions already asked about this action
	// MaxAttempts caps Attempts. 0 ⇒ DefaultMaxAttempts.
	MaxAttempts int
}

// maxAttempts is MaxAttempts with the default filled in.
func (a *PendingAction) maxAttempts() int {
	if a.MaxAttempts <= 0 {
		return DefaultMaxAttempts
	}
	return a.MaxAttempts
}

// IsExpired — the answer came too late for this action to still be his answer.
func (a *PendingAction) IsExpired(now time.Time) bool {
	return now.After(a.Asked.Add(a.TTL))
}

// CanAsk reports whether she may ask another question about this action.
func (a *PendingAction) CanAsk() bool {
	return a.Attempts < a.maxAttempts()
}

// Gaps lists the slots this action asked for and still does not have. Computed
// from the slots rather than trusted from Missing, because Missing is what she
// asked about and the slots are what she got — an answer can fill a gap she
// never asked about, and a re-park must not ask again for something now filled.
func (a *PendingAction) Gaps() []Slot {
	return StillMissing(a.Missing, a.Slots)
}

// Complete reports whether every slot this action was waiting for is filled, so
// it can run. Note that this is completeness against what she ASKED, not
// against the capability's whole schema — validating that is V-562.
func (a *PendingAction) Complete() bool {
	return len(a.Gaps()) == 0
}
