package main

import (
	"context"
	"time"

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

// voiceDialogueID — the dialogue-session and clarify key for the microphone.
// This is a single-user box (ponytail), so one slot per reach suffices; a
// second speaker would need per-speaker ids, which waits on voice-print
// attribution (see PROGRESS multi-user deferral).
const voiceDialogueID = "voice"

// textDialogueID — the clarify key for a text turn that named no conversation.
// Separate from the mic: an old client that sends no id still must not answer
// a question she asked out loud.
const textDialogueID = "text"

// dialogueKey — the context key carrying the id of the conversation this turn
// belongs to. It rides the context rather than a parameter for the same reason
// the correlation id does: every step of the turn needs it, most of them only
// to hand to the next one, and threading it by hand would put it in six
// clarify signatures that have nothing else to say about it.
type dialogueKey struct{}

// dialogueIDFor builds the id a turn is held under: the conversation the reach
// named, qualified by the tap it arrived on, or the tap's own fallback when it
// named none.
//
// A parked clarifying question used to be held under voiceDialogueID no matter
// where the turn came from, so one unanswerable question captured the next
// three utterances from anywhere. Three independent curl sessions fed a
// capture attempt that had already failed, and a reminder among them was lost
// (Vikunja #466).
func dialogueIDFor(src turnSource, conversation string) string {
	if conversation != "" {
		return string(src) + ":" + conversation
	}
	if src == sourceVoice {
		return voiceDialogueID
	}
	return textDialogueID
}

// withDialogueID tags a turn with that id.
func withDialogueID(ctx context.Context, id string) context.Context {
	return context.WithValue(ctx, dialogueKey{}, id)
}

// dialogueIDOf reads it back. Falls back to the microphone's slot, which is
// what an unthreaded caller — a test, an internal replay — gets.
func dialogueIDOf(ctx context.Context) string {
	if id, ok := ctx.Value(dialogueKey{}).(string); ok && id != "" {
		return id
	}
	return voiceDialogueID
}

// toDialogueSlots and applyDialogueSlots are the only bridge between
// router.Slots and dialogue.Slots. dialogue must not import router (import
// cycle), so the two structs are hand-kept copies and every field has to be
// carried by hand here. Adding a field to either struct without adding it to
// BOTH functions loses a slot silently — nothing fails to build. The tests in
// slotsparity_test.go fail when the field sets or the converters stop matching;
// when they do, fix these two functions, not the tests.

// toDialogueSlots projects the router's slots onto the dialogue layer's copy.
func toDialogueSlots(s router.Slots) dialogue.Slots {
	return dialogue.Slots{
		Time:    s.Time,
		HasTime: s.HasTime,
		Key:     s.Key,
		Value:   s.Value,
		HasKey:  s.HasKey,
		Text:    s.Text,
		Fn:      s.Fn,
		Args:    s.Args,
		HasFn:   s.HasFn,
	}
}

// applyDialogueSlots writes dialogue slots back onto router slots.
func applyDialogueSlots(base router.Slots, d dialogue.Slots) router.Slots {
	base.Time, base.HasTime = d.Time, d.HasTime
	base.Key, base.Value, base.HasKey = d.Key, d.Value, d.HasKey
	base.Text = d.Text
	base.Fn, base.Args, base.HasFn = d.Fn, d.Args, d.HasFn
	return base
}

// anaphoraResolver is a shared instance for pronoun detection.
var anaphoraResolver router.AnaphoraResolver

// followUpMerge fills the current turn's missing slots from a prior
// non-expired session — the multi-turn seam. It handles three cases:
//
//  1. Same-intent: inherit missing slots via InheritSlots (existing behavior),
//     except a reminder time the current sentence named and the parser missed.
//  2. Cross-intent anaphora: if the current utterance contains a pronoun
//     ("это" / "он" / "она" etc.) AND the prior session has a key, inherit
//     the key for fact-lookup queries and reminder creation.
//  3. Query after Fact: a query that references the prior fact's subject
//     inherits the key so the handler can do a fact-by-key lookup.
//
// A clarify turn resolves nothing, so it never inherits. InheritSlots only
// fills gaps, so a fully-slotted current turn is unaffected.
func followUpMerge(prev *dialogue.Session, dec router.Decision, now time.Time) router.Decision {
	if prev == nil || dec.Clarify || prev.IsExpired(now) {
		return dec
	}

	// Case 1: same-intent inheritance (existing).
	if prev.Intent == dialogue.Intent(dec.Intent) {
		// A reminder that named an hour nobody could read must not borrow the
		// last one's. Two reminders in a row and the second landed at the
		// first's time, confirmed as if it had been read from the sentence:
		// "напомни без четверти восемь выходить" fired at 07:30 (V-543). The
		// hour is also what fills before the action's own fallback parse can
		// run, so inheriting it hid a time that did parse.
		//
		// Inheriting is still right when the sentence names no time at all,
		// which is the follow-up this seam exists for.
		blockTime := dec.Intent == router.IntentReminder &&
			!dec.Slots.HasTime && router.MentionsTime(dec.Utterance)
		merged := dialogue.InheritSlots(prev.Slots, toDialogueSlots(dec.Slots))
		dec.Slots = applyDialogueSlots(dec.Slots, merged)
		if blockTime {
			dec.Slots.Time, dec.Slots.HasTime = time.Time{}, false
		}
		return dec
	}

	// Cases 2 & 3: cross-intent anaphora + query-after-fact.
	// A query after a fact may reference the fact's subject by pronoun.
	_, isAnaphoric := anaphoraResolver.Resolve(dec.Utterance)
	if !isAnaphoric && !dec.Slots.HasKey {
		// No anaphora and no explicit key — this is a truly new topic.
		return dec
	}

	// Inherit key from the prior session's key when the current utterance
	// refers to it (anaphora) or when a query follows a fact.
	switch {
	case dec.Intent == router.IntentQuery && prev.Slots.HasKey:
		dec.Slots.Key = prev.Slots.Key
		dec.Slots.HasKey = true
		if prev.Slots.HasTime {
			dec.Slots.Time = prev.Slots.Time
			dec.Slots.HasTime = true
		}
	case dec.Intent == router.IntentReminder && prev.Slots.HasKey && isAnaphoric:
		dec.Slots.Key = prev.Slots.Key
		dec.Slots.HasKey = true
	case dec.Intent == router.IntentFact && !dec.Slots.HasKey && prev.Slots.HasKey && isAnaphoric:
		dec.Slots.Key = prev.Slots.Key
		dec.Slots.HasKey = true
	}

	return dec
}
