/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package lockbox

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"sync"
	"time"

	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
	"github.com/yandex-cloud/go-sdk/iamkey"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/types"
	ctrl "sigs.k8s.io/controller-runtime"
	kclient "sigs.k8s.io/controller-runtime/pkg/client"

	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/grpc"
)

const maxSecretsClientLifetime = 5 * time.Minute // supposed SecretsClient lifetime is quite short
const iamTokenCleanupDelay = 1 * time.Hour       // specifies how often cleanUpIamTokenMap() is performed

var log = ctrl.Log.WithName("provider").WithName("yandex").WithName("lockbox")

type iamTokenKey struct {
	authorizedKeyID  string
	serviceAccountID string
	privateKeyHash   string
}

// lockboxProvider is a provider for Yandex Lockbox.
type lockboxProvider struct {
	yandexCloudCreator client.YandexCloudCreator

	lockboxClientMap      map[string]client.LockboxClient // apiEndpoint -> LockboxClient
	lockboxClientMapMutex sync.Mutex
	iamTokenMap           map[iamTokenKey]*client.IamToken
	iamTokenMapMutex      sync.Mutex
}

func newLockboxProvider(yandexCloudCreator client.YandexCloudCreator) *lockboxProvider {
	return &lockboxProvider{
		yandexCloudCreator: yandexCloudCreator,
		lockboxClientMap:   make(map[string]client.LockboxClient),
		iamTokenMap:        make(map[iamTokenKey]*client.IamToken),
	}
}

// NewClient constructs a Yandex Lockbox Provider.
func (p *lockboxProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
	storeSpec := store.GetSpec()
	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.YandexLockbox == nil {
		return nil, fmt.Errorf("received invalid Yandex Lockbox SecretStore resource")
	}
	storeSpecYandexLockbox := storeSpec.Provider.YandexLockbox

	authorizedKeySecretName := storeSpecYandexLockbox.Auth.AuthorizedKey.Name
	if authorizedKeySecretName == "" {
		return nil, fmt.Errorf("invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
	}
	objectKey := types.NamespacedName{
		Name:      authorizedKeySecretName,
		Namespace: namespace,
	}

	// only ClusterStore is allowed to set namespace (and then it's required)
	if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
		if storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace == nil {
			return nil, fmt.Errorf("invalid ClusterSecretStore: missing AuthorizedKey Namespace")
		}
		objectKey.Namespace = *storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace
	}

	authorizedKeySecret := &corev1.Secret{}
	err := kube.Get(ctx, objectKey, authorizedKeySecret)
	if err != nil {
		return nil, fmt.Errorf("could not fetch AuthorizedKey secret: %w", err)
	}

	authorizedKeySecretData := authorizedKeySecret.Data[storeSpecYandexLockbox.Auth.AuthorizedKey.Key]
	if (authorizedKeySecretData == nil) || (len(authorizedKeySecretData) == 0) {
		return nil, fmt.Errorf("missing AuthorizedKey")
	}

	var authorizedKey iamkey.Key
	err = json.Unmarshal(authorizedKeySecretData, &authorizedKey)
	if err != nil {
		return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
	}

	var caCertificateData []byte

	if storeSpecYandexLockbox.CAProvider != nil {
		certObjectKey := types.NamespacedName{
			Name:      storeSpecYandexLockbox.CAProvider.Certificate.Name,
			Namespace: namespace,
		}

		if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
			if storeSpecYandexLockbox.CAProvider.Certificate.Namespace == nil {
				return nil, fmt.Errorf("invalid ClusterSecretStore: missing CA certificate Namespace")
			}
			certObjectKey.Namespace = *storeSpecYandexLockbox.CAProvider.Certificate.Namespace
		}

		caCertificateSecret := &corev1.Secret{}
		err := kube.Get(ctx, certObjectKey, caCertificateSecret)
		if err != nil {
			return nil, fmt.Errorf("could not fetch CA certificate secret: %w", err)
		}

		caCertificateData = caCertificateSecret.Data[storeSpecYandexLockbox.CAProvider.Certificate.Key]
		if (caCertificateData == nil) || (len(caCertificateData) == 0) {
			return nil, fmt.Errorf("missing CA Certificate")
		}
	}

	lockboxClient, err := p.getOrCreateLockboxClient(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey, caCertificateData)
	if err != nil {
		return nil, fmt.Errorf("failed to create Yandex Lockbox client: %w", err)
	}

	iamToken, err := p.getOrCreateIamToken(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
	if err != nil {
		return nil, fmt.Errorf("failed to create IAM token: %w", err)
	}

	return &lockboxSecretsClient{lockboxClient, iamToken.Token}, nil
}

func (p *lockboxProvider) getOrCreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (client.LockboxClient, error) {
	p.lockboxClientMapMutex.Lock()
	defer p.lockboxClientMapMutex.Unlock()

	if _, ok := p.lockboxClientMap[apiEndpoint]; !ok {
		log.Info("creating LockboxClient", "apiEndpoint", apiEndpoint)

		lockboxClient, err := p.yandexCloudCreator.CreateLockboxClient(ctx, apiEndpoint, authorizedKey, caCertificate)
		if err != nil {
			return nil, err
		}
		p.lockboxClientMap[apiEndpoint] = lockboxClient
	}
	return p.lockboxClientMap[apiEndpoint], nil
}

