package utils

import (
	"bytes"
	emailverifier "github.com/AfterShip/email-verifier"
	"github.com/mohae/deepcopy"
	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors"
	"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils"
	"net/url"
	"os"
	"reflect"
	"regexp"
	"strings"
)

// GetEnv is a helper function for getting environment variables with a default
func GetEnv(name string, def string) (env string) {
	// If variable not set, provide default
	if env = os.Getenv(name); env == "" {
		env = def
	}
	return
}

// CorsHeaders returns a map to allow Cors
func CorsHeaders() map[string]string {
	return map[string]string{
		"Access-Control-Allow-Origin": "*",
		// do not wildcard: https://stackoverflow.com/questions/13146892/cors-access-control-allow-headers-wildcard-being-ignored
		"Access-Control-Allow-Headers":     "authorization, content-type, referer, sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform, user-agent, x-amz-date, x-amz-security-token",
		"Access-Control-Allow-Methods":     "OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD",
		"Access-Control-Max-Age":           "86400",
		"Access-Control-Allow-Credentials": "true",
	}
}

func DeepCopy(fromValue interface{}) (toValue interface{}) {
	return deepcopy.Copy(fromValue)
}

func Clone[T any](fromValue T) (toValue T) {
	return deepcopy.Copy(fromValue).(T)
}

func ValueToPointer[V any](value V) *V {
	return &value
}

func PointerToValue[V any](value *V) V {
	if value != nil {
		return *value
	}

	return *new(V) // zero value of V
}

func ValidateEmailAddress(email string) (string, error) {
	// To lower
	cleanedEmail := strings.ToLower(strings.TrimSpace(email))

	// Remove all whitespaces
	cleanedEmail = string_utils.RemoveAllWhiteSpaces(cleanedEmail)

	// Also remove unofficial whitespaces
	for _, char := range string_utils.NonOfficialWhitespaceChars {
		cleanedEmail = strings.ReplaceAll(cleanedEmail, char, "")
	}

	// Strip invalid characters
	cleanedEmail = stripInvalidCharacters(cleanedEmail)

	// Make sure the email is not empty
	if cleanedEmail == "" {
		return "", errors.Error("email address is empty")
	}

	// Parse and verify the email
	verifier := emailverifier.NewVerifier()
	result, err := verifier.Verify(cleanedEmail)
	if err != nil || !result.Syntax.Valid {
		return cleanedEmail, errors.Wrap(err, "could not parse email address")
	}

	return cleanedEmail, nil
}

func stripInvalidCharacters(email string) string {
	cleanEmail := email

	// Replace quotes, asterisks, etc.
	cleanEmail = strings.ReplaceAll(cleanEmail, "'", "")
	cleanEmail = strings.ReplaceAll(cleanEmail, "*", "")
	cleanEmail = strings.ReplaceAll(cleanEmail, "!", "")
	cleanEmail = strings.ReplaceAll(cleanEmail, "+", "")

	// Trim invalid characters, like underscore, so that it still fails if it's inside the email
	cleanEmail = strings.Trim(cleanEmail, "_")

	return cleanEmail
}

func SplitAndCleanEmailAddresses(emails string) []string {
	var destinationEmails []string

	splitEmails := string_utils.SplitString(emails, []rune{',', ';'})
	if len(splitEmails) >= 1 {
		// Success - return these emails
		for _, email := range splitEmails {
			cleanedEmail, err := ValidateEmailAddress(email)
			if err == nil && cleanedEmail != "" {
				destinationEmails = append(destinationEmails, cleanedEmail)
			}
		}
		if len(destinationEmails) > 0 {
			return destinationEmails
		}
	}

	return destinationEmails
}

func StripEmail(email string) (strippedEmail string, strippedDomain string) {
	// Strip the email address from the + to the @
	// Define a regular expression pattern to match the "+" to "@" part
	emailPattern := `(\+.*@)`
	// Define the regular expression pattern to match the domain part after "@"
	domainPattern := `@(.+)`

	// Compile the regular expression
	emailRegex := regexp.MustCompile(emailPattern)
	domainRegex := regexp.MustCompile(domainPattern)

	// Replace the matched part with an empty string
	strippedEmail = emailRegex.ReplaceAllString(email, "@")

	// Find the first match in the email address
	match := domainRegex.FindStringSubmatch(email)

	// Check if a match was found
	if len(match) > 1 {
		// The domain part (excluding "@") is in the first capture group (index 1)
		strippedDomain = match[1]
	}

	return strippedEmail, strippedDomain
}

// IsUrlStrict Returns whether a URL is valid in a strict way (Must have scheme and host)
func IsUrlStrict(str string) bool {
	u, err := url.Parse(str)
	return err == nil && u.Scheme != "" && u.Host != ""
}

func IsValidUsername(str string) bool {
	regex := regexp.MustCompile("^[a-zA-Z0-9-.@+_]*$")
	return regex.MatchString(str)
}

// StandardisePhoneNumber standardises phone numbers with +27 instead of 0 prefix
func StandardisePhoneNumber(number string) string {
	number = strings.TrimSpace(number)

	if number == "" {
		return number
	}

	// is the first rune/char of the string a 0
	if []rune(number)[0] == []rune("0")[0] {
		// Add south african country code (hardcoded for now)
		number = "+27" + number[1:]
	}
	return number
}

func FormatPhoneNumber(phoneNumber string) string {
	if len(phoneNumber) > 7 {
		return phoneNumber
	}

	// Format as 076 453 2188
	phoneNumber = insertInto(phoneNumber, 3, " ")
	phoneNumber = insertInto(phoneNumber, 7, " ")
	return phoneNumber
}

func insertInto(s string, index int, characters string) string {
	return s[:index] + characters + s[index:]
}

// RemoveUrlScheme Removes http:// or https:// from a URL
func RemoveUrlScheme(str string) string {
	newStr := strings.Replace(str, "http://", "", 1)
	newStr = strings.Replace(str, "https://", "", 1)
	return newStr
}

// StripQueryString - Strips the query parameters from a URL
func StripQueryString(inputUrl string) (string, error) {
	u, err := url.Parse(inputUrl)
	if err != nil {
		return inputUrl, err
	}
	u.RawQuery = ""
	return u.String(), nil
}

// EscapeOpenSearchSearchString See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#_reserved_characters
func EscapeOpenSearchSearchString(str string) string {
	searchString := str

	// Reserved characters
	// NOTE: first char must be "\" to prevent replacing it again after replacing other chars with "\\"
	reservedCharacters := []string{"\\", "+", "-", "=", "&&", "||", "!", "(", ")", "{", "}", "[", "]", "^", "\"", "~", "*", "?", ":", "/"}

	// Remove "<" and ">"
	strings.ReplaceAll(searchString, "<", "")
	strings.ReplaceAll(searchString, ">", "")

	// Escape the reserved characters with double backslashes ("\\")
	for _, char := range reservedCharacters {
		if strings.Contains(searchString, char) {
			re := regexp.MustCompile(char)
			searchString = re.ReplaceAllString(searchString, "\\"+char)
		}
	}
	return searchString
}

// IsEqual returns if the two objects are equal
func IsEqual(expected interface{}, actual interface{}) bool {
	if expected == nil || actual == nil {
		return expected == actual
	}

	if exp, ok := expected.([]byte); ok {
		act, ok := actual.([]byte)
		if !ok {
			return false
		}

		if exp == nil || act == nil {
			return true
		}

		return bytes.Equal(exp, act)
	}

	return reflect.DeepEqual(expected, actual)

}