package capabilities

import (
	"context"
	"regexp"
	"strings"
	"time"

	"code.justin.tv/sse/malachai/pkg/config"

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

//Manager holds dynamodb and client to perform various admin
// tasks on dynamodb
type Manager interface {
	Put(c *Capabilities) (err error)
	Get(caller, callee string) (c *Capabilities, err error)
	GetConsistent(caller, callee string) (c *Capabilities, err error)
	GetForCallee(ctx context.Context, callee string) (caps []*Capabilities, err error)
	GetPageForCaller(context.Context, *GetPageForCallerInput) (*GetPageForCallerOutput, error)
	GetPageForCallee(context.Context, *GetPageForCalleeInput) (*GetPageForCalleeOutput, error)
	Delete(caller, callee string) (err error)
}

type manager struct {
	DynamoDB dynamodbiface.DynamoDBAPI
	Config   *Config
}

// New creates a Inventory Manager from config. Merges configuration with
// DefaultConfig()
func New(cfg *Config) (m Manager, err error) {
	if cfg == nil {
		cfg = new(Config)
	}
	err = cfg.FillDefaults()
	if err != nil {
		return
	}

	sess, err := session.NewSession(config.AWSConfig(cfg.AWSConfigBase, cfg.AWSRegion, cfg.RoleArn))
	if err != nil {
		return
	}

	ddb := dynamodb.New(sess)
	m = &manager{
		DynamoDB: ddb,
		Config:   cfg,
	}
	return
}

// Put update an existing row in dynamodb
func (m *manager) Put(c *Capabilities) (err error) {
	c.UpdatedAt = time.Now().UTC()
	putInput, err := m.inputForPut(c)
	if err != nil {
		return
	}
	_, err = m.DynamoDB.PutItem(putInput)
	if err != nil {
		return
	}
	return
}

func (m *manager) inputForPut(c *Capabilities) (input *dynamodb.PutItemInput, err error) {
	item, err := c.Marshal()
	if err != nil {
		return
	}

	input = &dynamodb.PutItemInput{
		Item:      item,
		TableName: aws.String(m.Config.TableName),
	}
	return
}

// Get returns the Capabilities object associated with caller-callee
func (m *manager) Get(caller, callee string) (*Capabilities, error) {
	return m.get(caller, callee, false)
}

// GetConsistent returns the Capabilities object associated with caller-callee
func (m *manager) GetConsistent(caller, callee string) (*Capabilities, error) {
	return m.get(caller, callee, true)
}

func (m *manager) get(caller, callee string, consistentRead bool) (c *Capabilities, err error) {
	res, err := m.DynamoDB.GetItem(&dynamodb.GetItemInput{
		ConsistentRead: aws.Bool(consistentRead),
		TableName:      aws.String(m.Config.TableName),
		Key: map[string]*dynamodb.AttributeValue{
			CapabilitiesKeyCaller: {S: aws.String(caller)},
			CapabilitiesKeyCallee: {S: aws.String(callee)},
		},
	})
	if err != nil {
		return
	}

	if res == nil || res.Item == nil {
		err = ErrCapabilitiesNotFound
		return
	}

	c, err = m.unmarshalCapabilities(res.Item)
	return
}

// GetForCallee returns Capabilities for all callers for a callee
func (m *manager) GetForCallee(ctx context.Context, callee string) (caps []*Capabilities, err error) {
	input := &dynamodb.QueryInput{
		ConsistentRead: aws.Bool(true),
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
			":callee": {S: aws.String(callee)},
		},
		KeyConditionExpression: aws.String(CapabilitiesKeyCallee + " = :callee"),
		TableName:              aws.String(m.Config.TableName),
	}

	err = m.DynamoDB.QueryPagesWithContext(ctx, input, func(output *dynamodb.QueryOutput, lastPage bool) (continuePaging bool) {
		if caps == nil {
			caps = make([]*Capabilities, 0, *output.Count)
		}

		for _, item := range output.Items {
			var c *Capabilities
			c, err = m.unmarshalCapabilities(item)
			if err != nil {
				return
			}
			caps = append(caps, c)
		}

		continuePaging = !lastPage
		return
	})

	return
}

// GetPageForCalleeInput is the input for GetPageForCallee
type GetPageForCalleeInput struct {
	Callee string
	First  int32
	After  *Key
}

// GetPageForCalleeOutput in the output for GetPageForCallee
type GetPageForCalleeOutput struct {
	Items   []*Capabilities
	HasNext bool
}

