package wallpaper

import (
	"reflect"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
	"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)

type activeSchedule struct {
	cfg      ScheduleConfig
	nextFire time.Time
}

// Grace period before a missed-while-off schedule fires, so a freshly
// connected frontend has its cycle-seq baseline before the bump arrives.
var catchUpDelay = 3 * time.Second

type Manager struct {
	config      Config
	configMutex sync.RWMutex

	state      *State
	stateMutex sync.RWMutex

	subscribers syncmap.Map[string, chan State]

	stopChan      chan struct{}
	updateTrigger chan struct{}
	resetReq      chan string
	wg            sync.WaitGroup

	// Owned by schedulerLoop after start; keyed like schedules ("" = global).
	lastFires    map[string]time.Time
	persistFires func(map[string]time.Time)
}

func NewManager() *Manager {
	return newManager(loadLastFires(), saveLastFires)
}

func newManager(lastFires map[string]time.Time, persistFires func(map[string]time.Time)) *Manager {
	if lastFires == nil {
		lastFires = map[string]time.Time{}
	}
	m := &Manager{
		config: Config{
			Global:   ScheduleConfig{Mode: "interval", IntervalSec: 300, Time: "06:00"},
			Monitors: map[string]ScheduleConfig{},
		},
		stopChan:      make(chan struct{}),
		updateTrigger: make(chan struct{}, 1),
		resetReq:      make(chan string, 8),
		lastFires:     lastFires,
		persistFires:  persistFires,
	}
	m.state = &State{Config: m.getConfig()}

	m.wg.Add(1)
	go m.schedulerLoop()

	return m
}

func (m *Manager) GetState() State {
	m.stateMutex.RLock()
	defer m.stateMutex.RUnlock()
	if m.state == nil {
		return State{Config: m.getConfig()}
	}
	return *m.state
}

func (m *Manager) Subscribe(id string) chan State {
	ch := make(chan State, 64)
	m.subscribers.Store(id, ch)
	return ch
}

func (m *Manager) Unsubscribe(id string) {
	if val, ok := m.subscribers.LoadAndDelete(id); ok {
		close(val)
	}
}

func (m *Manager) SetConfig(config Config) {
	if config.Monitors == nil {
		config.Monitors = map[string]ScheduleConfig{}
	}
	m.configMutex.Lock()
	if reflect.DeepEqual(m.config, config) {
		m.configMutex.Unlock()
		return
	}
	m.config = config
	m.configMutex.Unlock()
	m.TriggerUpdate()
}

func (m *Manager) ResetSchedule(target string) {
	select {
	case m.resetReq <- target:
	default:
	}
}

func (m *Manager) TriggerUpdate() {
	select {
	case m.updateTrigger <- struct{}{}:
	default:
	}
}

func (m *Manager) Close() {
	select {
	case <-m.stopChan:
		return
	default:
		close(m.stopChan)
	}
	m.wg.Wait()
	m.subscribers.Range(func(key string, ch chan State) bool {
		close(ch)
		m.subscribers.Delete(key)
		return true
	})
}

func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
	ch := lm.Subscribe("wallpaper")
	m.wg.Go(func() {
		defer lm.Unsubscribe("wallpaper")
		for {
			select {
			case <-m.stopChan:
				return
			case state, ok := <-ch:
				if !ok {
					return
				}
				if state.PreparingForSleep {
					continue
				}
				m.TriggerUpdate()
			}
		}
	})
}

