package uuidv7

import (
	"crypto/rand"
	"encoding/hex"
	"errors"
	"net/url"
	"strings"
	"time"
)

// New returns a UUIDv7 (RFC 9562) as a 16-byte array.
func New() ([16]byte, error) {
	var out [16]byte

	var r [10]byte
	if _, err := rand.Read(r[:]); err != nil {
		return out, err
	}

	// 48-bit Unix epoch milliseconds
	ms := uint64(time.Now().UnixNano() / int64(time.Millisecond))
	out[0] = byte(ms >> 40)
	out[1] = byte(ms >> 32)
	out[2] = byte(ms >> 24)
	out[3] = byte(ms >> 16)
	out[4] = byte(ms >> 8)
	out[5] = byte(ms)

	// Version (0111) in high 4 bits of byte 6
	out[6] = (r[0] & 0x0f) | 0x70
	out[7] = r[1]

	// Variant (10) in high 2 bits of byte 8
	out[8] = (r[2] & 0x3f) | 0x80
	out[9] = r[3]
	out[10] = r[4]
	out[11] = r[5]
	out[12] = r[6]
	out[13] = r[7]
	out[14] = r[8]
	out[15] = r[9]

	return out, nil
}

// MustNew panics if UUID generation fails.
func MustNew() [16]byte {
	u, err := New()
	if err != nil {
		panic(err)
	}
	return u
}

// Bytes returns a new UUIDv7 as []byte for BINARY(16) columns.
func Bytes() ([]byte, error) {
	u, err := New()
	if err != nil {
		return nil, err
	}
	b := make([]byte, 16)
	copy(b, u[:])
	return b, nil
}

// MustBytes panics if UUID generation fails.
func MustBytes() []byte {
	u := MustNew()
	b := make([]byte, 16)
	copy(b, u[:])
	return b
}

// String formats a UUID as a canonical string (8-4-4-4-12).
func String(u [16]byte) (string, error) {
	if len(u) != 16 {
		return "", errors.New("invalid uuid length")
	}
	dst := make([]byte, 36)
	hex.Encode(dst[0:8], u[0:4])
	dst[8] = '-'
	hex.Encode(dst[9:13], u[4:6])
	dst[13] = '-'
	hex.Encode(dst[14:18], u[6:8])
	dst[18] = '-'
	hex.Encode(dst[19:23], u[8:10])
	dst[23] = '-'
	hex.Encode(dst[24:36], u[10:16])
	return string(dst), nil
}

// BytesToString formats a UUID stored in []byte as a canonical string.
func BytesToString(b []byte) (string, error) {
	if len(b) != 16 {
		return "", errors.New("invalid uuid length")
	}
	var u [16]byte
	copy(u[:], b)
	return String(u)
}

// ParseString parses a canonical UUID string into []byte (16 bytes).
func ParseString(s string) ([]byte, error) {
	if len(s) != 36 {
		return nil, errors.New("invalid uuid string length")
	}
	buf := make([]byte, 16)
	_, err := hex.Decode(buf[0:4], []byte(s[0:8]))
	if err != nil {
		return nil, err
	}
	_, err = hex.Decode(buf[4:6], []byte(s[9:13]))
	if err != nil {
		return nil, err
	}
	_, err = hex.Decode(buf[6:8], []byte(s[14:18]))
	if err != nil {
		return nil, err
	}
	_, err = hex.Decode(buf[8:10], []byte(s[19:23]))
	if err != nil {
		return nil, err
	}
	_, err = hex.Decode(buf[10:16], []byte(s[24:36]))
	if err != nil {
		return nil, err
	}
	return buf, nil
}

// ParsePathLikeString parses a UUID from a path-like value. This is useful for
// URL params that may arrive escaped, wrapped in a full URL, or with a trailing slash.
func ParsePathLikeString(raw string) ([]byte, error) {
	for _, candidate := range candidateUUIDStrings(raw) {
		if parsed, err := ParseString(candidate); err == nil {
			return parsed, nil
		}
	}
	return nil, errors.New("invalid uuid string")
}

func candidateUUIDStrings(raw string) []string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil
	}

	candidates := make([]string, 0, 6)
	seen := make(map[string]struct{}, 6)
	push := func(value string) {
		value = strings.TrimSpace(value)
		if value == "" {
			return
		}
		if _, ok := seen[value]; ok {
			return
		}
		seen[value] = struct{}{}
		candidates = append(candidates, value)
	}

	push(raw)
	if decoded, err := url.PathUnescape(raw); err == nil && decoded != raw {
		push(decoded)
	}
	if decoded, err := url.QueryUnescape(raw); err == nil && decoded != raw {
		push(decoded)
	}

	for i := 0; i < len(candidates); i++ {
		value := candidates[i]
		value = strings.TrimSuffix(value, "/")
		push(value)

		if idx := strings.IndexAny(value, "?#"); idx >= 0 {
			push(value[:idx])
		}
		if idx := strings.LastIndex(value, "/"); idx >= 0 && idx+1 < len(value) {
			push(value[idx+1:])
		}
		if idx := strings.LastIndex(value, "="); idx >= 0 && idx+1 < len(value) {
			push(value[idx+1:])
		}
		for _, part := range strings.Split(value, "/") {
			push(part)
		}
		for _, part := range strings.FieldsFunc(value, func(r rune) bool {
			switch r {
			case '/', '?', '#', '&', '=':
				return true
			default:
				return false
			}
		}) {
			push(part)
		}
	}

	return candidates
}
