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

// Package jobs provides background job scheduling and management.
// It handles scheduled backups, job queuing, and state persistence.
package jobs

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

// JobType represents the type of background job.
type JobType string

const (
	// JobTypeBackup is a backup job.
	JobTypeBackup JobType = "backup"
	// JobTypeRestore is a restore job.
	JobTypeRestore JobType = "restore"
	// JobTypePrune is a prune/cleanup job.
	JobTypePrune JobType = "prune"
	// JobTypeCheck is a repository check job.
	JobTypeCheck JobType = "check"
)

// JobStatus represents the current status of a job.
type JobStatus string

const (
	// JobStatusPending indicates the job is waiting to run.
	JobStatusPending JobStatus = "pending"
	// JobStatusRunning indicates the job is currently executing.
	JobStatusRunning JobStatus = "running"
	// JobStatusCompleted indicates the job finished successfully.
	JobStatusCompleted JobStatus = "completed"
	// JobStatusFailed indicates the job failed.
	JobStatusFailed JobStatus = "failed"
	// JobStatusCancelled indicates the job was cancelled.
	JobStatusCancelled JobStatus = "cancelled"
)

// Job represents a background job.
type Job struct {
	ID          string
	Type        JobType
	Status      JobStatus
	CreatedAt   time.Time
	StartedAt   time.Time
	CompletedAt time.Time
	Progress    int
	Error       string
	Metadata    map[string]interface{}
}

// Duration returns how long the job has been running or ran for.
func (j *Job) Duration() time.Duration {
	if j.StartedAt.IsZero() {
		return 0
	}
	if j.CompletedAt.IsZero() {
		return time.Since(j.StartedAt)
	}
	return j.CompletedAt.Sub(j.StartedAt)
}

// IsTerminal returns true if the job is in a terminal state.
func (j *Job) IsTerminal() bool {
	return j.Status == JobStatusCompleted ||
		j.Status == JobStatusFailed ||
		j.Status == JobStatusCancelled
}

// JobHandler is the function signature for job execution.
type JobHandler func(ctx context.Context, job *Job, progress func(int)) error

// Config holds configuration for the job manager.
type Config struct {
	MaxConcurrentJobs int
	JobTimeout        time.Duration // 0 = no overall timeout
	ProgressTimeout   time.Duration // cancel job if no progress for this long; 0 = disabled
	RetryAttempts     int
	RetryDelay        time.Duration
}

// DefaultConfig returns a Config with sensible defaults.
func DefaultConfig() Config {
	return Config{
		MaxConcurrentJobs: 1,
		JobTimeout:        0,
		ProgressTimeout:   2 * time.Hour, // hung if no progress update for 2 hours
		RetryAttempts:     3,
		RetryDelay:        5 * time.Minute,
	}
}

// Validate validates the configuration.
func (c *Config) Validate() error {
	if c.MaxConcurrentJobs < 1 {
		return errors.New("max concurrent jobs must be at least 1")
	}
	if c.JobTimeout < 0 {
		return errors.New("job timeout cannot be negative")
	}
	if c.ProgressTimeout < 0 {
		return errors.New("progress timeout cannot be negative")
	}
	if c.RetryAttempts < 0 {
		return errors.New("retry attempts cannot be negative")
	}
	if c.RetryDelay < 0 {
		return errors.New("retry delay cannot be negative")
	}
	return nil
}

// Manager handles background job scheduling and execution.
type Manager struct {
	config   Config
	handlers map[JobType]JobHandler
	mu       sync.RWMutex
	jobs     map[string]*Job
	queue    []*Job
	running  int
	shutdown bool
	jobChan  chan *Job
	doneChan chan struct{}

	onJobStart    func(job *Job)
	onJobComplete func(job *Job)
	onJobFailed   func(job *Job, err error)
}

