package eks

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/bucket/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/bucket/task/common"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
	s3v1beta1 "github.com/upbound/provider-aws/apis/s3/v1beta1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type BucketPolicy struct {
	*task.BaseTask
	policyDeleted bool
}

var errGrantedDataplaneNotFound = errors.New("granted dataplane not found")

func NewBucketPolicy(ctx *task.Context) *BucketPolicy {
	return &BucketPolicy{BaseTask: &task.BaseTask{Context: ctx}}
}

func (t *BucketPolicy) Name() string {
	return "BucketPolicy"
}

func (t *BucketPolicy) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	providerConfig := t.Bucket.Spec.AccountRef.ProviderConfigName
	bucketName := t.Bucket.Status.Bucket.Name

	dataPlaneCR := &v1alpha1.DataPlane{}
	if err := t.DataPlaneR.Get(ctx, client.ObjectKey{
		Namespace: t.Config.Namespace,
		Name:      t.Bucket.Spec.DataplaneId,
	}, dataPlaneCR); err != nil {
		if apierrors.IsNotFound(err) {
			dataPlaneCR = nil
		} else {
			return deploy.Error(err)
		}
	}

	if !common.IsBucketPolicyNeeded(dataPlaneCR, t.Bucket) {
		return t.Delete(ctx)
	}

	policyStringPtr, err := t.makeS3BucketPolicy(ctx, dataPlaneCR)
	if err != nil {
		return deploy.Error(err)
	}
	if policyStringPtr == nil {
		t.policyDeleted = true
		return t.Delete(ctx)
	}

	labels := map[string]string{
		consts.DataplaneLabelKey:       t.Bucket.Spec.DataplaneId,
		"tags.tidbcloud.com/bucket-id": t.Bucket.Spec.Id,
	}
	labels = util.MergeMaps(labels, t.Bucket.Labels)
	bucketPolicy := &s3v1beta1.BucketPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:   bucketName,
			Labels: labels,
		},
		Spec: s3v1beta1.BucketPolicySpec{
			ForProvider: s3v1beta1.BucketPolicyParameters{
				Region: &t.Bucket.Spec.Region,
				Bucket: &bucketName,
				Policy: policyStringPtr,
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: providerConfig,
				},
				ManagementPolicies: controllers.GetResourceManagementPolicies(),
			},
		},
	}

	if err := k8s.Apply(ctx, t.Local, bucketPolicy); err != nil {
		return deploy.Error(fmt.Errorf("failed to apply BucketPolicy resource: %w", err))
	}

	return deploy.Done()
}

func (t *BucketPolicy) PostCreate(ctx context.Context) deploy.Result {
	if t.policyDeleted {
		return deploy.Done()
	}

	dataPlaneCR := &v1alpha1.DataPlane{}
	if err := t.DataPlaneR.Get(ctx, client.ObjectKey{
		Namespace: t.Config.Namespace,
		Name:      t.Bucket.Spec.DataplaneId,
	}, dataPlaneCR); err != nil {
		if apierrors.IsNotFound(err) {
			dataPlaneCR = nil
		} else {
			return deploy.Error(err)
		}
	}

	if !common.IsBucketPolicyNeeded(dataPlaneCR, t.Bucket) {
		return deploy.Done()
	}

	var policy s3v1beta1.BucketPolicy
	if err := t.Local.Get(ctx, client.ObjectKey{Name: t.Bucket.Status.Bucket.Name}, &policy); err != nil {
		return deploy.Error(err)
	}
	if controllers.IsResourceReady(policy.Status.ResourceStatus) {
		t.Bucket.Status.Phase = v1alpha1.BucketPhaseReady
		return deploy.Done()
	}
	return deploy.Error(fmt.Errorf("bucket policy not ready"))
}

