package action

import (
	"context"
	"fmt"
	"strconv"
	"strings"

	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/config"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	"github.com/tidbcloud/cluster-service-ng/pkg/clusterconfig"
	"github.com/tidbcloud/cluster-service-ng/pkg/dataplane"
	"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/storage/repo"
	templateversion "github.com/tidbcloud/cluster-service-ng/pkg/template/version"
	"github.com/tidbcloud/cluster-service-ng/pkg/tioperator"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"
	"gopkg.in/yaml.v2"
)

type ApplyTiDBGroupAction struct {
	dataplaneManager    dataplane.Manager
	sharedPoolRepo      repo.SharedPoolRepo
	tidbGroupRepo       repo.TiDBGroupRepo
	tidbClusterRepo     repo.TiDBClusterRepo
	lbClient            dplanemgrv1.LoadBalancerServiceClient
	templateVersionSvc  *templateversion.Service
	imageConfig         *config.ImageConfig
	clusterConfigClient clusterconfig.Client
	dataplaneClient     dplanemgrv1.DataPlaneServiceClient
	meteringConfig      *config.MeteringConfig
	bizConfig           *config.BizConfig
}

const ApplyTiDBGroupActionName = "apply-tidb-group"

func NewApplyTiDBGroupAction(
	sharedPoolRepo repo.SharedPoolRepo,
	tidbGroupRepo repo.TiDBGroupRepo,
	tidbClusterRepo repo.TiDBClusterRepo,
	dataplaneManager dataplane.Manager,
	clusterConfigClient clusterconfig.Client,
	lbClient dplanemgrv1.LoadBalancerServiceClient,
	dataplaneClient dplanemgrv1.DataPlaneServiceClient,
	templateVersionSvc *templateversion.Service,
	imageConfig *config.ImageConfig,
	meteringConfig *config.MeteringConfig,
	bizConfig *config.BizConfig,
) actions.Action {
	return &ApplyTiDBGroupAction{
		sharedPoolRepo:      sharedPoolRepo,
		tidbGroupRepo:       tidbGroupRepo,
		tidbClusterRepo:     tidbClusterRepo,
		dataplaneManager:    dataplaneManager,
		dataplaneClient:     dataplaneClient,
		clusterConfigClient: clusterConfigClient,
		lbClient:            lbClient,
		templateVersionSvc:  templateVersionSvc,
		imageConfig:         imageConfig,
		meteringConfig:      meteringConfig,
		bizConfig:           bizConfig,
	}
}

func (a *ApplyTiDBGroupAction) Name() string {
	return ApplyTiDBGroupActionName
}

