package keystore

import (
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"time"

	"golang.org/x/sync/syncmap"

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

	"code.justin.tv/sse/malachai/pkg/config"
	"code.justin.tv/sse/malachai/pkg/internal/discovery"
	"code.justin.tv/sse/malachai/pkg/jwtvalidation"
)

// Store of keys, implemented as an in memory map with a mutex
type Store struct {
	//store  map[jwtvalidation.SigningEntity][]byte
	store  syncmap.Map
	dynamo discovery.Discoverer
	cfg    *Config
}

// Storer of keys
type Storer interface {
	InvalidateRSAPublicKey(caller *jwtvalidation.SigningEntity) error
	GetRSAPublicKey(caller *jwtvalidation.SigningEntity) (*rsa.PublicKey, error)
	CacheRSAPublicKey(*jwtvalidation.SigningEntity, *discovery.PublicKey) error
	PurgeExpiredKeys() (err error)
}

// New creates a new Storer
func New(cfg *Config) (store *Store, err error) {
	err = cfg.FillDefaults()
	if err != nil {
		return
	}

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

	store = &Store{
		dynamo: &discovery.Client{
			DB:        dynamodb.New(sess),
			TableName: cfg.TableName,
		},
		cfg: cfg,
	}
	return
}

// File Errors
var (
	ErrKeystoreHasInvalidPubkey = errors.New("keystore returned invalid pubkey")
	ErrInvalidPubkey            = errors.New("invalid public key loaded")
)

// ParseRSAPubkey converts bytes into an *rsa.PublicKey
func ParseRSAPubkey(bs []byte) (publicKey *rsa.PublicKey, err error) {
	pkixEncoded, _ := pem.Decode(bs)
	if pkixEncoded == nil {
		err = ErrInvalidPubkey
		return
	}

	i, err := x509.ParsePKIXPublicKey(pkixEncoded.Bytes)
	if err != nil {
		err = ErrInvalidPubkey
		return
	}

	var ok bool
	publicKey, ok = i.(*rsa.PublicKey)
	if !ok {
		err = ErrKeystoreHasInvalidPubkey
		return
	}
	return
}

// GetRSAPublicKey returns an rsa.PublicKey from store
func (s *Store) GetRSAPublicKey(caller *jwtvalidation.SigningEntity) (publicKey *rsa.PublicKey, err error) {
	getOutput, err := s.get(caller)
	if err != nil {
		return
	}

	publicKey, err = ParseRSAPubkey(getOutput.Pubkey)
	return
}

// Get retrieves a key from memory. if the key does not exist, pulls from dynamo and
// fills cache.
func (s *Store) get(caller *jwtvalidation.SigningEntity) (value *discovery.PublicKey, err error) {
	v, ok := s.store.Load(*caller)
	if ok {
		if value, ok = v.(*discovery.PublicKey); ok {
			if value.ExpireAt.After(time.Now()) {
				return
			}
			value = nil
		}
	}

	publicKey, err := s.dynamo.Get(caller)
	if err != nil {
		return
	}

	k := discovery.PublicKey(*publicKey)
	value = &k
	s.store.Store(*caller, value)
	return
}

// InvalidateRSAPublicKey removes a pubkey from cache
func (s *Store) InvalidateRSAPublicKey(caller *jwtvalidation.SigningEntity) (err error) {
	s.store.Delete(*caller)
	return
}

// CacheRSAPublicKey stores the given public key in cache
func (s *Store) CacheRSAPublicKey(caller *jwtvalidation.SigningEntity, key *discovery.PublicKey) error {
	storedKey, exists := s.store.LoadOrStore(*caller, key)
	if !exists {
		return nil
	}

	if storedKey, ok := storedKey.(*discovery.PublicKey); ok {
		if storedKey.CreatedAt.After(key.CreatedAt) {
			return nil
		}
	}

	s.store.Store(*caller, key)
	return nil
}
