// Package validators provides validators for various types of data.
//
// Domain validator is a simple utility that ensures two domains are exact
// matches while ensuring that techniques used to bypass such checks do
// not impact the validation.

package validators

import (
	"fmt"
	"net"
	"net/url"
	"strings"
)

// Errors
var (
	ErrSchemeMismatch   = fmt.Errorf("scheme mismatch")
	ErrPortMismatch     = fmt.Errorf("port mismatch")
	ErrHostnameMismatch = fmt.Errorf("hostname mismatch")
)

// DomainValidatorOptions is a set of options for DomainValidator.
type DomainValidatorOptions struct {
	// Ensure domains have the same scheme.
	WithScheme bool
	// Ensure domains have the same port.
	WithPort bool
	// Specify a list of allowed schemes if WithScheme is set to true.
	AllowedSchemes []string
}

// DomainValidator is a simple utility that ensures two domains are exact
// matches while ensuring that techniques used to bypass such checks do
// not impact the validation.
type DomainValidator struct {
	opts DomainValidatorOptions
}

// NewDomainValidator creates a new DomainValidator.
func NewDomainValidator(opts DomainValidatorOptions) *DomainValidator {
	return &DomainValidator{
		opts: opts,
	}
}

func (v *DomainValidator) checkScheme(rawURL string) error {
	if !v.opts.WithScheme {
		return nil
	}

	if len(v.opts.AllowedSchemes) == 0 {
		return fmt.Errorf("allowed schemes must be specified")
	}

	for _, scheme := range v.opts.AllowedSchemes {
		if strings.HasPrefix(strings.ToLower(rawURL), strings.ToLower(scheme)+"://") {
			return nil
		}
	}

	return fmt.Errorf("invalid scheme")

}

func (v *DomainValidator) getURL(i string) (*url.URL, error) {
	if i == "" {
		return nil, fmt.Errorf("url cannot be empty")
	}

	if v.opts.WithScheme {
		err := v.checkScheme(i)

		if err != nil {
			return nil, fmt.Errorf("invalid scheme: %w", err)
		}

		u, err := url.Parse(i)

		if err != nil {
			return nil, fmt.Errorf("failed to parse input url: %w", err)
		}

		if u.Host == "" || u.Scheme == "" {
			return nil, fmt.Errorf("missing host or scheme in url: %s", i)
		}

		return u, nil
	}

	rawURL := i

	if !strings.Contains(i, "://") {
		// From godoc: [scheme:][//[userinfo@]host][/]path[?query][#fragment]
		// So, we can omit the colon and tell the Go URL lib that we want
		// to parse the URL without the scheme. If we don't do this,
		// the URL lib will parse our entire domain as the path.
		rawURL = "//" + i
	}

	u, err := url.Parse(rawURL)

	if err != nil {
		return nil, fmt.Errorf("failed to parse host: %w", err)
	}

	if u.Host == "" {
		return nil, fmt.Errorf("missing host in url: %s", i)
	}

	return u, nil
}

func (v *DomainValidator) getHostname(hostname string) (string, error) {
	hostname = strings.ToLower(hostname)
	hostname = strings.TrimRight(hostname, ".")
	if net.ParseIP(hostname) != nil {
		return "", fmt.Errorf("ip addresses are not supported")
	}
	return hostname, nil
}

// Validate ensures that two domains are exact matches with the
// options defined in the DomainValidatorOptions. It ensures that the
// inputs are proper URLs and contain a host. It lowercases the hostnames
// and removes the trailing dot. Finally, it checks that the hostnames are
// equal unless WithScheme or WithPort is set to true where it also
// validates the scheme and port respectively.
func (v *DomainValidator) Validate(expected, actual string) error {
	eu, err := v.getURL(expected)

	if err != nil {
		return err
	}

	au, err := v.getURL(actual)

	if err != nil {
		return err
	}

	if v.opts.WithScheme {
		if eu.Scheme != au.Scheme {
			return ErrSchemeMismatch
		}
	}

	if v.opts.WithPort {
		if eu.Port() != au.Port() {
			return ErrPortMismatch
		}
	}

	euf, err := v.getHostname(eu.Hostname())

	if err != nil {
		return err
	}

	auf, err := v.getHostname(au.Hostname())

	if err != nil {
		return err
	}

	if euf != auf {
		return ErrHostnameMismatch
	}

	return nil
}

// SafeHostname uses the internal validation for domains that the validator uses
// to parse a hostname. It ensures the input URL is a valid URL, that a host
// is present and that the hostname is lowercased and without a trailing dot.
func (v *DomainValidator) SafeHostname(input string) (string, error) {
	u, err := v.getURL(input)

	if err != nil {
		return "", err
	}

	return v.getHostname(u.Hostname())
}
