package playwright

import (
	"net/url"
	"regexp"
	"strconv"
	"strings"
)

var escapedChars = map[rune]bool{
	'$':  true,
	'^':  true,
	'+':  true,
	'.':  true,
	'*':  true,
	'(':  true,
	')':  true,
	'|':  true,
	'\\': true,
	'?':  true,
	'{':  true,
	'}':  true,
	'[':  true,
	']':  true,
}

func globMustToRegex(glob string) *regexp.Regexp {
	tokens := []string{"^"}
	inGroup := false

	for i := 0; i < len(glob); i++ {
		c := rune(glob[i])
		if c == '\\' && i+1 < len(glob) {
			char := rune(glob[i+1])
			if _, ok := escapedChars[char]; ok {
				tokens = append(tokens, "\\"+string(char))
			} else {
				tokens = append(tokens, string(char))
			}
			i++
		} else if c == '*' {
			beforeDeep := rune(0)
			if i > 0 {
				beforeDeep = rune(glob[i-1])
			}
			starCount := 1
			for i+1 < len(glob) && glob[i+1] == '*' {
				starCount++
				i++
			}
			afterDeep := rune(0)
			if i+1 < len(glob) {
				afterDeep = rune(glob[i+1])
			}
			isDeep := starCount > 1 && (beforeDeep == '/' || beforeDeep == 0) && (afterDeep == '/' || afterDeep == 0)
			if isDeep {
				tokens = append(tokens, "((?:[^/]*(?:/|$))*)")
				i++
			} else {
				tokens = append(tokens, "([^/]*)")
			}
		} else {
			switch c {
			case '{':
				inGroup = true
				tokens = append(tokens, "(")
			case '}':
				inGroup = false
				tokens = append(tokens, ")")
			case ',':
				if inGroup {
					tokens = append(tokens, "|")
				} else {
					tokens = append(tokens, "\\"+string(c))
				}
			default:
				if _, ok := escapedChars[c]; ok {
					tokens = append(tokens, "\\"+string(c))
				} else {
					tokens = append(tokens, string(c))
				}
			}
		}
	}

	tokens = append(tokens, "$")
	return regexp.MustCompile(strings.Join(tokens, ""))
}

func resolveGlobToRegex(baseURL *string, glob string, isWebSocketUrl bool) *regexp.Regexp {
	if isWebSocketUrl {
		baseURL = toWebSocketBaseURL(baseURL)
	}
	glob = resolveGlobBase(baseURL, glob)
	return globMustToRegex(glob)
}

func resolveGlobBase(baseURL *string, match string) string {
	if strings.HasPrefix(match, "*") {
		return match
	}

	tokenMap := make(map[string]string)
	mapToken := func(original string, replacement string) string {
		if len(original) == 0 {
			return ""
		}
		tokenMap[replacement] = original
		return replacement
	}
	// Escaped `\\?` behaves the same as `?` in our glob patterns.
	match = strings.ReplaceAll(match, `\\?`, "?")
	// Glob symbols may be escaped in the URL and some of them such as ? affect resolution,
	// so we replace them with safe components first.
	relativePath := strings.Split(match, "/")
	for i, token := range relativePath {
		if token == "." || token == ".." || token == "" {
			continue
		}
		// Handle special case of http*://, note that the new schema has to be
		// a web schema so that slashes are properly inserted after domain.
		if i == 0 && strings.HasSuffix(token, ":") {
			relativePath[i] = mapToken(token, "http:")
		} else {
			questionIndex := strings.Index(token, "?")
			if questionIndex == -1 {
				relativePath[i] = mapToken(token, "$_"+strconv.Itoa(i)+"_$")
			} else {
				newPrefix := mapToken(token[:questionIndex], "$_"+strconv.Itoa(i)+"_$")
				newSuffix := mapToken(token[questionIndex:], "?$"+strconv.Itoa(i)+"_$")
				relativePath[i] = newPrefix + newSuffix
			}
		}
	}
	resolved := constructURLBasedOnBaseURL(baseURL, strings.Join(relativePath, "/"))
	for token, original := range tokenMap {
		resolved = strings.ReplaceAll(resolved, token, original)
	}
	return resolved
}

func constructURLBasedOnBaseURL(baseURL *string, givenURL string) string {
	u, err := url.Parse(givenURL)
	if err != nil {
		return givenURL
	}
	if baseURL != nil {
		base, err := url.Parse(*baseURL)
		if err != nil {
			return givenURL
		}
		u = base.ResolveReference(u)
	}
	if u.Path == "" { // In Node.js, new URL('http://localhost') returns 'http://localhost/'.
		u.Path = "/"
	}
	return u.String()
}

func toWebSocketBaseURL(baseURL *string) *string {
	if baseURL == nil {
		return nil
	}

	// Allow http(s) baseURL to match ws(s) urls.
	re := regexp.MustCompile(`(?m)^http(s?://)`)
	wsBaseURL := re.ReplaceAllString(*baseURL, "ws$1")

	return &wsBaseURL
}
