package util

import (
	"errors"
	"fmt"
	"net"
	"strings"
	"time"

	"github.com/pingcap/dbaas/pkg/constants"
	"github.com/pingcap/dbaas/pkg/tidb/component"
	"github.com/pingcap/errcode"
	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	"github.com/tidbcloud/central-sdk/config"
	"github.com/tidbcloud/central-sdk/db/entity"
	central "github.com/tidbcloud/central-sdk/proto/output"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
)

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 ComponentStatus 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"
)

var (
	tikvFailoverPeriod    = 5 * time.Minute
	tiflashFailoverPeriod = 5 * time.Minute
	ProfileMap            = map[string]int{
		constants.T1TinyProfile:     0,
		constants.T1SmallProfile:    1,
		constants.T1Small2Profile:   2,
		constants.T1StandardProfile: 3,
		constants.T2StandardProfile: 4,
		constants.T1HighcpuProfile:  5,
		constants.H1StandardProfile: 101,
		constants.H1HighcpuProfile:  102,
	}
)

// Check the status in conditions one by one,
// if the status other than complete or failed is running
func GetBackupStatus(backup *v1alpha1.Backup) string {
	status := PendingState
	phase := backup.Status.Phase
	if phase == v1alpha1.BackupComplete {
		status = SucceededState
	} else if phase == v1alpha1.BackupFailed || phase == v1alpha1.BackupInvalid {
		status = FailedState
	} else if phase == v1alpha1.BackupRunning || phase == v1alpha1.BackupPrepare {
		status = RunningState
	}
	return status
}

// GetBackupStatusForInfraAPI checks the status in conditions one by one.
func GetBackupStatusForInfraAPI(backup *cloudv1.Backup) string {
	status := PendingState
	phase := backup.Status.Phase
	if phase == cloudv1.BackupComplete {
		status = SucceededState
	} else if phase == cloudv1.BackupFailed {
		status = FailedState
	} else if phase == cloudv1.BackupRunning || phase == cloudv1.BackupPrepare {
		status = RunningState
	}
	return status
}

// Check the status in conditions one by one,
// if the status other than complete or failed is running
func GetRestoreStatus(restore *v1alpha1.Restore) entity.RestoreStatus {
	var status entity.RestoreStatus
	status = entity.RestoreStatus_RUNNING
	phase := restore.Status.Phase
	if phase == v1alpha1.RestoreComplete {
		status = entity.RestoreStatus_SUCCESS
	} else if phase == v1alpha1.RestoreFailed || phase == v1alpha1.RestoreInvalid {
		status = entity.RestoreStatus_FAILED
	}

	return status
}

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

// For PD, TiDB, TiKV, TiFlash components in nowaday DBaaS system
// failover and scale in/out should all consider as scaling status
// in tidb-operator, only running pod or failed after running pod will added to Members
// so just use members != desirereplicas can judge weather the component in scale phase
func PDMembersStatus(tc *v1alpha1.TidbCluster) ComponentStatus {
	lowerLimit := int(tc.Spec.PD.Replicas/2 + 1)
	var healthyNum, unhealthyNum int

	for _, member := range tc.Status.PD.Members {
		if member.Health {
			healthyNum++
		} else {
			unhealthyNum++
		}
	}

	if healthyNum < lowerLimit {
		return ComponentUnavailable
	}

	if tc.PDUpgrading() {
		return ComponentUpgrading
	}

	if len(tc.Status.PD.Members) != int(tc.PDStsDesiredReplicas()) {
		return ComponentScaling
	}

	if len(tc.Status.PD.FailureMembers) > 0 || unhealthyNum > 0 {
		return ComponentUnhealthy
	}

	return ComponentNormal
}

func TiDBMembersStatus(tc *v1alpha1.TidbCluster) (ComponentStatus, uint32) {
	var healthyNum, unhealthyNum uint32

	for _, member := range tc.Status.TiDB.Members {
		if member.Health {
			healthyNum++
		} else {
			unhealthyNum++
		}
	}

	if healthyNum < 1 {
		return ComponentUnavailable, healthyNum
	}

	if tc.TiDBUpgrading() {
		return ComponentUpgrading, healthyNum
	}

	if len(tc.Status.TiDB.Members) != int(tc.TiDBStsDesiredReplicas()) {
		return ComponentScaling, healthyNum
	}

	if len(tc.Status.TiDB.FailureMembers) > 0 || unhealthyNum > 0 {
		return ComponentUnhealthy, healthyNum
	}

	return ComponentNormal, healthyNum
}

func TiKVStoresStatus(tc *v1alpha1.TidbCluster) (ComponentStatus, uint32) {
	var healthyNum, unhealthyNum uint32
	for _, store := range tc.Status.TiKV.Stores {
		if store.State == v1alpha1.TiKVStateUp {
			healthyNum++
		}

		if store.State == v1alpha1.TiKVStateDown && !store.LastTransitionTime.IsZero() {
			deadline := store.LastTransitionTime.Add(tikvFailoverPeriod)
			if time.Now().After(deadline) {
				unhealthyNum++
			}
		}
	}

	if healthyNum < 2 {
		return ComponentUnavailable, healthyNum
	}

	if tc.TiKVUpgrading() {
		return ComponentUpgrading, healthyNum
	}

	if len(tc.Status.TiKV.Stores) != int(tc.TiKVStsDesiredReplicas()) {
		return ComponentScaling, healthyNum
	}

	if unhealthyNum > 0 {
		return ComponentUnhealthy, healthyNum
	}

	return ComponentNormal, healthyNum
}

