package schedule

import (
	"context"
	"fmt"
	"time"

	"go.uber.org/zap"
)

type Handler func(ctx context.Context) error

type Job interface {
	Run(ctx context.Context) error
}
type Worker interface {
	Stop()
	Start()
}

type Scheduler struct {
	workers []Worker
}

func NewScheduler() *Scheduler {
	return &Scheduler{}
}

func (s *Scheduler) AddJob(log *zap.Logger, interval time.Duration, job Job) {
	worker := NewPeriodWorker(log, interval, job.Run)
	s.workers = append(s.workers, worker)
}

func (s *Scheduler) Run() {
	for _, worker := range s.workers {
		worker.Start()
	}
}

type PeriodWorker struct {
	runnable     Handler
	initialDelay time.Duration
	interval     time.Duration
	log          *zap.Logger

	fistTime bool
	stopped  bool
	stopCh   chan bool
	doneCh   chan bool
}

func NewPeriodWorker(log *zap.Logger, interval time.Duration, f Handler) *PeriodWorker {
	return NewPeriodWorkerWithDelay(log, time.Second*0, interval, f)
}

func NewPeriodWorkerWithDelay(log *zap.Logger, delay time.Duration, interval time.Duration, f Handler) *PeriodWorker {
	return &PeriodWorker{
		runnable:     f,
		initialDelay: delay,
		interval:     interval,
		log:          log,
		fistTime:     true,
		stopped:      false,
		stopCh:       make(chan bool),
		doneCh:       make(chan bool),
	}
}

func (worker *PeriodWorker) Stop() {
	worker.log.Info("stopping worker")
	worker.stopCh <- true
	<-worker.doneCh
	worker.log.Info("worker is stopped")
}

func (worker *PeriodWorker) Start() {
	worker.log.Info("running worker in the background")
	go func() {
		for !worker.stopped {
			var t *time.Timer
			if worker.fistTime {
				t = time.NewTimer(worker.initialDelay)
				worker.fistTime = false
			} else {
				t = time.NewTimer(worker.interval)
			}

			func() {
				defer func() {
					if r := recover(); r != nil {
						worker.log.Error("recover "+fmt.Sprintf("%+v", r), zap.Any("error", r))
					}
				}()

				select {
				case <-worker.stopCh:
					worker.stopped = true
					worker.doneCh <- true
					return
				case <-t.C:
					ctx := context.Background()
					_ = worker.callRunnable(ctx)
				}
			}()
		}
	}()
}

func (worker *PeriodWorker) callRunnable(ctx context.Context) (err error) {
	logger := worker.log.With(zap.String("worker", "periodic"))
	err = worker.runnable(ctx)
	if err != nil {
		logger.Error("worker failed", zap.Error(err))
	}
	return err
}
