package v1

import (
	"context"
	"fmt"
	"net/url"
	"strings"

	"github.com/aws/aws-sdk-go-v2/aws/arn"
	storagev1 "k8s.io/api/storage/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
	AWSARNAnnotationKey = "aws-kms-arn"
	CMEKProvider        = "cmek-provider"

	AzureStorageClassParameterDiskEncryptionSetID = "diskEncryptionSetID"
	azureKeyVaultURIDomainSuffix                  = ".vault.azure.net"
)

// +kubebuilder:object:generate=false
// CMEKStorageClassMapperV2 converts a storage class to a CMEK-enabled storage class
type CMEKStorageClassMapperV2 interface {
	// Map converts a storage class to a CMEK-enabled storage class.
	// Note: you should DeepCopy the sc before modifying it when you are implementing this method.
	StorageClass(ctx context.Context, cluster *Cluster, sc *storagev1.StorageClass) (_ *storagev1.StorageClass, ok bool)
	// Name returns the name of the CMEK-enabled storage class.
	Name(originalStorageClassName string) string
}

// +kubebuilder:object:root=false
type awsStorageClassMapper struct {
	kmsARN  string
	account string
	keyID   string
}

var _ CMEKStorageClassMapperV2 = (*awsStorageClassMapper)(nil)

func NewAWSStorageClassMapper(kmsARN string) (*awsStorageClassMapper, error) {
	if kmsARN == "" {
		return nil, fmt.Errorf("kmsArn is required for CMEK enabled cluster")
	}

	parsedArn, err := arn.Parse(kmsARN)
	if err != nil {
		return nil, err
	}

	return &awsStorageClassMapper{
		kmsARN:  kmsARN,
		account: parsedArn.AccountID,
		keyID:   strings.ReplaceAll(parsedArn.Resource, "/", "-"),
	}, nil
}

func (m *awsStorageClassMapper) Name(originalStorageClassName string) string {
	return fmt.Sprintf("%s-%s-%s", originalStorageClassName, m.account, m.keyID)
}

func (m *awsStorageClassMapper) StorageClass(ctx context.Context, cluster *Cluster, sc *storagev1.StorageClass) (_ *storagev1.StorageClass, ok bool) {
	out := sc.DeepCopy()
	if out.Parameters == nil {
		out.Parameters = make(map[string]string)
	}
	out.Parameters["kmsKeyId"] = m.kmsARN
	out.Name = m.Name(sc.Name)
	return out, true
}

// +kubebuilder:object:root=false
type azuresStorageClassMapper struct {
	cluster *Cluster
	spec    *AzureCMEKSpec
}

var _ CMEKStorageClassMapperV2 = (*azuresStorageClassMapper)(nil)

func NewAzureStorageClassMapper(c *Cluster) (*azuresStorageClassMapper, error) {
	aksSpec, err := AKSClusterSpecForCluster(c)
	if err != nil {
		return nil, fmt.Errorf("unable to get cluster aks spec: %v", err)
	}
	spec := aksSpec.AzureCMEK
	if spec.Empty() {
		return nil, fmt.Errorf("azureCMEK is required for CMEK enabled cluster %s", c.Name)
	}
	if err := spec.Validate(); err != nil {
		return nil, fmt.Errorf("azureCMEk validate: %w", err)
	}

	mapper := &azuresStorageClassMapper{
		cluster: c,
		spec:    spec,
	}

	return mapper, nil
}

func (m *azuresStorageClassMapper) StorageClass(ctx context.Context, cluster *Cluster, sc *storagev1.StorageClass) (_ *storagev1.StorageClass, ok bool) {
	logger := log.FromContext(ctx)
	if cluster.Status.Azure == nil || cluster.Status.Azure.DiskEncryptionSetResourceID == "" {
		logger.Info("diskEncryptionSet is not ready, skip StorageClass map", "storageClass", sc.Name)
		return nil, false
	}

	out := sc.DeepCopy()
	out.Name = m.Name(sc.Name)
	if out.Parameters == nil {
		out.Parameters = make(map[string]string)
	}
	out.Parameters[AzureStorageClassParameterDiskEncryptionSetID] = cluster.Status.Azure.DiskEncryptionSetResourceID
	if out.Annotations == nil {
		out.Annotations = make(map[string]string)
	}
	out.Annotations[AnnoAzureKeyvaultKeyURI] = m.spec.KeyURI()
	if out.Labels == nil {
		out.Labels = make(map[string]string)
	}
	out.Labels[ClusterTagKey] = cluster.Name
	out.Labels[LabelUsedFor] = ClusterFeatureUseCMEK
	return out, true
}

