package openapi3

import (
	"fmt"
	"math"
	"net/netip"
	"regexp"
)

type (
	// FormatValidator is an interface for custom format validators.
	FormatValidator[T any] interface {
		Validate(value T) error
	}
	// StringFormatValidator is a type alias for FormatValidator[string]
	StringFormatValidator = FormatValidator[string]
	// NumberFormatValidator is a type alias for FormatValidator[float64]
	NumberFormatValidator = FormatValidator[float64]
	// IntegerFormatValidator is a type alias for FormatValidator[int64]
	IntegerFormatValidator = FormatValidator[int64]
)

var (
	// SchemaStringFormats is a map of custom string format validators.
	SchemaStringFormats = make(map[string]StringFormatValidator)
	// SchemaNumberFormats is a map of custom number format validators.
	SchemaNumberFormats = make(map[string]NumberFormatValidator)
	// SchemaIntegerFormats is a map of custom integer format validators.
	SchemaIntegerFormats = make(map[string]IntegerFormatValidator)
)

const (
	// FormatOfStringForUUIDOfRFC4122 is an optional predefined format for UUID v1-v5 as specified by RFC4122
	FormatOfStringForUUIDOfRFC4122 = `^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$`

	// FormatOfStringForEmail pattern catches only some suspiciously wrong-looking email addresses.
	// Use DefineStringFormat(...) if you need something stricter.
	FormatOfStringForEmail = `^[^@]+@[^@<>",\s]+$`

	// FormatOfStringByte is a regexp for base64-encoded characters, for example, "U3dhZ2dlciByb2Nrcw=="
	FormatOfStringByte = `(^$|^[a-zA-Z0-9+/\-_]*=*$)`

	// FormatOfStringDate is a RFC3339 date format regexp, for example "2017-07-21".
	FormatOfStringDate = `^[0-9]{4}-(0[1-9]|10|11|12)-(0[1-9]|[12][0-9]|3[01])$`

	// FormatOfStringDateTime is a RFC3339 date-time format regexp, for example "2017-07-21T17:32:28Z".
	FormatOfStringDateTime = `^[0-9]{4}-(0[1-9]|10|11|12)-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})$`
)

func init() {
	DefineStringFormatValidator("byte", NewRegexpFormatValidator(FormatOfStringByte))
	DefineStringFormatValidator("date", NewRegexpFormatValidator(FormatOfStringDate))
	DefineStringFormatValidator("date-time", NewRegexpFormatValidator(FormatOfStringDateTime))
	DefineIntegerFormatValidator("int32", NewRangeFormatValidator(int64(math.MinInt32), int64(math.MaxInt32)))
	DefineIntegerFormatValidator("int64", NewRangeFormatValidator(int64(math.MinInt64), int64(math.MaxInt64)))
}

// DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec
func DefineIPv4Format() {
	DefineStringFormatValidator("ipv4", NewIPValidator(true))
}

// DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec
func DefineIPv6Format() {
	DefineStringFormatValidator("ipv6", NewIPValidator(false))
}

type stringRegexpFormatValidator struct {
	re *regexp.Regexp
}

func (s stringRegexpFormatValidator) Validate(value string) error {
	if !s.re.MatchString(value) {
		return fmt.Errorf(`string doesn't match pattern "%s"`, s.re.String())
	}
	return nil
}

type callbackValidator[T any] struct {
	fn func(T) error
}

func (c callbackValidator[T]) Validate(value T) error {
	return c.fn(value)
}

type rangeFormat[T int64 | float64] struct {
	min, max T
}

func (r rangeFormat[T]) Validate(value T) error {
	if value < r.min || value > r.max {
		return fmt.Errorf("value should be between %v and %v", r.min, r.max)
	}
	return nil
}

// NewRangeFormatValidator creates a new FormatValidator that validates the value is within a given range.
func NewRangeFormatValidator[T int64 | float64](min, max T) FormatValidator[T] {
	return rangeFormat[T]{min: min, max: max}
}

// NewRegexpFormatValidator creates a new FormatValidator that uses a regular expression to validate the value.
func NewRegexpFormatValidator(pattern string) StringFormatValidator {
	re, err := regexp.Compile(pattern)
	if err != nil {
		err := fmt.Errorf("string regexp format has invalid pattern %q: %w", pattern, err)
		panic(err)
	}
	return stringRegexpFormatValidator{re: re}
}

// NewCallbackValidator creates a new FormatValidator that uses a callback function to validate the value.
func NewCallbackValidator[T any](fn func(T) error) FormatValidator[T] {
	return callbackValidator[T]{fn: fn}
}

// DefineStringFormatValidator defines a custom format validator for a given string format.
func DefineStringFormatValidator(name string, validator StringFormatValidator) {
	SchemaStringFormats[name] = validator
}

// DefineNumberFormatValidator defines a custom format validator for a given number format.
func DefineNumberFormatValidator(name string, validator NumberFormatValidator) {
	SchemaNumberFormats[name] = validator
}

// DefineIntegerFormatValidator defines a custom format validator for a given integer format.
func DefineIntegerFormatValidator(name string, validator IntegerFormatValidator) {
	SchemaIntegerFormats[name] = validator
}

// DefineStringFormat defines a regexp pattern for a given string format
//
// Deprecated: Use openapi3.DefineStringFormatValidator(name, NewRegexpFormatValidator(pattern)) instead.
func DefineStringFormat(name string, pattern string) {
	DefineStringFormatValidator(name, NewRegexpFormatValidator(pattern))
}

// DefineStringFormatCallback defines a callback function for a given string format
//
// Deprecated: Use openapi3.DefineStringFormatValidator(name, NewCallbackValidator(fn)) instead.
func DefineStringFormatCallback(name string, callback func(string) error) {
	DefineStringFormatValidator(name, NewCallbackValidator(callback))
}

// NewIPValidator creates a new FormatValidator that validates the value is an IP address.
func NewIPValidator(isIPv4 bool) FormatValidator[string] {
	return callbackValidator[string]{fn: func(ip string) error {
		addr, err := netip.ParseAddr(ip)
		if err != nil {
			return &SchemaError{
				Value:  ip,
				Reason: "Not an IP address",
			}
		}
		if isIPv4 && !addr.Is4() {
			return &SchemaError{
				Value:  ip,
				Reason: "Not an IPv4 address (it's IPv6)",
			}
		}
		if !isIPv4 && !addr.Is6() {
			return &SchemaError{
				Value:  ip,
				Reason: "Not an IPv6 address (it's IPv4)",
			}
		}
		return nil
	}}
}
