// validate package is a collection of validation functions
// the validation errors are carried to the frontened
// so be mindful of the error messages by making them user friendly
package validate

import (
	"fmt"
	"net/mail"
	"net/url"
	"regexp"
	"slices"
	"strings"
	"time"

	"github.com/go-errors/errors"

	"github.com/google/uuid"
	"github.com/oapi-codegen/nullable"
	"github.com/phishingclub/phishingclub/errs"
	"github.com/phishingclub/phishingclub/utils"
)

// ErrorIfStringEqual checks if two strings are equal
func ErrorIfStringEqual(a string, b string) error {
	if a == b {
		return errs.NewValidationError(
			fmt.Errorf(
				"must not be equal to %s",
				b,
			),
		)
	}
	return nil
}

// StringGreaterThan checks if a string is not empty
func StringGreaterThan(s string, length int) bool {
	return len(s) > length
}

// ErrorIfStringGreaterThan checks if a string is not empty and
// returns an error if it is
func ErrorIfStringGreaterThan(s string, length int) error {
	if StringGreaterThan(s, length) {
		return errs.NewValidationError(
			fmt.Errorf(
				"is greater than %d",
				length,
			),
		)
	}
	return nil
}

// StringLessThan checks if a string is not empty
func StringLessThan(s string, length int) bool {
	return len(s) < length
}

// ErrorIfStringLessThan checks if a string is not empty
// and returns an error if it is
func ErrorIfStringLessThan(s string, length int) error {
	if StringLessThan(s, length) {
		return errs.NewValidationError(
			fmt.Errorf(
				"is less than %d characters",
				length,
			),
		)
	}
	return nil
}

// StringBetween checks if a string is between min and max
func StringBetween(s string, min int, max int) bool {
	return len(s) > min && len(s) < max
}

// StringBetweenOrEqualTo checks if a string is between or equal to min and max
func StringBetweenOrEqualTo(s string, min int, max int) bool {
	return len(s) >= min && len(s) <= max
}

// ErrorIfStringBetween checks if a string is between min and max
// and returns an error if it is not
func ErrorIfStringNotBetween(s string, min int, max int) error {
	if StringBetween(s, min, max) {
		return nil
	}
	return errs.NewValidationError(
		fmt.Errorf(
			"must be between %d and %d characters",
			min,
			max,
		),
	)
}

// ErrorIfStringNotbetweenOrEqualTo checks if a string is between or equal to min and max
// and returns an error if it is not
func ErrorIfStringNotbetweenOrEqualTo(s string, min int, max int) error {
	if StringBetweenOrEqualTo(s, min, max) {
		return nil
	}
	return errs.NewValidationError(
		fmt.Errorf(
			"must be between %d and %d characters",
			min,
			max,
		),
	)
}

// ErrorIfIntEqual checks if two ints are equal
func ErrorIfIntEqual(a int, b int) error {
	if a == b {
		return errs.NewValidationError(
			fmt.Errorf("must not be equal to %d", b),
		)
	}
	return nil
}

// ErrorIfLessThan checks if an int is less than
func ErrorIfLessThan(a int, b int) error {
	if a < b {
		return errs.NewValidationError(
			fmt.Errorf("must be greater than or equal to %d", b),
		)
	}
	return nil
}

// ErrorIfIntLargerThan checks if an int is larger than
func ErrorIfIntLargerThan(a int, b int) error {
	if a > b {
		return errs.NewValidationError(
			fmt.Errorf("must be less than or equal to %d", b),
		)
	}
	return nil
}

// ErrorIfIntEqualOrLargerThan checks if an int is larger than
func ErrorIfIntEqualOrLargerThan(a int, b int) error {
	if a >= b {
		return errs.NewValidationError(
			fmt.Errorf("must be less than %d", b),
		)
	}
	return nil
}

// ErrorIfIntEqualOrLessThan checks if an int is less than
func ErrorIfIntEqualOrLessThan(a int, b int) error {
	if a <= b {
		return errs.NewValidationError(
			fmt.Errorf("must be greater than %d but is %d", b, a),
		)
	}
	return nil
}

// ErrorIfNotbetweenOrEqualTo checks if a string is between or equal to min and max
// and returns an error if it is not
func ErrorIfNotbetweenOrEqualTo(s, min, max int) error {
	if (s >= min) && (s <= max) {
		return nil
	}
	return errs.NewValidationError(
		fmt.Errorf(
			"must be between %d and %d",
			min,
			max,
		),
	)
}

