package dttm

import (
	"regexp"
	"time"

	"github.com/alcionai/clues"
)

type TimeFormat string

const (
	// Standard is the canonical format used for all data storage in corso
	Standard TimeFormat = time.RFC3339Nano

	// DateOnly is accepted by the CLI as a valid input for timestamp-based
	// filters.  Time and timezone are assumed to be 00:00:00 and UTC.
	DateOnly TimeFormat = "2006-01-02"

	// TabularOutput is used when displaying time values to the user in
	// non-json cli outputs.
	TabularOutput TimeFormat = "2006-01-02T15:04:05Z"

	// Legacy is used in /exchange/service_restore to comply with certain
	// graphAPI time format requirements.
	Legacy TimeFormat = time.RFC3339

	// HumanReadable is the default value appended to the root restoration folder name.
	HumanReadable TimeFormat = "02-Jan-2006_15:04:05"
	// HumanReadableDriveItem modifies SimpleDateTimeFormat to comply with onedrive folder
	// restrictions: primarily swapping `-` instead of `:` which is a reserved character.
	HumanReadableDriveItem TimeFormat = "02-Jan-2006_15-04-05"

	// m365 will remove the :00 second suffix on folder names, resulting in the following formats.
	ClippedHuman          TimeFormat = "02-Jan-2006_15:04"
	ClippedHumanDriveItem TimeFormat = "02-Jan-2006_15-04"

	// SafeForTesting is used for testing restore destination folders.
	// Microsecond granularity prevents collisions in parallel package or workflow runs.
	SafeForTesting TimeFormat = HumanReadableDriveItem + ".000000"

	// M365dateTimeTimeZoneTimeFormat is the format used by M365 for datetimetimezone resource
	// https://learn.microsoft.com/en-us/graph/api/resources/datetimetimezone?view=graph-rest-1.0
	M365DateTimeTimeZone TimeFormat = "2006-01-02T15:04:05.0000000"
)

// these regexes are used to extract time formats from strings.  Their primary purpose is to
// identify the folders produced in external data during automated testing.  For safety, each
// time format described above should have a matching regexp.
var (
	clippedHumanRE         = regexp.MustCompile(`.*(\d{2}-[a-zA-Z]{3}-\d{4}_\d{2}:\d{2}).*`)
	clippedHumanOneDriveRE = regexp.MustCompile(`.*(\d{2}-[a-zA-Z]{3}-\d{4}_\d{2}-\d{2}).*`)
	dateOnlyRE             = regexp.MustCompile(`.*(\d{4}-\d{2}-\d{2}).*`)
	legacyRE               = regexp.MustCompile(
		`.*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}?([Zz]|[a-zA-Z]{2}|([\+|\-]([01]\d|2[0-3])))).*`)
	SafeForTestingRE        = regexp.MustCompile(`.*(\d{2}-[a-zA-Z]{3}-\d{4}_\d{2}-\d{2}-\d{2}.\d{6}).*`)
	HumanReadableRE         = regexp.MustCompile(`.*(\d{2}-[a-zA-Z]{3}-\d{4}_\d{2}:\d{2}:\d{2}).*`)
	HumanReadableOneDriveRE = regexp.MustCompile(`.*(\d{2}-[a-zA-Z]{3}-\d{4}_\d{2}-\d{2}-\d{2}).*`)
	standardRE              = regexp.MustCompile(
		`.*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[a-zA-Z]{2}|([\+|\-]([01]\d|2[0-3])))).*`)
	tabularOutputRE = regexp.MustCompile(`.*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([Zz]|[a-zA-Z]{2})).*`)
)

var (
	// shortened formats (clipped*, DateOnly) must follow behind longer formats, otherwise they'll
	// get eagerly chosen as the parsable format, slicing out some data.
	formats = []TimeFormat{
		Standard,
		SafeForTesting,
		HumanReadable,
		HumanReadableDriveItem,
		M365DateTimeTimeZone,
		Legacy,
		TabularOutput,
		ClippedHuman,
		ClippedHumanDriveItem,
		DateOnly,
	}
	regexes = []*regexp.Regexp{
		standardRE,
		SafeForTestingRE,
		HumanReadableRE,
		HumanReadableOneDriveRE,
		legacyRE,
		tabularOutputRE,
		clippedHumanRE,
		clippedHumanOneDriveRE,
		dateOnlyRE,
	}
)

var (
	ErrNoTimeString        = clues.New("no substring contains a known time format")
	errParsingStringToTime = clues.New("parsing string as time.Time")
)

// Now produces the current time as a string in the standard format.
func Now() string {
	return FormatNow(Standard)
}

func OrNow(t time.Time) time.Time {
	if t.IsZero() {
		return time.Now().UTC()
	}

	return t
}

// FormatNow produces the current time in UTC using the provided
// time format.
func FormatNow(fmt TimeFormat) string {
	return FormatTo(time.Now().UTC(), fmt)
}

// FormatTo produces the a datetime with the given format.
func FormatTo(t time.Time, fmt TimeFormat) string {
	return t.UTC().Format(string(fmt))
}

// Format produces the standard format for corso time values.
// Always formats into the UTC timezone.
func Format(t time.Time) string {
	return FormatTo(t, Standard)
}

// FormatToHumanReadable produces a simple datetime of the format
// "02-Jan-2006_15:04:05"
func FormatToHumanReadable(t time.Time) string {
	return FormatTo(t, HumanReadable)
}

// FormatToTabularDisplay produces the standard format for displaying
// a timestamp as part of user-readable cli output.
// "2016-01-02T15:04:05Z"
func FormatToTabularDisplay(t time.Time) string {
	return FormatTo(t, TabularOutput)
}

// FormatToLegacy produces standard format for string values
// that are placed in SingleValueExtendedProperty tags
func FormatToLegacy(t time.Time) string {
	return FormatTo(t, Legacy)
}

// ParseTime makes a best attempt to produce a time value from
// the provided string.  Always returns a UTC timezone value.
func ParseTime(s string) (time.Time, error) {
	if len(s) == 0 {
		return time.Time{}, clues.Stack(errParsingStringToTime, clues.New("empty string"))
	}

	for _, form := range formats {
		t, err := time.Parse(string(form), s)
		if err == nil {
			return t.UTC(), nil
		}
	}

	return time.Time{}, clues.Stack(errParsingStringToTime, clues.New(s))
}

// ExtractTime greedily retrieves a timestamp substring from the provided string.
// returns ErrNoTimeString if no match is found.
func ExtractTime(s string) (time.Time, error) {
	if len(s) == 0 {
		return time.Time{}, clues.Stack(errParsingStringToTime, clues.New("empty string"))
	}

	for _, re := range regexes {
		ss := re.FindAllStringSubmatch(s, -1)
		if len(ss) > 0 && len(ss[0]) > 1 {
			return ParseTime(ss[0][1])
		}
	}

	return time.Time{}, clues.Stack(ErrNoTimeString, clues.New(s))
}
