package host

import (
	"regexp"
	"strings"

	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

// DefaultMaxCharacterLength defines the default maximum character length used
// in validation of identifiers including the client, connection, port and
// channel identifiers.
//
// NOTE: this restriction is specific to this golang implementation of IBC. If
// your use case demands a higher limit, please open an issue and we will consider
// adjusting this restriction.
const DefaultMaxCharacterLength = 64

// IsValidID defines regular expression to check if the string consist of
// characters in one of the following categories only:
// - Alphanumeric
// - `.`, `_`, `+`, `-`, `#`
// - `[`, `]`, `<`, `>`
var IsValidID = regexp.MustCompile(`^[a-zA-Z0-9\.\_\+\-\#\[\]\<\>]+$`).MatchString

// ICS 024 Identifier and Path Validation Implementation
//
// This file defines ValidateFn to validate identifier and path strings
// The spec for ICS 024 can be located here:
// https://github.com/cosmos/ics/tree/master/spec/ics-024-host-requirements

// ValidateFn function type to validate path and identifier bytestrings
type ValidateFn func(string) error

func defaultIdentifierValidator(id string, min, max int) error { //nolint:unparam
	if strings.TrimSpace(id) == "" {
		return sdkerrors.Wrap(ErrInvalidID, "identifier cannot be blank")
	}
	// valid id MUST NOT contain "/" separator
	if strings.Contains(id, "/") {
		return sdkerrors.Wrapf(ErrInvalidID, "identifier %s cannot contain separator '/'", id)
	}
	// valid id must fit the length requirements
	if len(id) < min || len(id) > max {
		return sdkerrors.Wrapf(ErrInvalidID, "identifier %s has invalid length: %d, must be between %d-%d characters", id, len(id), min, max)
	}
	// valid id must contain only lower alphabetic characters
	if !IsValidID(id) {
		return sdkerrors.Wrapf(
			ErrInvalidID,
			"identifier %s must contain only alphanumeric or the following characters: '.', '_', '+', '-', '#', '[', ']', '<', '>'",
			id,
		)
	}
	return nil
}

// ClientIdentifierValidator is the default validator function for Client identifiers.
// A valid Identifier must be between 9-20 characters and only contain lowercase
// alphabetic characters,
func ClientIdentifierValidator(id string) error {
	return defaultIdentifierValidator(id, 9, DefaultMaxCharacterLength)
}

// ConnectionIdentifierValidator is the default validator function for Connection identifiers.
// A valid Identifier must be between 10-20 characters and only contain lowercase
// alphabetic characters,
func ConnectionIdentifierValidator(id string) error {
	return defaultIdentifierValidator(id, 10, DefaultMaxCharacterLength)
}

// ChannelIdentifierValidator is the default validator function for Channel identifiers.
// A valid Identifier must be between 10-20 characters and only contain lowercase
// alphabetic characters,
func ChannelIdentifierValidator(id string) error {
	return defaultIdentifierValidator(id, 10, DefaultMaxCharacterLength)
}

// PortIdentifierValidator is the default validator function for Port identifiers.
// A valid Identifier must be between 2-20 characters and only contain lowercase
// alphabetic characters,
func PortIdentifierValidator(id string) error {
	return defaultIdentifierValidator(id, 2, DefaultMaxCharacterLength)
}

// NewPathValidator takes in a Identifier Validator function and returns
// a Path Validator function which requires path only has valid identifiers
// alphanumeric character strings, and "/" separators
func NewPathValidator(idValidator ValidateFn) ValidateFn {
	return func(path string) error {
		pathArr := strings.Split(path, "/")
		if len(pathArr) > 0 && pathArr[0] == path {
			return sdkerrors.Wrapf(ErrInvalidPath, "path %s doesn't contain any separator '/'", path)
		}

		for _, p := range pathArr {
			// a path beginning or ending in a separator returns empty string elements.
			if p == "" {
				return sdkerrors.Wrapf(ErrInvalidPath, "path %s cannot begin or end with '/'", path)
			}

			if err := idValidator(p); err != nil {
				return err
			}
			// Each path element must either be a valid identifier or constant number
			if err := defaultIdentifierValidator(p, 1, DefaultMaxCharacterLength); err != nil {
				return sdkerrors.Wrapf(err, "path %s contains an invalid identifier: '%s'", path, p)
			}
		}

		return nil
	}
}

// PathValidator takes in path string and validates with default identifier rules.
// This is optimized by simply checking that all path elements are alphanumeric.
func PathValidator(path string) error {
	pathArr := strings.Split(path, "/")
	if len(pathArr) > 0 && pathArr[0] == path {
		return sdkerrors.Wrapf(ErrInvalidPath, "path %s doesn't contain any separator '/'", path)
	}

	for _, p := range pathArr {
		// a path beginning or ending in a separator returns empty string elements.
		if p == "" {
			return sdkerrors.Wrapf(ErrInvalidPath, "path %s cannot begin or end with '/'", path)
		}

		// Each path element must be a valid identifier or constant number
		if err := defaultIdentifierValidator(p, 1, DefaultMaxCharacterLength); err != nil {
			return sdkerrors.Wrapf(err, "path %s contains an invalid identifier: '%s'", path, p)
		}
	}

	return nil
}
