package gocron

import (
	
	
	
	
	
	

	
	
	
)

// Job struct stores the information necessary to run a Job
type Job struct {
	mu *jobMutex
	jobFunction
	interval          int             // interval * unit between runs
	random                            // details for randomness
	duration          time.Duration   // time duration between runs
	unit              schedulingUnit  // time units, e.g. 'minutes', 'hours'...
	startsImmediately bool            // if the Job should run upon scheduler start
	atTimes           []time.Duration // optional time(s) at which this Job runs when interval is day
	startAtTime       time.Time       // optional time at which the Job starts
	error             error           // error related to Job

	scheduledWeekdays []time.Weekday // Specific days of the week to start on
	daysOfTheMonth    []int          // Specific days of the month to run the job
	tags              []string       // allow the user to tag jobs with certain labels
	timer             *time.Timer    // handles running tasks at specific time
	cronSchedule      cron.Schedule  // stores the schedule when a task uses cron
	runWithDetails    bool           // when true the job is passed as the last arg of the jobFunc
}

type jobRunTimes struct {
	jobRunTimesMu *sync.Mutex
	previousRun   time.Time // datetime of the run before last run
	lastRun       time.Time // datetime of last run
	nextRun       time.Time // datetime of next run
}

type random struct {
	rand                *rand.Rand
	randomizeInterval   bool   // whether the interval is random
	randomIntervalRange [2]int // random interval range
}

type jobFunction struct {
	id                uuid.UUID          // unique identifier for the job
	*jobRunTimes                         // tracking all the markers for job run times
	eventListeners                       // additional functions to allow run 'em during job performing
	function          interface{}        // task's function
	parameters        []interface{}      // task's function parameters
	parametersLen     int                // length of the passed parameters
	jobName           string             // key of the distributed lock
	funcName          string             // the name of the function - e.g. main.func1
	runConfig         runConfig          // configuration for how many times to run the job
	singletonQueueMu  *sync.Mutex        // mutex for singletonQueue
	singletonQueue    chan struct{}      // queues jobs for the singleton runner to handle
	singletonRunnerOn *atomic.Bool       // whether the runner function for singleton is running
	ctx               context.Context    // for cancellation
	cancel            context.CancelFunc // for cancellation
	isRunning         *atomic.Bool       // whether the job func is currently being run
	runStartCount     *atomic.Int64      // number of times the job was started
	runFinishCount    *atomic.Int64      // number of times the job was finished
	singletonWg       *sync.WaitGroup    // used by singleton runner
	singletonWgMu     *sync.Mutex        // use to protect the singletonWg
	stopped           *atomic.Bool       // tracks whether the job is currently stopped
	jobFuncNextRun    time.Time          // the next time the job is scheduled to run
}

type eventListeners struct {
	onAfterJobExecution  interface{}                     // deprecated
	onBeforeJobExecution interface{}                     // deprecated
	beforeJobRuns        func(jobName string)            // called before the job executes
	afterJobRuns         func(jobName string)            // called after the job executes
	onError              func(jobName string, err error) // called when the job returns an error
	noError              func(jobName string)            // called when no error is returned
}

type jobMutex struct {
	sync.RWMutex
}

func ( *jobFunction) () jobFunction {
	 := jobFunction{
		id:                .id,
		jobRunTimes:       .jobRunTimes,
		eventListeners:    .eventListeners,
		function:          .function,
		parameters:        nil,
		parametersLen:     .parametersLen,
		funcName:          .funcName,
		jobName:           .jobName,
		runConfig:         .runConfig,
		singletonQueue:    .singletonQueue,
		singletonQueueMu:  .singletonQueueMu,
		ctx:               .ctx,
		cancel:            .cancel,
		isRunning:         .isRunning,
		runStartCount:     .runStartCount,
		runFinishCount:    .runFinishCount,
		singletonWg:       .singletonWg,
		singletonWgMu:     .singletonWgMu,
		singletonRunnerOn: .singletonRunnerOn,
		stopped:           .stopped,
		jobFuncNextRun:    .jobFuncNextRun,
	}
	.parameters = append(.parameters, .parameters...)
	return 
}

func ( *jobFunction) () string {
	if .jobName != "" {
		return .jobName
	}
	return .funcName
}

type runConfig struct {
	finiteRuns bool
	maxRuns    int
	mode       mode
}

// mode is the Job's running mode
type mode int8