func TiFlashStoresStatus(tc *v1alpha1.TidbCluster) (ComponentStatus, uint32) {
	var healthyNum, unhealthyNum uint32
	for _, store := range tc.Status.TiFlash.Stores {
		if store.State == v1alpha1.TiKVStateUp {
			healthyNum++
		}

		if store.State == v1alpha1.TiKVStateDown && !store.LastTransitionTime.IsZero() {
			deadline := store.LastTransitionTime.Add(tiflashFailoverPeriod)
			if time.Now().After(deadline) {
				unhealthyNum++
			}
		}
	}

	if tc.TiFlashUpgrading() {
		return ComponentUpgrading, healthyNum
	}

	if len(tc.Status.TiFlash.Stores) != int(tc.TiFlashStsDesiredReplicas()) {
		return ComponentScaling, healthyNum
	}

	if unhealthyNum > 0 {
		return ComponentUnhealthy, healthyNum
	}

	return ComponentNormal, healthyNum
}

func GetCentralFrontendDns() string {
	return config.Global().ServerConfig.FrontendDNS
}

func GetCentralBackendDns() string {
	return config.Global().ServerConfig.BackendDNS
}

func GetPocManualBackupReservedDays() int {
	return *config.Global().ServerConfig.PocManualBackupReservedDays
}

func GetPocAutoBackupReservedDays() int {
	return *config.Global().ServerConfig.PocAutoBackupReservedDays
}

func GetOnDemandAutoBackupReservedDays() int {
	return *config.Global().ServerConfig.OnDemandAutoBackupReservedDays
}

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 EstimateRestoreTime(tikvCount uint32, clusterProfile string, restoreSize int64) uint32 {
	// if tikvCount is zero, default to 3 to use for avoid divide 0 error
	if tikvCount == 0 {
		tikvCount = 3
	}
	var restoreSpeed uint32
	if clusterProfile == "standard" {
		restoreSpeed = StandardRestoreSpeed
	} else if clusterProfile == "high" {
		restoreSpeed = HighRestoreSpeed
	} else {
		return MinimunAlertSecond
	}

	return (uint32(restoreSize) * 2) / (tikvCount * restoreSpeed)
}

// Get Default auto backup time via time zone, assume the default backup time is 2:00
// so backup time in GMT should be (2 + 24 - utcoffset) % 24
func GetDefaultAutoBackupTime(timeZone int32) string {
	locTime := (-timeZone + 26) % 24
	if locTime >= 10 {
		return fmt.Sprintf("%d:00", locTime)
	}
	return fmt.Sprintf("0%d:00", locTime)
}

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 {
	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
}

func ValidateScaleComponent(req *central.ScaleComponentReq) error {
	if req.Components == nil {
		return errors.New("Components in ScaleComponentReq can not be nil")
	}
	for _, comp := range req.Components {
		if comp.ComponentName == component.TiKV.String() && comp.Replicas%3 != 0 {
			return errors.New("tikv replicas should be divided by 3")
		}
	}
	return nil
}

func ConvertTC2ClusterConfiguration(tc *v1alpha1.TidbCluster) *entity.ClusterConfigurations {
	dbName := tc.GetName()
	if tc.Spec.TiFlash != nil && tc.Spec.TiFlash.Config != nil {
		return &entity.ClusterConfigurations{
			Configurations: map[string]entity.ClusterConfiguration{
				dbName: {
					PDConfig:      tc.Spec.PD.Config,
					TiDBConfig:    tc.Spec.TiDB.Config,
					TiKVConfig:    tc.Spec.TiKV.Config,
					TiFlashConfig: tc.Spec.TiFlash.Config,
				},
			},
		}
	}
	return &entity.ClusterConfigurations{
		Configurations: map[string]entity.ClusterConfiguration{
			dbName: {
				PDConfig:   tc.Spec.PD.Config,
				TiDBConfig: tc.Spec.TiDB.Config,
				TiKVConfig: tc.Spec.TiKV.Config,
			},
		},
	}
}

func GetBackupExpireTime(plan entity.TenantPlan, backupType entity.BackupType) (time.Duration, error) {
	if plan == entity.TenantPlanPOC && backupType == entity.BackupType_Manual {
		return time.Duration(GetPocManualBackupReservedDays()) * 24 * time.Hour, nil
	} else if plan == entity.TenantPlanPOC && backupType == entity.BackupType_Auto {
		return time.Duration(GetPocAutoBackupReservedDays()) * 24 * time.Hour, nil
	} else if plan == entity.TenantPlanOnDemand && backupType == entity.BackupType_Auto {
		return time.Duration(GetOnDemandAutoBackupReservedDays()) * 24 * time.Hour, nil
	}
	return time.Second, errors.New("not supported plan and type")
}

func GetProxyBaseURL(ip, serviceRoute string) string {
	return fmt.Sprintf("https://%s:443/proxy/central/%s", ip, serviceRoute)
}

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)
}
