// mavend/patterns.go — the shared detect+propose step of pattern inference
// (Vikunja #43). Event *extraction* (fact -> action/object) happens at fact-
// write time in detectPattern below, tied to whichever channel wrote the
// fact. Detection — turning a run of events into a proposed routine — is
// channel-agnostic: it only needs what's already in the events table, so it
// runs both right after a voice fact-write (for the immediate "напоминать?"
// confirmation) and, proactively, from the digestion tick (tick.go's
// detectPatterns) over every action+object pair on record, not just the one
// that was just talked about.
package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/kami/maven/internal/pattern"
	"github.com/kami/maven/internal/store"
)

// detectAndPropose runs the pattern detector over every recorded event for
// action+object and, if a stable pattern is found and nothing has been
// proposed/accepted/dismissed for this pair yet, creates a proposed_routines
// row. Returns (nil, 0, nil) — not an error — whenever there is nothing new
// to report: too few events, irregular intervals, or a pair that already has
// a row in any status. That last case is the one that matters most: it is
// how a routine the owner already DISMISSED stays dismissed forever, because
// the row survives dismissal (status flips in place, see
// store.DismissProposedRoutine) and both the Lookup check here and the
// table's UNIQUE(action, object) constraint refuse to create a second one.
func detectAndPropose(ctx context.Context, ds *store.Store, action, object string, ts time.Time) (*pattern.ProposedRoutine, int64, error) {
	events, err := ds.EventsFor(ctx, action, object)
	if err != nil {
		return nil, 0, fmt.Errorf("events for %s/%s: %w", action, object, err)
	}
	patEvents := make([]pattern.Event, len(events))
	for i, e := range events {
		patEvents[i] = pattern.Event{
			FactID: e.FactID,
			Action: e.Action,
			Object: e.Object,
			Ts:     e.Ts,
		}
	}
	r, err := pattern.Detect(patEvents)
	if err != nil {
		return nil, 0, fmt.Errorf("detect %s/%s: %w", action, object, err)
	}
	if r == nil {
		return nil, 0, nil // not enough data or intervals too irregular
	}

	// Belt: check first so the common "nothing new" case never even attempts
	// an insert. Suspenders: CreateProposedRoutine's ON CONFLICT DO NOTHING
	// (backed by the UNIQUE(action,object) constraint) is the actual
	// guarantee — this Lookup is an optimization, not the source of truth.
	existing, err := ds.LookupProposedRoutine(ctx, r.Action, r.Object)
	if err != nil {
		return nil, 0, fmt.Errorf("lookup proposed routine %s/%s: %w", action, object, err)
	}
	if existing != nil {
		return nil, 0, nil // already proposed, accepted, or dismissed — say nothing
	}

	id, err := ds.CreateProposedRoutine(ctx, r.Action, r.Object, r.IntervalDays, ts)
	if err != nil {
		if errors.Is(err, store.ErrProposedRoutineExists) {
			return nil, 0, nil // lost a race with another caller — not an error
		}
		return nil, 0, fmt.Errorf("create proposed routine %s/%s: %w", action, object, err)
	}
	return r, id, nil
}

// detectPattern extracts an event from the written fact and runs the pattern
// detector. If a stable recurring pattern is found and no proposed routine
// exists for this action+object yet, one is created and the user is prompted
// to confirm via the park() mechanism. Returns the suggestion phrase when a
// new proposal was created and parked; "" otherwise.
func (h *reactiveHandler) detectPattern(ctx context.Context, factID int64, key, value string, ts time.Time) string {
	ev := pattern.Extract(factID, key, value, ts)
	if ev == nil {
		return "" // not an actionable event
	}
	if _, err := h.dataStore.CreateEvent(ctx, factID, ev.Action, ev.Object, ts); err != nil {
		log.Printf("voice: create event: %v", err)
		return ""
	}
	// Detect+propose (Vikunja #43) is shared with the digestion tick's
	// proactive scan — see detectAndPropose above. Event *extraction* stays
	// here, tied to this fact write; detection over the accumulated history does
	// not need to happen right now for the voice path to have already done
	// its job — it's dedupe-safe to also let the next tick find the same
	// pattern independently.
	r, id, err := detectAndPropose(ctx, h.dataStore, ev.Action, ev.Object, ts)
	if err != nil {
		log.Printf("voice: detect pattern %s/%s: %v", ev.Action, ev.Object, err)
		return ""
	}
	if r == nil {
		return "" // not enough data, too irregular, or already proposed/decided
	}
	log.Printf("voice: proposed routine: %s/%s every %.1f days", r.Action, r.Object, r.IntervalDays)

	// Park the proposal for voice confirmation.
	phrase := pattern.PhraseRoutine(r)
	h.mu.Lock()
	h.pendingRoutine = &pendingRoutineConfirm{
		routineID: id,
		action:    r.Action,
		object:    r.Object,
		interval:  r.IntervalDays,
		phrase:    phrase,
		expiry:    ts.Add(confirmTTL),
	}
	h.mu.Unlock()
	return phrase
}