func (p *lockboxProvider) getOrCreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
	p.iamTokenMapMutex.Lock()
	defer p.iamTokenMapMutex.Unlock()

	iamTokenKey := buildIamTokenKey(authorizedKey)
	if iamToken, ok := p.iamTokenMap[iamTokenKey]; !ok || !p.isIamTokenUsable(iamToken) {
		log.Info("creating IAM token", "authorizedKeyId", authorizedKey.Id)

		iamToken, err := p.yandexCloudCreator.CreateIamToken(ctx, apiEndpoint, authorizedKey)
		if err != nil {
			return nil, err
		}

		log.Info("created IAM token", "authorizedKeyId", authorizedKey.Id, "expiresAt", iamToken.ExpiresAt)

		p.iamTokenMap[iamTokenKey] = iamToken
	}
	return p.iamTokenMap[iamTokenKey], nil
}

func (p *lockboxProvider) isIamTokenUsable(iamToken *client.IamToken) bool {
	now := p.yandexCloudCreator.Now()
	return now.Add(maxSecretsClientLifetime).Before(iamToken.ExpiresAt)
}

func buildIamTokenKey(authorizedKey *iamkey.Key) iamTokenKey {
	privateKeyHash := sha256.Sum256([]byte(authorizedKey.PrivateKey))
	return iamTokenKey{
		authorizedKey.GetId(),
		authorizedKey.GetServiceAccountId(),
		hex.EncodeToString(privateKeyHash[:]),
	}
}

// Used for testing.
func (p *lockboxProvider) isIamTokenCached(authorizedKey *iamkey.Key) bool {
	p.iamTokenMapMutex.Lock()
	defer p.iamTokenMapMutex.Unlock()

	_, ok := p.iamTokenMap[buildIamTokenKey(authorizedKey)]
	return ok
}

func (p *lockboxProvider) cleanUpIamTokenMap() {
	p.iamTokenMapMutex.Lock()
	defer p.iamTokenMapMutex.Unlock()

	for key, value := range p.iamTokenMap {
		if p.yandexCloudCreator.Now().After(value.ExpiresAt) {
			log.Info("deleting IAM token", "authorizedKeyId", key.authorizedKeyID)
			delete(p.iamTokenMap, key)
		}
	}
}

func (p *lockboxProvider) ValidateStore(store esv1beta1.GenericStore) error {
	return nil
}

// lockboxSecretsClient is a secrets client for Yandex Lockbox.
type lockboxSecretsClient struct {
	lockboxClient client.LockboxClient
	iamToken      string
}

// Empty GetAllSecrets.
func (c *lockboxSecretsClient) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
	// TO be implemented
	return nil, fmt.Errorf("GetAllSecrets not implemented")
}

// GetSecret returns a single secret from the provider.
func (c *lockboxSecretsClient) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
	entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
	if err != nil {
		return nil, fmt.Errorf("unable to request secret payload to get secret: %w", err)
	}

	if ref.Property == "" {
		keyToValue := make(map[string]interface{}, len(entries))
		for _, entry := range entries {
			value, err := getValueAsIs(entry)
			if err != nil {
				return nil, err
			}
			keyToValue[entry.Key] = value
		}
		out, err := json.Marshal(keyToValue)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal secret: %w", err)
		}
		return out, nil
	}

	entry, err := findEntryByKey(entries, ref.Property)
	if err != nil {
		return nil, err
	}
	return getValueAsBinary(entry)
}

// GetSecretMap returns multiple k/v pairs from the provider.
func (c *lockboxSecretsClient) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
	entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
	if err != nil {
		return nil, fmt.Errorf("unable to request secret payload to get secret map: %w", err)
	}

	secretMap := make(map[string][]byte, len(entries))
	for _, entry := range entries {
		value, err := getValueAsBinary(entry)
		if err != nil {
			return nil, err
		}
		secretMap[entry.Key] = value
	}
	return secretMap, nil
}

func (c *lockboxSecretsClient) Close(ctx context.Context) error {
	return nil
}

func (c *lockboxSecretsClient) Validate() error {
	return nil
}

func getValueAsIs(entry *lockbox.Payload_Entry) (interface{}, error) {
	switch entry.Value.(type) {
	case *lockbox.Payload_Entry_TextValue:
		return entry.GetTextValue(), nil
	case *lockbox.Payload_Entry_BinaryValue:
		return entry.GetBinaryValue(), nil
	default:
		return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
	}
}

func getValueAsBinary(entry *lockbox.Payload_Entry) ([]byte, error) {
	switch entry.Value.(type) {
	case *lockbox.Payload_Entry_TextValue:
		return []byte(entry.GetTextValue()), nil
	case *lockbox.Payload_Entry_BinaryValue:
		return entry.GetBinaryValue(), nil
	default:
		return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
	}
}

func findEntryByKey(entries []*lockbox.Payload_Entry, key string) (*lockbox.Payload_Entry, error) {
	for i := range entries {
		if entries[i].Key == key {
			return entries[i], nil
		}
	}
	return nil, fmt.Errorf("payload entry with key '%s' not found", key)
}

func init() {
	lockboxProvider := newLockboxProvider(&grpc.YandexCloudCreator{})

	go func() {
		for {
			time.Sleep(iamTokenCleanupDelay)
			lockboxProvider.cleanUpIamTokenMap()
		}
	}()

	esv1beta1.Register(
		lockboxProvider,
		&esv1beta1.SecretStoreProvider{
			YandexLockbox: &esv1beta1.YandexLockboxProvider{},
		},
	)
}