// ErrorIfNil
func ErrorIfNil(i any) error {
	if i == nil {
		return errs.NewValidationError(
			fmt.Errorf("must not be nil"),
		)
	}
	return nil
}

// ErrorIfFailsToParseUUID
func ErrorIfFailsToParseUUID(s string) (*uuid.UUID, error) {
	id, err := uuid.Parse(s)
	if err != nil {
		return nil, errs.NewValidationError(
			fmt.Errorf("must be a valid uuid"),
		)
	}
	if uuid.Nil == id {
		return nil, errs.NewValidationError(
			fmt.Errorf("must not be nil UUID"),
		)
	}
	return &id, nil
}

// UuidIsNil checks if a uuid is not nil
func UuidIsNil(id uuid.UUID) bool {
	return id == uuid.Nil
}

// ErrorIfUuidIsNil checks if a uuid is not nil
func ErrorIfUuidIsNil(id uuid.UUID) error {
	if UuidIsNil(id) {
		return errs.NewValidationError(
			fmt.Errorf("uuid must not be nil"),
		)
	}
	return nil
}

// UuidRefIsNilOrZero checks if a uuid is not nil and not zero value
func UuidRefIsNilOrZero(id *uuid.UUID) bool {
	return id == nil || *id == uuid.Nil
}

// ErrorIfUuidRefIsNilOrZero checks if a uuid is not nil and not zero value
func ErrorIfUuidRefIsNilOrZero(id *uuid.UUID) error {
	if UuidRefIsNilOrZero(id) {
		return errs.NewValidationError(
			fmt.Errorf("uuid must not be nil or zero valued"),
		)
	}
	return nil
}

// TimeRefIsNilOrZero checks if a time is nil or zero value
func TimeRefIsNilOrZero(t *time.Time) bool {
	return t == nil || t.IsZero()
}

// ErrorIfTimeRefIsNilOrZero checks if a time is nil or zero value
func ErrorIfTimeRefIsNilOrZero(t *time.Time) error {
	if TimeRefIsNilOrZero(t) {
		return errs.NewValidationError(
			fmt.Errorf("must not be nil or zero valued"),
		)
	}
	return nil
}

// TimeIsNil checks if a time is not nil
func TimeIsNil(t time.Time) bool {
	return t.IsZero()
}

// ErrorIfTimeIsNil checks if a time is not nil
func ErrorIfTimeIsNil(t time.Time) error {
	if TimeIsNil(t) {
		return errs.NewValidationError(
			fmt.Errorf("must not be nil"),
		)
	}
	return nil
}

// IsAlphaNumeric checks if a string is alphanumeric
func IsAlphaNumeric(s string) bool {
	return regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(s)
}

// ErrorIfNotAlphaNumeric checks if a string is alphanumeric
// and returns an error if it is not
func ErrorIfNotAlphaNumeric(s string) error {
	if IsAlphaNumeric(s) {
		return nil
	}
	return errs.NewValidationError(
		fmt.Errorf("must be alphanumeric"),
	)
}

// IsValidEmail checks if a string is a valid email
func ErrorIfMailInvalid(s string) error {
	const min = 5
	const max = 254
	l := len(s)
	if l < min || l > max {
		return errs.NewValidationError(
			fmt.Errorf(
				"must be between %d and %d characters",
				min,
				max,
			),
		)
	}
	// Check is mail RFC 5322 (and extension by RFC 6532) valid
	_, err := mail.ParseAddress(s)
	if err != nil {
		// Remove the "mail:" prefix from the error message
		err = errors.New(strings.TrimPrefix(err.Error(), "mail:"))
		return errs.NewValidationError(err)
	}

	r := `^.+@.+\..+`
	pattern := regexp.MustCompile(r)
	// check if the email address matches the pattern.
	if !pattern.MatchString(s) {
		return errs.NewValidationError(
			fmt.Errorf(
				"simple pattern '%s' failed",
				r,
			),
		)
	}
	return nil
}

