package worker

import (
	"context"
	"fmt"
	"time"

	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/logid"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/codes"
	"go.uber.org/zap"
)

var (
	Tracer = otel.Tracer("pkgv2/worker")
)

type Worker interface {
	Stop()
	Start()
}
type Handler func(ctx context.Context) error

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()
					ctx = logid.WithLogid(ctx, logid.GenLogid())

					_ = worker.callRunnable(ctx)
				}
			}()
		}
	}()
}

func (worker *PeriodWorker) callRunnable(ctx context.Context) (err error) {
	logger := worker.log.With(log.FieldsFromContext(ctx)...)
	ctx, span := Tracer.Start(ctx, "PeriodWorker.callRunnable")
	defer func() {
		if err != nil {
			span.RecordError(err)
			span.SetStatus(codes.Error, err.Error())
		}
		span.End()
	}()

	err = worker.runnable(ctx)
	if err != nil {
		if berr, ok := err.(bizerr.BizError); ok && berr.IsError() {
			logger.Info("worker failed", zap.Error(err))
		} else {
			logger.Error("worker failed", zap.Error(err))
		}
	}
	return err
}
