package util

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

	"github.com/Masterminds/semver/v3"
	"github.com/pingcap/errors"
	perrors "github.com/pingcap/errors"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	"github.com/tidbcloud/dataflow-service/app/repo/entity"
	"github.com/tidbcloud/dataflow-service/internal/config"
	"github.com/tidbcloud/dataflow-service/internal/grpc/grpcli"
	pkg_grpc "github.com/tidbcloud/dataflow-service/pkg/grpc/grpcli"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/pkgv2/clusterproxy"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/secret/encrypt"
	"go.uber.org/zap"
)

type (
	GetProxyFunc func(
		clusterID uint64, clusterVersion, proxyEndpoint string, timeout int,
	) (clusterproxy.ClusterProxy, error)
	GetProxyFuncV2 func(
		ctx context.Context, clusterID uint64, clusterVersion, proxyEndpoint string, timeout int,
	) (clusterproxy.ClusterProxy, error)
	GetServerlessProxyFunc func(
		ctx context.Context, clusterID uint64, version, endpoint string, timeout int,
	) (clusterproxy.ClusterProxy, error)
)

type (
	ComponentStatus string
	JobStatus       string
)

const (
	CDCClusterIDPrefix string = "cdc"
)

var tidbSemVersionConstraint, _ = semver.NewConstraint(">= v5.3.0")

// GetRestoreStatusForInfraAPI checks the status in conditions one by one,
func GetRestoreStatusForInfraAPI(restore *cloudv1.Restore) entity.RestoreStatus {
	status := entity.RestoreStatusRUNNING
	phase := restore.Status.Phase
	if phase == cloudv1.RestoreComplete || phase == cloudv1.RestoreDataComplete {
		status = entity.RestoreStatusSUCCESS
	} else if phase == cloudv1.RestoreFailed || phase == cloudv1.RestoreInvalid {
		status = entity.RestoreStatusFAILED
	}
	return status
}

func LogBackupCRName(logBackup *entity.LogBackup) string {
	return fmt.Sprintf("log-%s-%d", strings.ToLower(logBackup.BackupSchedule.Name), logBackup.ID)
}

type StatusDBNotAvailable struct{}

func (s StatusDBNotAvailable) Error() string {
	return "UseSemCheck: DB Status not available"
}

type NonSemanticVersion struct{ Version string }

func (s NonSemanticVersion) Error() string {
	return fmt.Sprintf("UseSemCheck: Not a semantic version: %s", s.Version)
}

func UseSemForCluster(version string) (bool, error) {
	if config.Global().ServerConfig.UsePrivilegedTiDB {
		return false, nil
	}
	return SemVersionCheck(version)
}

func SemVersionCheck(version string) (bool, error) {
	log.Info(context.TODO(), "cluster DB version", zap.String("version", version))
	semVer, err := semver.NewVersion(version)
	if err != nil {
		return false, NonSemanticVersion{Version: version}
	}
	return tidbSemVersionConstraint.Check(semVer), nil
}

var cdcMultiClusterVersion, _ = semver.NewConstraint(">= v6.2.0")

// SupportCDCMultiClusterVersion return true if the cluster version support
// multiple CDC clusters
func SupportCDCMultiClusterVersion(v string) (bool, error) {
	v = strings.Split(v, "-")[0]
	semVer, err := semver.NewVersion(v)
	if err != nil {
		return false, NonSemanticVersion{Version: v}
	}
	return cdcMultiClusterVersion.Check(semVer), nil
}

func ExecuteSqlCommon(
	ctx context.Context, projectId uint64, clusterId uint64, clause string, coreGrpcClient pkg_grpc.CoreGrpcClient,
	enc encrypt.Encryptor, getProxy GetProxyFuncV2, timeout int,
) error {
	var (
		err      error
		adminPwd encrypt.Secret
		proxy    clusterproxy.ClusterProxy
	)

	cluster, bizErr := coreGrpcClient.GetAvailableClusterByID(ctx, clusterId)
	if bizErr != nil {
		return errors.Annotatef(bizErr, "failed to get cluster")
	}

	proxy, err = getProxy(ctx, clusterId, cluster.Version, cluster.InternalEndpoint, timeout)
	if err != nil {
		return perrors.Annotatef(err, "failed to get proxy")
	}

	adminPwd, err = DecryptCloudAdminPassword(ctx, enc, clusterId, cluster.Version)
	if err != nil {
		return perrors.Annotatef(err, "failed to decrypt admin password")
	}

	return proxy.ExecuteSQLV2(ctx, GetOperatorCloudAdmin(adminPwd), clause)
}

func GetOperatorCloudAdmin(adminPwd encrypt.Secret) *clusterproxy.ClusterOperator {
	return &clusterproxy.ClusterOperator{
		Username:   "cloud_admin",
		AuthMethod: clusterproxy.AuthMethodPassword,
		Credential: string(adminPwd),
	}
}

func DecryptCloudAdminPassword(
	ctx context.Context, encryptor encrypt.Encryptor, clusterId uint64, clusterVersion string,
) (encrypt.Secret, error) {
	useSem, err := UseSemForCluster(clusterVersion)
	if !useSem {
		return "", errors.AddStack(err)
	}

	resp, err := grpcli.GetClusterConfidentialInfo(ctx, &core.GetClusterConfidentialInfoReq{
		ClusterId: clusterId,
		BaseReq:   nil,
	})
	if err != nil {
		return "", err
	}

	// Clusters that have not yet been upgraded to use cloud_admin
	if len(resp.Info.EncryptedCloudAdminPasswd) == 0 {
		return "", nil
	}
	pwd, err := encrypt.DecryptBase64(encryptor, resp.Info.EncryptedCloudAdminPasswd)
	if err != nil {
		return "", perrors.Annotate(err, "decrypt encrypted cloud admin password")
	}
	return pwd, nil
}

func ByteToGBFloat64(raw float64) float64 {
	return raw / 1024 / 1024 / 1024
}

// GetCDCClusterID returns a CDC cluster ID from regionID and cluster ID.
// It is base on the assumption: one region has only one CDC cluster,
// and every CDCTask must belong to only one CDC cluster.
func GetCDCClusterID(regionID entity.ID, clusterID entity.ID) string {
	return fmt.Sprintf("%s-%s-%s",
		CDCClusterIDPrefix,
		strconv.FormatUint(regionID.Uint64(), 10),
		strconv.FormatUint(clusterID.Uint64(), 10))
}

// EBSVersionCheck checks whether EBS feature can be enabled on specified version.
func EBSVersionCheck(version string) bool {
	v, err := semver.NewVersion(version)
	if err != nil {
		return false
	}
	// check tidb version
	if !config.Global().BackupConfig.EBSBackupEnableMinVerConstraint.Check(v) {
		return false
	}
	return true
}

func GetIPFromHost(host string) net.IP {
	if ip := net.ParseIP(host); ip != nil {
		return ip
	}
	if ips, err := net.LookupIP(host); err == nil {
		return ips[0]
	}
	return nil
}

func ParseImage(image string) (version string) {
	if pos := strings.LastIndex(image, ":"); pos == -1 {
		version = image
	} else {
		version = image[pos+1:]
	}
	return
}
