package service

import (
	"context"
	"time"

	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/logger"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/metrics"

	"github.com/robfig/cron/v3"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/trace"
)

type (
	CronJobDo     func(ctx context.Context) error
	CronJobOption func(*cronJob)
)

// WithInitDelay set the initial delay before the first run of the job.
//
// If not set, the job will run after the next schedule.
func WithInitDelay(delay time.Duration) CronJobOption {
	return func(job *cronJob) {
		job.initDelay = delay
	}
}

type cronJob struct {
	name   string
	stat   JobStat
	tracer trace.Tracer

	initDelay time.Duration
	sched     cron.Schedule
	do        CronJobDo
}

// MustNewCronJob create a job with cron spec.
//
// It accepts
//   - Standard crontab specs, e.g. "* * * * ?"
//   - Descriptors, e.g. "@midnight", "@every 1h30m"
func MustNewCronJob(name string, spec string, do CronJobDo, opts ...CronJobOption) Job {
	j, err := NewCronJob(name, spec, do, opts...)
	if err != nil {
		panic(err)
	}
	return j
}

// NewCronJob create a job with cron spec.
//
// It accepts
//   - Standard crontab specs, e.g. "* * * * ?"
//   - Descriptors, e.g. "@midnight", "@every 1h30m"
func NewCronJob(name string, spec string, do CronJobDo, opts ...CronJobOption) (Job, error) {
	sched, err := cron.ParseStandard(spec)
	if err != nil {
		return nil, err
	}
	job := newCronJob(name, sched, do, opts...)
	return job, nil
}

// NewConstantCronJob add a job with constant interval
func NewConstantCronJob(name string, interval time.Duration, do CronJobDo, opts ...CronJobOption) Job {
	sched := cron.Every(interval)
	return newCronJob(name, sched, do, opts...)
}

func newCronJob(name string, sched cron.Schedule, do CronJobDo, opts ...CronJobOption) Job {
	job := &cronJob{
		name:   name,
		tracer: otel.Tracer(TraceScopeName),

		sched: sched,
		do:    do,
	}

	for _, opt := range opts {
		opt(job)
	}

	return job
}

func (j *cronJob) Name() string {
	return j.name
}

func (j *cronJob) Stat() JobStat {
	return j.stat
}

func (j *cronJob) Run(ctx context.Context) {
	var timer *time.Timer
	initDelay := j.initDelay
	if initDelay == 0 {
		initDelay = j.next(time.Now())
	}

	timer = time.NewTimer(initDelay)
	defer timer.Stop()

	for {
		select {
		case <-ctx.Done():
			return
		case now := <-timer.C:
			j.stat.TotalCount++
			subCtx, span := j.tracer.Start(ctx, j.name)
			subLogger := logger.FromContext(subCtx).WithName(j.name)
			beginAt := time.Now()

			err := j.do(logger.NewContext(subCtx, subLogger))

			// record metrics
			if metrics.ServiceCronJob != nil {
				result := MetricResultFromErr(err)
				serviceName := GetServiceNameFromContext(subCtx)
				metrics.ServiceCronJob.SyncCount.WithLabelValues(serviceName, j.name, result).Inc()
				metrics.ServiceCronJob.SyncDur.WithLabelValues(serviceName, j.name, result).Observe(time.Since(beginAt).Seconds())
			}

			if err != nil {
				if rerr, is := AsRetriableError(err); is {
					j.stat.RetriableCount++
					subLogger.Info("the result of a cron job run is retriable", "reason", rerr.Error())
				} else {
					j.stat.FailedCount++
					subLogger.Error(err, "the result of a cron job run is failed")
				}
			} else {
				j.stat.SuccessCount++
			}

			span.End()
			timer.Reset(j.next(now))
		}
	}
}

func (j *cronJob) next(now time.Time) time.Duration {
	nextAt := j.sched.Next(now)
	return nextAt.Sub(now)
}