// NewManager creates a new job manager.
func NewManager(config Config) (*Manager, error) {
	if err := config.Validate(); err != nil {
		return nil, fmt.Errorf("invalid config: %w", err)
	}

	m := &Manager{
		config:   config,
		handlers: make(map[JobType]JobHandler),
		jobs:     make(map[string]*Job),
		queue:    make([]*Job, 0),
		jobChan:  make(chan *Job, 100),
		doneChan: make(chan struct{}),
	}

	return m, nil
}

// RegisterHandler registers a handler for a job type.
func (m *Manager) RegisterHandler(jobType JobType, handler JobHandler) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.handlers[jobType] = handler
}

// OnJobStart sets a callback for when jobs start.
func (m *Manager) OnJobStart(fn func(job *Job)) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.onJobStart = fn
}

// OnJobComplete sets a callback for when jobs complete successfully.
func (m *Manager) OnJobComplete(fn func(job *Job)) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.onJobComplete = fn
}

// OnJobFailed sets a callback for when jobs fail.
func (m *Manager) OnJobFailed(fn func(job *Job, err error)) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.onJobFailed = fn
}

// Start starts the job manager's worker goroutines.
func (m *Manager) Start(ctx context.Context) {
	for i := 0; i < m.config.MaxConcurrentJobs; i++ {
		go m.worker(ctx)
	}
}

// Stop gracefully stops the job manager.
func (m *Manager) Stop() {
	m.mu.Lock()
	m.shutdown = true
	close(m.jobChan)
	m.mu.Unlock()
}

// Submit submits a new job for execution.
func (m *Manager) Submit(jobType JobType, metadata map[string]interface{}) (*Job, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	if m.shutdown {
		return nil, errors.New("job manager is shutting down")
	}

	if _, ok := m.handlers[jobType]; !ok {
		return nil, fmt.Errorf("no handler registered for job type: %s", jobType)
	}

	job := &Job{
		ID:        generateJobID(),
		Type:      jobType,
		Status:    JobStatusPending,
		CreatedAt: time.Now(),
		Metadata:  metadata,
	}

	m.jobs[job.ID] = job
	m.queue = append(m.queue, job)

	select {
	case m.jobChan <- job:
	default:
	}

	return job, nil
}

// GetJob returns a job by ID.
func (m *Manager) GetJob(id string) *Job {
	m.mu.RLock()
	defer m.mu.RUnlock()

	job, ok := m.jobs[id]
	if !ok {
		return nil
	}

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

// ListJobs returns all jobs, optionally filtered by status.
func (m *Manager) ListJobs(status *JobStatus) []*Job {
	m.mu.RLock()
	defer m.mu.RUnlock()

	result := make([]*Job, 0)
	for _, job := range m.jobs {
		if status != nil && job.Status != *status {
			continue
		}
		jobCopy := *job
		if job.Metadata != nil {
			jobCopy.Metadata = make(map[string]interface{})
			for k, v := range job.Metadata {
				jobCopy.Metadata[k] = v
			}
		}
		result = append(result, &jobCopy)
	}
	return result
}

// CancelJob cancels a pending job.
func (m *Manager) CancelJob(id string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	job, ok := m.jobs[id]
	if !ok {
		return fmt.Errorf("job not found: %s", id)
	}

	if job.Status != JobStatusPending {
		return fmt.Errorf("cannot cancel job in status: %s", job.Status)
	}

	job.Status = JobStatusCancelled
	job.CompletedAt = time.Now()

	for i, qJob := range m.queue {
		if qJob.ID == id {
			m.queue = append(m.queue[:i], m.queue[i+1:]...)
			break
		}
	}

	return nil
}

// ClearCompleted removes all completed jobs from history.
func (m *Manager) ClearCompleted() int {
	m.mu.Lock()
	defer m.mu.Unlock()

	count := 0
	for id, job := range m.jobs {
		if job.IsTerminal() {
			delete(m.jobs, id)
			count++
		}
	}
	return count
}

// HasActiveJob returns true if any job of the given type is running or pending.
func (m *Manager) HasActiveJob(jobType JobType) bool {
	m.mu.RLock()
	defer m.mu.RUnlock()
	for _, job := range m.jobs {
		if job.Type == jobType && (job.Status == JobStatusRunning || job.Status == JobStatusPending) {
			return true
		}
	}
	return false
}

// PendingCount returns the number of pending jobs.
func (m *Manager) PendingCount() int {
	m.mu.RLock()
	defer m.mu.RUnlock()

	count := 0
	for _, job := range m.jobs {
		if job.Status == JobStatusPending {
			count++
		}
	}
	return count
}

// RunningCount returns the number of running jobs.
func (m *Manager) RunningCount() int {
	m.mu.RLock()
	defer m.mu.RUnlock()
	return m.running
}

func (m *Manager) worker(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			return
		case job, ok := <-m.jobChan:
			if !ok {
				return
			}
			m.executeJob(ctx, job)
		}
	}
}

