package scheduler

import (
	"sync"
	"time"
	"woke/db"

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

// AlarmEvent is sent when an alarm triggers.
type AlarmEvent struct {
	Alarm db.Alarm
}

// Scheduler checks alarms every second and fires events when they trigger.
type Scheduler struct {
	store      db.AlarmStore
	events     chan AlarmEvent
	stop       chan struct{}
	mu         sync.Mutex
	parser     cron.Parser
	clientMode bool // If true, skip MarkTriggered/ToggleAlarm (server handles it)
}

func New(store db.AlarmStore, clientMode bool) *Scheduler {
	return &Scheduler{
		store:      store,
		clientMode: clientMode,
		events:     make(chan AlarmEvent, 10),
		stop:       make(chan struct{}),
		parser:     cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow),
	}
}

// Events returns the channel that receives alarm trigger events.
func (s *Scheduler) Events() <-chan AlarmEvent {
	return s.events
}

// Start begins the alarm checking loop.
func (s *Scheduler) Start() {
	go s.loop()
}

// Stop halts the scheduler.
func (s *Scheduler) Stop() {
	close(s.stop)
}

func (s *Scheduler) loop() {
	ticker := time.NewTicker(1 * time.Second)
	defer ticker.Stop()

	// Track which alarms already fired this minute to avoid duplicates
	firedThisMinute := make(map[int]string) // alarm ID -> "HH:MM" when last fired

	for {
		select {
		case <-s.stop:
			return
		case now := <-ticker.C:
			s.check(now, firedThisMinute)
		}
	}
}

func (s *Scheduler) check(now time.Time, firedThisMinute map[int]string) {
	alarms, err := s.store.ListAlarms()
	if err != nil {
		return
	}

	currentMinute := now.Format("15:04")

	// Clean up old entries from firedThisMinute
	for id, minute := range firedThisMinute {
		if minute != currentMinute {
			delete(firedThisMinute, id)
		}
	}

	for _, a := range alarms {
		if !a.Enabled {
			continue
		}

		// Already fired this minute
		if firedThisMinute[a.ID] == currentMinute {
			continue
		}

		if s.shouldTrigger(a, now) {
			firedThisMinute[a.ID] = currentMinute

			// In host mode, update the DB; in client mode, server handles this
			if !s.clientMode {
				_ = s.store.MarkTriggered(a.ID)
				// Disable one-shot alarms after triggering
				if a.Trigger == "once" {
					_ = s.store.ToggleAlarm(a.ID)
				}
			}

			select {
			case s.events <- AlarmEvent{Alarm: a}:
			default:
				// Don't block if channel is full
			}
		}
	}
}

func (s *Scheduler) shouldTrigger(a db.Alarm, now time.Time) bool {
	if a.Trigger == "once" {
		// Simple time match: HH:MM
		return now.Format("15:04") == a.Time
	}

	// Cron expression
	sched, err := s.parser.Parse(a.Trigger)
	if err != nil {
		return false
	}

	// Check if now falls within the current minute of the cron schedule.
	// We truncate to the minute and check if the next time after (now - 1min) is now.
	truncated := now.Truncate(time.Minute)
	prev := truncated.Add(-time.Minute)
	next := sched.Next(prev)

	return next.Equal(truncated)
}