const (
	// defaultMode disable any mode
	defaultMode mode = iota

	// singletonMode switch to single job mode
	singletonMode
)

// newJob creates a new Job with the provided interval
func newJob( int,  bool,  bool) *Job {
	,  := context.WithCancel(context.Background())
	 := &Job{
		mu:       &jobMutex{},
		interval: ,
		unit:     seconds,
		jobFunction: jobFunction{
			id: uuid.New(),
			jobRunTimes: &jobRunTimes{
				jobRunTimesMu: &sync.Mutex{},
				lastRun:       time.Time{},
				nextRun:       time.Time{},
			},
			ctx:               ,
			cancel:            ,
			isRunning:         atomic.NewBool(false),
			runStartCount:     atomic.NewInt64(0),
			runFinishCount:    atomic.NewInt64(0),
			singletonRunnerOn: atomic.NewBool(false),
			stopped:           atomic.NewBool(false),
		},
		tags:              []string{},
		startsImmediately: ,
	}
	if  {
		.SingletonMode()
	}
	return 
}

// Name sets the name of the current job.
//
// If the scheduler is running using WithDistributedLocker(),
// the job name is used as the distributed lock key.
func ( *Job) ( string) {
	.mu.Lock()
	defer .mu.Unlock()
	.jobName = 
}

// GetName returns the name of the current job.
// The name is either the name set using Job.Name() / Scheduler.Name() or
// the name of the funcion as Go sees it, for example `main.func1`
func ( *Job) () string {
	.mu.Lock()
	defer .mu.Unlock()
	return .jobFunction.getName()
}

func ( *Job) (,  int) {
	.random.rand = rand.New(rand.NewSource(time.Now().UnixNano())) // nolint

	.random.randomizeInterval = true
	if  <  {
		.random.randomIntervalRange[0] = 
		.random.randomIntervalRange[1] =  + 1
	} else {
		.random.randomIntervalRange[0] = 
		.random.randomIntervalRange[1] =  + 1
	}
}

func ( *Job) () int {
	 := .rand.Intn(.randomIntervalRange[1] - .randomIntervalRange[0])
	return .randomIntervalRange[0] + 
}

func ( *Job) () int {
	if .randomizeInterval {
		return .getRandomInterval()
	}
	return .interval
}

func ( *Job) () bool {
	 := .LastRun()
	return .IsZero()
}

func ( *Job) () bool {
	return .startsImmediately
}

func ( *Job) ( bool) {
	.startsImmediately = 
}

func ( *Job) ( *time.Timer) {
	.mu.Lock()
	defer .mu.Unlock()
	.timer = 
}

func ( *Job) () time.Duration {
	var  time.Duration
	if len(.atTimes) > 0 {
		 = .atTimes[0]
	}

	return 
}

func ( *Job) ( time.Time) time.Duration {
	if len(.atTimes) == 0 {
		return 0
	}

	 := .atTimes[0]

	if len(.atTimes) == 1 || .IsZero() {
		return 
	}

	for ,  := range .atTimes {
		 := time.Date(.Year(), .Month(), .Day(), 0, 0, 0, 0, .Location()).Add()
		if .After() {
			 = 
			break
		}
	}

	return 
}

func ( *Job) ( time.Duration) {
	if len(.atTimes) == 0 {
		.atTimes = append(.atTimes, )
		return
	}
	 := false
	 := sort.Search(len(.atTimes), func( int) bool {
		 := .atTimes[]
		 :=  >= 
		if  {
			 =  == 
		}
		return 
	})

	// ignore if present
	if  {
		return
	}

	.atTimes = append(.atTimes, time.Duration(0))
	copy(.atTimes[+1:], .atTimes[:])
	.atTimes[] = 
}

func ( *Job) () time.Time {
	.mu.RLock()
	defer .mu.RUnlock()
	return .startAtTime
}

func ( *Job) ( time.Time) {
	.mu.Lock()
	defer .mu.Unlock()
	.startAtTime = 
}

func ( *Job) () schedulingUnit {
	.mu.RLock()
	defer .mu.RUnlock()
	return .unit
}

func ( *Job) ( schedulingUnit) {
	.mu.Lock()
	defer .mu.Unlock()
	.unit = 
}

func ( *Job) () time.Duration {
	.mu.RLock()
	defer .mu.RUnlock()
	return .duration
}

