package ack

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

	ossv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/oss/v1alpha1"
	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"
	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
}

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.makeOssBucketPolicy(ctx, dataPlaneCR)
	if err != nil {
		return deploy.Error(err)
	}

	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 := &ossv1alpha1.BucketPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name:   bucketName,
			Labels: labels,
		},
		Spec: ossv1alpha1.BucketPolicySpec{
			ForProvider: ossv1alpha1.BucketPolicyParameters{
				Region: &t.Bucket.Spec.Region,
				BucketRef: &xpv1.Reference{
					Name: 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 Aliyun BucketPolicy: %w", err))
	}

	return deploy.Done()
}

func (t *BucketPolicy) PostCreate(ctx context.Context) deploy.Result {
	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 ossv1alpha1.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 := &ossv1alpha1.BucketPolicy{
		ObjectMeta: metav1.ObjectMeta{
			Name: bucketName,
		},
	}
	if err := controllers.DeleteUpboundResource(ctx, t.Local, bucketPolicy); err != nil {
		return deploy.Error(fmt.Errorf("failed to delete bucket: %w", err))
	}
	return deploy.Done()
}

func (t *BucketPolicy) makeOssBucketPolicy(ctx context.Context, currentDataPlaneCR *v1alpha1.DataPlane) (*string, error) {
	bucketName := t.Bucket.Status.Bucket.Name
	statements := []map[string]interface{}{}

	if common.IsDefaultStatementInBucketPolicyNeeded(currentDataPlaneCR) {
		statement, err := makeBucketPolicyStatement(ctx, t.Config.Namespace, currentDataPlaneCR.Name, bucketName, t.Local)
		if err != nil {
			return nil, fmt.Errorf("make bucket policy statement for self dataplane '%s' error: %w", currentDataPlaneCR.Name, err)
		}
		statements = append(statements, statement)
	}

	for _, grantedDataplaneId := range t.Bucket.Spec.GrantedDataplaneIds {
		statement, err := makeBucketPolicyStatement(ctx, t.Config.Namespace, grantedDataplaneId, bucketName, t.Local)
		if err != nil {
			return nil, fmt.Errorf("make bucket policy statement for granted dataplane '%s' error: %w", grantedDataplaneId, err)
		}
		statements = append(statements, statement)
	}

	policy := map[string]interface{}{
		"Version":   "1",
		"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 {
		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(dataPlaneCR.Spec.AccountRef.AccountID, appBoxCR.Status.IAM.NodeRoleName, bucketName), nil
}

func genPolicyStatement(accountID, nodeRoleARN, bucketName string) map[string]interface{} {
	return map[string]interface{}{
		"Effect":    "Allow",
		"Principal": []string{fmt.Sprintf("arn:sts::%s:assumed-role/%s/*", accountID, nodeRoleARN)},
		"Action":    "oss:*",
		"Resource": []string{
			fmt.Sprintf("acs:oss:*:*:%s", bucketName),
			fmt.Sprintf("acs:oss:*:*:%s/*", bucketName),
		},
	}
}
