package store import ( "context" "database/sql" "errors" "fmt" "time" ) // ProposedRoutine — a detected pattern the system wants to turn into a // recurring reminder. Status 'proposed' means awaiting human confirmation; // 'accepted' means the human confirmed and a reminder was created (reminder_id // set); 'dismissed' means the human declined and we won't re-propose. type ProposedRoutine struct { ID int64 Action string Object string IntervalDays float64 Status string // proposed | accepted | dismissed CreatedTs time.Time ReminderID *int64 // set when accepted } var ( ErrProposedRoutineNotFound = errors.New("store: proposed routine not found") ErrProposedRoutineExists = errors.New("store: proposed routine already exists for this action+object") ) // CreateProposedRoutine inserts a new proposed routine. Returns // ErrProposedRoutineExists if one already exists for this action+object (any // status) — the pattern detector should only propose once per pair. func (s *Store) CreateProposedRoutine(ctx context.Context, action, object string, intervalDays float64, ts time.Time) (int64, error) { res, err := s.db.ExecContext(ctx, `INSERT INTO proposed_routines (action, object, interval_days, status, created_ts) VALUES (?,?,?,'proposed',?) ON CONFLICT(action, object) DO NOTHING`, action, object, intervalDays, ts.UnixMilli()) if err != nil { return 0, fmt.Errorf("create proposed routine: %w", err) } n, err := res.RowsAffected() if err != nil { return 0, fmt.Errorf("create proposed routine: rows affected: %w", err) } if n == 0 { return 0, ErrProposedRoutineExists } id, err := res.LastInsertId() if err != nil { return 0, fmt.Errorf("create proposed routine: last insert id: %w", err) } return id, nil } // LookupProposedRoutine returns the proposed routine for action+object, or // nil (no error) when no row exists. func (s *Store) LookupProposedRoutine(ctx context.Context, action, object string) (*ProposedRoutine, error) { row := s.db.QueryRowContext(ctx, ` SELECT id, action, object, interval_days, status, created_ts, reminder_id FROM proposed_routines WHERE action = ? AND object = ?`, action, object) r, err := scanProposedRoutine(row) if errors.Is(err, sql.ErrNoRows) { return nil, nil } if err != nil { return nil, err } return &r, nil } // ListProposedRoutines returns all proposed routines with status='proposed', // newest first. func (s *Store) ListProposedRoutines(ctx context.Context) ([]ProposedRoutine, error) { rows, err := s.db.QueryContext(ctx, ` SELECT id, action, object, interval_days, status, created_ts, reminder_id FROM proposed_routines WHERE status = 'proposed' ORDER BY created_ts DESC, id DESC`) if err != nil { return nil, fmt.Errorf("list proposed routines: %w", err) } defer rows.Close() var out []ProposedRoutine for rows.Next() { r, err := scanProposedRoutine(rows) if err != nil { return nil, err } out = append(out, r) } return out, rows.Err() } // AcceptProposedRoutine flips status to 'accepted', links a reminder_id. // Returns error if not in 'proposed' status. func (s *Store) AcceptProposedRoutine(ctx context.Context, id, reminderID int64) error { res, err := s.db.ExecContext(ctx, `UPDATE proposed_routines SET status = 'accepted', reminder_id = ? WHERE id = ? AND status = 'proposed'`, reminderID, id) if err != nil { return fmt.Errorf("accept proposed routine: %w", err) } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("%w: id=%d not in 'proposed' status", ErrProposedRoutineNotFound, id) } return nil } // DismissProposedRoutine flips status to 'dismissed'. Idempotent. func (s *Store) DismissProposedRoutine(ctx context.Context, id int64) error { _, err := s.db.ExecContext(ctx, `UPDATE proposed_routines SET status = 'dismissed' WHERE id = ? AND status = 'proposed'`, id) if err != nil { return fmt.Errorf("dismiss proposed routine: %w", err) } return nil } // scanProposedRoutine scans a row into ProposedRoutine. func scanProposedRoutine(sc scanner) (ProposedRoutine, error) { var r ProposedRoutine var created int64 var reminderID sql.NullInt64 if err := sc.Scan(&r.ID, &r.Action, &r.Object, &r.IntervalDays, &r.Status, &created, &reminderID); err != nil { return ProposedRoutine{}, err } r.CreatedTs = time.UnixMilli(created).UTC() if reminderID.Valid { r.ReminderID = &reminderID.Int64 } return r, nil }