package gardener

import (
	"context"
	"time"

	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	s3v1beta1 "github.com/crossplane/provider-aws/apis/s3/v1beta1"
	storagev1alpha3 "github.com/crossplane/provider-gcp/apis/storage/v1alpha3"
	"github.com/pingcap/errors"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/kubernetes/scheme"
	"sigs.k8s.io/controller-runtime/pkg/client"

	utilruntime "k8s.io/apimachinery/pkg/util/runtime"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	v1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/controllers/common"
	"github.com/tidbcloud/infra-api/pkg/consts"
	"github.com/tidbcloud/infra-api/pkg/k8s"
)

func init() {
	utilruntime.Must(s3v1beta1.SchemeBuilder.AddToScheme(scheme.Scheme))
	utilruntime.Must(storagev1alpha3.SchemeBuilder.AddToScheme(scheme.Scheme))
}

// MarkBucketReady update the TypeReady condition as true.
// only used for test.
func MarkBucketReady(ctx context.Context, cli client.Client, providerType string, ns string, name string) error {
	switch providerType {
	case consts.ProviderTypeAWS:
		b := new(s3v1beta1.Bucket)
		err := cli.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, b)
		if err != nil {
			return errors.AddStack(err)
		}

		b.SetConditions(xpv1.Condition{
			Type:               xpv1.TypeReady,
			Status:             corev1.ConditionTrue,
			Reason:             xpv1.ReasonAvailable,
			LastTransitionTime: metav1.NewTime(time.Now()),
		})
		err = cli.Status().Update(ctx, b)
		if err != nil {
			return errors.AddStack(err)
		}

		return nil
	case consts.ProviderTypeGCP:
		b := new(storagev1alpha3.Bucket)
		err := cli.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, b)
		if err != nil {
			return errors.AddStack(err)
		}

		b.SetConditions(xpv1.Condition{
			Type:               xpv1.TypeReady,
			Status:             corev1.ConditionTrue,
			Reason:             xpv1.ReasonAvailable,
			LastTransitionTime: metav1.NewTime(time.Now()),
		})
		err = cli.Status().Update(ctx, b)
		if err != nil {
			return errors.AddStack(err)
		}

		return nil
	default:
		return errors.Errorf("unknown provider type %q", providerType)
	}

}

// GetBucketStatus get the bucket status.
// only export for test
func GetBucketStatus(ctx context.Context, cli client.Client, providerType string, ns string, name string) (status *xpv1.ResourceStatus, err error) {
	switch providerType {
	case consts.ProviderTypeAWS:
		return getAWSBucketStatus(ctx, cli, ns, name)
	case consts.ProviderTypeGCP:
		return getGCPBucketStatus(ctx, cli, ns, name)
	default:
		return nil, errors.Errorf("unknown provider type %q", providerType)
	}
}

func cleanupBucket(
	ctx context.Context,
	cli client.Client,
	schema *runtime.Scheme,
	providerType string,
	name string,
) error {
	switch providerType {
	case consts.ProviderTypeAWS:
		b := &s3v1beta1.Bucket{
			ObjectMeta: metav1.ObjectMeta{
				Name: name,
			},
		}
		err := cli.Delete(ctx, b)
		if client.IgnoreNotFound(err) != nil {
			return err
		}

		return nil
	case consts.ProviderTypeGCP:
		b := &storagev1alpha3.Bucket{
			ObjectMeta: metav1.ObjectMeta{
				Name: name,
			},
		}
		err := cli.Delete(ctx, b)
		if client.IgnoreNotFound(err) != nil {
			return err
		}

		return nil
	default:
		return errors.Errorf("unknown provider type %q", providerType)
	}
}

func ensureBucket(
	ctx context.Context,
	cli client.Client,
	schema *runtime.Scheme,
	providerType string,
	gp *cloudv1.GardenerProviderSpec,
	network *cloudv1.Network,
	ns string,
	name string,
	region string,
	labels map[string]string,
) (controllerutil.OperationResult, error) {
	var bucket client.Object
	// concatenate basic resource tags
	finalLabels := common.ConcatenateTags(
		labels,
		map[string]string{
			common.Component:   string(v1.ComponentBackup),
			common.ServiceType: common.Dedicated,
			common.UsedBy:      common.CustomerTiDB,
		})
	switch providerType {
	case consts.ProviderTypeAWS:
		bucket = buildAWSBucket(ns, name, region, finalLabels, gp.AWS.CrossplaneProvider)
	case consts.ProviderTypeGCP:
		bucket = buildGCPBucket(ns, name, region, finalLabels, gp.GCP.CrossplaneProvider)
	default:
		return controllerutil.OperationResultNone, errors.Errorf("unknown provider type %q", providerType)
	}

	return k8s.CreateIgnoreExists(ctx, cli, bucket)
}

func getGCPBucketStatus(ctx context.Context, cli client.Client, ns string, name string) (status *xpv1.ResourceStatus, err error) {
	b := new(storagev1alpha3.Bucket)
	err = cli.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, b)
	if err != nil {
		return nil, errors.AddStack(err)
	}

	return &b.Status.ResourceStatus, nil
}

func buildGCPBucket(
	ns string,
	name string,
	region string,
	labels map[string]string,
	providerConfigRef string,
) (b *storagev1alpha3.Bucket) {
	b = &storagev1alpha3.Bucket{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: ns,
			Name:      name,
		},
		Spec: storagev1alpha3.BucketSpec{
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: providerConfigRef},
				WriteConnectionSecretToReference: &xpv1.SecretReference{
					Namespace: ns,
					Name:      name,
				},
			},
			BucketParameters: storagev1alpha3.BucketParameters{
				BucketSpecAttrs: storagev1alpha3.BucketSpecAttrs{
					Location: region,
					BucketUpdatableAttrs: storagev1alpha3.BucketUpdatableAttrs{
						Labels: labels,
					},
				},
			},
		},
	}

	return b
}

func getAWSBucketStatus(ctx context.Context, cli client.Client, ns string, name string) (status *xpv1.ResourceStatus, err error) {
	b := new(s3v1beta1.Bucket)
	err = cli.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, b)
	if err != nil {
		return nil, errors.AddStack(err)
	}

	return &b.Status.ResourceStatus, 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,
	providerConfigRef 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: providerConfigRef},
				WriteConnectionSecretToReference: &xpv1.SecretReference{
					Namespace: ns,
					Name:      name,
				},
			},
			ForProvider: s3v1beta1.BucketParameters{
				LocationConstraint: region,
				BucketTagging:      tagging,
				ServerSideEncryptionConfiguration: &s3v1beta1.ServerSideEncryptionConfiguration{
					Rules: []s3v1beta1.ServerSideEncryptionRule{
						{
							ApplyServerSideEncryptionByDefault: s3v1beta1.ServerSideEncryptionByDefault{
								SSEAlgorithm: "AES256",
							},
						},
					},
				},
			},
		},
	}

	return b
}