// GetPageForCallee returns page of Capabilities for a callee
func (m *manager) GetPageForCallee(ctx context.Context, input *GetPageForCalleeInput) (output *GetPageForCalleeOutput, err error) {
	var exclusiveStartKey map[string]*dynamodb.AttributeValue
	if input.After != nil {
		exclusiveStartKey, err = dynamodbattribute.MarshalMap(input.After)
		if err != nil {
			return
		}
	}

	queryInput := &dynamodb.QueryInput{
		ExclusiveStartKey: exclusiveStartKey,
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
			":callee": {S: aws.String(input.Callee)},
		},
		Limit:                  aws.Int64(int64(input.First)),
		KeyConditionExpression: aws.String(CapabilitiesKeyCallee + " = :callee"),
		TableName:              aws.String(m.Config.TableName),
	}

	queryOutput, err := m.DynamoDB.QueryWithContext(ctx, queryInput)

	output = &GetPageForCalleeOutput{
		Items:   make([]*Capabilities, len(queryOutput.Items)),
		HasNext: queryOutput.LastEvaluatedKey != nil,
	}

	for nItem, item := range queryOutput.Items {
		output.Items[nItem], err = m.unmarshalCapabilities(item)
		if err != nil {
			return
		}
	}

	return
}

// GetPageForCallerInput is the input for GetPageForCallee
type GetPageForCallerInput struct {
	Caller string
	First  int32
	After  *Key
}

// GetPageForCallerOutput in the output for GetPageForCallee
type GetPageForCallerOutput struct {
	Items   []*Capabilities
	HasNext bool
}

// GetPageForCaller returns page of Capabilities for a callee
func (m *manager) GetPageForCaller(ctx context.Context, input *GetPageForCallerInput) (output *GetPageForCallerOutput, err error) {
	var exclusiveStartKey map[string]*dynamodb.AttributeValue
	if input.After != nil {
		exclusiveStartKey, err = dynamodbattribute.MarshalMap(input.After)
		if err != nil {
			return
		}
	}

	queryInput := &dynamodb.QueryInput{
		ExclusiveStartKey: exclusiveStartKey,
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
			":caller": {S: aws.String(input.Caller)},
		},
		IndexName:              aws.String(capabilitiesIndexCaller),
		Limit:                  aws.Int64(int64(input.First)),
		KeyConditionExpression: aws.String(CapabilitiesKeyCaller + " = :caller"),
		TableName:              aws.String(m.Config.TableName),
	}

	queryOutput, err := m.DynamoDB.QueryWithContext(ctx, queryInput)

	output = &GetPageForCallerOutput{
		Items:   make([]*Capabilities, len(queryOutput.Items)),
		HasNext: queryOutput.LastEvaluatedKey != nil,
	}

	for nItem, item := range queryOutput.Items {
		output.Items[nItem], err = m.unmarshalCapabilities(item)
		if err != nil {
			return
		}
	}

	return
}

//Delete callee-caller capabilities map from dynamodb
func (m *manager) Delete(caller, callee string) (err error) {
	deleteInput := &dynamodb.DeleteItemInput{
		Key: map[string]*dynamodb.AttributeValue{
			CapabilitiesKeyCallee: {S: aws.String(callee)},
			CapabilitiesKeyCaller: {S: aws.String(caller)},
		},
		TableName: aws.String(m.Config.TableName),
	}
	_, err = m.DynamoDB.DeleteItem(deleteInput)
	return
}

func (m *manager) unmarshalCapabilities(item map[string]*dynamodb.AttributeValue) (c *Capabilities, err error) {
	c = &Capabilities{}
	err = dynamodbattribute.UnmarshalMap(item, &c)
	c.UpdatedAt = c.UpdatedAt.UTC()
	if c.Capabilities == nil {
		c.Capabilities = stringSet{}
	}
	c.wildcardCapabilities = compileCapabilities(c.Capabilities)
	return
}

/**
 * This compiles the regular expressions at load time to improve runtime performance, any thing it doesn't have to
 * compile it just skips.
 */
func compileCapabilities(capabilities stringSet) (compiledCapabilities map[string]*regexp.Regexp) {
	compiledCapabilities = make(map[string]*regexp.Regexp)

	for k := range capabilities {
		if strings.Contains(k, "*") {
			// QuoteMeta removes all regex characters,
			// since we're only doing * support we want to do a simple replace for all *
			re, err := regexp.Compile(strings.Replace(regexp.QuoteMeta(k), "\\*", ".*", -1))
			// only add ones that didn't error, we shouldn't expect any errors as we sanitized with QuoteMeta first.
			if err == nil {
				compiledCapabilities[k] = re.Copy()
			}
		}
	}

	return
}
