package timeutil

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

var (
	ErrDateFormat    = errors.New("wrong date format")
	ErrInvalidDate   = errors.New("invalid date")
	ErrEmptyLocation = errors.New("empty location")
	ErrInvalidPeriod = errors.New("invalid period")

	dateLayouts = []string{
		"_2 Jan, 2006", // ua 1
		"Jan _2, 2006",
		"_2 January 2006",
		"January _2, 2006",
		"2006-01-02",
		"2006/01/02",
		"2006.01.02",
	}
	defaultDateLayout = "2006-01-02"

	dateTimeLayout = "2006-01-02 15:04:05"
)

// IsValid
func IsValid(dateStr string, layout string) bool {
	if layout == "" {
		layout = defaultDateLayout
	}
	t, err := time.Parse(layout, dateStr)
	if err != nil {
		return false
	}
	if dateStr != t.Format(layout) {
		return false
	}
	return true
}

func IsYmdValid(ymd int) bool {
	day := ymd % 100
	ymd /= 100
	month := time.Month(ymd % 100)
	year := ymd / 100

	t := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
	y, m, d := t.Date()
	if y == year && m == month && d == day {
		return true
	}
	return false
}

// YmdToDate - на вход получает строку формата 2023-10-25, возвращает isValid флаг и
// число вида 20231025
func DateToYmd(dateStr string) (bool, int) {
	t, err := time.Parse(defaultDateLayout, dateStr)
	if err != nil {
		return false, 0
	}

	if dateStr != t.Format(defaultDateLayout) {
		return false, 0
	}

	return true, t.Year()*10000 + int(t.Month())*100 + t.Day()
}

// YmdToDate - на вход получает число вида 20231025, возвращает isValid флаг и
// строку формата 2023-10-25
func YmdToDate(ymd int) (bool, string) {
	day := ymd % 100
	ymd /= 100
	month := time.Month(ymd % 100)
	year := ymd / 100

	t := time.Date(year, month, day, 0, 0, 0, 0, time.Local)
	y, m, d := t.Date()
	if y == year && m == month && d == day {
		return true, t.Format(defaultDateLayout)
	}
	return false, ""
}

// ParseDate пытается распарсить дату, подставляя по очереди все известные layout-ы.
// В шаблоне вида 2006-01-02 - порядок всегда Год-Месяц-День.
// Если распарсить не удалось - возвращает nil.
func ParseDate(str string) *time.Time {
	for _, layout := range dateLayouts {
		t, err := time.Parse(layout, str)
		if err == nil {
			return &t
		}
	}
	return nil
}

func HumanTimeInterval(delta int64) string {
	var parts []string
	hours := delta / 3600
	minutes := (delta - hours*3600) / 60
	seconds := delta - hours*3600 - minutes*60

	if hours > 0 {
		parts = append(parts, fmt.Sprintf("%dh", hours))
	}

	if minutes > 0 {
		parts = append(parts, fmt.Sprintf("%dm", minutes))
	}

	if seconds > 0 {
		parts = append(parts, fmt.Sprintf("%ds", seconds))
	}
	return strings.Join(parts, " ")
}

func ParseDateTime(str string, loc *time.Location) (tm time.Time, err error) {
	tm, err = time.ParseInLocation(dateTimeLayout, str, loc)
	if err != nil {
		err = fmt.Errorf("ParseDateTime %q error: %s", str, err)
	}
	return
}

func ParseDateTimeToUnix(str string, loc *time.Location) (_ int64, err error) {
	tm, err := time.ParseInLocation(dateTimeLayout, str, loc)
	if err != nil {
		err = fmt.Errorf("ParseDateTimeToUnix %q error: %s", str, err)
		return
	}
	return tm.Unix(), nil
}

// FormatTimeInMinutes - на вход получает количество минут, на выходе время в формате 01:30
func FormatTimeInMinutes(minutes int) string {
	if minutes == 24*60 {
		return "00:00"
	}

	hour := minutes / 60
	minute := minutes % 60

	return fmt.Sprintf("%02d:%02d", hour, minute)
}

func UnixInMilliseconds(tm time.Time) int64 {
	return tm.UnixNano() / 1000000
}

func NowAsUnixInMilliseconds() int64 {
	return time.Now().UnixNano() / 1000000
}