func (a *ApplyTiDBGroupAction) Invoke(args actions.ActionArgs) error {
	// generate context
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "apply tidb group action")

	_, err := args.GetString(workflow.ComponentIdsKey)
	if err != nil {
		return fmt.Errorf("target component id is required, but not found")
	}

	sharedPoolId, err := args.GetString(workflow.SharedPoolIdKey)
	if err != nil {
		return fmt.Errorf("failed to get sharedPool id: %w", err)
	}

	// load sharedPool
	sharedPool, err := a.sharedPoolRepo.Load(ctx, sharedPoolId, false)
	if err != nil {
		return fmt.Errorf("failed to load sharedPool: %w", err)
	}

	sharedPoolStr, err := args.GetString(workflow.SharedPoolKey)
	if err != nil {
		return fmt.Errorf("failed to get sharedPool: %w", err)
	}

	var tempSharedPool entity.SharedPool
	err = protojson.Unmarshal([]byte(sharedPoolStr), &tempSharedPool)
	if err != nil {
		tilog.Error(ctx, "failed to unmarshal sharedPool", zap.Error(err))
		return fmt.Errorf("failed to unmarshal sharedPool: %w", err)
	}

	sharedPool.ComponentGroups = tempSharedPool.GetComponentGroups()
	if sharedPool.GetVersion() == "" {
		sharedPool.Version = tempSharedPool.GetVersion()
	}

	remoteCli, err := a.dataplaneManager.GetClient(sharedPool.GetDataplaneName())
	if err != nil {
		tilog.Error(ctx, "failed to get remote client for shared pool", zap.String("dataplaneName", sharedPool.GetDataplaneName()), zap.Error(err))
		return fmt.Errorf("failed to get remote client for shared pool: %w", err)
	}

	nodeVersionMap, err := getNodeVersions(ctx, a.dataplaneClient, sharedPool.GetDataplaneName())
	if err != nil {
		tilog.Error(ctx, "failed to get node versions for shared pool", zap.String("sharedPoolId", sharedPool.GetId()), zap.Error(err))
		return fmt.Errorf("failed to get node versions for shared pool: %w", err)
	}

	for _, comp := range sharedPool.GetComponentGroups() {

		if comp.GetComponentType() != apiv1.ComponentType_TIDB {
			tilog.Info(ctx, "only applying tidb component group, skip other component types",
				zap.String("componentId", comp.GetId()),
				zap.String("componentType", entity.GetComponentTypeName(comp)),
			)
			continue
		}

		// check if the component is target component
		if !workflow.CheckIsTargetComponent(ctx, comp, args) {
			tilog.Info(ctx, "skip apply component", zap.String("componentId", comp.GetId()), zap.String("componentType", comp.GetComponentType().String()))
			continue
		}

		if comp.GetPhase() == apiv1.Component_AVAILABLE {
			tilog.Info(ctx, "component is already available, skip applying", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)))
			continue
		}

		deletetd := false
		if comp.GetPhase() == apiv1.Component_DELETING {
			deletetd = true
		}

		tidbGroup, err := a.tidbGroupRepo.Load(ctx, comp.GetId(), deletetd)
		if err != nil {
			tilog.Error(ctx, "failed to load tidb group", zap.String("tidbGroupId", comp.GetId()), zap.Error(err))
			return fmt.Errorf("failed to load tidb group: %w", err)
		}

		// full component config from config center
		err = workflow.FullComponentConfigVersion(ctx, a.clusterConfigClient, a.meteringConfig, sharedPool, comp, tidbGroup.GetClusterId())
		if err != nil {
			tilog.Error(ctx, "failed to full component config", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
			return fmt.Errorf("failed to full component config: %w", err)
		}

		// Set labels for TiDB before building template args
		if comp.Labels == nil {
			comp.Labels = make(map[string]string)
		}
		switch {
		case tidbGroup.GetType() == apiv1.TiDBGroupType_INTERNAL:
			// Internal TiDB groups use internal label.
			comp.Labels[tioperator.LabelKeyInternal] = tioperator.LabelValueInternalTrue
			tilog.Info(ctx, "internal tidb group, skip setting keyspace label", zap.String("componentId", comp.GetId()))
		default:
			comp.Labels[tioperator.LabelKeyKeyspace] = tidbGroup.GetClusterId()
		}

		// only modifying phase need load balancer cidrs
		if comp.GetPhase() == apiv1.Component_MODIFYING {
			lbcidrs, err := getTiDBCidrs(ctx, a.lbClient, tidbGroup, "")
			if err != nil {
				tilog.Error(ctx, "failed to get load balancer cidrs", zap.Error(err))
				return fmt.Errorf("failed to get load balancer cidrs: %w", err)
			}

			if len(lbcidrs) > 0 {
				comp.Labels[tioperator.LabelKeyCidr] = strings.Join(lbcidrs, ",")
			}
		}

		// build template args
		templArgs, err := buildCompTempArgs(a.imageConfig, comp, sharedPool, nodeVersionMap)
		if err != nil {
			tilog.Error(ctx, "failed to build component template args", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
			return fmt.Errorf("failed to build component template args: %w", err)
		}

		if err := workflow.AddAzureDfsArgs(sharedPool, templArgs); err != nil {
			tilog.Error(ctx, "failed to add azure args", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
			return fmt.Errorf("failed to add azure args: %w", err)
		}

		// get tidb cluster
		tidbCluster, err := a.tidbClusterRepo.Load(ctx, tidbGroup.GetClusterId(), deletetd)
		if err != nil {
			tilog.Error(ctx, "failed to load tidb cluster", zap.String("tidbClusterId", tidbGroup.GetClusterId()), zap.Error(err))
			return fmt.Errorf("failed to load tidb cluster: %w", err)
		}

		templArgs["SlowLogImage"] = fmt.Sprintf("%s/%s", a.imageConfig.Registry, a.imageConfig.SlowLogImage)
		templArgs["AuditLogImage"] = fmt.Sprintf("%s/%s", a.imageConfig.Registry, a.imageConfig.AuditLogImage)
		templArgs["TiDBGroupId"] = tidbGroup.GetId()
		templArgs["UserClusterId"] = tidbGroup.GetClusterId()
		templArgs["UserTenantId"] = tidbGroup.GetTenantId()

		templArgs["TiDBKeyspace"] = tidbGroup.GetClusterId()

		if a.bizConfig.EnableSemv2 {
			templArgs["Semv2Enabled"] = "true"
		}

		if err := setTiDBSlowLogTemplateArgs(ctx, comp, sharedPool, templArgs, deletetd); err != nil {
			return err
		}

		// set auth jws
		templArgs["AuthJws"] = fmt.Sprintf("%s-auth-token-jwks-secret", entity.GetComponentGroupName(comp))

		// get tidb config
		conf := schemav1.NewTomlConfig(map[string]any{})
		if err := conf.UnmarshalTOML([]byte(comp.GetConfiguration().GetConfigurationFileContent())); err != nil {
			return fmt.Errorf("failed to unmarshal tidb config: %w", err)
		}

		sleepSeconds := 30
		v := conf.Get(entity.TiDBGracefulWaitShutdownKey)
		if v != nil {
			tilog.Info(ctx, "get graceful wait before shutdown time from tidb config", zap.Any("gracefulTime", v))
			if vInt, ok := v.(int64); ok {
				sleepSeconds += int(vInt)
			}
		}
		templArgs["PreStopSleepSeconds"] = strconv.Itoa(sleepSeconds)

		auditLogConfig, err := genAuditlogConfig(ctx, comp, tidbCluster, sharedPool)
		if err != nil {
			tilog.Error(ctx, "failed to generate audit log config", zap.String("componentId", comp.GetId()), zap.Error(err))
			return fmt.Errorf("failed to generate audit log config: %w", err)
		}

		templArgs["AuditLogConfig"] = auditLogConfig

		templateName, err := entity.GetComponentTemplateName(comp)
		if err != nil {
			tilog.Error(ctx, "failed to get component template name", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
			return fmt.Errorf("failed to get component template name: %w", err)
		}

		// if component phase is deleting, set needDelete to true
		err = templateExecuteWithVersionResolver(
			ctx, remoteCli, templateName, comp.GetTemplateVersion(), templArgs,
			comp.GetPhase() == apiv1.Component_DELETING, a.templateVersionSvc,
		)
		if err != nil {
			tilog.Error(ctx, "failed to execute template", zap.String("componentId", comp.GetId()), zap.String("componentType", entity.GetComponentTypeName(comp)), zap.Error(err))
			return fmt.Errorf("failed to execute template: %w", err)
		}
	}

	return nil
}

func setTiDBSlowLogTemplateArgs(ctx context.Context, comp *entity.Component, sharedPool *entity.SharedPool, templArgs map[string]string, needDelete bool) error {
	for _, storage := range comp.GetStorages() {
		switch storage.GetName() {
		case apiv1.Component_Storage_SLOWLOG:
			templArgs["SlowLogStorageClass"] = workflow.GenStorageClassName(sharedPool, storage)
			templArgs["SlowLogStorageSize"] = strconv.FormatUint(uint64(storage.GetSizeGi()), 10)
			templArgs["SlowLogStoragePath"] = storage.GetMountPath()
			templArgs["SlowLogVolumeAttributesClass"] = workflow.GenVolumeAttributesClassName(storage)
		default:
			tilog.Warn(ctx, "unsupported storage type for tidb", zap.String("storage", storage.GetName().String()))
			continue
		}
	}

	if needDelete {
		if _, ok := templArgs["SlowLogStorageClass"]; !ok {
			templArgs["SlowLogStorageClass"] = "delete-placeholder"
		}
		if _, ok := templArgs["SlowLogStorageSize"]; !ok {
			templArgs["SlowLogStorageSize"] = "1"
		}
		if _, ok := templArgs["SlowLogStoragePath"]; !ok {
			templArgs["SlowLogStoragePath"] = entity.AuditAndSlowLogPath
		}
		if _, ok := templArgs["SlowLogVolumeAttributesClass"]; !ok {
			templArgs["SlowLogVolumeAttributesClass"] = "delete-placeholder"
		}
		return nil
	}

	if _, ok := templArgs["SlowLogStorageClass"]; !ok {
		tilog.Warn(ctx, "slow log storage class is not set")
		return fmt.Errorf("slow log storage class is not set")
	}

	if _, ok := templArgs["SlowLogVolumeAttributesClass"]; !ok {
		tilog.Warn(ctx, "slow log volume attributes class is not set")
		return fmt.Errorf("slow log volume attributes class is not set")
	}

	return nil
}

func genAuditlogConfig(ctx context.Context, tidbComp *entity.Component, tidbCluster *entity.TiDBCluster, sharedPool *entity.SharedPool) (string, error) {

	// get tidb config
	conf := schemav1.NewTomlConfig(map[string]any{})
	if err := conf.UnmarshalTOML([]byte(tidbComp.GetConfiguration().GetConfigurationFileContent())); err != nil {
		return "", fmt.Errorf("failed to unmarshal tidb config: %w", err)
	}

	sleepSeconds := 30
	v := conf.Get(entity.TiDBGracefulWaitShutdownKey)
	if v != nil {
		tilog.Info(ctx, "get graceful wait before shutdown time from tidb config", zap.Any("gracefulTime", v))
		if vInt, ok := v.(int64); ok {
			sleepSeconds += int(vInt)
		}
	}

	// gen audit log config
	auditLogConfig := &entity.AuditLogConfig{
		LogDir:                  entity.AuditAndSlowLogPath,
		BucketURL:               tidbCluster.GetAuditLogSetting().GetBucketUrl(),
		Region:                  tidbCluster.GetAuditLogSetting().GetRegion(),
		GracefulStopWaitSeconds: int32(sleepSeconds - 3),
	}
	switch sharedPool.GetProvider().GetType() {
	case apiv1.ProviderType_AWS:
		// set aws config
		auditLogConfig.AWS = &entity.AuditLogAwsConfig{
			RoleARN:    tidbCluster.GetAuditLogSetting().GetProviderIdentity().GetCloudRoleIdentity(),
			ExternalID: tidbCluster.GetAuditLogSetting().GetProviderIdentity().GetExternalId(),
		}
	case apiv1.ProviderType_GCP:
		// do nothing for now
		tilog.Warn(ctx, "GCP provider type is not supported for audit log yet")
	case apiv1.ProviderType_AZURE:
		auditLogConfig.Azure = &entity.AuditLogAzureConfig{
			SASToken: tidbCluster.GetAuditLogSetting().GetProviderIdentity().GetCloudRoleIdentity(),
		}
	case apiv1.ProviderType_ALICLOUD:
		auditLogConfig.AliCloud = &entity.AliCloudRoleIdentity{
			RoleARN:         tidbCluster.GetAuditLogSetting().GetProviderIdentity().GetCloudRoleIdentity(),
			ExternalID:      tidbCluster.GetAuditLogSetting().GetProviderIdentity().GetExternalId(),
			RoleSessionName: "tidb-audit-log",
		}
	default:
		tilog.Warn(ctx, "unsupported provider type for audit log", zap.String("providerType", sharedPool.GetProvider().GetType().String()))
	}

	// set audit log config
	auditLogConfigYaml, err := yaml.Marshal(auditLogConfig)
	if err != nil {
		return "", fmt.Errorf("failed to marshal audit log config: %w", err)
	}

	return string(auditLogConfigYaml), nil
}

func getTiDBCidrs(ctx context.Context, lbClient dplanemgrv1.LoadBalancerServiceClient, tidbGroup *entity.TiDBGroup, lbtype string) ([]string, error) {
	lbcidrs := []string{}

	switch lbtype {
	case "private":
		privateCidrs, err := getPrivateTiDBCidrs(ctx, lbClient, tidbGroup)
		if err != nil {
			return lbcidrs, err
		}
		lbcidrs = append(lbcidrs, privateCidrs...)
	case "public":
		publicCidrs, err := getPublicTiDBCidrs(ctx, lbClient, tidbGroup)
		if err != nil {
			return lbcidrs, err
		}
		lbcidrs = append(lbcidrs, publicCidrs...)
	default:
		privateCidrs, err := getPrivateTiDBCidrs(ctx, lbClient, tidbGroup)
		if err != nil {
			return lbcidrs, err
		}
		lbcidrs = append(lbcidrs, privateCidrs...)

		publicCidrs, err := getPublicTiDBCidrs(ctx, lbClient, tidbGroup)
		if err != nil {
			return lbcidrs, err
		}
		lbcidrs = append(lbcidrs, publicCidrs...)
	}

	return lbcidrs, nil
}

func getPrivateTiDBCidrs(ctx context.Context, lbClient dplanemgrv1.LoadBalancerServiceClient, tidbGroup *entity.TiDBGroup) ([]string, error) {
	lbcidrs := []string{}

	// load balancer cidrs
	if tidbGroup.GetSqlEndpoint().GetPrivateLb().GetEnabled() {
		privateLb, err := lbClient.GetLoadBalancer(ctx, &dplanemgrv1.GetLoadBalancerRequest{
			DataPlaneId: tidbGroup.GetDataplaneName(),
			Id:          entity.GetPrivateSvcName(tidbGroup.GetId()),
		})
		if err != nil {
			tilog.Error(ctx, "failed to get private load balancer", zap.Error(err))
			return lbcidrs, fmt.Errorf("failed to get private load balancer: %w", err)
		}

		if len(privateLb.GetPrivateIps()) == 0 {
			tilog.Error(ctx, "private load balancer has no private ips", zap.String("loadBalancerId", privateLb.GetId()))
			return lbcidrs, fmt.Errorf("private load balancer has no private ips, waiting for it to be ready")
		}
		for _, ip := range privateLb.GetPrivateIps() {
			lbcidrs = append(lbcidrs, ip+"/32")
		}
	}

	return lbcidrs, nil
}

func getPublicTiDBCidrs(ctx context.Context, lbClient dplanemgrv1.LoadBalancerServiceClient, tidbGroup *entity.TiDBGroup) ([]string, error) {
	lbcidrs := []string{}

	if tidbGroup.GetSqlEndpoint().GetPublicLb().GetEnabled() {
		publicLb, err := lbClient.GetLoadBalancer(ctx, &dplanemgrv1.GetLoadBalancerRequest{
			DataPlaneId: tidbGroup.GetDataplaneName(),
			Id:          entity.GetPublicSvcName(tidbGroup.GetId()),
		})
		if err != nil {
			tilog.Error(ctx, "failed to get public load balancer", zap.Error(err))
			return lbcidrs, fmt.Errorf("failed to get public load balancer: %w", err)
		}

		if len(publicLb.GetPrivateIps()) == 0 {
			tilog.Error(ctx, "public load balancer has no private ips", zap.String("loadBalancerId", publicLb.GetId()))
			return lbcidrs, fmt.Errorf("public load balancer has no private ips, waiting for it to be ready")
		}

		for _, ip := range publicLb.GetPrivateIps() {
			lbcidrs = append(lbcidrs, ip+"/32")
		}
	}
	return lbcidrs, nil
}
