package util

import (
	"context"
	"fmt"
	"net"
	"strings"
	"time"

	"github.com/Masterminds/semver"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	perrors "github.com/pingcap/errors"
	"github.com/tidbcloud/core-portal/internal/central/config"
	api "github.com/tidbcloud/core-portal/internal/central/infra-api"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/central"
	entity "github.com/tidbcloud/core-svc/model"
	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"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
)

const (
	ClusterQuotasPrompt = `the total number of your nodes has exceeded the maximum allowed limit for your organization, please <a href="https://support.pingcap.com/hc/en-us/" target="_blank">contact us</a> to request to increase the quota, or see the <a href="https://docs.pingcap.com/tidbcloud/beta/limitations-and-quotas" target="_blank">details</a> for the limitations`
)

type GetProxyFunc func(clusterID uint64, status cloudv1.ClusterStatus, timeout int) (clusterproxy.ClusterProxy, error)

type ComponentStatus string
type JobStatus string

const (
	ComponentUnavailable ComponentStatus = "UNAVAILABLE"
	ComponentUnhealthy   ComponentStatus = "UNHEALTHY"
	ComponentUpgrading   ComponentStatus = "UPGRADING"
	ComponentNormal      ComponentStatus = "NORMAL"
	ComponentScaling     ComponentStatus = "SCALING"
	FailedState          string          = "failed"
	RunningState         string          = "running"
	SucceededState       string          = "success"
	PendingState         string          = "pending"
	StandardRestoreSpeed uint32          = 70 * 1024 * 1024
	HighRestoreSpeed     uint32          = 70 * 1024 * 1024
	MinimunAlertSecond   uint32          = 300
	DefaultMaxBackup     uint32          = 7
	DefaultInternalCIDR  string          = "10.0.0.0/8"
	JobComplete          JobStatus       = "complete"
	JobFailed            JobStatus       = "failed"
	JobRunning           JobStatus       = "running"
)

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

var (
	ExecuteSqlCommon = executeSqlCommon
)

func BackupCRDName(backup *entity.Backup) string {
	if backup.OldBackupName != "" {
		return backup.OldBackupName
	}
	return fmt.Sprintf("%s-%d", strings.ToLower(backup.BackupSchedule.Name), backup.ID)
}

func MarshalTrafficFilter(trafficFilterList *central.TrafficFilterList) *entity.TrafficFilters {
	cidrs := make([]entity.TrafficFilter, len(trafficFilterList.Cidrs))
	for idx, trafficFilter := range trafficFilterList.Cidrs {
		cidr := entity.TrafficFilter{
			Cidr:        trafficFilter.Cidr,
			Description: trafficFilter.Description,
		}
		cidrs[idx] = cidr
	}
	return &entity.TrafficFilters{
		Cidrs: cidrs,
	}
}

func UnmarshalTrafficFilter(cidrs *entity.TrafficFilters) *central.TrafficFilterList {
	trafficFilters := make([]*central.TrafficFilter, len(cidrs.Cidrs))
	for idx, cidr := range cidrs.Cidrs {
		trafficFilter := &central.TrafficFilter{
			Cidr:        cidr.Cidr,
			Description: cidr.Description,
		}
		trafficFilters[idx] = trafficFilter
	}
	return &central.TrafficFilterList{
		Cidrs: trafficFilters,
	}
}

func ValidateCidr(trafficFilterList *central.TrafficFilterList) error {
	if trafficFilterList == nil {
		return errcode.NewInvalidInputErr(fmt.Errorf("trafficFilterList can't be empty"))
	}

	existMap := map[string]bool{}
	for idx, trafficFilter := range trafficFilterList.Cidrs {
		if strings.Contains(trafficFilter.Cidr, "/") {
			_, ipv4Net, err := net.ParseCIDR(trafficFilter.Cidr)
			if err != nil {
				return err
			}
			if trafficFilter.Cidr != ipv4Net.String() {
				return errcode.NewInvalidInputErr(fmt.Errorf("invalid CIDR: %s", trafficFilter.Cidr))
			}
			if val, ok := existMap[trafficFilter.Cidr]; ok && val {
				return errcode.NewInvalidInputErr(fmt.Errorf("duplicate CIDR: %s", trafficFilter.Cidr))
			}
			existMap[trafficFilter.Cidr] = true
		} else {
			ip := net.ParseIP(trafficFilter.Cidr)
			if ip == nil {
				return errcode.NewInvalidInputErr(fmt.Errorf("invalid IP address: %s", trafficFilter.Cidr))
			}
			trafficFilterList.Cidrs[idx].Cidr = fmt.Sprintf("%s/32", trafficFilter.Cidr)
			if val, ok := existMap[trafficFilterList.Cidrs[idx].Cidr]; ok && val {
				return errcode.NewInvalidInputErr(fmt.Errorf("duplicate IP: %s", trafficFilter.Cidr))
			}
			existMap[trafficFilterList.Cidrs[idx].Cidr] = true
		}
	}
	return nil
}