/*
func FirstSecondInPeriod(date string, loc *time.Location, period string) (_ time.Time, err error) {
	year, month, day, hour, minute, _, err := parsePartialDateTime(date)
	if err != nil {
		return
	}

	if month == -1 {
		month = 1
	}

	if day == -1 {
		day = 1
	}

	if hour == -1 {
		hour = 0
	}

	if minute == -1 {
		minute = 0
	}

	var tm time.Time

	switch period {
	case "min", "h", "d", "m", "y":
		tm = time.Date(
			year,
			time.Month(month),
			day,    // day
			hour,   // hour fix как передать часы в date string
			minute, // min
			0,      // sec
			0,      // nsec
			loc,
		)

	default:
		err = ErrInvalidPeriod
		return
	}

	// Go преобразовывает невалидную дату 2021-13-25 в валидную 2022-01-25,
	// что меня не устраивает, поэтому валидирую дату сравнивая отформатированное
	// представление до и после

	formatted := fmt.Sprintf("%04d-%02d-%02d %02d:%02d", year, month, day, hour, minute)

	if formatted != tm.Format("2006-01-02 15:04") {
		err = ErrInvalidDate
		return
	}

	return tm, nil
}
*/

/*
func LastSecondInPeriod(date string, loc *time.Location, period string) (_ time.Time, err error) {
	year, month, day, hour, minute, _, err := parsePartialDateTime(date)
	if err != nil {
		return
	}

	if month == -1 {
		month = 12
	}

	if day == -1 {
		// Определяем последний день месяца (библиотека time правильно определит
		// высокосный год и 28-29 февраля)
		t := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, loc)
		// Добавляем месяц и отнимаем день
		t = t.AddDate(0, 1, 0).AddDate(0, 0, -1)

		day = t.Day()
	}

	if hour == -1 {
		hour = 23
	}

	if minute == -1 {
		minute = 59
	}

	var tm time.Time

	switch period {
	case "min", "h", "d", "m", "y":
		tm = time.Date(
			year,
			time.Month(month),
			day,    // day
			hour,   // hour
			minute, // min
			59,     // sec
			0,      // nsec, 0, потому что метка в секундах
			loc,
		)

	default:
		err = ErrInvalidPeriod
		return
	}

	// Go преобразовывает невалидную дату 2021-13-25 в валидную 2022-01-25,
	// что меня не устраивает, поэтому валидирую дату сравнивая отформатированное
	// представление до и после

	formatted := fmt.Sprintf("%04d-%02d-%02d %02d:%02d", year, month, day, hour, minute)

	if formatted != tm.Format("2006-01-02 15:04") {
		err = ErrInvalidDate
		return
	}

	return tm, nil
}

*/

func FirstSecondInPeriod(since time.Time, period string) (_ time.Time) {
	switch period {
	case "h":
		return time.Date(
			since.Year(),
			since.Month(),
			since.Day(),
			since.Hour(),
			0, // min
			0, // sec
			0, // nsec
			since.Location(),
		)

	case "d":
		return time.Date(
			since.Year(),
			since.Month(),
			since.Day(),
			0, // hour
			0, // min
			0, // sec
			0, // nsec
			since.Location(),
		)

	case "m":
		return time.Date(
			since.Year(),
			since.Month(),
			1, // day
			0, // hour
			0, // min
			0, // sec
			0, // nsec
			since.Location(),
		)
	}
	return
}

func LastSecondInPeriod(until time.Time, period string) (_ time.Time) {
	switch period {
	case "h":
		return time.Date(
			until.Year(),
			until.Month(),
			until.Day(),
			until.Hour(),
			59, // min
			59, // sec
			0,  // nsec, 0, потому что метка в секундах
			until.Location(),
		)

	case "d":
		return time.Date(
			until.Year(),
			until.Month(),
			until.Day(),
			23,
			59, // min
			59, // sec
			0,  // nsec, 0, потому что метка в секундах
			until.Location(),
		)

	case "m":
		corrected := time.Date(
			until.Year(),
			until.Month(),
			1,  // day
			23, // hour
			59, // min
			59, // sec
			0,  // nsec
			until.Location(),
		)
		// Добавляем месяц
		corrected = corrected.AddDate(0, 1, 0)
		// Отнимаем день
		corrected = corrected.AddDate(0, 0, -1)

		return corrected
	}
	return
}

