package network

import (
	"context"
	"fmt"

	"github.com/aws/aws-sdk-go-v2/aws"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	s3v1beta1 "github.com/crossplane/provider-aws/apis/s3/v1beta1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	apitypes "k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/tidbcloud/shared-provider/pkg/types"
	"github.com/tidbcloud/shared-provider/pkg/utils/kubeapi"
	"github.com/tidbcloud/shared-provider/pkg/utils/labels"
)

// ensureBackupBucket ensures the backup bucket is ready for the network.
func (r *Reconciler) ensureBackupBucket(ctx context.Context, net *cloudv1.Network) error {
	region := net.Spec.Region
	bucketName := getBackupBucketName(r.Env, region, labels.GetProjectFromLabels(net.Labels))

	// set bucket name in the network status
	status, err := types.SharedNetworkStatusFromProvider(net)
	if err != nil {
		return err
	}
	if status.BackupBucket != bucketName {
		status.BackupBucket = bucketName
		if err = types.SetSharedNetworkStatus(net, status); err != nil {
			return err
		}
		if err = r.InfraClient.Status().Update(ctx, net); err != nil {
			return err
		}
	}

	tenantID := labels.GetTenantFromLabels(net.Labels)
	projectID := labels.GetProjectFromLabels(net.Labels)
	tags := buildTags(tenantID, projectID, r.Env)

	switch net.Spec.Provider {
	case types.ProviderTypeAWSSharedTier:
		bucket := buildAWSBucket(net.GetNamespace(), bucketName, region, tags)
		if err := kubeapi.CreateIgnoreExists(ctx, r.InfraClient, bucket); err != nil {
			return err
		}
	case types.ProviderTypeGCPSharedTier:
		// TODO: GCP support
	default:
		logger := log.FromContext(ctx)
		logger.Error(nil, "unknown provider type", "type", net.Spec.Provider)
		return nil
	}

	return r.observeBucketStatus(ctx, net, bucketName)
}

// deleteBackupBucket deletes the backup bucket if exists.
func (r *Reconciler) deleteBackupBucket(ctx context.Context, net *cloudv1.Network) error {
	region := net.Spec.Region
	bucketName := getBackupBucketName(r.Env, region, labels.GetProjectFromLabels(net.Labels))

	switch net.Spec.Provider {
	case types.ProviderTypeAWSSharedTier:
		bucket := &s3v1beta1.Bucket{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: net.GetNamespace(),
				Name:      bucketName,
			},
		}
		err := r.InfraClient.Delete(ctx, bucket)
		if client.IgnoreNotFound(err) != nil {
			return err
		}
		return nil
	case types.ProviderTypeGCPSharedTier:
		// TODO: GCP support
		return nil
	default:
		logger := log.FromContext(ctx)
		logger.Error(nil, "unknown provider type", "type", net.Spec.Provider)
		return nil
	}
}

func (r *Reconciler) observeBucketStatus(ctx context.Context, net *cloudv1.Network, bucketName string) error {
	var status *xpv1.ResourceStatus
	switch net.Spec.Provider {
	case types.ProviderTypeAWSSharedTier:
		bucket := new(s3v1beta1.Bucket)
		err := r.InfraClient.Get(ctx, apitypes.NamespacedName{
			Namespace: net.GetNamespace(), Name: bucketName}, bucket)
		if err != nil {
			return err
		}
		status = &bucket.Status.ResourceStatus

		if status.GetCondition(xpv1.TypeReady).Status != corev1.ConditionTrue {
			return fmt.Errorf("wait bucket %s ready", bucketName) // re-enqueue
		}

	case types.ProviderTypeGCPSharedTier:
		// TODO: GCP support
	}

	return nil
}

// example: https://github.com/crossplane/provider-aws/blob/master/examples/s3/bucket.yaml
func buildAWSBucket(
	ns string,
	name string,
	region string,
	tags map[string]string,
) (b *s3v1beta1.Bucket) {
	var tagging *s3v1beta1.Tagging
	if len(tags) > 0 {
		tagging = new(s3v1beta1.Tagging)
		for k, v := range tags {
			tagging.TagSet = append(tagging.TagSet, s3v1beta1.Tag{Key: k, Value: v})
		}
	}

	b = &s3v1beta1.Bucket{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: ns,
			Name:      name,
		},
		Spec: s3v1beta1.BucketSpec{
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: types.CrossplaneProviderConfigReferenceName},
				WriteConnectionSecretToReference: &xpv1.SecretReference{
					Namespace: ns,
					Name:      name,
				},
			},
			ForProvider: s3v1beta1.BucketParameters{
				LocationConstraint: region,
				ACL:                aws.String("private"),
				BucketTagging:      tagging,
				PublicAccessBlockConfiguration: &s3v1beta1.PublicAccessBlockConfiguration{
					BlockPublicAcls:       aws.Bool(true),
					BlockPublicPolicy:     aws.Bool(true),
					IgnorePublicAcls:      aws.Bool(true),
					RestrictPublicBuckets: aws.Bool(true),
				},
				ServerSideEncryptionConfiguration: &s3v1beta1.ServerSideEncryptionConfiguration{
					Rules: []s3v1beta1.ServerSideEncryptionRule{
						{
							ApplyServerSideEncryptionByDefault: s3v1beta1.ServerSideEncryptionByDefault{
								SSEAlgorithm: "AES256",
							},
						},
					},
				},
			},
		},
	}

	return b
}

// buildTags builds bucket and role tags for billing (no charge for role).
func buildTags(tenant, project, env string) map[string]string {
	return map[string]string{
		types.BillingTagKeyTenant:      tenant,
		types.BillingTagKeyProject:     project,
		types.BillingTagKeyEnvironment: env,
		types.BillingTagKeyUsedBy:      types.BillingTagValUsedByCustomerTiDB,
		types.BillingTagKeyComponent:   types.BillingTagValComponentBackup,
		types.BillingTagKeyServiceType: types.BillingTagValServiceTypeSharedTier,
	}
}

// get backup bucket name via environment, region and network projectID.
// a `-sharedtier` suffix is added to diff from dedicated buckets.
func getBackupBucketName(env, region, projectID string) string {
	return fmt.Sprintf("dbaas-%s-%s-%s-sharedtier", env, region, projectID)
}
