package action

import (
	"context"
	"errors"
	"fmt"
	"strconv"

	"github.com/tidbcloud/fastflow/pkg/entity/run"
	regionaldedicatedv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"github.com/tidbcloud/pkgv2/db"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/regionserver"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	cidrrepo "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/cidr"
	clusterrepo "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/cluster"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/lock"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/region"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/service/workflow/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/workflow"
)

const (
	EnsureVPC = "ensure_vpc"
)

type EnsureVPCAction struct {
	clusterRepo clusterrepo.Repo
	regionRepo  region.Repo
	cidrRepo    cidrrepo.Repo
	lockRepo    lock.Repo
	// transaction is required by lock repo operation.
	transaction db.Transactioner

	regionalCli regionserver.Client
}

func NewEnsureVPCAction(
	clusterRepo clusterrepo.Repo,
	regionRepo region.Repo,
	cidrRepo cidrrepo.Repo,
	lockRepo lock.Repo,
	regionalCli regionserver.Client,
	transaction db.Transactioner,
) *EnsureVPCAction {
	return &EnsureVPCAction{
		clusterRepo: clusterRepo,
		regionRepo:  regionRepo,
		cidrRepo:    cidrRepo,
		lockRepo:    lockRepo,
		regionalCli: regionalCli,
		transaction: transaction,
	}
}

func (e *EnsureVPCAction) Name() string {
	return EnsureVPC
}

func (e *EnsureVPCAction) ParameterNew() any {
	return &common.ClusterIdParameter{}
}

func (e *EnsureVPCAction) Run(ctx run.ExecuteContext, params any) error {
	cctx, span := workflow.GenerateContext(ctx)

	clusterId, err := common.GetClusterId(params)
	if err != nil {
		tilog.Error(cctx, "failed to get cluster id", zap.Error(err))
		return err
	}

	tilog.Info(cctx, "start to ensure vpc", zap.Uint64("cluster_id", clusterId))
	defer func() {
		tilog.Info(cctx, "end to ensure vpc", zap.Uint64("cluster_id", clusterId))
		span.End()
	}()

	cluster, berr := e.clusterRepo.Get(cctx, clusterId, false)
	if berr != nil {
		tilog.Error(cctx, "failed to get cluster", zap.Uint64("cluster_id", clusterId), zap.Error(berr))
		return err
	}

	cidrEntity, berr := e.cidrRepo.LoadByCloudProviderAndRegion(
		cctx,
		cluster.ProjectId,
		cluster.OrganizationId,
		cluster.CloudProviderId,
		cluster.RegionId,
	)
	if berr != nil {
		if berr.RawErrCode() == bizerr.ErrCodeRecordNotFound {
			tilog.Error(
				cctx,
				"the CIDR is not set for the region and the cluster cannot be created",
				zap.Uint64("cluster_id", clusterId),
			)
			return errors.New("the CIDR is not set for the region and the cluster cannot be created")
		}
		tilog.Error(cctx, "failed to load cidr", zap.Uint64("cluster_id", clusterId), zap.Error(berr))
		return err
	}

	regionEntity, err := e.regionRepo.SelectRegionById(cctx, cluster.RegionId, cluster.OrganizationId)
	if err != nil {
		tilog.Error(cctx, "failed to find region", zap.String("region_id", cluster.RegionId.String()), zap.Error(err))
		return err
	}

	// access iam principal
	if err := e.checkAccount(cctx, cluster, regionEntity); err != nil {
		tilog.Error(cctx, "failed to check account", zap.Uint64("cluster_id", clusterId), zap.Error(err))
		return err
	}

	// check vpc exists
	isExist, vpc, err := e.ensureVpc(cctx, cluster, regionEntity, cidrEntity)
	if err != nil {
		tilog.Error(cctx, "failed to ensure vpc", zap.Uint64("cluster_id", clusterId), zap.Error(err))
		return err
	}

	// create new vpc
	if !isExist {
		vpc, err = e.createVpc(cctx, cluster, regionEntity, cidrEntity)
		if err != nil {
			tilog.Error(cctx, "failed to create vpc", zap.Uint64("cluster_id", clusterId), zap.Error(err))
			return err
		}
	}

	if vpc.VpcId == "" {
		tilog.Warn(cctx, "vpc is not ready", zap.Uint64("cluster_id", clusterId))
		return errors.New("vpc is not ready")
	}

	ctx.ShareData().Set(common.KeyVpcId, vpc.VpcId)

	return nil
}

func (e *EnsureVPCAction) ensureVpc(
	ctx context.Context,
	cluster *entity.Cluster,
	regionEntity *entity.Region,
	cidrEntity *entity.CIDR,
) (bool, *regionaldedicatedv1.Vpc, error) {
	// get vpc from regional service
	resp, err := e.regionalCli.ListVpcs(ctx, regionEntity, &regionaldedicatedv1.ListVpcsRequest{
		TenantId:  strconv.FormatUint(cluster.OrganizationId, 10),
		ProjectId: strconv.FormatUint(cluster.ProjectId, 10),
		ClusterId: "", // leave it empty
		Statuses: []regionaldedicatedv1.Vpc_Status{
			regionaldedicatedv1.Vpc_ACTIVE,
			regionaldedicatedv1.Vpc_CREATING,
			regionaldedicatedv1.Vpc_FAILED,
		},
	})
	if err != nil {
		tilog.Error(ctx, "failed to list vpc", zap.Error(err))
		return false, nil, err
	}

	// not found vpc
	if len(resp.Vpcs) == 0 {
		tilog.Warn(ctx, "not found vpc", zap.Any("vpc list", resp.GetVpcs()))
		return false, nil, nil
	}

	// check vpc cidr
	for _, vpc := range resp.GetVpcs() {
		if vpc.CidrBlock == cidrEntity.CIDR {
			tilog.Info(ctx, "found vpc with the same cidr", zap.String("cidr", cidrEntity.CIDR), zap.Any("vpc", vpc))
			return true, vpc, nil
		}
	}

	tilog.Warn(
		ctx,
		"failed to find vpc with the same cidr",
		zap.String("cidr", cidrEntity.CIDR),
		zap.Any("vpc list", resp.GetVpcs()),
	)
	return false, nil, errors.New("failed to find vpc with the same cidr")
}

