package discovery

import (
	"bytes"
	"context"
	"errors"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"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"

	"code.justin.tv/sse/malachai/pkg/jwtvalidation"
)

// ddb column names
const (
	CallerServiceAttributeName = "caller_service"
	FingerprintAttributeName   = "fingerprint"
	PubkeyAttributeName        = "pubkey"
)

// errors
var (
	ErrPubkeyNotFound   = errors.New("public key not found for caller and fingerprint")
	ErrPubkeyTombstoned = errors.New("public key is invalidated")
)

// Discoverer is the interface to interact with the discovery table
type Discoverer interface {
	Put(*PutInput) (output *PutOutput, err error)
	Get(caller *jwtvalidation.SigningEntity) (output *GetOutput, err error)
	TombstoneService(ctx context.Context, serviceID string) (err error)
}

// PutInput is the parameters for Put
type PutInput struct {
	ServiceID string
	Pubkey    []byte
	ExpireAt  time.Time
}

func (input *PutInput) validate() (err error) {
	if input.ServiceID == "" {
		err = errors.New("discovery: ServiceID parameter is missing")
		return
	}
	return
}

type publicKey struct {
	Caller      string    `dynamodbav:"caller_service"`
	Fingerprint string    `dynamodbav:"fingerprint"`
	Value       []byte    `dynamodbav:"pubkey"`
	Tombstone   bool      `dynamodbav:"tombstone"`
	ExpireAt    time.Time `dynamodbav:"expire_at"`
	CreatedAt   time.Time `dynamodbav:"created_at"`
}

// DynamoPublicKey is the dynamo representation of PublicKey
type DynamoPublicKey publicKey

// Client implements Discoverer
type Client struct {
	DB        dynamodbiface.DynamoDBAPI
	TableName string
}

// PutOutput is the output of Put
type PutOutput struct {
	Fingerprint string
}

// Put implements Discoverer
func (c *Client) Put(input *PutInput) (output *PutOutput, err error) {
	if err = input.validate(); err != nil {
		return
	}

	fp, err := jwtvalidation.CalculatePubkeyFingerprint(bytes.NewBuffer(input.Pubkey))
	if err != nil {
		return
	}

	av, err := dynamodbattribute.MarshalMap(&publicKey{
		Caller:      input.ServiceID,
		Fingerprint: fp,
		Value:       input.Pubkey,
		ExpireAt:    input.ExpireAt,
		CreatedAt:   time.Now(),
	})
	if err != nil {
		return
	}

	params := &dynamodb.PutItemInput{
		Item: av,
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
			":true": {BOOL: aws.Bool(true)},
		},
		ConditionExpression: aws.String("attribute_not_exists(tombstone) OR NOT tombstone = :true"),
		TableName:           aws.String(c.TableName),
	}

	_, err = c.DB.PutItem(params)
	if err != nil {
		if e, ok := err.(awserr.Error); ok {
			if e.Code() == dynamodb.ErrCodeConditionalCheckFailedException {
				err = ErrPubkeyTombstoned
			}
		}
	}

	output = &PutOutput{
		Fingerprint: fp,
	}
	return
}

// PublicKey is a public key in the discovery table
type PublicKey GetOutput

// GetOutput contains the output of Get
type GetOutput struct {
	Pubkey      []byte
	Fingerprint string
	ExpireAt    time.Time
	CreatedAt   time.Time
}

// Get implements Discoverer
func (c *Client) Get(caller *jwtvalidation.SigningEntity) (output *GetOutput, err error) {
	params := &dynamodb.GetItemInput{
		ConsistentRead: aws.Bool(true),
		Key: map[string]*dynamodb.AttributeValue{
			CallerServiceAttributeName: {S: aws.String(caller.Caller)},
			FingerprintAttributeName:   {S: aws.String(caller.Fingerprint)},
		},
		TableName: aws.String(c.TableName),
	}

	dbOutput, err := c.DB.GetItem(params)
	if err != nil {
		return
	}
	if dbOutput.Item == nil {
		err = ErrPubkeyNotFound
		return
	}

	dp := new(publicKey)

	err = dynamodbattribute.UnmarshalMap(dbOutput.Item, dp)
	if err != nil {
		return
	}

	if dp.Tombstone {
		err = ErrPubkeyTombstoned
		return
	}

	output = &GetOutput{
		Pubkey:      dp.Value,
		Fingerprint: dp.Fingerprint,
		ExpireAt:    dp.ExpireAt,
	}
	return
}

// TombstoneService tombstones all keys associated with a service
func (c *Client) TombstoneService(ctx context.Context, serviceID string) (err error) {
	queryInput := &dynamodb.QueryInput{
		TableName:              aws.String(c.TableName),
		KeyConditionExpression: aws.String("caller_service = :caller_service"),
		ConsistentRead:         aws.Bool(true),
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
			":caller_service": {S: aws.String(serviceID)},
			":true":           {BOOL: aws.Bool(true)},
		},
		ProjectionExpression: aws.String(FingerprintAttributeName),
		FilterExpression:     aws.String("(attribute_not_exists(tombstone) OR NOT tombstone = :true)"),
	}

	err = c.DB.QueryPagesWithContext(ctx, queryInput, func(output *dynamodb.QueryOutput, lastPage bool) (continuePaging bool) {
		for _, item := range output.Items {
			dp := new(publicKey)

			err = dynamodbattribute.UnmarshalMap(item, dp)
			if err != nil {
				return
			}

			err = c.tombstone(ctx, serviceID, dp.Fingerprint)
			if err != nil {
				return
			}
		}

		continuePaging = !lastPage
		return
	})
	return
}

// sets a serviceid, fingerprint to be tombstoned
func (c *Client) tombstone(ctx context.Context, serviceID, fingerprint string) (err error) {
	params := &dynamodb.UpdateItemInput{
		TableName: aws.String(c.TableName),
		Key: map[string]*dynamodb.AttributeValue{
			CallerServiceAttributeName: {S: aws.String(serviceID)},
			FingerprintAttributeName:   {S: aws.String(fingerprint)},
		},
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
			":tombstone": {BOOL: aws.Bool(true)},
		},
		ReturnValues:     aws.String("NONE"),
		UpdateExpression: aws.String("SET tombstone = :tombstone"),
	}
	_, err = c.DB.UpdateItem(params)
	return
}
