//nolint:gomnd
package utils

import (
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/go-openapi/swag"
	"github.com/gorhill/cronexpr"
	"github.com/pingcap/errors"
)

const (
	ScheduleLength = 7
	WeekDayLength  = 7
	HoursPerDay    = 24
)

// ConvertBackupTime input weekdays range [0,6], 0:Sunday, 1:Monday,..., 6:Saturday
func ConvertBackupTime(
	timeZone string, backupTime string, weekdays []int32, opposite bool,
) (string, []int32, error) {
	timezone, err := strconv.Atoi(timeZone)
	if err != nil {
		return "", nil, err
	}
	utcOffset := int32(timezone)
	if !opposite {
		utcOffset = -utcOffset
	}

	t, w, err := convertBackupTimeByUTCOffset(backupTime, weekdays, utcOffset)
	if err != nil {
		return "", nil, errors.Annotate(err, "convert backup time by UTC offset")
	}
	return t, w, nil
}

// convertBackupTimeByUTCOffset trans time and weekday between current timezone and utc timezone
func convertBackupTimeByUTCOffset(backupTime string, weekdays []int32, utcOffset int32) (string, []int32, error) {
	if backupTime == "" {
		return "", nil, errors.New("empty backupTime")
	}
	timeArr := strings.Split(backupTime, ":")
	if len(timeArr) != 2 {
		return "", nil, fmt.Errorf("invalid backupTime format %s", backupTime)
	}

	hour, err := ParseHour(timeArr[0])
	if err != nil {
		return "", nil, err
	}
	minute, err := ParseMinute(timeArr[1])
	if err != nil {
		return "", nil, err
	}

	var utcHour int32
	var utcWeekDays []int32
	if hour+utcOffset >= HoursPerDay { // tomorrow
		utcHour = (hour + utcOffset) - HoursPerDay
		for _, day := range weekdays {
			utcDay := (day + 1) % WeekDayLength
			utcWeekDays = append(utcWeekDays, utcDay)
		}
	} else if hour+utcOffset < 0 { // yesterday
		utcHour = (hour + utcOffset) + HoursPerDay
		for _, day := range weekdays {
			utcDay := (day - 1) % WeekDayLength
			if utcDay < 0 {
				utcDay += WeekDayLength
			}
			utcWeekDays = append(utcWeekDays, utcDay)
		}
	} else { // same day
		utcHour = hour + utcOffset
		utcWeekDays = weekdays
	}
	return fmt.Sprintf("%02d:%02d", utcHour, minute), utcWeekDays, nil
}

func ParseHour(h string) (int32, error) {
	h = strings.TrimPrefix(h, "0")
	hour, err := swag.ConvertInt32(h)
	if err != nil {
		return hour, fmt.Errorf("failed to convert hour[%s] to integer, err: %v", h, err)
	}
	if hour < 0 || hour > 23 {
		return hour, fmt.Errorf("invalid hour: %d", hour)
	}
	return hour, nil
}

func ParseMinute(m string) (int32, error) {
	m = strings.TrimPrefix(m, "0")
	minute, err := swag.ConvertInt32(m)
	if err != nil {
		return minute, fmt.Errorf("failed to convert minute[%s] to integer, err: %v", m, err)
	}
	if minute < 0 || minute > 59 {
		return minute, fmt.Errorf("invalid minute: %s", m)
	}
	return minute, nil
}

func parseWeekDays(w string) ([]int32, error) {
	var weekdays []int32
	if w == "*" || w == "?" {
		weekdays = []int32{0, 1, 2, 3, 4, 5, 6}
		return weekdays, nil
	}
	weekFormat := strings.Split(w, ",")
	for _, dayStr := range weekFormat {
		day, err := strconv.Atoi(dayStr)
		if err != nil {
			return nil, err
		}
		weekdays = append(weekdays, int32(day))
	}
	return weekdays, nil
}

func ParseCron(schedule string) (string, []int32, error) {
	timeFormat := strings.Split(schedule, " ")
	if len(timeFormat) != ScheduleLength {
		return "", nil, fmt.Errorf("invalid schedule %s", schedule)
	}
	utcHour, err := ParseHour(timeFormat[2])
	if err != nil {
		return "", nil, err
	}
	minute, err := ParseMinute(timeFormat[1])
	if err != nil {
		return "", nil, err
	}
	weekdays, err := parseWeekDays(timeFormat[5])
	if err != nil {
		return "", nil, err
	}
	return fmt.Sprintf("%02d:%02d", utcHour, minute), weekdays, nil
}

func FormatWeekDays(weekdays []int32) string {
	var weekdayStr string
	for _, day := range weekdays {
		if weekdayStr != "" {
			weekdayStr += ","
		}
		weekdayStr += strconv.Itoa(int(day))
	}
	return weekdayStr
}

func CalculateNextBackupTime(scheduleCron string) time.Time {
	curTime := time.Now()
	expr := cronexpr.MustParse(scheduleCron)
	nextTime := expr.Next(curTime)
	return nextTime
}
