package types

import (
	"database/sql/driver"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"time"
)

var numOfDigitsOfUnixTimestamp = len(strconv.FormatInt(time.Now().Unix(), 10))
var numOfDigitsOfMilliSecondUnixTimestamp = len(strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10))
var numOfDigitsOfNanoSecondsUnixTimestamp = len(strconv.FormatInt(time.Now().UnixNano(), 10))

type NanosecondTimestamp time.Time

func (t NanosecondTimestamp) Time() time.Time {
	return time.Time(t)
}

func (t *NanosecondTimestamp) UnmarshalJSON(data []byte) error {
	var v int64

	var err = json.Unmarshal(data, &v)
	if err != nil {
		return err
	}

	*t = NanosecondTimestamp(time.Unix(0, v))
	return nil
}

type MillisecondTimestamp time.Time

func NewMillisecondTimestampFromInt(i int64) MillisecondTimestamp {
	return MillisecondTimestamp(time.Unix(0, i*int64(time.Millisecond)))
}

func MustParseMillisecondTimestamp(a string) MillisecondTimestamp {
	m, err := strconv.ParseInt(a, 10, 64) // startTime
	if err != nil {
		panic(fmt.Errorf("millisecond timestamp parse error %v", err))
	}

	return NewMillisecondTimestampFromInt(m)
}

func MustParseUnixTimestamp(a string) time.Time {
	m, err := strconv.ParseInt(a, 10, 64) // startTime
	if err != nil {
		panic(fmt.Errorf("millisecond timestamp parse error %v", err))
	}

	return time.Unix(m, 0)
}

func (t MillisecondTimestamp) String() string {
	return time.Time(t).String()
}

func (t MillisecondTimestamp) Time() time.Time {
	return time.Time(t)
}

func (t *MillisecondTimestamp) UnmarshalJSON(data []byte) error {
	var v interface{}

	var err = json.Unmarshal(data, &v)
	if err != nil {
		return err
	}

	switch vt := v.(type) {
	case string:
		if vt == "" {
			// treat empty string as 0
			*t = MillisecondTimestamp(time.Time{})
			return nil
		}

		f, err := strconv.ParseFloat(vt, 64)
		if err == nil {
			tt, err := convertFloat64ToTime(vt, f)
			if err != nil {
				return err
			}

			*t = MillisecondTimestamp(tt)
			return nil
		}

		tt, err := time.Parse(time.RFC3339Nano, vt)
		if err == nil {
			*t = MillisecondTimestamp(tt)
			return nil
		}

		return err

	case float64:
		str := strconv.FormatFloat(vt, 'f', -1, 64)
		tt, err := convertFloat64ToTime(str, vt)
		if err != nil {
			return err
		}

		*t = MillisecondTimestamp(tt)
		return nil

	default:
		return fmt.Errorf("can not parse %T %+v as millisecond timestamp", vt, vt)

	}

	// Unreachable
}

func convertFloat64ToTime(vt string, f float64) (time.Time, error) {
	idx := strings.Index(vt, ".")
	if idx > 0 {
		vt = vt[0 : idx-1]
	}

	if len(vt) <= numOfDigitsOfUnixTimestamp {
		return time.Unix(0, int64(f*float64(time.Second))), nil
	} else if len(vt) <= numOfDigitsOfMilliSecondUnixTimestamp {
		return time.Unix(0, int64(f)*int64(time.Millisecond)), nil
	} else if len(vt) <= numOfDigitsOfNanoSecondsUnixTimestamp {
		return time.Unix(0, int64(f)), nil
	}

	return time.Time{}, fmt.Errorf("the floating point value %f is out of the timestamp range", f)
}

// Time type implements the driver value for sqlite
type Time time.Time

// layout defines the time layout without timezone
// because sqlite3 and mysql does not output datetime with timezone.
//
// sqlite3 format (in UTC): 2022-06-01 11:38:45
// mysql format in (in Local): 2022-06-02 20:28:10 or 2022-06-02 20:28:10.000
var layout = "2006-01-02 15:04:05.999Z07:00"

func (t *Time) UnmarshalJSON(data []byte) error {
	// fallback to RFC3339
	return (*time.Time)(t).UnmarshalJSON(data)
}

func (t Time) MarshalJSON() ([]byte, error) {
	return time.Time(t).MarshalJSON()
}

func (t Time) String() string {
	return time.Time(t).String()
}

func (t Time) Time() time.Time {
	return time.Time(t)
}

func (t Time) Unix() int64 {
	return time.Time(t).Unix()
}

func (t Time) UnixMilli() int64 {
	return time.Time(t).UnixMilli()
}

func (t Time) Equal(time2 time.Time) bool {
	return time.Time(t).Equal(time2)
}

