package infra

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

	"github.com/tidbcloud/aws-shared-provider/pkg/types"
	"github.com/tidbcloud/aws-shared-provider/pkg/utils"
	"github.com/tidbcloud/aws-shared-provider/pkg/utils/labels"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/tidb-management-service/api/serverless_infra"
	inframysql "github.com/tidbcloud/tidb-management-service/api/serverless_infra/client/mysql"
	corev1 "k8s.io/api/core/v1"
)

var (
	ErrNotFound = errors.New("not found")
)

type RegionalInfra interface {
	GetCluster(ctx context.Context, namespace, name string) (*serverless_infra.Cluster, error)
	GetClusterWithToken(ctx context.Context, token string) (*serverless_infra.Cluster, error)
}

type InfraAPI interface {
	GetBranch(ctx context.Context, namespace, name string) (*cloudv1.Branch, error)
	GetBranchWithToken(ctx context.Context, token string) (*cloudv1.Branch, error)
	PatchBranch(ctx context.Context, namespace, name string, data []byte) (*cloudv1.Branch, error)
	Start(ctx context.Context) error
	WaitForCacheSync(ctx context.Context) bool
}

type LocalKube interface {
	GetPodList(ctx context.Context, ip string) (*corev1.PodList, error)
	Start(ctx context.Context) error
	WaitForCacheSync(ctx context.Context) bool
}

type ClusterType string

const (
	TypeCluster ClusterType = "Cluster"
	TypeBranch  ClusterType = "Branch"
)

type ClusterOrBranchMeta struct {
	ClusterType    ClusterType
	EnableAuditLog bool

	// InfraTiDBReplicas is the gateway's primary replica source. The gateway falls
	// back to BackendResponseV2.replicas when this value is unavailable.
	InfraTiDBReplicas             int
	InfraNamespace                string
	ClusterID                     string
	TenantID                      string
	ProjectID                     string
	ZoneRedundantEnabled          bool
	KeyspaceID                    string
	RuMode                        string
	TiDBPool                      string
	AuditLogRedacted              bool
	Token                         string
	PublicEP                      bool
	PrivateEP                     bool
	Annotations                   map[string]string
	AuthorizedNetworks            []serverless_infra.AuthorizedNetwork
	PrivatelinkAuthorizedNetworks []serverless_infra.PrivatelinkAuthorizedNetwork
	MinRcu                        uint32
	MaxRcu                        uint32
	ServicePlan                   string
}

func (meta *ClusterOrBranchMeta) IsEssential() bool {
	return meta.MaxRcu > 0 || meta.MinRcu > 0
}

