// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: MPL-2.0

package rotation

import (
	"encoding/json"
	"fmt"
	"time"

	"github.com/robfig/cron/v3"
)

const (
	// Minimum allowed value for rotation_window
	minRotationWindowSeconds = 3600
	parseOptions             = cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow
)

// RotationSchedule holds the parsed and unparsed versions of the schedule, along with the projected next rotation time.
type RotationSchedule struct {
	Schedule          *cron.SpecSchedule `json:"-"`
	RotationWindow    time.Duration      `json:"rotation_window"` // seconds of window
	RotationSchedule  string             `json:"rotation_schedule"`
	RotationPeriod    time.Duration      `json:"rotation_period"`
	NextVaultRotation time.Time          `json:"next_vault_rotation"`
	LastVaultRotation time.Time          `json:"last_vault_rotation"`
}

func (r *RotationSchedule) UnmarshalJSON(bt []byte) error {
	in := &struct {
		RotationWindow    time.Duration `json:"rotation_window"`
		RotationSchedule  string        `json:"rotation_schedule"`
		RotationPeriod    time.Duration `json:"rotation_period"`
		NextVaultRotation time.Time     `json:"next_vault_rotation"`
		LastVaultRotation time.Time     `json:"last_vault_rotation"`
	}{}
	if err := json.Unmarshal(bt, in); err != nil {
		return err
	}

	var sched *cron.SpecSchedule
	var err error

	if in.RotationSchedule != "" {
		sched, err = DefaultScheduler.Parse(in.RotationSchedule)
		if err != nil {
			return err
		}
	}
	r.Schedule = sched
	r.RotationWindow = in.RotationWindow
	r.RotationSchedule = in.RotationSchedule
	r.RotationPeriod = in.RotationPeriod
	r.NextVaultRotation = in.NextVaultRotation
	r.LastVaultRotation = in.LastVaultRotation

	return nil
}

type Scheduler interface {
	Parse(rotationSchedule string) (*cron.SpecSchedule, error)
	ValidateRotationWindow(s int) error
	NextRotationTimeFromInput(rs *RotationSchedule, input time.Time) time.Time
	IsInsideRotationWindow(rs *RotationSchedule, t time.Time) bool
	ShouldRotate(rs *RotationSchedule, priority int64, t time.Time) bool
	NextRotationTime(rs *RotationSchedule) time.Time
	SetNextVaultRotation(rs *RotationSchedule, t time.Time)
	UsesTTL(rs *RotationSchedule) bool
	UsesRotationSchedule(rs *RotationSchedule) bool
}

var DefaultScheduler Scheduler = &DefaultSchedule{}

type DefaultSchedule struct{}

func (d *DefaultSchedule) Parse(rotationSchedule string) (*cron.SpecSchedule, error) {
	parser := cron.NewParser(parseOptions)
	schedule, err := parser.Parse(rotationSchedule)
	if err != nil {
		return nil, err
	}
	sched, ok := schedule.(*cron.SpecSchedule)
	if !ok {
		return nil, fmt.Errorf("invalid rotation schedule")
	}

	// override the timezone in all cases, even if was set otherwise
	sched.Location = time.UTC

	return sched, nil
}

func (d *DefaultSchedule) ValidateRotationWindow(s int) error {
	if s < minRotationWindowSeconds {
		return fmt.Errorf("rotation_window must be %d seconds or more", minRotationWindowSeconds)
	}
	return nil
}

func (d *DefaultSchedule) UsesRotationSchedule(rs *RotationSchedule) bool {
	return rs.RotationSchedule != "" && rs.RotationPeriod == 0
}

func (d *DefaultSchedule) UsesTTL(rs *RotationSchedule) bool {
	return rs.RotationPeriod != 0 && rs.RotationSchedule == ""
}

// NextRotationTime calculates the next scheduled rotation
func (d *DefaultSchedule) NextRotationTime(rs *RotationSchedule) time.Time {
	if d.UsesTTL(rs) {
		return rs.LastVaultRotation.Add(rs.RotationPeriod)
	}
	return rs.Schedule.Next(time.Now())
}

// NextRotationTimeFromInput calculates and returns the next rotation time based on the provided  schedule and input time
func (d *DefaultSchedule) NextRotationTimeFromInput(rs *RotationSchedule, input time.Time) time.Time {
	if d.UsesTTL(rs) {
		return input.Add(rs.RotationPeriod)
	}
	return rs.Schedule.Next(input)
}

// IsInsideRotationWindow checks if the current time is before the calculated end of the rotation window,
// to make sure that t time is within the specified rotation window
// It returns true if rotation window is not specified
func (d *DefaultSchedule) IsInsideRotationWindow(rs *RotationSchedule, t time.Time) bool {
	if rs.RotationWindow != 0 {
		return t.Before(rs.NextVaultRotation.Add(rs.RotationWindow))
	}
	return true
}

// ShouldRotate checks if the rotation should occur based on  priority, current time, and rotation window
// It returns true if the priority is less than or equal to the current time and the current time is within the rotation window
func (d *DefaultSchedule) ShouldRotate(rs *RotationSchedule, priority int64, t time.Time) bool {
	return priority <= t.Unix() && d.IsInsideRotationWindow(rs, t)
}

// SetNextVaultRotation calculates the next rotation time of a given schedule based on the time.
func (d *DefaultSchedule) SetNextVaultRotation(rs *RotationSchedule, t time.Time) {
	if d.UsesTTL(rs) {
		rs.NextVaultRotation = t.Add(rs.RotationPeriod)
	} else {
		rs.NextVaultRotation = rs.Schedule.Next(t)
	}
}