func (m *azuresStorageClassMapper) Name(originalStorageClassName string) string {
	return fmt.Sprintf("%s-%s", originalStorageClassName, m.cluster.Name)
}

// +kubebuilder:object:root=false
type nopMapper struct{}

var _ CMEKStorageClassMapperV2 = (*nopMapper)(nil)

func NewNopMapper() *nopMapper {
	return &nopMapper{}
}

func (m *nopMapper) Map(originalStorageClassName string) string {
	return originalStorageClassName
}

func (m *nopMapper) StorageClass(ctx context.Context, cluster *Cluster, sc *storagev1.StorageClass) (_ *storagev1.StorageClass, ok bool) {
	return nil, false
}

func (m *nopMapper) Name(originalStorageClassName string) string {
	return originalStorageClassName
}

// SetEKSCMEKAnnotations sets the CMEK annotations for objects for eks provider.
func SetEKSCMEKAnnotations(obj *metav1.ObjectMeta, kmsARN string) {
	if obj.Annotations == nil {
		obj.Annotations = make(map[string]string)
	}
	obj.Annotations[AWSARNAnnotationKey] = kmsARN
	obj.Annotations[CMEKProvider] = ProviderTypeEKS
}

// RetrieveMapper returns a CMEKStorageClassNameMapper based on the annotations in the object.
func RetrieveMapper(c *Cluster) (CMEKStorageClassMapperV2, error) {
	if !c.FeatureEnabled(ClusterFeatureUseCMEK) {
		return NewNopMapper(), nil
	}

	provider := c.GetProviderType()
	switch provider {
	case ProviderTypeEKS:
		// TODO(ideascf): directly read this from cluster spec
		return NewAWSStorageClassMapper(c.Annotations[AWSARNAnnotationKey])
	case ProviderTypeGKE:
		return nil, fmt.Errorf("GKE does not support CMEKStorageClassMapper yet")
	case ProviderTypeAKS:
		return NewAzureStorageClassMapper(c)
	}

	return nil, fmt.Errorf("unknown provider type %s for CMEKStorageClassMapper", provider)
}

func ParseAzureKeyvaultKeyURI(keyURI string) (vaultName, keyName, keyVersion string, err error) {
	// The naming rule: https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#object-identifiers
	parsedURL, err := url.Parse(keyURI)
	if err != nil {
		return "", "", "", fmt.Errorf("parse KeyURI: %w", err)
	}
	if !strings.HasSuffix(parsedURL.Host, azureKeyVaultURIDomainSuffix) {
		return "", "", "", fmt.Errorf("invalid KeyURI, mismatch on domain suffix. got: %s", keyURI)
	}
	if len(parsedURL.Query()) != 0 {
		return "", "", "", fmt.Errorf("invalid KeyURI, query parameters are not allowed. got: %s", keyURI)
	}

	// path example: /keys/{key-name}/{key-version}
	// paths examples: "", "keys", "{key-name}", "{key-version}"
	paths := strings.Split(parsedURL.Path, "/")
	if len(paths) < 3 || paths[2] == "" {
		return "", "", "", fmt.Errorf("invalid KeyURI, mismatch on path. got: %s", keyURI)
	}
	if len(paths) > 4 {
		return "", "", "", fmt.Errorf("invalid KeyURI, too many path segments. got: %s", keyURI)
	}

	vaultName = parsedURL.Host[:len(parsedURL.Host)-len(azureKeyVaultURIDomainSuffix)]
	keyName = paths[2]
	if len(paths) == 4 {
		keyVersion = paths[3]
	}
	return vaultName, keyName, keyVersion, nil
}
