// mavend/tick_morning.go — the morning checklist and the day plan.
//
// Split out of tick.go, move-only (Vikunja #422). One window per configured
// routine, nudging once at the end for what is still open, plus the read
// surfaces /morning renders.
package main

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

	"github.com/kami/maven/internal/calendar"
	"github.com/kami/maven/internal/delivery"
	"github.com/kami/maven/internal/ipc"
	"github.com/kami/maven/internal/loop"
	"github.com/kami/maven/internal/morning"
	"github.com/kami/maven/internal/store"
)

// fireMorningRoutines checks each configured checklist against today's facts
// and dispatches a nag listing exactly what's still missing, at most once per
// routine per calendar day. Fact reads happen here (not in loop.Gatherer)
// because the item↔fact-key mapping is morning-routine-specific, not a rule
// concern — pulling it into the shared gather path would leak that mapping
// into loop's "rules declare wanted keys" contract. Bodies are literal
// operator text (item labels joined), not LLM-phrased, same rationale as
// cron routines: deterministic, can't hallucinate a checklist item.
func (t *tickLoop) fireMorningRoutines(ctx context.Context, now time.Time, state loop.State) {
	if len(t.morningRoutines) == 0 {
		return
	}
	facts := t.gatherMorningFacts(ctx)

	for _, cand := range morning.Due(t.morningRoutines, facts, t.morningLast, now) {
		body := morningNudgeBody(cand)
		pn := delivery.PhrasedNudge{
			Candidate: loop.Candidate{
				Rule:     loop.Rule{Name: "morning:" + cand.Routine.Name, Severity: loop.Severity(cand.Routine.Severity)},
				Severity: loop.Severity(cand.Routine.Severity),
				State:    state,
			},
			Body:    body,
			Summary: body,
		}
		if _, err := t.dispatcher.DispatchNudge(ctx, pn, now); err != nil {
			log.Printf("tick: dispatch morning routine %s: %v", cand.Routine.Name, err)
		}
	}
}

// morningNudgeBody words the one message a routine gets per day. Required
// items are what she says was not done; optional ones follow, worded as
// something he could still do rather than something he owes (Vikunja #473).
// Operator text, not phrased by the model, for the same reason it always was:
// a checklist item must not be invented.
func morningNudgeBody(cand morning.Candidate) string {
	labels := func(items []morning.Item) string {
		out := make([]string, len(items))
		for i, it := range items {
			out[i] = it.Label
		}
		return strings.Join(out, ", ")
	}
	body := fmt.Sprintf("%s: не сделано — %s", cand.Routine.Name, labels(morning.Required(cand.Missing)))
	if opt := morning.OptionalOnly(cand.Missing); len(opt) > 0 {
		body += fmt.Sprintf(". если будет время — %s", labels(opt))
	}
	return body
}

// gatherMorningFacts reads the latest fact for every item's fact_key across
// all configured morning routines. Shared by fireMorningRoutines (nudge
// decision) and morningStatus (read-only query) so the two paths can never
// disagree about what evidence exists.
func (t *tickLoop) gatherMorningFacts(ctx context.Context) map[string]store.Fact {
	keys := make(map[string]struct{})
	for _, r := range t.morningRoutines {
		for _, it := range r.Items {
			keys[it.FactKey] = struct{}{}
		}
	}
	facts := make(map[string]store.Fact, len(keys))
	for k := range keys {
		f, err := t.store.LatestFact(ctx, k)
		if err == nil {
			facts[k] = f
			continue
		}
		if err != store.ErrNoFact {
			log.Printf("tick: morning: latest fact %s: %v", k, err)
		}
	}
	return facts
}

// morningStatus is the read-only "what's missing" query the web UI (and
// eventually a voice query) calls. Pure recompute over the current facts —
// no dedupe/nudge-time gating, unlike fireMorningRoutines: this answers
// "state right now," not "should we nag."
func (t *tickLoop) morningStatus(ctx context.Context, now time.Time) []ipc.MorningRoutineStatus {
	if len(t.morningRoutines) == 0 {
		return nil
	}
	facts := t.gatherMorningFacts(ctx)
	out := make([]ipc.MorningRoutineStatus, 0, len(t.morningRoutines))
	for _, r := range t.morningRoutines {
		st := morning.Evaluate(r, facts, now)
		done := make(map[string]bool, len(st.Completed))
		for _, it := range st.Completed {
			done[it.Key] = true
		}
		items := make([]ipc.MorningRoutineItem, len(r.Items))
		for i, it := range r.Items {
			items[i] = ipc.MorningRoutineItem{Key: it.Key, Label: it.Label, Done: done[it.Key]}
		}
		out = append(out, ipc.MorningRoutineStatus{
			Name:        r.Name,
			Active:      st.Active,
			WindowStart: r.WindowStart,
			WindowEnd:   r.WindowEnd,
			Items:       items,
		})
	}
	return out
}

// dayPlan is the read-only "what does today hold" query (Vikunja #128). It is
// the impure half of morning.BuildPlan: it reads the calendar events, the
// pending reminders and the checklist facts, and the pure builder orders them.
//
// It never dispatches. Asking for the plan is a query like any other; the only
// unprompted delivery in maven stays with the morning nudge and the
// dispatcher's policy.
func (t *tickLoop) dayPlan(ctx context.Context, now time.Time) ipc.DayPlan {
	y, m, d := now.Date()
	dayStart := time.Date(y, m, d, 0, 0, 0, 0, now.Location())
	dayEnd := dayStart.AddDate(0, 0, 1)

	var events []morning.PlanEntry
	facts, err := t.store.CalendarEvents(ctx, dayStart, dayEnd)
	if err != nil {
		log.Printf("tick: day plan: calendar events: %v", err)
	}
	for _, f := range facts {
		events = append(events, morning.PlanEntry{
			At: f.Ts,
			// The plan prints the hour itself, so the "@ 14:00-14:30" tail the
			// fact value carries would say it twice.
			Text: calendar.FactSummary(f.Value),
			Kind: morning.PlanEvent,
			// Provenance below a calendar read (an ambient relay, #126) is
			// hedged rather than recited as fact.
			Uncertain: f.Confidence < 1.0,
		})
	}

	var reminders []morning.PlanEntry
	rems, err := t.store.PendingReminders(ctx, dayStart, dayEnd)
	if err != nil {
		log.Printf("tick: day plan: pending reminders: %v", err)
	}
	for _, r := range rems {
		if r.Status != store.ReminderPending {
			continue
		}
		fire := r.NextFireTs
		if fire.IsZero() {
			fire = r.FireTs
		}
		reminders = append(reminders, morning.PlanEntry{
			At:   fire,
			Text: r.Text(),
			Kind: morning.PlanReminder,
		})
	}

	var checklistFacts map[string]store.Fact
	if len(t.morningRoutines) > 0 {
		checklistFacts = t.gatherMorningFacts(ctx)
	}
	plan := morning.BuildPlan(t.morningRoutines, checklistFacts, events, reminders, now)

	out := ipc.DayPlan{Date: plan.Date, Spoken: plan.FormatRU()}
	out.Items = make([]ipc.DayPlanItem, len(plan.Items))
	for i, it := range plan.Items {
		out.Items[i] = ipc.DayPlanItem{
			At:        it.At,
			Text:      it.Text,
			Kind:      string(it.Kind),
			Uncertain: it.Uncertain,
		}
	}
	return out
}
