package store

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/robfig/cron/v3"
)

// Reminder — user-stated future intent. fires once or recurring (if cron set).
type Reminder struct {
	ID         int64
	CreatedTs  time.Time
	FireTs     time.Time
	NextFireTs time.Time // computed next fire (for recurring) or same as FireTs
	Payload    string    // raw json
	Status     string    // pending | fired | cancelled
	Cron       string    // cron expression, empty for one-shot

	// Collapsed — set only on a synthetic digest reminder (ID=0): the original
	// due reminders it stands in for. Not persisted. The dispatcher completes
	// (marks fired / reschedules) each of these after the digest delivers.
	Collapsed []Reminder
}

// Text — what the user actually asked for, out of the raw-JSON payload.
//
// The router's reminder slot extraction owns the payload shape and the
// conventional field is "text". A payload that is not JSON, or that lacks the
// field, is returned as-is: he said it, so they are his words, and showing
// them beats showing nothing.
//
// Here rather than in a caller because there is more than one caller and they
// disagreed. The phraser unwrapped the payload; the day plan did not, so
// "какие у меня планы на сегодня" recited a reminder as the literal string
// {"text":"..."} on the deployed daemon, 01-08-2026.
func (r Reminder) Text() string { return ReminderText(r.Payload) }

// ReminderText — Reminder.Text for callers holding a bare payload string.
func ReminderText(payload string) string {
	var m map[string]any
	if err := json.Unmarshal([]byte(payload), &m); err == nil {
		if t, ok := m["text"].(string); ok && t != "" {
			return t
		}
		if t, ok := m["text"]; ok {
			return fmt.Sprintf("%v", t)
		}
	}
	return strings.TrimSpace(payload)
}

// Reminder lifecycle states. Named for the same reason DigestStatus is: a
// caller filtering on the string literal "pending" is one typo away from a
// filter that silently matches nothing.
const (
	ReminderPending   = "pending"
	ReminderFired     = "fired"
	ReminderCancelled = "cancelled"
)

var (
	ErrReminderNotFound = errors.New("store: reminder not found")
	ErrReminderState    = errors.New("store: reminder not in a mutable state")
)

func scanReminder(sc scanner) (Reminder, error) {
	var r Reminder
	var created, fire, nextFire int64
	var cron *string
	if err := sc.Scan(&r.ID, &created, &fire, &nextFire, &r.Payload, &r.Status, &cron); err != nil {
		return Reminder{}, err
	}
	r.CreatedTs = time.UnixMilli(created).UTC()
	r.FireTs = time.UnixMilli(fire).UTC()
	r.NextFireTs = time.UnixMilli(nextFire).UTC()
	if cron != nil {
		r.Cron = *cron
	}
	return r, nil
}

// CreateReminder persists a reminder with a resolved absolute fire time.
// The caller (router/capture path) MUST have already converted "in 4h" → now+4h.
// We do not accept strings here. cron is a cron expression for recurring
// reminders; empty for one-shot.
func (s *Store) CreateReminder(ctx context.Context, fire time.Time, payload, cron string) (int64, error) {
	now := time.Now().UTC()
	var cronPtr *string
	if cron != "" {
		cronPtr = &cron
	}
	res, err := s.db.ExecContext(ctx,
		`INSERT INTO reminders (created_ts, fire_ts, next_fire_ts, payload, status, cron) VALUES (?,?,?,?, 'pending', ?)`,
		now.UnixMilli(), fire.UnixMilli(), fire.UnixMilli(), payload, cronPtr)
	if err != nil {
		return 0, fmt.Errorf("create reminder: %w", err)
	}
	id, err := res.LastInsertId()
	if err != nil {
		return 0, fmt.Errorf("create reminder: last insert id: %w", err)
	}
	return id, nil
}

// DueReminders returns pending reminders with next_fire_ts <= now, oldest first.
// This is the predicate input from the loop side: `next_fire_ts <= now AND status='pending'`.
func (s *Store) DueReminders(ctx context.Context, now time.Time) ([]Reminder, error) {
	rows, err := s.db.QueryContext(ctx, `
		SELECT id, created_ts, fire_ts, next_fire_ts, payload, status, cron
		  FROM reminders
		 WHERE status = 'pending' AND next_fire_ts <= ?
		 ORDER BY next_fire_ts ASC`, now.UnixMilli())
	if err != nil {
		return nil, fmt.Errorf("due reminders: %w", err)
	}
	defer rows.Close()
	var out []Reminder
	for rows.Next() {
		r, err := scanReminder(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, r)
	}
	return out, rows.Err()
}

