// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.

package cron

import (
	"context"
	"errors"
	"fmt"

	cron "github.com/robfig/cron/v3"
	"github.com/rs/zerolog/log"
)

// Format: seconds minute(0-59) hour(0-23) day of month(1-31) month(1-12) day of week(0-6).
const (
	Hourly      = "0 0 * * * *" // once an hour at minute 0
	Nightly     = "0 0 0 * * *" // once a day at midnight
	Weekly      = "0 0 0 * * 0" // once a week on Sun midnight
	Monthly     = "0 0 0 1 * *" // once a month on the first day of the month
	EverySecond = "* * * * * *" // every second (for testing)
)

var ErrFatal = errors.New("fatal error occurred")

type Manager struct {
	c      *cron.Cron
	ctx    context.Context
	cancel context.CancelFunc
	fatal  chan error
}

// NewManager creates a cron manager.
func NewManager() *Manager {
	return &Manager{
		c:     cron.New(cron.WithSeconds()),
		fatal: make(chan error),
	}
}

// NewCronTask adds a new func to cron job.
func (c *Manager) NewCronTask(sepc string, job func(ctx context.Context) error) error {
	_, err := c.c.AddFunc(sepc, func() {
		jerr := job(c.ctx)
		if jerr != nil { // check different severity of errors
			log.Ctx(c.ctx).Error().Err(jerr).Msg("gitrpc cron job failed")

			if errors.Is(jerr, ErrFatal) {
				c.fatal <- jerr
				return
			}
		}
	})
	if err != nil {
		return fmt.Errorf("gitrpc cron manager failed to add cron job function: %w", err)
	}
	return nil
}

// Run the cron scheduler, or no-op if already running.
func (c *Manager) Run(ctx context.Context) error {
	c.ctx, c.cancel = context.WithCancel(ctx)
	var err error
	go func() {
		select {
		case <-ctx.Done():
			err = fmt.Errorf("context done: %w", ctx.Err())
		case fErr := <-c.fatal:
			err = fmt.Errorf("fatal error occurred: %w", fErr)
		}

		// stop scheduling of new jobs.
		// NOTE: doesn't wait for running jobs, but c.Run() does, and we don't have to wait here
		_ = c.c.Stop()

		// cancel running jobs (redundant for ctx.Done(), but makes code simpler)
		c.cancel()
	}()

	c.c.Run()
	close(c.fatal)
	return err
}
