package action

import (
	"fmt"

	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	schemav1 "github.com/tidbcloud/cluster-service-ng/pkg/storage/etcd/schema/v1"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
)

const CreateBucketActionName = "create-bucket"

// Bucket status constants
const (
	BucketStatusCreating = "Creating"
	BucketStatusReady    = "Ready"
	BucketStatusDeleting = "Deleting"
	BucketStatusFailed   = "Failed"
)

type CreateBucketAction struct {
	client dplanemgrv1.BucketServiceClient
}

func NewCreateBucketAction(client dplanemgrv1.BucketServiceClient) actions.AsyncAction {
	return &CreateBucketAction{
		client: client,
	}
}

func (a *CreateBucketAction) Name() string {
	return CreateBucketActionName
}

func (a *CreateBucketAction) Start(args actions.ActionArgs) error {
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "create bucket action invoked")
	dataplaneId, err := args.GetString(workflow.DataplaneIdKey)
	if err != nil {
		return fmt.Errorf("failed to get dataplane id: %w", err)
	}
	sharedPoolStr, err := args.GetString(workflow.SharedPoolKey)
	if err != nil {
		return fmt.Errorf("failed to get sharedPool: %w", err)
	}
	var sharedPool entity.SharedPool
	err = protojson.Unmarshal([]byte(sharedPoolStr), &sharedPool)
	if err != nil {
		tilog.Error(ctx, "failed to unmarshal sharedpool", zap.Error(err))
		return fmt.Errorf("failed to unmarshal sharedpool: %w", err)
	}
	if spec := sharedPool.GetInternalStorage(); spec != nil {
		// create internal bucket
		// skip if already exists
		exists := false
		bucketId := entity.GetInternalBucketId(sharedPool.GetId(), spec.GetRegion())
		if _, err := a.client.GetBucket(ctx, &dplanemgrv1.GetBucketRequest{
			DataplaneId: dataplaneId,
			BucketId:    bucketId,
		}); err == nil {
			tilog.Info(ctx, "bucket already exists", zap.String("bucketId", bucketId))
			exists = true
		}
		if !exists {
			in := new(dplanemgrv1.CreateBucketRequest)
			in.DataplaneId = dataplaneId
			in.Bucket = new(dplanemgrv1.Bucket)
			in.Bucket.Id = bucketId
			in.Bucket.Tags = map[string]string{}
			// set the tags, try our best to get tenantId and sharedPoolId
			if tenantId, _ := args.GetString(workflow.TenantIdKey); tenantId != "" {
				in.Bucket.Tags[schemav1.LabelUserTenantKey] = tenantId
			} else if tenantId := sharedPool.TenantId; tenantId != "" {
				in.Bucket.Tags[schemav1.LabelUserTenantKey] = tenantId
			} else {
				tilog.Warn(ctx, "not get tenant id from args or shared pool")
				in.Bucket.Tags[schemav1.LabelUserTenantKey] = schemav1.MultiTenantTag
			}
			in.Bucket.Tags[schemav1.LabelSharedPoolKey] = sharedPool.GetId()
			if _, err := a.client.CreateBucket(ctx, in); err != nil {
				if status.Code(err) == codes.AlreadyExists {
					return nil
				}
				tilog.Error(ctx, "failed to create bucket", zap.String("in", in.String()), zap.Error(err))
				return err
			}
			tilog.Info(ctx, "create internal bucket successfully", zap.String("bucketId", bucketId))
		}
	}
	if spec := sharedPool.GetObjectStorage(); spec != nil {
		// create data bucket
		// skip if already exists
		exists := false
		bucketId := entity.GetDataBucketId(sharedPool.GetId(), spec.GetRegion())
		if _, err := a.client.GetBucket(ctx, &dplanemgrv1.GetBucketRequest{
			DataplaneId: dataplaneId,
			BucketId:    bucketId,
		}); err == nil {
			tilog.Info(ctx, "bucket already exists", zap.String("bucketId", bucketId))
			return nil
		}

		if !exists {
			in := new(dplanemgrv1.CreateBucketRequest)
			in.DataplaneId = dataplaneId
			in.Bucket = new(dplanemgrv1.Bucket)
			in.Bucket.Id = bucketId
			in.Bucket.Tags = map[string]string{}
			// set the tags, try our best to get tenantId and sharedPoolId
			if tenantId, _ := args.GetString(workflow.TenantIdKey); tenantId != "" {
				in.Bucket.Tags[schemav1.LabelUserTenantKey] = tenantId
			} else if tenantId := sharedPool.TenantId; tenantId != "" {
				in.Bucket.Tags[schemav1.LabelUserTenantKey] = tenantId
			} else {
				tilog.Warn(ctx, "not get tenant id from args or shared pool")
				in.Bucket.Tags[schemav1.LabelUserTenantKey] = schemav1.MultiTenantTag
			}
			in.Bucket.Tags[schemav1.LabelSharedPoolKey] = sharedPool.GetId()
			if _, err := a.client.CreateBucket(ctx, in); err != nil {
				if status.Code(err) == codes.AlreadyExists {
					return nil
				}
				tilog.Error(ctx, "failed to create bucket", zap.String("in", in.String()), zap.Error(err))
				return err
			}
			tilog.Info(ctx, "create data bucket successfully", zap.String("bucketId", bucketId))
		}
	}
	return nil
}