func (m *Manager) schedulerLoop() {
	defer m.wg.Done()

	schedules := map[string]*activeSchedule{}
	resets := map[string]bool{}
	var seq uint64
	var timer *time.Timer

	for {
		now := time.Now()
		config := m.getConfig()
		active := activeSchedules(config)

		for key := range schedules {
			if _, ok := active[key]; !ok {
				delete(schedules, key)
			}
		}
		firesDirty := false
		for key, cfg := range active {
			s, ok := schedules[key]
			switch {
			case !ok:
				s = &activeSchedule{cfg: cfg, nextFire: computeNext(now, cfg)}
				schedules[key] = s
				if cfg.Mode == "time" {
					last := m.lastFires[key]
					prev, valid := prevDailyTime(now, cfg.Time)
					switch {
					case last.IsZero():
						// First enable (or no history): seed instead of firing.
						m.lastFires[key] = now
						firesDirty = true
					case valid && last.Before(prev):
						// Scheduled time passed while dms wasn't running.
						s.nextFire = now.Add(catchUpDelay)
					}
				}
			case s.cfg != cfg || resets[key]:
				s.cfg = cfg
				s.nextFire = computeNext(now, cfg)
			}
			delete(resets, key)
		}

		var dueKeys []string
		for key, s := range schedules {
			if !s.nextFire.After(now) {
				dueKeys = append(dueKeys, key)
				s.nextFire = computeNext(now, s.cfg)
				if s.cfg.Mode == "time" {
					m.lastFires[key] = now
					firesDirty = true
				}
			}
		}

		if firesDirty {
			for key := range m.lastFires {
				if _, ok := schedules[key]; !ok {
					delete(m.lastFires, key)
				}
			}
			m.persistFires(m.lastFires)
		}

		next, hasNext := soonest(schedules)
		if len(dueKeys) == 0 {
			m.setState(config, next, seq, "")
		}
		for _, key := range dueKeys {
			seq++
			m.setState(config, next, seq, key)
		}

		waitDur := 24 * time.Hour
		if hasNext {
			waitDur = max(time.Until(next), time.Second)
		}

		if timer != nil {
			timer.Stop()
		}
		timer = time.NewTimer(waitDur)

		select {
		case <-m.stopChan:
			timer.Stop()
			return
		case <-m.updateTrigger:
			timer.Stop()
		case key := <-m.resetReq:
			timer.Stop()
			resets[key] = true
		case <-timer.C:
		}
	}
}

func (m *Manager) setState(config Config, next time.Time, seq uint64, target string) {
	newState := State{Config: config, NextRotation: next, CycleSeq: seq, Target: target}

	m.stateMutex.Lock()
	if m.state != nil && statesEqual(m.state, &newState) {
		m.stateMutex.Unlock()
		return
	}
	m.state = &newState
	m.stateMutex.Unlock()

	m.notifySubscribers()
}

func (m *Manager) notifySubscribers() {
	state := m.GetState()
	m.subscribers.Range(func(key string, ch chan State) bool {
		select {
		case ch <- state:
		default:
		}
		return true
	})
}

func (m *Manager) getConfig() Config {
	m.configMutex.RLock()
	defer m.configMutex.RUnlock()
	return m.config
}

func activeSchedules(config Config) map[string]ScheduleConfig {
	out := map[string]ScheduleConfig{}
	if config.PerMonitor {
		for name, cfg := range config.Monitors {
			if cfg.Enabled {
				out[name] = cfg
			}
		}
		return out
	}
	if config.Global.Enabled {
		out[""] = config.Global
	}
	return out
}

func computeNext(now time.Time, cfg ScheduleConfig) time.Time {
	switch cfg.Mode {
	case "time":
		return nextDailyTime(now, cfg.Time)
	default:
		sec := max(cfg.IntervalSec, 1)
		return now.Add(time.Duration(sec) * time.Second)
	}
}

func nextDailyTime(now time.Time, hhmm string) time.Time {
	hour, minute, ok := parseHHMM(hhmm)
	if !ok {
		return now.Add(24 * time.Hour)
	}
	next := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location())
	if !next.After(now) {
		next = next.Add(24 * time.Hour)
	}
	return next
}

func prevDailyTime(now time.Time, hhmm string) (time.Time, bool) {
	hour, minute, ok := parseHHMM(hhmm)
	if !ok {
		return time.Time{}, false
	}
	prev := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location())
	if prev.After(now) {
		prev = prev.Add(-24 * time.Hour)
	}
	return prev, true
}

func parseHHMM(hhmm string) (int, int, bool) {
	parts := strings.Split(hhmm, ":")
	if len(parts) != 2 {
		return 0, 0, false
	}
	hour, err := strconv.Atoi(parts[0])
	if err != nil || hour < 0 || hour > 23 {
		return 0, 0, false
	}
	minute, err := strconv.Atoi(parts[1])
	if err != nil || minute < 0 || minute > 59 {
		return 0, 0, false
	}
	return hour, minute, true
}

func soonest(schedules map[string]*activeSchedule) (time.Time, bool) {
	var best time.Time
	found := false
	for _, s := range schedules {
		if !found || s.nextFire.Before(best) {
			best = s.nextFire
			found = true
		}
	}
	return best, found
}

func statesEqual(a, b *State) bool {
	switch {
	case a == nil || b == nil:
		return a == b
	case a.CycleSeq != b.CycleSeq:
		return false
	case a.Target != b.Target:
		return false
	case !a.NextRotation.Equal(b.NextRotation):
		return false
	}
	return reflect.DeepEqual(a.Config, b.Config)
}
