package cronutil

import (
	"errors"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"
)

// ConvertScheduleFormat returns crontab's format of schedule for instance scheduled backup
// minute、 hour and day of the week are customizable
// See https://en.wikipedia.org/wiki/Cron
// TODO: Temporarily add a zone for time zone conversion, which will be changed to profile to save it.
// zone is the offset of UTC time in minutes
// See http://momentjs.cn/docs/#/manipulating/utc-offset/
func ConvertScheduleFormat(stime string, date []string, zone string) (string, error) {
	schedule := ""
	var offset, utcHour, crossDay int
	var week string
	var err error
	if len(date) == 0 {
		err = errors.New("backup date can't be empty")
		return schedule, err
	}
	offset, err = strconv.Atoi(zone)
	if err != nil {
		return schedule, err
	}
	offset = -offset / 60
	// the format of the stime variable is like xx:yy, xx is the hour, and yy is the minute.
	timeSlice := strings.Split(stime, ":")
	if len(timeSlice) != 2 {
		return schedule, fmt.Errorf("invalid time format %s", stime)
	}

	utcHour, crossDay, err = checkFormatAndGetOffsetHour(timeSlice[0], offset)
	if err != nil {
		return schedule, err
	}

	err = checkMinuteFormatValid(timeSlice[1])
	if err != nil {
		return schedule, err
	}
	week, err = formatWeekInCron(date, crossDay)
	if err != nil {
		return schedule, err
	}
	schedule = fmt.Sprintf("%s %d * * %s", timeSlice[1], utcHour, week)
	return schedule, nil
}

// ParseSchedule parses crontab format of schedule for instance scheduled backup to time and day of the week
// TODO: Temporarily add a zone for time zone conversion, which will be changed to profile to save it.
// zone is the offset of UTC time in minutes
// See http://momentjs.cn/docs/#/manipulating/utc-offset/
func ParseSchedule(schedule string, zone string) (stime string, date []string, err error) {
	if schedule == "" {
		return
	}
	var locHour, crossDay, offset int
	s := strings.Split(schedule, " ")
	if len(s) != 5 {
		err = fmt.Errorf("invalid schedule format: %s", schedule)
		return
	}
	offset, err = strconv.Atoi(zone)
	if err != nil {
		return
	}
	offset = offset / 60
	locHour, crossDay, err = checkFormatAndGetOffsetHour(s[1], offset)
	if err != nil {
		return
	}
	err = checkMinuteFormatValid(s[0])
	if err != nil {
		return
	}
	date, err = parseWeekInCron(s[4], crossDay)
	if err != nil {
		return
	}
	stime = fmt.Sprintf("%d:%s", locHour, s[0])
	return
}

// parseWeekInCron parses week with crontab format (0-6) separated by commas to the week slice to (Sunday to Saturday)
// Example: parses "0,6" to []string{"Monday","Saturday"}
func parseWeekInCron(s string, crossDay int) (date []string, err error) {
	daysOfWeek := strings.Split(s, ",")
	var num int
	for _, d := range daysOfWeek {
		num, err = strconv.Atoi(d)
		if err != nil {
			return
		}
		if num >= 0 && num <= 6 {
			date = append(date, time.Weekday((num+crossDay+7)%7).String())
		} else {
			err = fmt.Errorf("invalid schedule week format: %s", s)
			return
		}
	}
	return
}

// formatWeekInCron formats day of the week slice to (Sunday to Saturday) to crontab format(0-6), separated by commas.
// Example: formats []string{"Monday","Saturday"} to "0,6"
func formatWeekInCron(date []string, crossDay int) (string, error) {
	var week = map[string]string{
		"Sunday":    "0",
		"Monday":    "1",
		"Tuesday":   "2",
		"Wednesday": "3",
		"Thursday":  "4",
		"Friday":    "5",
		"Saturday":  "6",
	}

	daysOfWeek := []string{}
	for _, day := range date {
		d, ok := week[day]
		if !ok {
			err := fmt.Errorf("invalid backup date: %v", date)
			return "", err
		}
		// Plus cross-day time offset
		locDay, err := strconv.Atoi(d)
		if err != nil {
			return "", err
		}
		daysOfWeek = append(daysOfWeek, strconv.Itoa((locDay+crossDay+7)%7))
	}
	sort.Strings(daysOfWeek)
	return strings.Join(daysOfWeek, ","), nil
}

// Check if the format of the hour is correct, and transfer the local time and utc time by offset,
// and judge whether it is cross-day
func checkFormatAndGetOffsetHour(shour string, offset int) (int, int, error) {
	hour, err := strconv.Atoi(shour)
	if err != nil {
		return 0, 0, fmt.Errorf("convert time's hour failed: %s, err: %v", shour, err)
	}
	if hour < 0 || hour > 23 {
		return 0, 0, fmt.Errorf("invalid backup hour time: %s", shour)
	}
	if offset+hour >= 24 {
		return (hour + offset) - 24, 1, nil
	}
	if offset+hour < 0 {
		return (hour + offset) + 24, -1, nil
	}
	return hour + offset, 0, nil
}

func checkMinuteFormatValid(sminute string) error {
	minute, err := strconv.Atoi(sminute)
	if err != nil {
		return fmt.Errorf("convert time's minute failed: %s, err: %v", sminute, err)
	}
	if minute < 0 || minute > 59 {
		return fmt.Errorf("invalid backup minute time: %s", sminute)
	}
	return nil
}

// ParseScheduleViaUTCOffset parses crontab format of schedule with an offset, UTCOffset is between -12 ~ 14
// See: https://en.wikipedia.org/wiki/List_of_UTC_time_offsets
func ParseScheduleViaUTCOffset(schedule string, UTCOffset int) (string, error) {
	var locHour, crossDay int
	var weekDay string
	var err error
	if schedule == "" {
		return "", fmt.Errorf("invalid empty schedule")
	}
	s := strings.Split(schedule, " ")
	if len(s) != 5 {
		return "", fmt.Errorf("invalid schedule format: %s", schedule)
	}

	if locHour, crossDay, err = checkFormatAndGetOffsetHour(s[1], UTCOffset); err != nil {
		return "", err
	}
	if err = checkMinuteFormatValid(s[0]); err != nil {
		return "", err
	}
	if weekDay, err = parseWeekViaCrossday(s[4], crossDay); err != nil {
		return "", err
	}
	return fmt.Sprintf("%s %d %s %s %s", s[0], locHour, s[2], s[3], weekDay), nil
}

// parseWeekViaCrossday parses week with crontab format (0-6) with an offset cross day
func parseWeekViaCrossday(s string, crossDay int) (string, error) {
	date := ""
	var err error
	daysOfWeek := strings.Split(s, ",")
	var num int
	for idx, d := range daysOfWeek {
		num, err = strconv.Atoi(d)
		if err != nil {
			return "", err
		}
		if num >= 0 && num <= 6 {
			if idx != 0 {
				date = fmt.Sprintf("%s,%d", date, (num+crossDay+7)%7)
			} else {
				date = fmt.Sprintf("%d", (num+crossDay+7)%7)
			}
		} else {
			err = fmt.Errorf("invalid schedule week format: %s", s)
			return "", err
		}
	}
	return date, nil
}
