package scheduler

import (
	"context"
	"sync"
	"time"
)

// Job is the work executed on each schedule tick.
// It must be thread-safe and should respect ctx cancellation.
type Job func(ctx context.Context) error

// Engine runs scheduled jobs and emits status/events.
type Engine struct {
	mu sync.Mutex

	cfg Config
	job Job

	running bool
	cancel  context.CancelFunc
	wg      sync.WaitGroup

	status Status
	events chan Event
}

func New(job Job) *Engine {
	return &Engine{
		job:    job,
		events: make(chan Event, 64),
	}
}

// Events returns a receive-only channel of scheduler events.
func (e *Engine) Events() <-chan Event { return e.events }

// Status returns the current status (copy).
func (e *Engine) Status() Status {
	e.mu.Lock()
	defer e.mu.Unlock()
	return e.status
}

func (e *Engine) Start(cfg Config) error {
	if err := cfg.Validate(); err != nil {
		return err
	}

	e.mu.Lock()
	defer e.mu.Unlock()
	if e.running {
		return nil
	}

	e.cfg = cfg
	e.running = true
	e.status = Status{Running: true, Mode: cfg.Mode, StartedAt: time.Now()}
	ctx, cancel := context.WithCancel(context.Background())
	e.cancel = cancel

	e.emitLocked(Event{Type: EventStatus, When: time.Now(), Status: e.status, Note: "Started"})

	e.wg.Add(1)
	go func() {
		defer e.wg.Done()
		switch cfg.Mode {
		case ModeInterval:
			e.runInterval(ctx, cfg.Interval)
		case ModeRolling:
			// Placeholder: emit an error and stop.
			e.emit(Event{Type: EventError, When: time.Now(), Status: e.Status(), Note: "rolling mode not wired yet"})
			cancel := e.stopNoWait("rolling mode not wired")
			if cancel != nil {
				cancel()
			}
		case ModeCronGen:
			// Cron generation doesn't run a goroutine loop.
			cancel := e.stopNoWait("cron mode")
			if cancel != nil {
				cancel()
			}
		}
	}()
	return nil
}

func (e *Engine) Stop() {
	cancel := e.stopNoWait("Stopped")
	if cancel != nil {
		cancel()
	}
	e.wg.Wait()
}

// stopNoWait transitions the engine to a stopped state and returns the cancel
// func (if any) without waiting for the run goroutine. This avoids deadlocks
// when the engine stops itself from inside the run loop.
func (e *Engine) stopNoWait(note string) context.CancelFunc {
	e.mu.Lock()
	defer e.mu.Unlock()
	if !e.running {
		return nil
	}
	cancel := e.cancel
	e.running = false
	e.cancel = nil
	e.status.Running = false
	e.status.StoppedAt = time.Now()
	e.emitLocked(Event{Type: EventStop, When: time.Now(), Status: e.status, Note: note})
	return cancel
}

func (e *Engine) runInterval(ctx context.Context, ic IntervalConfig) {
	if ic.RunOnStart {
		e.doJob(ctx)
	}

	start := time.Now()
	ticker := time.NewTicker(ic.Every)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return
		case t := <-ticker.C:
			if ic.StopAfter > 0 && time.Since(start) >= ic.StopAfter {
				cancel := e.stopNoWait("Completed")
				if cancel != nil {
					cancel()
				}
				return
			}
			e.mu.Lock()
			e.status.NextRun = t.Add(ic.Every)
			e.mu.Unlock()

			e.emit(Event{Type: EventTick, When: t, Status: e.Status(), Note: "Generating Snapshot"})
			e.doJob(ctx)
		}
	}
}

func (e *Engine) doJob(ctx context.Context) {
	err := e.job(ctx)

	e.mu.Lock()
	e.status.LastRun = time.Now()
	e.status.Count++
	if err != nil {
		e.status.LastError = err.Error()
		e.emitLocked(Event{Type: EventError, When: time.Now(), Status: e.status, Note: err.Error()})
	} else {
		e.status.LastError = ""
		e.emitLocked(Event{Type: EventStatus, When: time.Now(), Status: e.status, Note: "[Pause Interval]"})
	}
	e.mu.Unlock()
}

func (e *Engine) emit(ev Event) {
	e.mu.Lock()
	e.emitLocked(ev)
	e.mu.Unlock()
}

func (e *Engine) emitLocked(ev Event) {
	select {
	case e.events <- ev:
	default:
		// Drop if the UI isn't reading fast enough.
	}
}

