package cluster

import (
	"context"
	"strconv"

	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	pkgclusterproxy "github.com/tidbcloud/pkgv2/clusterproxy"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/clusterproxy"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/regionalserver"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/tidbgroup"
	svccommon "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

// ChangeRootPasswd changes the root password for a cluster.
// It uses the cluster-agent SetPassword API (regular cluster routing).
// Falls back to ClusterProxy if cluster-agent fails.
func (s *serviceImpl) ChangeRootPasswd(
	ctx context.Context,
	req *globalngv1.ChangeClusterPasswdRequest,
) bizerr.BizError {
	err := s.transaction.Transaction(ctx, func(ctx context.Context) error {
		cluster, err := s.clusterRepo.Get(ctx, req.GetClusterId(), false)
		if err != nil {
			return err
		}

		if cluster.State != globalngv1.Cluster_AVAILABLE &&
			cluster.State != globalngv1.Cluster_MODIFYING {
			return bizerr.New(
				bizerr.ErrCodeParams,
				"root password can only be changed when the cluster is available or modifying",
			)
		}

		if err := validateClusterPassword(req.GetPasswd()); err != nil {
			return err
		}

		region, err := s.regionRepo.Get(ctx, cluster.RegionId)
		if err != nil {
			return err
		}
		if region.State != globalcommonv1.Region_ACTIVE {
			return bizerr.New(bizerr.ErrCodePermission, "region is not active")
		}

		tidbgroups, err := s.tidbgroupRepo.Query(ctx, &tidbgroup.Filter{
			ClusterIds: []uint64{cluster.ClusterId},
		})
		if err != nil {
			return err
		}
		if len(tidbgroups) == 0 {
			return bizerr.New(bizerr.ErrCodeRecordNotFound, "tidb group not found for cluster %d", cluster.ClusterId)
		}
		tidbgroupId := tidbgroups[0].Id
		regionCluster, err := s.regionalCli.GetTidbGroup(ctx, region, &clusterngv1beta.GetTiDBGroupRequest{
			ClusterId:   strconv.FormatUint(cluster.ClusterId, 10),
			TidbGroupId: strconv.FormatUint(tidbgroupId, 10),
		})
		if err != nil {
			return err
		}
		clusterIDStr := strconv.FormatUint(cluster.ClusterId, 10)
		sharedPoolIdStr := regionCluster.GetSharedPoolId()
		tidbGroupIdStr := strconv.FormatUint(tidbgroupId, 10)

		// Try to reset root password via cluster agent first
		err = s.resetPasswordWithClusterAgent(ctx, region, clusterIDStr, sharedPoolIdStr, clusterproxy.RootUser, req.GetPasswd())
		if err != nil {
			tilog.Error(ctx, "reset root password via cluster agent failed, fallback to cluster proxy",
				zap.Uint64("cluster_id", cluster.ClusterId),
				zap.String("shared_pool_id", sharedPoolIdStr),
				zap.Error(err),
			)
			// Fallback to cluster proxy
			if err := s.resetPasswordWithClusterProxy(ctx, cluster, region, clusterIDStr, sharedPoolIdStr, tidbGroupIdStr,
				regionCluster.GetVersion(), clusterproxy.RootUser, req.GetPasswd(),
			); err != nil {
				return err
			}
		}

		// Root password only available for us during the creating stage.
		cluster.RootPassword = ""
		cluster.HasRootPassword = true

		if err := s.clusterRepo.Save(ctx, cluster); err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		return bizerr.WrapError(err, "change root password")
	}

	return nil
}

// resetPasswordWithClusterAgent resets the password for a user via the cluster-agent SetPassword API.
// It fetches the cluster-agent public key, encrypts the password with RSA, and calls the SetPassword endpoint.
func (s *serviceImpl) resetPasswordWithClusterAgent(
	ctx context.Context,
	region *entity.Region,
	clusterId, sharedPoolId string,
	username, newPassword string,
) bizerr.BizError {
	// get the public key from cluster agent
	publicKeyPem, berr := s.regionalCli.GetClusterAgentPublicKey(ctx, region, clusterId, sharedPoolId)
	if berr != nil {
		return berr
	}
	publicKey, err := svccommon.ParsePublicKeyFromPEM(publicKeyPem)
	if err != nil {
		return bizerr.New(bizerr.ErrCodeRegionalAPI, "parse public key: %s", err.Error())
	}
	encryptedPassword, err := svccommon.EncryptWithPublicKey(publicKey, []byte(newPassword))
	if err != nil {
		return bizerr.New(bizerr.ErrCodeRegionalAPI, "encrypt password: %s", err.Error())
	}

	// Map username to PasswordResetTarget
	target, err := regionalserver.MapUsernameToTarget(username)
	if err != nil {
		return bizerr.New(bizerr.ErrCodeParams, "%s", err.Error())
	}

	berr = s.regionalCli.SetPassword(ctx, region, clusterId, sharedPoolId, &regionalserver.SetPasswordRequest{
		EncryptedPassword: encryptedPassword,
		Target:            target,
	})
	if berr != nil {
		return berr
	}
	return nil
}

func (s *serviceImpl) resetPasswordWithClusterProxy(
	ctx context.Context,
	cluster *entity.Cluster,
	region *entity.Region,
	clusterId, sharedPoolId, tidbGroupId, version string,
	username, newPassword string,
) bizerr.BizError {
	cluster.ProxyBaseUrl = region.ServiceAddress +
		"/dataplane/proxy/clusters/" + clusterId +
		"/proxy/cluster/" + sharedPoolId +
		"/group/" + tidbGroupId
	cluster.Version = version

	cloudAdminPwd, err := s.encryptor.DecryptBase64(ctx, cluster.EncryptedCloudAdminPassword)
	if err != nil {
		return err
	}

	proxy, err := s.clusterProxyProvider.GetClusterProxy(
		ctx,
		cluster.ClusterId,
		cluster.ProxyBaseUrl,
		cluster.Version,
		&pkgclusterproxy.ClusterOperator{
			Username:   clusterproxy.CloudAdminUser,
			AuthMethod: pkgclusterproxy.AuthMethodPassword,
			Credential: cloudAdminPwd,
		},
	)
	if err != nil {
		return err
	}
	if err := proxy.ChangePassword(ctx, username, newPassword); err != nil {
		return err
	}
	return nil
}

func validateClusterPassword(password string) bizerr.BizError {
	passwordLen := len(password)
	if passwordLen < minClusterPasswordLength || passwordLen > maxClusterPasswordLength {
		return bizerr.New(bizerr.ErrCodeParams, "Password must be 8-64 characters")
	}
	return nil
}