// ErrorIfStringNotMatch checks if a string matches a pattern
func ErrorIfStringNotMatch(s string, r string) error {
	pattern := regexp.MustCompile(r)
	if !pattern.MatchString(s) {
		return errs.NewValidationError(
			fmt.Errorf(
				"pattern '%s' failed",
				r,
			),
		)
	}
	return nil
}

// ErrorIfStringEmpty checks if a string is empty
func ErrorIfStringEmpty(s string) error {
	if s == "" {
		return errs.NewValidationError(
			errors.New(
				"must not be empty",
			),
		)
	}
	return nil
}

// ErrorIfNotContains checks if a slice of strings contains a string
func ErrorIfNotContains(s []string, v string) error {
	if !slices.Contains(s, v) {
		return errs.NewValidationError(
			fmt.Errorf(
				"must contain %s",
				v,
			),
		)

	}
	return nil
}

// WrapErrorWithField wraps an error with a field name
func WrapErrorWithField(err error, field string) error {
	return errs.NewValidationError(
		fmt.Errorf(
			"%s: %w",
			field,
			err,
		),
	)
}

// D validates the id is not nil or zero and returns an error if it is
// with a field error indicator
func ID(id *uuid.UUID) error {
	if err := ErrorIfUuidRefIsNilOrZero(id); err != nil {
		return WrapErrorWithField(err, "id")
	}
	return nil
}

// NotNil validates the id is not nil or zero and returns an error if it is
// with a field error indicator
func NotNilField(value any, key string) error {
	if err := ErrorIfNil(value); err != nil {
		return WrapErrorWithField(err, key)
	}
	return nil
}

// NullableFieldRequired validates the field is not nil or zero and returns an error if it is
func NullableFieldRequired[T any](fieldName string, value nullable.Nullable[T]) error {
	if !value.IsSpecified() || value.IsNull() {
		return WrapErrorWithField(
			errs.NewValidationError(
				errors.New("required"),
			),
			fieldName,
		)
	}
	return nil
}

// OneOfNullableFieldsRequired validates that one of the fields is supplied and not null
// input is a map of map[string]nullable.Nullable[T] where the string is the fieldname of T
func OneOfNullableFieldsRequired(fields map[string]any) error {
	for _, v := range fields {
		v, ok := v.(nullable.Nullable[any])
		// if any field is not castable to nullable.Nullable[any] then
		// break and return the error
		if !ok {
			continue
		}
		if v.IsSpecified() && !v.IsNull() {
			return nil
		}
	}
	keys := utils.MapKeys(fields)
	return fmt.Errorf("one of the fields (%s) must be supplied", strings.Join(keys, ", "))
}

// ErrorIfInvalidURL validates that a string is a valid URL with http/https scheme
func ErrorIfInvalidURL(urlStr string) error {
	if urlStr == "" {
		return errs.NewValidationError(
			errors.New("URL cannot be empty"),
		)
	}

	// validate that URL is parseable
	parsedURL, err := url.Parse(urlStr)
	if err != nil {
		return errs.NewValidationError(
			errors.New("must be a valid URL"),
		)
	}

	// ensure it has a valid scheme (http or https)
	if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
		return errs.NewValidationError(
			errors.New("must use http or https protocol"),
		)
	}

	// ensure it has a valid host
	if parsedURL.Host == "" {
		return errs.NewValidationError(
			errors.New("must have a valid host"),
		)
	}

	// extract hostname (removes port if present) for domain validation
	hostname := parsedURL.Hostname()
	if hostname == "" {
		return errs.NewValidationError(
			errors.New("must have a valid hostname"),
		)
	}

	// basic domain validation
	if !isValidDomain(hostname) {
		return errs.NewValidationError(
			errors.New("must have a valid domain"),
		)
	}

	return nil
}

// isValidDomain performs basic domain name validation
// supports international domain names (IDNs)
func isValidDomain(domain string) bool {
	// basic checks - length limits
	if len(domain) == 0 || len(domain) > 253 {
		return false
	}

	// must contain at least one dot
	if !strings.Contains(domain, ".") {
		return false
	}

	// cannot start or end with dash or dot
	if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") ||
		strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") {
		return false
	}

	// check each label
	labels := strings.Split(domain, ".")
	for _, label := range labels {
		if len(label) == 0 || len(label) > 63 {
			return false
		}

		// label cannot start or end with dash
		if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
			return false
		}

		// removed restrictive ascii-only character check to support international domains
	}

	return true
}