func (t *BucketPolicy) Delete(ctx context.Context) deploy.Result {
	bucketName := t.Bucket.Status.Bucket.Name
	bucketPolicy := &s3v1beta1.BucketPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name: bucketName,
		},
	}

	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(bucketPolicy), bucketPolicy); err != nil {
		if apierrors.IsNotFound(err) {
			return deploy.Done() // BucketPolicy already deleted, nothing to do
		}
		return deploy.Error(fmt.Errorf("failed to get BucketPolicy resource: %w", err))
	}
	bucketPolicy.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}
	if err := k8s.PatchObject(ctx, t.Local, bucketPolicy, controllers.ManagementPoliciesPatch); client.IgnoreNotFound(err) != nil {
		return deploy.Error(err)
	}

	if err := t.Local.Delete(ctx, bucketPolicy); err != nil {
		return deploy.Error(fmt.Errorf("failed to delete BucketPolicy resource: %w", err))
	}

	return deploy.Done()
}

func (t *BucketPolicy) makeS3BucketPolicy(ctx context.Context, currentDataPlaneCR *v1alpha1.DataPlane) (*string, error) {
	defaultNodeRole := t.Bucket.Spec.NodeRoleARN
	bucketName := t.Bucket.Status.Bucket.Name

	statements := []map[string]interface{}{}

	if common.IsDefaultStatementInBucketPolicyNeeded(currentDataPlaneCR) {
		statements = append(statements, genPolicyStatement("DefaultAuth", defaultNodeRole, bucketName))
	}

	for _, grantedDataplaneId := range t.Bucket.Spec.GrantedDataplaneIds {
		statement, err := makeBucketPolicyStatement(ctx, t.Config.Namespace, grantedDataplaneId, bucketName, t.Local)
		if err != nil {
			if errors.Is(err, errGrantedDataplaneNotFound) {
				t.Logger.Info("skip missing granted dataplane when building bucket policy", "dataplane", grantedDataplaneId)
				continue
			}
			return nil, fmt.Errorf("make bucket policy statement for granted dataplane '%s' error: %w", grantedDataplaneId, err)
		}
		statements = append(statements, statement)
	}
	if len(statements) == 0 {
		return nil, nil
	}

	policy := map[string]interface{}{
		"Version":   "2012-10-17",
		"Statement": statements,
	}
	policyJSON, err := json.Marshal(policy)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal policy, error: %w, policy: %+v", err, policy)
	}
	policyString := string(policyJSON)
	return &policyString, nil
}

func makeBucketPolicyStatement(
	ctx context.Context,
	namespace, permittedDpid, bucketName string,
	cli client.Client,
) (map[string]interface{}, error) {
	dataPlaneCR := &v1alpha1.DataPlane{}
	if err := cli.Get(ctx, client.ObjectKey{
		Namespace: namespace,
		Name:      permittedDpid,
	}, dataPlaneCR); err != nil {
		if apierrors.IsNotFound(err) {
			return nil, fmt.Errorf("%w: get dataplane '%s' error: %w", errGrantedDataplaneNotFound, permittedDpid, err)
		}
		return nil, fmt.Errorf("get dataplane '%s' error: %w", permittedDpid, err)
	}
	appBoxCR := &v1alpha1.APPBox{}
	if err := cli.Get(ctx, client.ObjectKey{
		Namespace: namespace,
		Name:      dataPlaneCR.Status.AppBox.Name,
	}, appBoxCR); err != nil {
		return nil, fmt.Errorf("get appbox '%s' for dataplane '%s' error: %w",
			dataPlaneCR.Status.AppBox.Name, permittedDpid, err)
	}

	return genPolicyStatement(permittedDpid, appBoxCR.Status.IAM.NodeRoleARN, bucketName), nil
}

func genPolicyStatement(sid, nodeRoleARN, bucketName string) map[string]interface{} {
	return map[string]interface{}{
		"Sid":    sid,
		"Effect": "Allow",
		"Principal": map[string]interface{}{
			"AWS": nodeRoleARN,
		},
		"Action": "s3:*",
		"Resource": []string{
			fmt.Sprintf("arn:aws:s3:::%s", bucketName),
			fmt.Sprintf("arn:aws:s3:::%s/*", bucketName),
		},
	}
}
