package endpoints

import (
	"fmt"
	"github.com/aws/smithy-go/logging"
	"regexp"
	"strings"

	"github.com/aws/aws-sdk-go-v2/aws"
)

// DefaultKey is a compound map key of a variant and other values.
type DefaultKey struct {
	Variant        EndpointVariant
	ServiceVariant ServiceVariant
}

// EndpointKey is a compound map key of a region and associated variant value.
type EndpointKey struct {
	Region         string
	Variant        EndpointVariant
	ServiceVariant ServiceVariant
}

// EndpointVariant is a bit field to describe the endpoints attributes.
type EndpointVariant uint64

const (
	// FIPSVariant indicates that the endpoint is FIPS capable.
	FIPSVariant EndpointVariant = 1 << (64 - 1 - iota)

	// DualStackVariant indicates that the endpoint is DualStack capable.
	DualStackVariant
)

// ServiceVariant is a bit field to describe the service endpoint attributes.
type ServiceVariant uint64

const (
	defaultProtocol = "https"
	defaultSigner   = "v4"
)

var (
	protocolPriority = []string{"https", "http"}
	signerPriority   = []string{"v4", "s3v4"}
)

// Options provide configuration needed to direct how endpoints are resolved.
type Options struct {
	// Logger is a logging implementation that log events should be sent to.
	Logger logging.Logger

	// LogDeprecated indicates that deprecated endpoints should be logged to the provided logger.
	LogDeprecated bool

	// ResolvedRegion is the resolved region string. If provided (non-zero length) it takes priority
	// over the region name passed to the ResolveEndpoint call.
	ResolvedRegion string

	// Disable usage of HTTPS (TLS / SSL)
	DisableHTTPS bool

	// Instruct the resolver to use a service endpoint that supports dual-stack.
	// If a service does not have a dual-stack endpoint an error will be returned by the resolver.
	UseDualStackEndpoint aws.DualStackEndpointState

	// Instruct the resolver to use a service endpoint that supports FIPS.
	// If a service does not have a FIPS endpoint an error will be returned by the resolver.
	UseFIPSEndpoint aws.FIPSEndpointState

	// ServiceVariant is a bitfield of service specified endpoint variant data.
	ServiceVariant ServiceVariant
}

// GetEndpointVariant returns the EndpointVariant for the variant associated options.
func (o Options) GetEndpointVariant() (v EndpointVariant) {
	if o.UseDualStackEndpoint == aws.DualStackEndpointStateEnabled {
		v |= DualStackVariant
	}
	if o.UseFIPSEndpoint == aws.FIPSEndpointStateEnabled {
		v |= FIPSVariant
	}
	return v
}

// Partitions is a slice of partition
type Partitions []Partition

// ResolveEndpoint resolves a service endpoint for the given region and options.
func (ps Partitions) ResolveEndpoint(region string, opts Options) (aws.Endpoint, error) {
	if len(ps) == 0 {
		return aws.Endpoint{}, fmt.Errorf("no partitions found")
	}

	if opts.Logger == nil {
		opts.Logger = logging.Nop{}
	}

	if len(opts.ResolvedRegion) > 0 {
		region = opts.ResolvedRegion
	}

	for i := range ps {
		if !ps[i].canResolveEndpoint(region, opts) {
			continue
		}

		return ps[i].ResolveEndpoint(region, opts)
	}

	// fallback to first partition format to use when resolving the endpoint.
	return ps[0].ResolveEndpoint(region, opts)
}

// Partition is an AWS partition description for a service and its' region endpoints.
type Partition struct {
	ID                string
	RegionRegex       *regexp.Regexp
	PartitionEndpoint string
	IsRegionalized    bool
	Defaults          map[DefaultKey]Endpoint
	Endpoints         Endpoints
}

func (p Partition) canResolveEndpoint(region string, opts Options) bool {
	_, ok := p.Endpoints[EndpointKey{
		Region:  region,
		Variant: opts.GetEndpointVariant(),
	}]
	return ok || p.RegionRegex.MatchString(region)
}

// ResolveEndpoint resolves and service endpoint for the given region and options.
func (p Partition) ResolveEndpoint(region string, options Options) (resolved aws.Endpoint, err error) {
	if len(region) == 0 && len(p.PartitionEndpoint) != 0 {
		region = p.PartitionEndpoint
	}

	endpoints := p.Endpoints

	variant := options.GetEndpointVariant()
	serviceVariant := options.ServiceVariant

	defaults := p.Defaults[DefaultKey{
		Variant:        variant,
		ServiceVariant: serviceVariant,
	}]

	return p.endpointForRegion(region, variant, serviceVariant, endpoints).resolve(p.ID, region, defaults, options)
}