func ( *Job) ( time.Duration) {
	.mu.Lock()
	defer .mu.Unlock()
	.duration = 
}

func ( *Job) ( int) {
	.mu.Lock()
	defer .mu.Unlock()
	.interval = 
}

// hasTags returns true if all tags are matched on this Job
func ( *Job) ( ...string) bool {
	// Build map of all Job tags for easy comparison
	 := map[string]int{}
	for ,  := range .tags {
		[] = 0
	}

	// Loop through required tags and if one doesn't exist, return false
	for ,  := range  {
		,  := []
		if ! {
			return false
		}
	}
	return true
}

// Error returns an error if one occurred while creating the Job.
// If multiple errors occurred, they will be wrapped and can be
// checked using the standard unwrap options.
func ( *Job) () error {
	return .error
}

// Context returns the job's context. The context controls cancellation.
func ( *Job) () context.Context {
	return .ctx
}

// Tag allows you to add arbitrary labels to a Job that do not
// impact the functionality of the Job
func ( *Job) ( ...string) {
	.tags = append(.tags, ...)
}

// Untag removes a tag from a Job
func ( *Job) ( string) {
	var  []string
	for ,  := range .tags {
		if  !=  {
			 = append(, )
		}
	}

	.tags = 
}

// Tags returns the tags attached to the Job
func ( *Job) () []string {
	return .tags
}

// EventListener functions utilize the job's name and are triggered
// by or in the condition that the name suggests
type EventListener func(j *Job)

// BeforeJobRuns is called before the job is run
func ( func( string)) EventListener {
	return func( *Job) {
		.mu.Lock()
		defer .mu.Unlock()
		.eventListeners.beforeJobRuns = 
	}
}

// AfterJobRuns is called after the job is run
// This is called even when an error is returned
func ( func( string)) EventListener {
	return func( *Job) {
		.mu.Lock()
		defer .mu.Unlock()
		.eventListeners.afterJobRuns = 
	}
}

// WhenJobReturnsError is called when the job returns an error
func ( func( string,  error)) EventListener {
	return func( *Job) {
		.mu.Lock()
		defer .mu.Unlock()
		.eventListeners.onError = 
	}
}

// WhenJobReturnsNoError is called when the job does not return an error
// the function must accept a single parameter, which is an error
func ( func( string)) EventListener {
	return func( *Job) {
		.mu.Lock()
		defer .mu.Unlock()
		.eventListeners.noError = 
	}
}

// RegisterEventListeners accepts EventListeners and registers them for the job
// The event listeners are then called at the times described by each listener.
func ( *Job) ( ...EventListener) {
	for ,  := range  {
		()
	}
}

// Deprecated: SetEventListeners accepts two functions that will be called, one before and one after the job is run
func ( *Job) ( interface{},  interface{}) {
	.eventListeners = eventListeners{
		onBeforeJobExecution: ,
		onAfterJobExecution:  ,
	}
}

// ScheduledTime returns the time of the Job's next scheduled run
func ( *Job) () time.Time {
	.mu.RLock()
	defer .mu.RUnlock()
	return .nextRun
}

// ScheduledAtTime returns the specific time of day the Job will run at.
// If multiple times are set, the earliest time will be returned.
func ( *Job) () string {
	if len(.atTimes) == 0 {
		return "00:00"
	}

	return fmt.Sprintf("%02d:%02d", .getFirstAtTime()/time.Hour, (.getFirstAtTime()%time.Hour)/time.Minute)
}

// ScheduledAtTimes returns the specific times of day the Job will run at
func ( *Job) () []string {
	 := make([]string, len(.atTimes))
	for ,  := range .atTimes {
		[] = fmt.Sprintf("%02d:%02d", /time.Hour, (%time.Hour)/time.Minute)
	}

	return 
}

// Weekday returns which day of the week the Job will run on and
// will return an error if the Job is not scheduled weekly
func ( *Job) () (time.Weekday, error) {
	if len(.scheduledWeekdays) == 0 {
		return time.Sunday, ErrNotScheduledWeekday
	}
	return .scheduledWeekdays[0], nil
}

// Weekdays returns a slice of time.Weekday that the Job will run in a week and
// will return an error if the Job is not scheduled weekly
func ( *Job) () []time.Weekday {
	// appending on j.scheduledWeekdays may cause a side effect
	if len(.scheduledWeekdays) == 0 {
		return []time.Weekday{time.Sunday}
	}
	sort.Slice(.scheduledWeekdays, func(,  int) bool {
		return .scheduledWeekdays[] < .scheduledWeekdays[]
	})

	return .scheduledWeekdays
}

