package capabilities

import (
	"errors"
	"regexp"
	"time"

	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)

// dynamodb key names
const (
	CapabilitiesKeyCaller   = "caller"
	CapabilitiesKeyCallee   = "callee"
	capabilitiesIndexCaller = "by-caller"
)

const (
	//CapabilityAllowAll allows all caps for caller callee map
	CapabilityAllowAll = "*"
)

// errors
var (
	ErrCapabilitiesNotFound = errors.New("capabilities not found")
)

// stringSet is a set of strings.
type stringSet map[string]interface{}

// Capabilities holds callee to caller capabilities map
type Capabilities struct {
	// Capabilities assigned to the caller
	Capabilities stringSet `dynamodbav:"capabilities,stringset"`

	// Compiled capabilities regular expressions (only contains those with wildcards)
	wildcardCapabilities map[string]*regexp.Regexp

	// The name of the callee service
	Callee string `dynamodbav:"callee"`

	// The name of the caller service
	Caller string `dynamodbav:"caller"`

	UpdatedAt         time.Time `dynamodbav:"updated_at,unixtime"`
	LastUpdatedByUser string    `dynamodbav:"last_updated_by_user"`
}

// Key is the composite key for Capabilities
type Key struct {
	Caller string `dynamodbav:"caller"`
	Callee string `dynamodbav:"callee"`
}

// Add appends the provided capabilities to the existing set
func (c *Capabilities) Add(capabilities []string) {
	if c.Capabilities == nil {
		c.Capabilities = stringSet{}
	}
	for _, cap := range capabilities {
		c.Capabilities[cap] = nil
	}
}

// Remove removes the provided capabilities from the existing set of
// capabilities. Does not do anything if a capability is not a member of the
// existing set.
func (c *Capabilities) Remove(capabilities ...string) {
	if c.Capabilities == nil {
		c.Capabilities = stringSet{}
	}
	for _, cap := range capabilities {
		delete(c.Capabilities, cap)
	}
}

// CursorKey returns the row cursor for pagination
func (c *Capabilities) CursorKey() *Key {
	return &Key{
		Caller: c.Caller,
		Callee: c.Callee,
	}
}

// ContainsAll returns true if the registered capabilities is a superset of
// the provided capabilities
func (c *Capabilities) ContainsAll(capabilities []string) (containsAll bool) {
	for _, item := range capabilities {
		if !c.Contains(item) {
			return
		}
	}
	containsAll = true
	return
}

// Contains returns true if the capabilities set contains the provided
// capability
func (c *Capabilities) Contains(capability string) (hasCapability bool) {
	if c.Capabilities == nil {
		c.Capabilities = stringSet{}
		return
	}
	if _, hasCapability = c.Capabilities[CapabilityAllowAll]; hasCapability {
		return
	}
	_, hasCapability = c.Capabilities[capability]
	return
}

// ContainsWildCard returns true if the capabilities set contains the provided
// capability with support for wildcarding.
func (c *Capabilities) ContainsWildCard(capability string) (hasCapability bool) {

	// try existing logic first to handle more efficiently non-wildcards.
	hasCapability = c.Contains(capability)
	if hasCapability {
		return
	}

	for _, v := range c.wildcardCapabilities {
		hasCapability = v.Copy().MatchString(capability)
		if hasCapability {
			return
		}
	}
	return
}

// Equal returns true when capabilities are the equal.
func (c *Capabilities) Equal(other *Capabilities) (equal bool) {
	if c.Capabilities == nil {
		c.Capabilities = stringSet{}
	}
	if other.Capabilities == nil {
		other.Capabilities = stringSet{}
	}

	switch {
	case c.Caller != other.Caller:
		return
	case c.Callee != other.Callee:
		return
	case len(c.Capabilities) != len(other.Capabilities):
		return
	}

	for item := range c.Capabilities {
		if !other.Contains(item) {
			return
		}
	}

	equal = true
	return
}

// Marshal as an input for dynamodb
func (c *Capabilities) Marshal() (attributeValues map[string]*dynamodb.AttributeValue, err error) {
	if c.Capabilities == nil {
		c.Capabilities = stringSet{}
	}
	attributeValues, err = dynamodbattribute.MarshalMap(c)
	return
}

// ToStringSlice returns the capabilities as a slice of strings.
func (c *Capabilities) ToStringSlice() (caps []string) {
	if c.Capabilities == nil {
		caps = []string{}
		return
	}
	caps = make([]string, 0, len(c.Capabilities))
	for cap := range c.Capabilities {
		caps = append(caps, cap)
	}
	return
}