func (a *CreateBucketAction) CheckFinish(args actions.ActionArgs) (bool, error) {
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "create bucket check finish, wait bucket to be ready")
	dataplaneId, err := args.GetString(workflow.DataplaneIdKey)
	if err != nil {
		return false, fmt.Errorf("failed to get dataplane id: %w", err)
	}
	sharedPoolStr, err := args.GetString(workflow.SharedPoolKey)
	if err != nil {
		return false, fmt.Errorf("failed to get sharedPool: %w", err)
	}
	var sharedPool entity.SharedPool
	err = protojson.Unmarshal([]byte(sharedPoolStr), &sharedPool)
	if err != nil {
		tilog.Error(ctx, "failed to unmarshal sharedpool", zap.Error(err))
		return false, fmt.Errorf("failed to unmarshal sharedpool: %w", err)
	}
	if spec := sharedPool.GetInternalStorage(); spec != nil {
		in := new(dplanemgrv1.GetBucketRequest)
		in.DataplaneId = dataplaneId
		in.BucketId = entity.GetInternalBucketId(sharedPool.GetId(), spec.GetRegion())
		out, err := a.client.GetBucket(ctx, in)
		if err != nil {
			tilog.Error(ctx, "failed to get bucket", zap.String("in", in.String()), zap.Error(err))
			return false, err
		}
		if out.GetName() == "" || out.GetStatus() != BucketStatusReady {
			tilog.Info(
				ctx,
				"bucket is not ready",
				zap.String("id", out.GetId()),
				zap.String("name", out.GetName()),
				zap.String("status", out.GetStatus()),
			)
			return false, nil
			// return false, fmt.Errorf("bucket %s is not ready: name=%s, status=%s", out.GetId(), out.GetName(), out.GetStatus())
		}
	}

	if spec := sharedPool.GetObjectStorage(); spec != nil {
		in := new(dplanemgrv1.GetBucketRequest)
		in.DataplaneId = dataplaneId
		in.BucketId = entity.GetDataBucketId(sharedPool.GetId(), spec.GetRegion())
		out, err := a.client.GetBucket(ctx, in)
		if err != nil {
			tilog.Error(ctx, "failed to get bucket", zap.String("in", in.String()), zap.Error(err))
			return false, err
		}
		if out.GetName() == "" || out.GetStatus() != BucketStatusReady {
			tilog.Info(
				ctx,
				"bucket is not ready",
				zap.String("id", out.GetId()),
				zap.String("name", out.GetName()),
				zap.String("status", out.GetStatus()),
			)
			return false, nil
			// return false, fmt.Errorf("bucket %s is not ready: name=%s, status=%s", out.GetId(), out.GetName(), out.GetStatus())
		}
	}
	return true, nil
}
