package fs

import (
	"encoding/json"
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"
)

// Duration is a time.Duration with some more parsing options
type Duration time.Duration

// DurationOff is the default value for flags which can be turned off
const DurationOff = Duration((1 << 63) - 1)

// Turn Duration into a string
func (d Duration) String() string {
	if d == DurationOff {
		return "off"
	}
	for i := len(ageSuffixes) - 2; i >= 0; i-- {
		ageSuffix := &ageSuffixes[i]
		if math.Abs(float64(d)) >= float64(ageSuffix.Multiplier) {
			timeUnits := float64(d) / float64(ageSuffix.Multiplier)
			return strconv.FormatFloat(timeUnits, 'f', -1, 64) + ageSuffix.Suffix
		}
	}
	return time.Duration(d).String()
}

// IsSet returns if the duration is != DurationOff
func (d Duration) IsSet() bool {
	return d != DurationOff
}

// We use time conventions
var ageSuffixes = []struct {
	Suffix     string
	Multiplier time.Duration
}{
	{Suffix: "d", Multiplier: time.Hour * 24},
	{Suffix: "w", Multiplier: time.Hour * 24 * 7},
	{Suffix: "M", Multiplier: time.Hour * 24 * 30},
	{Suffix: "y", Multiplier: time.Hour * 24 * 365},

	// Default to second
	{Suffix: "", Multiplier: time.Second},
}

// parse the age as suffixed ages
func parseDurationSuffixes(age string) (time.Duration, error) {
	var period float64

	for _, ageSuffix := range ageSuffixes {
		if strings.HasSuffix(age, ageSuffix.Suffix) {
			numberString := age[:len(age)-len(ageSuffix.Suffix)]
			var err error
			period, err = strconv.ParseFloat(numberString, 64)
			if err != nil {
				return time.Duration(0), err
			}
			period *= float64(ageSuffix.Multiplier)
			break
		}
	}

	return time.Duration(period), nil
}

// time formats to try parsing ages as - in order
var timeFormats = []string{
	time.RFC3339,
	"2006-01-02T15:04:05",
	"2006-01-02 15:04:05",
	"2006-01-02",
}

// parse the date as time in various date formats
func parseTimeDates(date string) (t time.Time, err error) {
	var instant time.Time
	for _, timeFormat := range timeFormats {
		instant, err = time.ParseInLocation(timeFormat, date, time.Local)
		if err == nil {
			return instant, nil
		}
	}
	return t, err
}

// parse the age as time before the epoch in various date formats
func parseDurationDates(age string, epoch time.Time) (d time.Duration, err error) {
	instant, err := parseTimeDates(age)
	if err != nil {
		return d, err
	}

	return epoch.Sub(instant), nil
}

// parseDurationFromNow parses a duration string. Allows ParseDuration to match the time
// package and easier testing within the fs package.
func parseDurationFromNow(age string, getNow func() time.Time) (d time.Duration, err error) {
	if age == "off" {
		return time.Duration(DurationOff), nil
	}

	// Attempt to parse as a time.Duration first
	d, err = time.ParseDuration(age)
	if err == nil {
		return d, nil
	}

	d, err = parseDurationSuffixes(age)
	if err == nil {
		return d, nil
	}

	d, err = parseDurationDates(age, getNow())
	if err == nil {
		return d, nil
	}

	return d, err
}

// ParseDuration parses a duration string. Accept ms|s|m|h|d|w|M|y suffixes. Defaults to second if not provided
func ParseDuration(age string) (time.Duration, error) {
	return parseDurationFromNow(age, timeNowFunc)
}

// ReadableString parses d into a human-readable duration with units.
// Examples: "3s", "1d2h23m20s", "292y24w3d23h47m16s".
func (d Duration) ReadableString() string {
	return d.readableString(0)
}

// ShortReadableString parses d into a human-readable duration with units.
// This method returns it in short format, including the 3 most significant
// units only, sacrificing precision if necessary. E.g. returns "292y24w3d"
// instead of "292y24w3d23h47m16s", and "3d23h47m" instead of "3d23h47m16s".
func (d Duration) ShortReadableString() string {
	return d.readableString(3)
}

// readableString parses d into a human-readable duration with units.
// Parameter maxNumberOfUnits limits number of significant units to include,
// sacrificing precision. E.g. with argument 3 it returns "292y24w3d" instead
// of "292y24w3d23h47m16s", and "3d23h47m" instead of "3d23h47m16s". Zero or
// negative argument means include all.
// Based on https://github.com/hako/durafmt
func (d Duration) readableString(maxNumberOfUnits int) string {
	switch d {
	case DurationOff:
		return "off"
	case 0:
		return "0s"
	}

	readableString := ""

	// Check for minus durations.
	if d < 0 {
		readableString += "-"
	}

	duration := time.Duration(math.Abs(float64(d)))

	// Convert duration.
	seconds := int64(duration.Seconds()) % 60
	minutes := int64(duration.Minutes()) % 60
	hours := int64(duration.Hours()) % 24
	days := int64(duration/(24*time.Hour)) % 365 % 7

	// Edge case between 364 and 365 days.
	// We need to calculate weeks from what is left from years
	leftYearDays := int64(duration/(24*time.Hour)) % 365
	weeks := leftYearDays / 7
	if leftYearDays >= 364 && leftYearDays < 365 {
		weeks = 52
	}

	years := int64(duration/(24*time.Hour)) / 365
	milliseconds := int64(duration/time.Millisecond) -
		(seconds * 1000) - (minutes * 60000) - (hours * 3600000) -
		(days * 86400000) - (weeks * 604800000) - (years * 31536000000)

	// Create a map of the converted duration time.
	durationMap := map[string]int64{
		"ms": milliseconds,
		"s":  seconds,
		"m":  minutes,
		"h":  hours,
		"d":  days,
		"w":  weeks,
		"y":  years,
	}

	// Construct duration string.
	numberOfUnits := 0
	for _, u := range [...]string{"y", "w", "d", "h", "m", "s", "ms"} {
		v := durationMap[u]
		strval := strconv.FormatInt(v, 10)
		if v == 0 {
			continue
		}
		readableString += strval + u
		numberOfUnits++
		if maxNumberOfUnits > 0 && numberOfUnits >= maxNumberOfUnits {
			break
		}
	}

	return readableString
}

// Set a Duration
func (d *Duration) Set(s string) error {
	duration, err := ParseDuration(s)
	if err != nil {
		return err
	}
	*d = Duration(duration)
	return nil
}

// Type of the value
func (d Duration) Type() string {
	return "Duration"
}

// UnmarshalJSON makes sure the value can be parsed as a string or integer in JSON
func (d *Duration) UnmarshalJSON(in []byte) error {
	// Check if the input is a string value.
	if len(in) >= 2 && in[0] == '"' && in[len(in)-1] == '"' {
		strVal := string(in[1 : len(in)-1]) // Remove the quotes

		// Attempt to parse the string as a duration.
		parsedDuration, err := ParseDuration(strVal)
		if err != nil {
			return err
		}
		*d = Duration(parsedDuration)
		return nil
	}
	// Handle numeric values.
	var i int64
	err := json.Unmarshal(in, &i)
	if err != nil {
		return err
	}
	*d = Duration(i)
	return nil
}

// Scan implements the fmt.Scanner interface
func (d *Duration) Scan(s fmt.ScanState, ch rune) error {
	token, err := s.Token(true, func(rune) bool { return true })
	if err != nil {
		return err
	}
	return d.Set(string(token))
}