// ParsePartialDate - парсит полную или частичную дату в заданном часовом поясе.
// Если месяц не указан - это январь, если день не указан - это 1.
// Валидные форматы:
// YYYY // 4
// YYYY-MM // 7, 6
// YYYYMM
// YYYY-MM-DD // 10, 8
// YYYYMMDD
//
// Произвольный, разделитель размера 1 байт:
// - (минус)
// . (точка)
// / (слэш)
//
//	(пробел)
//
// и т.д.
// Либо без разделителя.
func ParsePartialDate(value string, loc *time.Location) (_ time.Time, err error) {
	if loc == nil {
		err = ErrEmptyLocation
		return
	}

	y, m, d, err := parsePartialDate(value)
	if err != nil {
		return
	}

	if d == 0 {
		d = 1
	}

	if m == 0 {
		m = 1
	}

	tm := time.Date(y, time.Month(m), d, 0, 0, 0, 0, loc)

	fmt.Printf("ParsePartialDate: %s; value=%s\n", tm, value)

	// Go преобразовывает невалидную дату 2021-13-25 в валидную 2022-01-25,
	// что меня не устраивает, поэтому валидирую дату сравнивая отформатированное
	// представление до и после

	if fmt.Sprintf("%04d-%02d-%02d", y, m, d) != tm.Format("2006-01-02") {
		err = ErrInvalidDate
		return
	}
	return tm, nil
}

func parsePartialDate(value string) (y int, m int, d int, err error) {
	size := len(value)

	switch size {
	case 10, 8:
		d, err = strconv.Atoi(value[size-2 : size])
		if err != nil {
			err = ErrDateFormat
			return
		}
		fallthrough

	case 7, 6:
		var (
			i = 5
			j = 7
		)
		if size == 6 || size == 8 {
			i = 4
			j = 6
		}
		m, err = strconv.Atoi(value[i:j])
		if err != nil {
			err = ErrDateFormat
			return
		}
		fallthrough

	case 4:
		y, err = strconv.Atoi(value[:4])
		if err != nil {
			err = ErrDateFormat
			return
		}

	default:
		// неверный формат
		err = ErrDateFormat
		return
	}
	return
}

// parsePartialDateTime - парсит полную или частичную дату в заданном часовом поясе.
// Если месяц не указан - это январь, если день не указан - это 1.
// Валидные форматы:
// YYYY
// YYYY-MM
// YYYY-MM-DD
// YYYY-MM-DD HH
// YYYY-MM-DD HH:MM
// YYYY-MM-DD HH:mm:ss
//
// Вместо минуса (-), пробела ( ) и двоеточия (:) допускается любой разделитель
// размером 1 байт.
func parsePartialDateTime(value string) (year int, month int, day int, hour int, minute int, second int, err error) {
	second = -1
	minute = -1
	hour = -1
	day = -1
	month = -1

	switch len(value) {
	case 19:
		// YYYY-MM-DD HH:mm:ss
		second, err = strconv.Atoi(value[17:19])
		if err != nil {
			err = ErrDateFormat
			return
		}
		fallthrough

	case 16:
		// YYYY-MM-DD HH:mm
		minute, err = strconv.Atoi(value[14:16])
		if err != nil {
			err = ErrDateFormat
			return
		}
		fallthrough

	case 13:
		// YYYY-MM-DD HH
		hour, err = strconv.Atoi(value[11:13])
		if err != nil {
			err = ErrDateFormat
			return
		}
		fallthrough

	case 10:
		// YYYY-MM-DD
		day, err = strconv.Atoi(value[8:10])
		if err != nil {
			err = ErrDateFormat
			return
		}
		fallthrough

	case 7:
		// YYYY-MM
		month, err = strconv.Atoi(value[5:7])
		if err != nil {
			err = ErrDateFormat
			return
		}
		fallthrough

	case 4:
		// YYYY
		year, err = strconv.Atoi(value[:4])
		if err != nil {
			err = ErrDateFormat
			return
		}

	default:
		// неверный формат
		err = ErrDateFormat
		return
	}
	return
}

func DaysInMonth(year int, month time.Month) int {
	switch month {
	case time.January:
		return 31

	case time.February:
		date := time.Date(year, time.March, 1, 0, 0, 0, 0, time.Local)
		date = date.AddDate(0, 0, -1)
		return date.Day()

	case time.March:
		return 31

	case time.April:
		return 30

	case time.May:
		return 31

	case time.June:
		return 30

	case time.July:
		return 31

	case time.August:
		return 31

	case time.September:
		return 30

	case time.October:
		return 31

	case time.November:
		return 30

	case time.December:
		return 31
	}
	return 0
}