func (t Time) After(time2 time.Time) bool {
	return time.Time(t).After(time2)
}

func (t Time) Before(time2 time.Time) bool {
	return time.Time(t).Before(time2)
}

func NewTimeFromUnix(sec int64, nsec int64) Time {
	return Time(time.Unix(sec, nsec))
}

// Value implements the driver.Valuer interface
// see http://jmoiron.net/blog/built-in-interfaces/
func (t Time) Value() (driver.Value, error) {
	if time.Time(t) == (time.Time{}) {
		return nil, nil
	}
	return time.Time(t), nil
}

func (t *Time) Scan(src interface{}) error {
	// skip nil time
	if src == nil {
		return nil
	}

	switch d := src.(type) {

	case *time.Time:
		*t = Time(*d)
		return nil

	case time.Time:
		*t = Time(d)
		return nil

	case string:
		// 2020-12-16 05:17:12.994+08:00
		tt, err := time.Parse(layout, d)
		if err != nil {
			return err
		}

		*t = Time(tt)
		return nil

	case []byte:
		// 2019-10-20 23:01:43.77+08:00
		tt, err := time.Parse(layout, string(d))
		if err != nil {
			return err
		}

		*t = Time(tt)
		return nil

	default:

	}

	return fmt.Errorf("datatype.Time scan error, type: %T is not supported, value; %+v", src, src)
}

var looseTimeFormats = []string{
	time.RFC3339,
	time.RFC822,
	"2006-01-02T15:04:05",
	"2006-01-02",
}

// LooseFormatTime parses date time string with a wide range of formats.
type LooseFormatTime time.Time

func ParseLooseFormatTime(s string) (LooseFormatTime, error) {
	var t time.Time
	switch s {
	case "now":
		t = time.Now()
		return LooseFormatTime(t), nil

	case "yesterday":
		t = time.Now().AddDate(0, 0, -1)
		return LooseFormatTime(t), nil

	case "last month":
		t = time.Now().AddDate(0, -1, 0)
		return LooseFormatTime(t), nil

	case "last 30 days":
		t = time.Now().AddDate(0, 0, -30)
		return LooseFormatTime(t), nil

	case "last year":
		t = time.Now().AddDate(-1, 0, 0)
		return LooseFormatTime(t), nil

	}

	tv, err := ParseTimeWithFormats(s, looseTimeFormats)
	if err != nil {
		return LooseFormatTime{}, err
	}

	return LooseFormatTime(tv), nil
}

func (t *LooseFormatTime) UnmarshalYAML(unmarshal func(interface{}) error) error {
	var str string
	if err := unmarshal(&str); err != nil {
		return err
	}

	lt, err := ParseLooseFormatTime(str)
	if err != nil {
		return err
	}

	*t = lt
	return nil
}

func (t *LooseFormatTime) UnmarshalJSON(data []byte) error {
	var v string
	err := json.Unmarshal(data, &v)
	if err != nil {
		return err
	}

	tv, err := ParseTimeWithFormats(v, looseTimeFormats)
	if err != nil {
		return err
	}

	*t = LooseFormatTime(tv)
	return nil
}

func (t LooseFormatTime) MarshalJSON() ([]byte, error) {
	return []byte(strconv.Quote(time.Time(t).Format(time.RFC3339))), nil
}

func (t LooseFormatTime) Time() time.Time {
	return time.Time(t)
}

// Timestamp is used for parsing unix timestamp (seconds)
type Timestamp time.Time

func (t Timestamp) Format(layout string) string {
	return time.Time(t).Format(layout)
}

func (t Timestamp) Time() time.Time {
	return time.Time(t)
}

func (t Timestamp) String() string {
	return time.Time(t).String()
}

func (t Timestamp) MarshalJSON() ([]byte, error) {
	ts := time.Time(t).Unix()
	return json.Marshal(ts)
}

func (t *Timestamp) UnmarshalJSON(o []byte) error {
	var timestamp int64
	if err := json.Unmarshal(o, &timestamp); err != nil {
		return err
	}

	*t = Timestamp(time.Unix(timestamp, 0))
	return nil
}

func ParseTimeWithFormats(strTime string, formats []string) (time.Time, error) {
	for _, format := range formats {
		tt, err := time.Parse(format, strTime)
		if err == nil {
			return tt, nil
		}
	}
	return time.Time{}, fmt.Errorf("failed to parse time %s, valid formats are %+v", strTime, formats)
}

func BeginningOfTheDay(t time.Time) time.Time {
	year, month, day := t.Date()
	return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
}

func Over24Hours(since time.Time) bool {
	return time.Since(since) >= 24*time.Hour
}
