// SPDX-License-Identifier: GPL-3.0-or-later

package jobs

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"
)

// Schedule represents a scheduled job configuration.
type Schedule struct {
	Type     JobType
	Interval time.Duration
	Metadata map[string]interface{}
	Enabled  bool
	LastRun  time.Time
	NextRun  time.Time
}

// Scheduler manages scheduled jobs.
type Scheduler struct {
	manager   *Manager
	mu        sync.RWMutex
	schedules map[string]*Schedule
	stopChan  chan struct{}
	stopped   bool
}

// NewScheduler creates a new scheduler.
func NewScheduler(manager *Manager) *Scheduler {
	return &Scheduler{
		manager:   manager,
		schedules: make(map[string]*Schedule),
		stopChan:  make(chan struct{}),
	}
}

// AddSchedule adds a scheduled job.
func (s *Scheduler) AddSchedule(name string, schedule *Schedule) error {
	if name == "" {
		return errors.New("schedule name is required")
	}
	if schedule == nil {
		return errors.New("schedule is required")
	}
	if schedule.Interval <= 0 {
		return errors.New("schedule interval must be positive")
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	if schedule.NextRun.IsZero() {
		schedule.NextRun = time.Now().Add(schedule.Interval)
	}
	s.schedules[name] = schedule
	return nil
}

// RemoveSchedule removes a scheduled job.
func (s *Scheduler) RemoveSchedule(name string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	delete(s.schedules, name)
}

// GetSchedule returns a schedule by name.
func (s *Scheduler) GetSchedule(name string) *Schedule {
	s.mu.RLock()
	defer s.mu.RUnlock()

	schedule, ok := s.schedules[name]
	if !ok {
		return nil
	}

	scheduleCopy := *schedule
	if schedule.Metadata != nil {
		scheduleCopy.Metadata = make(map[string]interface{})
		for k, v := range schedule.Metadata {
			scheduleCopy.Metadata[k] = v
		}
	}
	return &scheduleCopy
}

// ListSchedules returns all schedules.
func (s *Scheduler) ListSchedules() map[string]*Schedule {
	s.mu.RLock()
	defer s.mu.RUnlock()

	result := make(map[string]*Schedule)
	for name, schedule := range s.schedules {
		scheduleCopy := *schedule
		if schedule.Metadata != nil {
			scheduleCopy.Metadata = make(map[string]interface{})
			for k, v := range schedule.Metadata {
				scheduleCopy.Metadata[k] = v
			}
		}
		result[name] = &scheduleCopy
	}
	return result
}

// EnableSchedule enables or disables a schedule.
func (s *Scheduler) EnableSchedule(name string, enabled bool) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	schedule, ok := s.schedules[name]
	if !ok {
		return fmt.Errorf("schedule not found: %s", name)
	}

	schedule.Enabled = enabled
	if enabled {
		schedule.NextRun = time.Now().Add(schedule.Interval)
	}
	return nil
}

// Start starts the scheduler.
func (s *Scheduler) Start(ctx context.Context) {
	go s.run(ctx)
}

// Stop stops the scheduler.
func (s *Scheduler) Stop() {
	s.mu.Lock()
	if !s.stopped {
		s.stopped = true
		close(s.stopChan)
	}
	s.mu.Unlock()
}

func (s *Scheduler) run(ctx context.Context) {
	ticker := time.NewTicker(time.Minute)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return
		case <-s.stopChan:
			return
		case <-ticker.C:
			s.checkSchedules()
		}
	}
}

func (s *Scheduler) checkSchedules() {
	s.mu.Lock()
	defer s.mu.Unlock()

	now := time.Now()
	for _, schedule := range s.schedules {
		if !schedule.Enabled {
			continue
		}
		if now.Before(schedule.NextRun) {
			continue
		}

		// Skip if a job of the same type is already active (running or queued).
		// Avoids stacking up redundant backups behind a long-running one.
		if s.manager.HasActiveJob(schedule.Type) {
			// Advance NextRun so we re-check at the next interval rather than
			// spinning every minute for the entire duration of the backup.
			schedule.NextRun = now.Add(schedule.Interval)
			continue
		}

		_, err := s.manager.Submit(schedule.Type, schedule.Metadata)
		if err != nil {
			continue
		}

		schedule.LastRun = now
		schedule.NextRun = now.Add(schedule.Interval)
	}
}

// TriggerSchedule manually triggers a scheduled job.
func (s *Scheduler) TriggerSchedule(name string) (*Job, error) {
	s.mu.Lock()
	schedule, ok := s.schedules[name]
	if !ok {
		s.mu.Unlock()
		return nil, fmt.Errorf("schedule not found: %s", name)
	}

	metadata := schedule.Metadata
	jobType := schedule.Type
	schedule.LastRun = time.Now()
	schedule.NextRun = time.Now().Add(schedule.Interval)
	s.mu.Unlock()

	return s.manager.Submit(jobType, metadata)
}