func GetClusterOrBranchMeta(
	ctx context.Context,
	regionalInfra RegionalInfra,
	infraApi InfraAPI,
	token string,
	kubeClusterName string,
) (*ClusterOrBranchMeta, error) {
	meta := &ClusterOrBranchMeta{
		EnableAuditLog: false,
		Token:          token,
	}
	infraCluster, err := regionalInfra.GetClusterWithToken(ctx, token)
	if err != nil { // branching
		infraBranch, err1 := infraApi.GetBranchWithToken(ctx, token)
		if err1 != nil {
			return nil, utils.NewExternalError(utils.ERR_CLUSTER_NOT_FOUND, err1)
		}
		meta.ClusterType = TypeBranch
		if labels.GetKubeClusterFromLabels(infraBranch.Labels) != kubeClusterName {
			return nil, utils.NewExternalError(utils.ERR_CLUSTER_NOT_FOUND, errors.New("cluster name mismatch"))
		}
		if infraBranch.Status.Phase != cloudv1.BranchPhaseRunning {
			err := errors.New("branch status is " + string(infraBranch.Status.Phase))
			return nil, utils.NewExternalError(fmt.Sprintf(utils.ERR_BRANCH_NOT_RUNNING, string(infraBranch.Status.Phase)), err)
		}
		clusterStatus, err1 := infraBranch.AwsFreeTierClusterStatusFromBranch()
		if err1 != nil {
			return nil, utils.NewInternalError(utils.ERR_CLUSTER_TEMPORARY_UNAVAILABLE, err1)
		}
		if infraBranch.Status.DB != nil && infraBranch.Status.DB.TiDB != nil {
			meta.InfraTiDBReplicas = int(infraBranch.Status.DB.TiDB.UpdatedReplicas)
		}
		meta.InfraNamespace = infraBranch.Namespace
		meta.ClusterID = infraBranch.Name
		branchMeta, err := utils.GetBranchMetadata(infraBranch)
		if err != nil {
			return nil, utils.NewInternalError(utils.ERR_CLUSTER_NOT_FOUND, err)
		}
		meta.TenantID = strconv.FormatUint(branchMeta.TenantID, 10)
		meta.ProjectID = strconv.FormatUint(branchMeta.ProjectID, 10)
		meta.Annotations = infraBranch.Annotations
		if infraBranch.Status.SqlEndpoints.PublicEndpoint != nil {
			meta.PublicEP = true
			meta.AuthorizedNetworks = convertToServerlessInfraAuthorizedNetworks(
				infraBranch.Spec.SqlEndpoints.Public.AuthorizedNetworks,
			)
		}
		if infraBranch.Status.SqlEndpoints.PrivateLinkService != nil {
			meta.PrivateEP = true
		}
		meta.ZoneRedundantEnabled = clusterStatus.ZoneRedundantEnabled

		meta.KeyspaceID = fmt.Sprint(clusterStatus.KeyspaceId)
		meta.RuMode = clusterStatus.RuLimitMode
		meta.TiDBPool = clusterStatus.TidbPool
		meta.ServicePlan = infraBranch.Annotations[types.AnnoTidbServicePlan]
	} else { // cluster
		meta.ClusterType = TypeCluster
		if infraCluster.TagsKubernetes != kubeClusterName {
			return nil, utils.NewExternalError(utils.ERR_CLUSTER_NOT_FOUND, errors.New("cluster name mismatch"))
		}
		if infraCluster.Status.Phase != serverless_infra.ClusterPhaseRunning {
			err := errors.New("cluster status is " + string(infraCluster.Status.Phase))
			return nil, utils.NewExternalError(utils.ERR_CLUSTER_RESOURCE_INIT, err)
		}
		if IsClusterRestoring(infraCluster) {
			return nil, utils.NewExternalError(utils.ERR_CLUSTER_RESTORING)
		}
		meta.InfraTiDBReplicas = infraCluster.Status.TiDB.UpdatedReplicas
		meta.InfraNamespace = infraCluster.Namespace
		meta.ClusterID = infraCluster.Name
		meta.TenantID = infraCluster.TagsTenant
		meta.ProjectID = infraCluster.TagsProject
		meta.Annotations = infraCluster.Annotations
		meta.MinRcu = infraCluster.Spec.TiDB.MinRcu
		meta.MaxRcu = infraCluster.Spec.TiDB.MaxRcu
		if infraCluster.Status.SqlEndpoints.Public != nil &&
			len(infraCluster.Status.SqlEndpoints.Public.Host) != 0 {
			meta.PublicEP = true
			meta.AuthorizedNetworks = infraCluster.Spec.SqlEndpoints.Public.AuthorizedNetworks
		}
		if infraCluster.Status.SqlEndpoints.AwsPrivatelinkService != nil &&
			len(infraCluster.Status.SqlEndpoints.AwsPrivatelinkService.PrivateDnsName) != 0 {
			meta.PrivateEP = true
			if infraCluster.Spec.SqlEndpoints.AwsPrivatelinkService != nil {
				meta.PrivatelinkAuthorizedNetworks = infraCluster.Spec.SqlEndpoints.AwsPrivatelinkService.AuthorizedNetworks
			}
		}

		meta.EnableAuditLog = infraCluster.Spec.AuditLogConfig.Enabled
		meta.AuditLogRedacted = !infraCluster.Spec.AuditLogConfig.Unredacted
		if infraCluster.Status.ZoneRedundantEnabled == "yes" {
			meta.ZoneRedundantEnabled = true
		} else {
			meta.ZoneRedundantEnabled = false
		}

		meta.KeyspaceID = fmt.Sprint(infraCluster.Status.KeyspaceId)
		meta.RuMode = infraCluster.Status.RuLimitMode
		meta.TiDBPool = infraCluster.Status.TidbPool
		meta.ServicePlan = infraCluster.Annotations[types.AnnoTidbServicePlan]
	}
	return meta, nil
}

func IsClusterRestoring(cluster *serverless_infra.Cluster) bool {
	if value, ok := cluster.Annotations[types.AnnoKeyKeyspaceRestore]; ok {
		return value != types.AnnoKeyspaceRestoreFinish
	} else {
		return false
	}
}

func IsBranchRestoring(branch *cloudv1.Branch) bool {
	return branch.Status.Phase == cloudv1.BranchPhaseRestoring
}

func IsEssentialCluster(cluster *serverless_infra.Cluster) bool {
	return cluster.Spec.TiDB.MinRcu > 0 || cluster.Spec.TiDB.MaxRcu > 0
}

func IsEssentialClusterMysql(cluster *inframysql.Cluster) bool {
	return cluster.Spec.TiDBMinRCU > 0 || cluster.Spec.TiDBMaxRCU > 0
}

func IsEssentialBranch(branch *cloudv1.Branch) bool {
	return branch.Spec.AutoScaling != nil && (branch.Spec.AutoScaling.MinRcu > 0 || branch.Spec.AutoScaling.MaxRcu > 0)
}

func convertToServerlessInfraAuthorizedNetworks(
	peNetworks []cloudv1.AuthorizedNetwork,
) []serverless_infra.AuthorizedNetwork {
	authorizedNetworks := make([]serverless_infra.AuthorizedNetwork, 0, len(peNetworks))

	for _, peNetwork := range peNetworks {
		authorizedNetwork := serverless_infra.AuthorizedNetwork{
			StartIpAddress: peNetwork.StartIpAddress,
			EndIpAddress:   peNetwork.EndIpAddress,
			DisplayName:    peNetwork.DisplayName,
		}
		authorizedNetworks = append(authorizedNetworks, authorizedNetwork)
	}

	return authorizedNetworks
}