// PendingReminders returns the pending reminders whose next fire time falls in
// [from, to), earliest first.
//
// The day plan used to take the newest 500 rows out of ListReminders, which
// orders by creation, and then filter them by day. A reminder stated long ago
// for today fell off the end of that scan while a reminder stated this morning
// for next year stayed on it. Bounding by fire time drops what is out of range
// instead of what is old.
func (s *Store) PendingReminders(ctx context.Context, from, to time.Time) ([]Reminder, error) {
	rows, err := s.db.QueryContext(ctx, `
		SELECT id, created_ts, fire_ts, next_fire_ts, payload, status, cron
		  FROM reminders
		 WHERE status = ? AND next_fire_ts >= ? AND next_fire_ts < ?
		 ORDER BY next_fire_ts ASC, id ASC`,
		ReminderPending, from.UnixMilli(), to.UnixMilli())
	if err != nil {
		return nil, fmt.Errorf("pending reminders: %w", err)
	}
	defer rows.Close()
	var out []Reminder
	for rows.Next() {
		r, err := scanReminder(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, r)
	}
	return out, rows.Err()
}

// MarkReminder sets a reminder's status. Only valid transitions: pending→fired,
// pending→cancelled. Anything else is a programming error.
func (s *Store) MarkReminder(ctx context.Context, id int64, status string) error {
	if status != "fired" && status != "cancelled" {
		return fmt.Errorf("%w: %s", ErrReminderState, status)
	}
	// pending → fired|cancelled only.
	var current string
	err := s.db.QueryRowContext(ctx, "SELECT status FROM reminders WHERE id = ?", id).Scan(&current)
	if errors.Is(err, sql.ErrNoRows) {
		return ErrReminderNotFound
	}
	if err != nil {
		return err
	}
	if current != "pending" {
		return fmt.Errorf("%w: currently %s", ErrReminderState, current)
	}
	_, err = s.db.ExecContext(ctx, "UPDATE reminders SET status = ? WHERE id = ?", status, id)
	return err
}

// ListReminders returns the n most recent reminders, newest first.
func (s *Store) ListReminders(ctx context.Context, n int) ([]Reminder, error) {
	rows, err := s.db.QueryContext(ctx, `
		SELECT id, created_ts, fire_ts, next_fire_ts, payload, status, cron
		  FROM reminders ORDER BY created_ts DESC, id DESC LIMIT ?`, n)
	if err != nil {
		return nil, fmt.Errorf("list reminders: %w", err)
	}
	defer rows.Close()
	var out []Reminder
	for rows.Next() {
		r, err := scanReminder(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, r)
	}
	return out, rows.Err()
}

// RescheduleReminder computes the next fire time for a recurring reminder and
// updates next_fire_ts. Returns ErrReminderState if the reminder is not
// recurring or not pending. If no more valid fire times exist, marks it fired.
func (s *Store) RescheduleReminder(ctx context.Context, id int64, now time.Time) error {
	row := s.db.QueryRowContext(ctx, `
		SELECT id, created_ts, fire_ts, next_fire_ts, payload, status, cron
		  FROM reminders WHERE id = ?`, id)
	r, err := scanReminder(row)
	if err != nil {
		return err
	}
	if r.Cron == "" {
		return fmt.Errorf("%w: not a recurring reminder", ErrReminderState)
	}
	if r.Status != "pending" {
		return fmt.Errorf("%w: currently %s", ErrReminderState, r.Status)
	}

	sched, err := cron.ParseStandard(r.Cron)
	if err != nil {
		return fmt.Errorf("parse cron %q: %w", r.Cron, err)
	}
	next := sched.Next(r.NextFireTs.Add(time.Minute))
	if next.IsZero() || next.Before(now) {
		_, err = s.db.ExecContext(ctx, "UPDATE reminders SET status = 'fired' WHERE id = ?", id)
		return err
	}
	_, err = s.db.ExecContext(ctx, "UPDATE reminders SET next_fire_ts = ? WHERE id = ?", next.UnixMilli(), id)
	return err
}