func (e *EnsureVPCAction) createVpc(
	ctx context.Context,
	cluster *entity.Cluster,
	regionEntity *entity.Region,
	cidrEntity *entity.CIDR,
) (*regionaldedicatedv1.Vpc, error) {
	var vpc *regionaldedicatedv1.Vpc
	if err := e.transaction.Transaction(ctx, func(ctx context.Context) error {
		// check create vpc lock
		createVpcLock := &entity.Lock{
			Key:         fmt.Sprintf("create_vpc_%d", cluster.ProjectId),
			TimeoutSecs: 9, // default request timeout is 10s
		}

		if err := e.lockRepo.Lock(ctx, createVpcLock); err != nil {
			tilog.Error(ctx, "create vpc lock failed", zap.Error(err))
			return errors.New("create vpc lock failed, please try again")
		}
		tilog.Info(
			ctx,
			"create vpc lock success",
			zap.Uint64("cluster id", cluster.GetClusterId()),
			zap.Uint64("project id", cluster.GetProjectId()),
			zap.Any("lock", createVpcLock),
		)
		defer func() {
			// use new ctx to prevent user ctx timeout
			rctx := context.WithoutCancel(ctx)
			if err := e.lockRepo.Release(rctx, createVpcLock); err != nil {
				tilog.Error(ctx, "release create vpc lock failed", zap.Any("lock", createVpcLock), zap.Error(err))
			} else {
				tilog.Info(
					ctx,
					"release vpc lock success",
					zap.Uint64("cluster id", cluster.GetClusterId()),
					zap.Uint64("project id", cluster.GetProjectId()),
					zap.Any("lock", createVpcLock),
				)
			}
		}()

		var err error
		// check vpc exists again
		_, vpc, err = e.ensureVpc(ctx, cluster, regionEntity, cidrEntity)
		if err != nil {
			tilog.Error(ctx, "failed to ensure vpc", zap.Uint64("cluster_id", cluster.GetClusterId()), zap.Error(err))
			return err
		}
		if vpc != nil {
			tilog.Warn(ctx, "vpc is already created", zap.Uint64("cluster_id", cluster.GetClusterId()), zap.Any("vpc", vpc))
			return nil
		}

		createVpcRequest := &regionaldedicatedv1.CreateVpcRequest{
			RequestId: strconv.FormatUint(cluster.GetProjectId(), 10), // use projectID as requestID to ensure idempotence
			Vpc: &regionaldedicatedv1.Vpc{
				TenantIds:  []string{strconv.FormatUint(cluster.GetOrganizationId(), 10)},
				ProjectIds: []string{strconv.FormatUint(cluster.GetProjectId(), 10)},
				CidrBlock:  cidrEntity.CIDR,
				Info:       nil, // leave it empty
			},
		}

		vpc, err = e.regionalCli.CreateVpc(ctx, regionEntity, createVpcRequest)
		if err != nil {
			tilog.Error(ctx, "failed to create vpc", zap.Uint64("cluster_id", cluster.GetClusterId()), zap.Error(err))
			return err
		}

		return nil
	}); err != nil {
		tilog.Error(ctx, "failed to create vpc", zap.Uint64("cluster_id", cluster.GetClusterId()), zap.Error(err))
		return vpc, err
	}

	return vpc, nil
}

func (e *EnsureVPCAction) checkAccount(
	ctx context.Context,
	cluster *entity.Cluster,
	regionEntity *entity.Region,
) error {
	if cluster.CustomerManagedEncryptionKeyId != "" {
		// get access iam principal
		_, err := e.regionalCli.GetAccount(
			ctx,
			regionEntity,
			&regionaldedicatedv1.GetAccountRequest{ProjectId: strconv.FormatUint(cluster.ProjectId, 10)},
		)
		if err != nil {
			if err.ErrCode() != bizerr.ErrCodeRecordNotFound {
				tilog.Error(ctx, "failed to get account", zap.Uint64("cluster_id", cluster.ClusterId), zap.Error(err))
				return err
			}

			// if account not assigned, assign account
			_, err = e.regionalCli.AssignAccount(ctx, regionEntity, &regionaldedicatedv1.AssignAccountRequest{
				ProjectId:   strconv.FormatUint(cluster.ProjectId, 10),
				CmekEnabled: true,
			})

			if err != nil {
				tilog.Error(
					ctx,
					"failed to assign account",
					zap.Uint64("cluster_id", cluster.ClusterId),
					zap.Error(err),
				)
				return err
			}
			tilog.Info(ctx, "assign account success", zap.Uint64("cluster_id", cluster.ClusterId))
		}
	}

	return nil
}