func (p Partition) endpointForRegion(region string, variant EndpointVariant, serviceVariant ServiceVariant, endpoints Endpoints) Endpoint {
	key := EndpointKey{
		Region:  region,
		Variant: variant,
	}

	if e, ok := endpoints[key]; ok {
		return e
	}

	if !p.IsRegionalized {
		return endpoints[EndpointKey{
			Region:         p.PartitionEndpoint,
			Variant:        variant,
			ServiceVariant: serviceVariant,
		}]
	}

	// Unable to find any matching endpoint, return
	// blank that will be used for generic endpoint creation.
	return Endpoint{}
}

// Endpoints is a map of service config regions to endpoints
type Endpoints map[EndpointKey]Endpoint

// CredentialScope is the credential scope of a region and service
type CredentialScope struct {
	Region  string
	Service string
}

// Endpoint is a service endpoint description
type Endpoint struct {
	// True if the endpoint cannot be resolved for this partition/region/service
	Unresolveable aws.Ternary

	Hostname  string
	Protocols []string

	CredentialScope CredentialScope

	SignatureVersions []string

	// Indicates that this endpoint is deprecated.
	Deprecated aws.Ternary
}

// IsZero returns whether the endpoint structure is an empty (zero) value.
func (e Endpoint) IsZero() bool {
	switch {
	case e.Unresolveable != aws.UnknownTernary:
		return false
	case len(e.Hostname) != 0:
		return false
	case len(e.Protocols) != 0:
		return false
	case e.CredentialScope != (CredentialScope{}):
		return false
	case len(e.SignatureVersions) != 0:
		return false
	}
	return true
}

func (e Endpoint) resolve(partition, region string, def Endpoint, options Options) (aws.Endpoint, error) {
	var merged Endpoint
	merged.mergeIn(def)
	merged.mergeIn(e)
	e = merged

	if e.IsZero() {
		return aws.Endpoint{}, fmt.Errorf("unable to resolve endpoint for region: %v", region)
	}

	var u string
	if e.Unresolveable != aws.TrueTernary {
		// Only attempt to resolve the endpoint if it can be resolved.
		hostname := strings.Replace(e.Hostname, "{region}", region, 1)

		scheme := getEndpointScheme(e.Protocols, options.DisableHTTPS)
		u = scheme + "://" + hostname
	}

	signingRegion := e.CredentialScope.Region
	if len(signingRegion) == 0 {
		signingRegion = region
	}
	signingName := e.CredentialScope.Service

	if e.Deprecated == aws.TrueTernary && options.LogDeprecated {
		options.Logger.Logf(logging.Warn, "endpoint identifier %q, url %q marked as deprecated", region, u)
	}

	return aws.Endpoint{
		URL:           u,
		PartitionID:   partition,
		SigningRegion: signingRegion,
		SigningName:   signingName,
		SigningMethod: getByPriority(e.SignatureVersions, signerPriority, defaultSigner),
	}, nil
}

func (e *Endpoint) mergeIn(other Endpoint) {
	if other.Unresolveable != aws.UnknownTernary {
		e.Unresolveable = other.Unresolveable
	}
	if len(other.Hostname) > 0 {
		e.Hostname = other.Hostname
	}
	if len(other.Protocols) > 0 {
		e.Protocols = other.Protocols
	}
	if len(other.CredentialScope.Region) > 0 {
		e.CredentialScope.Region = other.CredentialScope.Region
	}
	if len(other.CredentialScope.Service) > 0 {
		e.CredentialScope.Service = other.CredentialScope.Service
	}
	if len(other.SignatureVersions) > 0 {
		e.SignatureVersions = other.SignatureVersions
	}
	if other.Deprecated != aws.UnknownTernary {
		e.Deprecated = other.Deprecated
	}
}

func getEndpointScheme(protocols []string, disableHTTPS bool) string {
	if disableHTTPS {
		return "http"
	}

	return getByPriority(protocols, protocolPriority, defaultProtocol)
}

func getByPriority(s []string, p []string, def string) string {
	if len(s) == 0 {
		return def
	}

	for i := range p {
		for j := range s {
			if s[j] == p[i] {
				return s[j]
			}
		}
	}

	return s[0]
}
