package strutil

import (
	"errors"
	"regexp"
	"strconv"
	"strings"
	"time"
	"unicode"

	"github.com/gookit/goutil/byteutil"
)

var regNumVersion = regexp.MustCompile(`[0-9][\d.]+([_-]\d+)?`)

// NumVersion parse input string, get valid number version. eg: go-1.22.3 -> 1.22.3
func NumVersion(s string) string {
	return regNumVersion.FindString(s)
}

// MustToTime convert date string to time.Time
func MustToTime(s string, layouts ...string) time.Time {
	t, err := ToTime(s, layouts...)
	if err != nil {
		panic(err)
	}
	return t
}

// auto match uses some common layouts.
// key is layout length.
var layoutMap = map[int][]string{
	6:  {"200601", "060102", time.Kitchen},
	8:  {"20060102", "06-01-02"},
	10: {"2006-01-02"},
	13: {"2006-01-02 15"},
	15: {time.Stamp},
	16: {"2006-01-02 15:04"},
	19: {"2006-01-02 15:04:05", time.RFC822, time.StampMilli},
	20: {"2006-01-02 15:04:05Z"},
	21: {time.RFC822Z},
	22: {time.StampMicro},
	23: {"2006-01-02 15:04:05.000", "2006-01-02 15:04:05.999"},
	24: {time.ANSIC},
	25: {time.RFC3339, time.StampNano},
	// time.Layout}, // must go >= 1.19
	26: {"2006-01-02 15:04:05.000000"},
	28: {time.UnixDate},
	29: {time.RFC1123, "2006-01-02 15:04:05.000000000"},
	30: {time.RFC850},
	31: {time.RFC1123Z},
	35: {time.RFC3339Nano},
}

// ToTime convert date string to time.Time
//
// NOTE: always use local timezone.
func ToTime(s string, layouts ...string) (t time.Time, err error) {
	return ToTimeIn(s, time.Local, layouts...)
}

// ToTimeIn convert date string to time.Time with location.
func ToTimeIn(s string, loc *time.Location, layouts ...string) (t time.Time, err error) {
	if loc == nil {
		loc = time.Local
	}

	// custom layout
	if len(layouts) > 0 {
		if len(layouts[0]) > 0 {
			return time.ParseInLocation(layouts[0], s, loc)
		}

		err = ErrDateLayout
		return
	}

	// auto match use some commonly layouts.
	strLn := len(s)
	maybeLayouts, ok := layoutMap[strLn]
	if !ok {
		err = ErrInvalidParam
		return
	}

	var hasAlphaT bool
	if pos := strings.IndexByte(s, 'T'); pos > 0 && pos < 12 {
		hasAlphaT = true
	}

	hasSlashR := strings.IndexByte(s, '/') > 0
	for _, layout := range maybeLayouts {
		// date string has "T". eg: "2006-01-02T15:04:05"
		if hasAlphaT {
			layout = strings.Replace(layout, " ", "T", 1)
		}

		// date string has "/". eg: "2006/01/02 15:04:05"
		if hasSlashR {
			layout = strings.ReplaceAll(layout, "-", "/")
		}
		if strLn > 10 && s[10] != ' ' && len(layout) > 10 && layout[10] == ' ' {
			layout = layout[:10] + string(s[10]) + layout[11:]
		}

		t, err = time.ParseInLocation(layout, s, loc)
		if err == nil {
			return
		}
	}

	// t, err = time.ParseInLocation(layout, s, loc)
	return
}

// ParseSizeOpt parse size expression options
type ParseSizeOpt struct {
	// OneAsMax if only one size value, use it as max size. default is false
	OneAsMax bool
	// SepChar is the separator char for time range string. default is '~'
	SepChar byte
	// KeywordFn is the function for parse keyword time string.
	KeywordFn func(string) (min, max uint64, err error)
}

func ensureOpt(opt *ParseSizeOpt) *ParseSizeOpt {
	if opt == nil {
		opt = &ParseSizeOpt{SepChar: '~'}
	} else {
		if opt.SepChar == 0 {
			opt.SepChar = '~'
		}
	}
	return opt
}

// ErrInvalidSizeExpr invalid size expression error
var ErrInvalidSizeExpr = errors.New("invalid size expr")

// ParseSizeRange parse range size expression to min and max size.
//
// Expression format:
//
//	"1KB~2MB"       => 1KB to 2MB
//	"-1KB"          => <1KB
//	"~1MB"          => <1MB
//	"< 1KB"         => <1KB
//	"1KB"           => >1KB
//	"1KB~"          => >1KB
//	">1KB"          => >1KB
//	"+1KB"          => >1KB
func ParseSizeRange(expr string, opt *ParseSizeOpt) (min, max uint64, err error) {
	opt = ensureOpt(opt)
	expr = strings.TrimSpace(expr)
	if expr == "" {
		err = ErrInvalidSizeExpr
		return
	}

	// parse size range. eg: "1KB~2MB"
	if strings.IndexByte(expr, '~') > -1 {
		s1, s2 := TrimCut(expr, "~")
		if s1 != "" {
			min, err = ToByteSize(s1)
			if err != nil {
				return
			}
		}

		if s2 != "" {
			max, err = ToByteSize(s2)
		}
		return
	}

	// parse single size. eg: "1KB"
	if byteutil.IsNumChar(expr[0]) {
		min, err = ToByteSize(expr)
		if err != nil {
			return
		}
		if opt.OneAsMax {
			max = min
		}
		return
	}

	// parse with prefix. eg: "<1KB", ">= 1KB", "-1KB", "+1KB"
	switch expr[0] {
	case '<', '-':
		max, err = ToByteSize(strings.Trim(expr[1:], " ="))
	case '>', '+':
		min, err = ToByteSize(strings.Trim(expr[1:], " ="))
	default:
		// parse keyword. eg: "small", "large"
		if opt.KeywordFn != nil {
			min, max, err = opt.KeywordFn(expr)
		} else {
			err = ErrInvalidSizeExpr
		}
	}
	return
}

// SafeByteSize converts size string like 1GB/1g or 12mb/12M into an unsigned integer number of bytes
func SafeByteSize(sizeStr string) uint64 {
	size, _ := ToByteSize(sizeStr)
	return size
}

// ToByteSize converts size string like 1GB/1g or 12mb/12M into an unsigned integer number of bytes
func ToByteSize(sizeStr string) (uint64, error) {
	sizeStr = strings.TrimSpace(sizeStr)
	lastPos := len(sizeStr) - 1
	if lastPos < 0 {
		return 0, nil
	}

	if sizeStr[lastPos] == 'b' || sizeStr[lastPos] == 'B' {
		// last second char is k,m,g,t
		lastSec := sizeStr[lastPos-1]
		if lastSec > 'A' {
			lastPos--
		}
	} else if IsNumChar(sizeStr[lastPos]) { // not unit suffix. eg: 346
		return strconv.ParseUint(sizeStr, 10, 32)
	}

	multiplier := float64(1)
	switch unicode.ToLower(rune(sizeStr[lastPos])) {
	case 'k':
		multiplier = 1 << 10
	case 'm':
		multiplier = 1 << 20
	case 'g':
		multiplier = 1 << 30
	case 't':
		multiplier = 1 << 40
	case 'p':
		multiplier = 1 << 50
	default: // b
		multiplier = 1
	}

	sizeNum := strings.TrimSpace(sizeStr[:lastPos])
	size, err := strconv.ParseFloat(sizeNum, 64)
	if err != nil {
		return 0, err
	}
	return uint64(size * multiplier), nil
}