func (m *Manager) executeJob(ctx context.Context, job *Job) {
	m.mu.RLock()
	if job.Status == JobStatusCancelled {
		m.mu.RUnlock()
		return
	}
	handler, ok := m.handlers[job.Type]
	onStart := m.onJobStart
	onComplete := m.onJobComplete
	onFailed := m.onJobFailed
	m.mu.RUnlock()

	if !ok {
		m.failJob(job, fmt.Errorf("no handler for job type: %s", job.Type))
		return
	}

	m.mu.Lock()
	job.Status = JobStatusRunning
	job.StartedAt = time.Now()
	m.running++
	m.mu.Unlock()

	if onStart != nil {
		onStart(job)
	}

	var jobCtx context.Context
	var cancel context.CancelFunc
	if m.config.JobTimeout > 0 {
		jobCtx, cancel = context.WithTimeout(ctx, m.config.JobTimeout)
	} else {
		jobCtx, cancel = context.WithCancel(ctx)
	}
	defer cancel()

	var lastProgress atomic.Int64
	lastProgress.Store(time.Now().UnixNano())

	progressFn := func(pct int) {
		m.mu.Lock()
		job.Progress = pct
		m.mu.Unlock()
		lastProgress.Store(time.Now().UnixNano())
	}

	// Progress watchdog: cancel the job if no progress update arrives within
	// ProgressTimeout. Catches hung Tor connections without imposing an overall
	// wall-clock limit on legitimately slow backups.
	if m.config.ProgressTimeout > 0 {
		go func() {
			ticker := time.NewTicker(time.Minute)
			defer ticker.Stop()
			for {
				select {
				case <-jobCtx.Done():
					return
				case <-ticker.C:
					since := time.Since(time.Unix(0, lastProgress.Load()))
					if since > m.config.ProgressTimeout {
						cancel()
						return
					}
				}
			}
		}()
	}

	err := handler(jobCtx, job, progressFn)

	m.mu.Lock()
	m.running--
	job.CompletedAt = time.Now()

	if err != nil {
		job.Status = JobStatusFailed
		job.Error = err.Error()
		m.mu.Unlock()

		if onFailed != nil {
			onFailed(job, err)
		}
	} else {
		job.Status = JobStatusCompleted
		job.Progress = 100
		m.mu.Unlock()

		if onComplete != nil {
			onComplete(job)
		}
	}
}

func (m *Manager) failJob(job *Job, err error) {
	m.mu.Lock()
	onFailed := m.onJobFailed
	job.Status = JobStatusFailed
	job.Error = err.Error()
	job.CompletedAt = time.Now()
	m.mu.Unlock()

	if onFailed != nil {
		onFailed(job, err)
	}
}

var jobIDCounter uint64

func generateJobID() string {
	return fmt.Sprintf("job_%d_%d", time.Now().UnixNano(), atomic.AddUint64(&jobIDCounter, 1))
}