const (
	timeFormat = "2006-01-02 15:04"
)

// ConvertUtcTime returns UTC time based on UTC offset
// Example: 2021-06-03 16:19 UTC +08:00
func ConvertUtcTime(t time.Time, utcOffset int32) string {
	var offsetStr string
	switch {
	case utcOffset > 0:
		offsetStr = fmt.Sprintf("UTC +%02d:00", utcOffset)
	case utcOffset < 0:
		offsetStr = fmt.Sprintf("UTC -%02d:00", utcOffset)
	default:
		offsetStr = "UTC ±00:00"
	}

	return fmt.Sprintf("%s %s", t.UTC().Add(time.Duration(utcOffset)*time.Hour).Format(timeFormat), offsetStr)
}

func GetPocStatusUpdateRelatedManagerEmails(ctx context.Context, tenant *entity.Tenant) []string {
	logger := log.WithContextL(ctx)
	tos := []string{config.Global().ServerConfig.BizOpsTeamEmail}
	if tenant.AccountManager != "" {
		tos = append(tos, tenant.AccountManager)
	}

	logger.Sugar().Infof("poc status update related emails: %s", tos)
	return tos
}

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(status cloudv1.ClusterStatus) (bool, error) {
	if config.Global().ServerConfig.UsePrivilegedTiDB {
		return false, nil
	}
	if status.DB == nil {
		return false, StatusDBNotAvailable{}
	}
	log.Info(context.TODO(), "cluster DB version", zap.String("version", status.DB.Version))
	semVer, err := semver.NewVersion(status.DB.Version)
	if err != nil {
		return false, NonSemanticVersion{Version: status.DB.Version}
	}
	return tidbSemVersionConstraint.Check(semVer), nil
}

func executeSqlCommon(
	ctx context.Context, cluster *entity.Cluster, clause string, api api.InfraFacade,
	enc encrypt.Encryptor, getProxy GetProxyFunc, timeout int,
) error {
	var (
		err      error
		adminPwd encrypt.Secret
		cr       *cloudv1.Cluster
		proxy    clusterproxy.ClusterProxy
	)

	cr, err = api.GetCluster(ctx, cluster.ProjectID.Uint64(), cluster.ID.Uint64())
	if err != nil && !apierrors.IsNotFound(err) {
		return perrors.Annotatef(err, "failed to get cluster")
	} else if apierrors.IsNotFound(err) {
		return nil
	}

	proxy, err = getProxy(cluster.ID.Uint64(), cr.Status, timeout)
	if err != nil {
		return perrors.Annotatef(err, "failed to get proxy")
	}

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

	return proxy.ExecuteSQL(ctx, adminPwd, clause)
}

func ExecuteMultiSqlCommon(
	ctx context.Context, cluster *entity.Cluster, clauses []string, api api.InfraFacade,
	enc encrypt.Encryptor, getProxy GetProxyFunc, timeout int,
) error {
	var (
		err      error
		adminPwd encrypt.Secret
		cr       *cloudv1.Cluster
		proxy    clusterproxy.ClusterProxy
	)

	cr, err = api.GetCluster(ctx, cluster.ProjectID.Uint64(), cluster.ID.Uint64())
	if err != nil && !apierrors.IsNotFound(err) {
		return perrors.Annotatef(err, "failed to get cluster")
	} else if apierrors.IsNotFound(err) {
		return nil
	}

	proxy, err = getProxy(cluster.ID.Uint64(), cr.Status, timeout)
	if err != nil {
		return perrors.Annotatef(err, "failed to get proxy")
	}

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

	return proxy.ExecuteMultiSQL(ctx, adminPwd, clauses)
}

func DecryptCloudAdminPassword(encryptor encrypt.Encryptor, cluster entity.Cluster, status cloudv1.ClusterStatus) (encrypt.Secret, error) {
	useSem, err := UseSemForCluster(status)
	if !useSem {
		return "", errors.AddStack(err)
	}
	// Clusters that have not yet been upgraded to use cloud_admin
	if len(cluster.EncryptedCloudAdminPassword) == 0 {
		return "", nil
	}
	pwd, err := encrypt.DecryptBase64(encryptor, cluster.EncryptedCloudAdminPassword)
	if err != nil {
		return "", perrors.Annotate(err, "decrypt encrypted cloud admin password")
	}
	return pwd, nil
}