// LimitRunsTo limits the number of executions of this job to n.
// Upon reaching the limit, the job is removed from the scheduler.
//
// Note: If a job is added to a running scheduler and this method is then used
// you may see the job run more than the set limit as job is scheduled immediately
// by default upon being added to the scheduler. It is recommended to use the
// LimitRunsTo() func on the scheduler chain when scheduling the job.
// For example: scheduler.LimitRunsTo(1).Do()
func ( *Job) ( int) {
	.mu.Lock()
	defer .mu.Unlock()
	.runConfig.finiteRuns = true
	.runConfig.maxRuns = 
}

// SingletonMode prevents a new job from starting if the prior job has not yet
// completed it's run
// Note: If a job is added to a running scheduler and this method is then used
// you may see the job run overrun itself as job is scheduled immediately
// by default upon being added to the scheduler. It is recommended to use the
// SingletonMode() func on the scheduler chain when scheduling the job.
func ( *Job) () {
	.mu.Lock()
	defer .mu.Unlock()
	.runConfig.mode = singletonMode

	.jobFunction.singletonWgMu = &sync.Mutex{}
	.jobFunction.singletonWgMu.Lock()
	.jobFunction.singletonWg = &sync.WaitGroup{}
	.jobFunction.singletonWgMu.Unlock()

	.jobFunction.singletonQueueMu = &sync.Mutex{}
	.jobFunction.singletonQueueMu.Lock()
	.jobFunction.singletonQueue = make(chan struct{}, 100)
	.jobFunction.singletonQueueMu.Unlock()
}

// shouldRun evaluates if this job should run again
// based on the runConfig
func ( *Job) () bool {
	.mu.RLock()
	defer .mu.RUnlock()
	return !.runConfig.finiteRuns || .runStartCount.Load() < int64(.runConfig.maxRuns)
}

// LastRun returns the time the job was run last
func ( *Job) () time.Time {
	.jobRunTimesMu.Lock()
	defer .jobRunTimesMu.Unlock()
	return .lastRun
}

func ( *Job) ( time.Time) {
	.previousRun = .lastRun
	.lastRun = 
}

// NextRun returns the time the job will run next
func ( *Job) () time.Time {
	.jobRunTimesMu.Lock()
	defer .jobRunTimesMu.Unlock()
	return .nextRun
}

func ( *Job) ( time.Time) {
	.jobRunTimesMu.Lock()
	defer .jobRunTimesMu.Unlock()
	.nextRun = 
	.jobFunction.jobFuncNextRun = 
}

// PreviousRun returns the job run time previous to LastRun
func ( *Job) () time.Time {
	.jobRunTimesMu.Lock()
	defer .jobRunTimesMu.Unlock()
	return .previousRun
}

// RunCount returns the number of times the job has been started
func ( *Job) () int {
	.mu.Lock()
	defer .mu.Unlock()
	return int(.runStartCount.Load())
}

// FinishedRunCount returns the number of times the job has finished running
func ( *Job) () int {
	.mu.Lock()
	defer .mu.Unlock()
	return int(.runFinishCount.Load())
}

func ( *Job) () {
	.mu.Lock()
	defer .mu.Unlock()
	if .timer != nil {
		.timer.Stop()
	}
	if .cancel != nil {
		.cancel()
		.ctx, .cancel = context.WithCancel(context.Background())
	}
	.stopped.Store(true)
}

// IsRunning reports whether any instances of the job function are currently running
func ( *Job) () bool {
	return .isRunning.Load()
}

// you must Lock the job before calling copy
func ( *Job) () Job {
	return Job{
		mu:                &jobMutex{},
		jobFunction:       .jobFunction,
		interval:          .interval,
		duration:          .duration,
		unit:              .unit,
		startsImmediately: .startsImmediately,
		atTimes:           .atTimes,
		startAtTime:       .startAtTime,
		error:             .error,
		scheduledWeekdays: .scheduledWeekdays,
		daysOfTheMonth:    .daysOfTheMonth,
		tags:              .tags,
		timer:             .timer,
		cronSchedule:      .cronSchedule,
		runWithDetails:    .runWithDetails,
	}
}