/*
Copyright (c) Edgeless Systems GmbH

SPDX-License-Identifier: AGPL-3.0-only
*/

package azure

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"strings"

	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
	"github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets"
	"github.com/edgelesssys/constellation/kms/internal/config"
	"github.com/edgelesssys/constellation/kms/internal/storage"
	"github.com/edgelesssys/constellation/kms/kms"
	"github.com/edgelesssys/constellation/kms/kms/util"
)

const (
	vaultPrefix = "https://"
	// DefaultCloud is the suffix for the default Vault URL.
	DefaultCloud VaultSuffix = ".vault.azure.net/"
	// ChinaCloud is the suffix for Vaults in Azure China Cloud.
	ChinaCloud VaultSuffix = ".vault.azure.cn/"
	// USGovCloud is the suffix for Vaults in Azure US Government Cloud.
	USGovCloud VaultSuffix = ".vault.usgovcloudapi.net/"
	// GermanCloud is the suffix for Vaults in Azure German Cloud.
	GermanCloud VaultSuffix = ".vault.microsoftazure.de/"
)

// VaultSuffix is the suffix added to a Vault name to create a valid Vault URL.
type VaultSuffix string

type kmsClientAPI interface {
	SetSecret(ctx context.Context, secretName string, parameters azsecrets.SetSecretParameters, options *azsecrets.SetSecretOptions) (azsecrets.SetSecretResponse, error)
	GetSecret(ctx context.Context, secretName string, version string, options *azsecrets.GetSecretOptions) (azsecrets.GetSecretResponse, error)
}

// KMSClient implements the CloudKMS interface for Azure Key Vault.
type KMSClient struct {
	client  kmsClientAPI
	storage kms.Storage
	opts    *Opts
}

// Opts are optional settings for AKV clients.
type Opts struct {
	credentials *azidentity.DefaultAzureCredentialOptions
	client      *azcore.ClientOptions
}

// New initializes a KMS client for Azure Key Vault.
func New(ctx context.Context, vaultName string, vaultType VaultSuffix, store kms.Storage, opts *Opts) (*KMSClient, error) {
	if opts == nil {
		opts = &Opts{}
	}
	cred, err := azidentity.NewDefaultAzureCredential(opts.credentials)
	if err != nil {
		return nil, fmt.Errorf("loading credentials: %w", err)
	}
	client := azsecrets.NewClient(vaultPrefix+vaultName+string(vaultType), cred, opts.client)

	// `azsecrets.NewClient()` does not error if the vault is non existent
	// Test here if we can reach the vault, and error otherwise
	pager := client.NewListSecretsPager(nil)
	if _, err := pager.NextPage(ctx); err != nil {
		return nil, fmt.Errorf("AKV not reachable: %w", err)
	}

	if store == nil {
		store = storage.NewMemMapStorage()
	}
	return &KMSClient{client: client, storage: store, opts: opts}, nil
}

// CreateKEK saves a new Key Encryption Key using Azure Key Vault.
//
// Keys are saved as software protected secrets.
// If no key material is provided, a new random 32 Byte key is generated and imported to the Vault.
func (c *KMSClient) CreateKEK(ctx context.Context, keyID string, key []byte) error {
	if len(key) == 0 {
		var err error
		key, err = util.GetRandomKey(config.SymmetricKeyLength)
		if err != nil {
			return fmt.Errorf("key generation: %w", err)
		}
	}

	// Saving symmetric keys in Azure Key Vault requires encoding them to base64
	secretValue := azsecrets.SetSecretParameters{
		Value:       to.Ptr(base64.StdEncoding.EncodeToString(key)),
		ContentType: to.Ptr("KeyEncryptionKey"),
		Tags:        toAzureTags(config.KmsTags),
	}
	_, err := c.client.SetSecret(ctx, keyID, secretValue, &azsecrets.SetSecretOptions{})
	if err != nil {
		return fmt.Errorf("importing KEK to Azure Key Vault: %w", err)
	}

	return nil
}

// GetDEK decrypts a DEK from storage.
func (c *KMSClient) GetDEK(ctx context.Context, kekID, keyID string, dekSize int) ([]byte, error) {
	kek, err := c.getKEK(ctx, kekID)
	if err != nil {
		return nil, fmt.Errorf("loading KEK from key vault: %w", err)
	}

	encryptedDEK, err := c.storage.Get(ctx, keyID)
	if err != nil {
		if !errors.Is(err, storage.ErrDEKUnset) {
			return nil, fmt.Errorf("loading encrypted DEK from storage: %w", err)
		}

		// If the DEK does not exist we generate a new random DEK and save it to storage
		newDEK, err := util.GetRandomKey(dekSize)
		if err != nil {
			return nil, fmt.Errorf("key generation: %w", err)
		}
		return newDEK, c.putDEK(ctx, keyID, kek, newDEK)
	}

	// Azure Key Vault does not support crypto operations with secrets, so we do the unwrapping ourselves
	return util.UnwrapAES(encryptedDEK, kek)
}

// putDEK encrypts a DEK and saves it to storage.
func (c *KMSClient) putDEK(ctx context.Context, keyID string, kek, plainDEK []byte) error {
	// Azure Key Vault does not support crypto operations with secrets, so we do the wrapping ourselves
	encryptedDEK, err := util.WrapAES(plainDEK, kek)
	if err != nil {
		return fmt.Errorf("encrypting DEK: %w", err)
	}
	return c.storage.Put(ctx, keyID, encryptedDEK)
}

// getKEK loads a Key Encryption Key from Azure Key Vault.
func (c *KMSClient) getKEK(ctx context.Context, kekID string) ([]byte, error) {
	res, err := c.client.GetSecret(ctx, kekID, "", nil)
	if err != nil {
		if strings.Contains(err.Error(), "SecretNotFound") {
			return nil, kms.ErrKEKUnknown
		}
		return nil, err
	}

	// Keys are saved in base64, decode them
	return base64.StdEncoding.DecodeString(*res.Value)
}
