// Port of web/kit/Validation.ts.
package webui

import (
	"regexp"
	"strings"
	"unicode/utf8"
)

// validationNonDigit matches every non-digit rune (the JS /\D/g).
var validationNonDigit = regexp.MustCompile(`\D`)

// validationDigits strips every non-digit character.
func validationDigits(s string) string {
	return validationNonDigit.ReplaceAllString(s, "")
}

// Validation describes a single form field's validation inputs. Solid's reactive
// Accessor<T> values collapse to plain values here: the caller reads its signals
// at the call site and passes the current values. FieldBlur is optional (nil
// falls back to Field), as is IsValidFunc.
type Validation struct {
	ID          string          // snake_case identifier that is "touched"
	Name        string          // human-readable field name, used in messages
	Required    bool            // whether an empty value is an error
	Touched     map[string]bool // which field IDs the user has interacted with
	Field       string          // the live field value
	FieldBlur   *string         // the value at last blur; nil uses Field
	IsValidFunc func(string) bool
	InvalidMsg  string // custom "is invalid" message; empty uses a default
}

// CreateValidation computes the validation error message for a field, or "" when
// valid. This is the collapsed (non-reactive) form of the Solid createMemo: the
// caller re-invokes it whenever the underlying signals change.
func CreateValidation(v Validation) string {
	field := strings.TrimSpace(v.Field)
	// When no blur value is provided, fall back to the live value so the
	// "has value but not blurred yet" guard is always false and validation
	// runs against the live value instead.
	fieldBlur := field
	if v.FieldBlur != nil {
		fieldBlur = strings.TrimSpace(*v.FieldBlur)
	}

	if !v.Touched[v.ID] || (field != "" && fieldBlur == "") || (v.IsValidFunc != nil && v.IsValidFunc(field)) {
		return ""
	}
	if v.Required && field == "" {
		return v.Name + " is required"
	}
	if v.IsValidFunc != nil && !v.IsValidFunc(fieldBlur) {
		if v.InvalidMsg != "" {
			return v.InvalidMsg
		}
		return v.Name + " is invalid"
	}
	return ""
}

// IsPhoneNumberValid reports whether phoneNumber has exactly 10 digits.
func IsPhoneNumberValid(phoneNumber string) bool {
	return len(validationDigits(phoneNumber)) == 10
}

var validationEmailRegex = regexp.MustCompile("^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\\.?[a-zA-Z0-9])*\\.[a-zA-Z](-?[a-zA-Z0-9])+$")

// IsEmailValid reports whether email is a syntactically valid email address.
func IsEmailValid(email string) bool {
	if email == "" {
		return false
	}

	emailParts := strings.Split(email, "@")
	if len(emailParts) != 2 {
		return false
	}

	account := emailParts[0]
	address := emailParts[1]

	if len(account) > 64 {
		return false
	} else if len(address) > 255 {
		return false
	}

	for _, part := range strings.Split(address, ".") {
		if len(part) > 63 {
			return false
		}
	}

	return validationEmailRegex.MatchString(email)
}

var validationURLRegex = regexp.MustCompile("[(http(s)?)://(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)")

// IsURLValid reports whether url contains a substring that looks like a URL.
func IsURLValid(url string) bool {
	return validationURLRegex.MatchString(url)
}

// IsZipCodeValid reports whether zip has exactly 5 or 9 digits.
func IsZipCodeValid(zip string) bool {
	rawZip := validationDigits(zip)
	return len(rawZip) == 5 || len(rawZip) == 9
}

// IsTaxIDValid reports whether id has exactly 9 digits.
func IsTaxIDValid(id string) bool {
	return len(validationDigits(id)) == 9
}

// IsAtLeastMinChars reports whether input is at least minLen characters long.
func IsAtLeastMinChars(input string, minLen int) bool {
	return utf8.RuneCountInString(input) >= minLen
}

// IsWithinMaxChars reports whether input is at most maxLen characters long.
func IsWithinMaxChars(input string, maxLen int) bool {
	return utf8.RuneCountInString(input) <= maxLen
}

var validationNameRegex = regexp.MustCompile(`^[\p{L}]*[\p{L} '\-]*[\p{L}]$`)

// IsNameValid reports whether name consists only of letters, spaces, hyphens,
// and apostrophes (and begins/ends with a letter).
func IsNameValid(name string) bool {
	return validationNameRegex.MatchString(name)
}

var validationUsernameRegex = regexp.MustCompile(`^[A-Za-z0-9]*$`)

// IsUsernameValid reports an error message when the username is invalid (5-50
// alphanumeric characters), or "" when valid.
func IsUsernameValid(username string) string {
	if n := utf8.RuneCountInString(username); n < 5 || n > 50 {
		return "Username must have 5-50 characters"
	}

	if !validationUsernameRegex.MatchString(username) {
		return "Username must only contain alphanumeric characters"
	}

	return "" // Valid
}
