package k8supgrade

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	gkev1beta2 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta2"
	"github.com/pingcap/log"
	"github.com/samber/lo"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	clustercontrollerv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	regionalpb "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/nodepool"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/timestamppb"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/vpc"
)

const (
	managedNodeGroupMaxSkewVersion = 2
	nodePoolMaxSkewVersion         = 2
)

func managedNodeGroups2PB(ngList manualv1alpha1.NodeGroupList) []*regionalpb.NodeGroup {
	ret := make([]*regionalpb.NodeGroup, 0, len(ngList.Items))
	for _, item := range ngList.Items {
		ng := &regionalpb.NodeGroup{
			Kind:           string(cloudv1.AdminNodes),
			Version:        item.Status.AtProvider.Version,
			MaxSkewVersion: managedNodeGroupMaxSkewVersion,
		}
		if item.Status.AtProvider.ScalingConfig.DesiredSize != nil {
			ng.Count = uint32(*item.Status.AtProvider.ScalingConfig.DesiredSize)
		}
		ret = append(ret, ng)
	}
	return ret
}

// FIXME(huaxi): simplify this
func infraInfo2PB(info *clustercontrollerv1.InfraInformation) *regionalpb.TiDBClusterInfraInformation {
	// TODO: use string type
	var phase string
	switch info.Phase {
	case clustercontrollerv1.InfraInformation_MODIFYING:
		phase = "modifying"
	case clustercontrollerv1.InfraInformation_RUNNING:
		phase = "running"
	case clustercontrollerv1.InfraInformation_DELETING:
		phase = "deleting"
	case clustercontrollerv1.InfraInformation_UNAVAILABLE:
		phase = "unavailable"
	case clustercontrollerv1.InfraInformation_PENDING:
		phase = "pending"
	case clustercontrollerv1.InfraInformation_PAUSED:
		phase = "paused"
	case clustercontrollerv1.InfraInformation_CREATING:
		phase = "creating"
	}

	nodes := make([]*regionalpb.NodeGroup, 0, len(info.ComponentNodes))
	for _, ng := range info.ComponentNodes {
		var ngKind string
		switch ng.Kind {
		case clustercontrollerv1.NodeGroup_TIDB_NODES:
			ngKind = string(cloudv1.TiDBNodes)
		case clustercontrollerv1.NodeGroup_CDC_NODES:
			ngKind = string(cloudv1.CDCNodes)
		case clustercontrollerv1.NodeGroup_DM_NODES:
			ngKind = string(cloudv1.DMNodes)
		default:
			log.Info("unknown node group kind", zap.Any("ng", ng))
			continue
		}

		nodes = append(nodes, &regionalpb.NodeGroup{
			Kind:           ngKind,
			Count:          ng.Count,
			Version:        ng.Version,
			MaxSkewVersion: ng.MaxSkewVersion,
		})
	}

	return &regionalpb.TiDBClusterInfraInformation{
		ClusterId:      info.ClusterId,
		Version:        info.Version,
		Phase:          phase,
		Topology:       info.Topology,
		ComponentNodes: nodes,
		Notes:          info.Notes,
	}
}

func getCloudAccountFromAppBox(ctx context.Context, reader vpc.VPCReader, box *v1alpha1.APPBox) (string, error) {
	if box.Spec.VPCRef != nil {
		var v v1alpha1.VPC
		if err := reader.Get(ctx, client.ObjectKey{
			Namespace: box.Spec.VPCRef.Namespace,
			Name:      box.Spec.VPCRef.Name,
		}, &v); err != nil {
			log.Error("failed to get vpc", zap.Error(err), zap.String("appbox", box.Name))
			return "", err
		}

		if v.Spec.AccountRef != nil {
			return v.Spec.AccountRef.AccountID, nil
		}
	}

	return "", fmt.Errorf("VPCRef or AccountRef is nil")
}

// getNodesInfoFromGKECR gets admin nodes and tidb nodes information from gke CR.
func getNodesInfoFromGKECR(ctx context.Context, gcpCli gcp.Client, gke *gkev1beta2.Cluster) (adminNodes []*regionalpb.NodeGroup, sharedTiDBNodes *regionalpb.NodeGroup, compNodesMap map[string]map[string]*regionalpb.NodeGroup, notes []string) {
	componentNameMap := map[string]string{
		"dm":    string(cloudv1.DMNodes),
		"ticdc": string(cloudv1.CDCNodes),
	}
	tidbNodesVersionSet := make(map[string]struct{})
	compNodesMap = make(map[string]map[string]*regionalpb.NodeGroup)

	for _, np := range gke.Status.AtProvider.NodePools {
		if np != nil && np.Config != nil {
			tidbComponent, component, clusterId := np.Config.Labels["tidb-component"], np.Config.Labels["component"], np.Config.Labels["cluster"]
			count, err := nodepool.GetNodeCount(ctx, gcpCli, np.InstanceGroupUrls)
			if err != nil {
				log.Error("failed to get node count", zap.String("nodepool", np.Name), zap.Error(err))
			}

			if np.Config.Labels[consts.LabelKeyNodeGroupType] == consts.LabelValNodeGroupTypeControlPlane || np.Config.Labels["usedby"] == "dbaas-control-plane-shoot" {
				adminNodes = append(adminNodes, &regionalpb.NodeGroup{
					Kind:           string(cloudv1.AdminNodes),
					Version:        np.Version,
					MaxSkewVersion: nodePoolMaxSkewVersion,
					Count:          uint32(count),
				})
			} else {
				if component == "" && tidbComponent != "" {
					component = tidbComponent
				}
				switch component {
				case "":
					tidbNodesVersionSet[np.Version] = struct{}{}
					sharedTiDBNodes = &regionalpb.NodeGroup{
						Kind:           string(cloudv1.TiDBNodes),
						Version:        np.Version,
						MaxSkewVersion: nodePoolMaxSkewVersion,
					}
				default:
					nodeKind, ok := componentNameMap[component]
					if !ok {
						nodeKind = "unknown"
						notes = append(notes, fmt.Sprintf("%s node group found: %s(version: %s, node count: %d)", component, np.Name, np.Version, count))
					}
					if compNodesMap[nodeKind] == nil {
						compNodesMap[nodeKind] = make(map[string]*regionalpb.NodeGroup)
					}
					n, ok := compNodesMap[nodeKind][clusterId]
					if ok {
						n.Count += uint32(count)
					} else {
						compNodesMap[nodeKind][clusterId] = &regionalpb.NodeGroup{
							Kind:           nodeKind,
							Version:        np.Version,
							MaxSkewVersion: nodePoolMaxSkewVersion,
							Count:          uint32(count),
						}
					}
				}
			}
		}
	}

	if len(tidbNodesVersionSet) > 1 {
		versions := lo.Keys(tidbNodesVersionSet)
		sort.Strings(versions)
		notes = append(notes, fmt.Sprintf(NoteMultipleVersions, strings.Join(versions, ",")))
	}
	if sharedTiDBNodes == nil {
		notes = append(notes, NoteNoTidbNodes)
	}
	return adminNodes, sharedTiDBNodes, compNodesMap, notes
}

func pbUpgradeOperation2UpgradeOperation(pbUO *regionalpb.UpgradeOperation) (*v1alpha1.UpgradeOperation, error) {
	duration, err := time.ParseDuration(pbUO.Duration)
	if err != nil {
		return nil, fmt.Errorf("failed to parse duration %s: %w", pbUO.Duration, err)
	}
	ttl, err := time.ParseDuration(pbUO.Ttl)
	if err != nil {
		return nil, fmt.Errorf("failed to parse ttl %s: %w", pbUO.Ttl, err)
	}

	mode := pbUO.MaintenancePolicyMode
	if mode == "" {
		mode = v1alpha1.MaintenancePolicyModeLegacy
	}
	switch mode {
	case v1alpha1.MaintenancePolicyModeLegacy, v1alpha1.MaintenancePolicyModeClusterMaintenance:
	default:
		return nil, fmt.Errorf("invalid maintenance_policy_mode %q: must be %q or %q",
			pbUO.MaintenancePolicyMode, v1alpha1.MaintenancePolicyModeLegacy, v1alpha1.MaintenancePolicyModeClusterMaintenance)
	}

	return &v1alpha1.UpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:        pbUO.Name,
			Annotations: cloneStringMap(pbUO.Annotations),
		},
		Spec: v1alpha1.UpgradeOperationSpec{
			Schedule:              pbUO.Schedule,
			Duration:              &metav1.Duration{Duration: duration},
			Paused:                false,
			TTL:                   &metav1.Duration{Duration: ttl},
			AppBoxName:            pbUO.AppboxName,
			Steps:                 convertSteps(pbUO.Steps),
			MaintenancePolicyMode: mode,
		},
	}, nil
}

func convertSteps(steps []*regionalpb.UpgradeStep) v1alpha1.Steps {
	ret := v1alpha1.NewSteps()
	for _, step := range steps {
		ret.Push(v1alpha1.Step{
			Target:  v1alpha1.UpgradeOperationTarget(step.Target),
			Version: step.Version,
			Cluster: step.ClusterId,
			Options: step.Options,
		})
	}
	return ret
}

func stepPhases2PB(phases []v1alpha1.StepPhase) []*regionalpb.StepPhase {
	if len(phases) == 0 {
		return nil
	}
	out := make([]*regionalpb.StepPhase, 0, len(phases))
	for i := range phases {
		p := &phases[i]
		pb := &regionalpb.StepPhase{
			Name:         p.Name,
			ProgressInfo: p.ProgressInfo,
			Done:         int32(p.Done),
			Total:        int32(p.Total),
			MaxDone:      int32(p.MaxDone),
		}
		if p.BeginTime != nil {
			pb.BeginTime = timestamppb.New(p.BeginTime.Time)
		}
		if p.EndTime != nil {
			pb.EndTime = timestamppb.New(p.EndTime.Time)
		}
		if p.LastProgressTime != nil {
			pb.LastProgressTime = timestamppb.New(p.LastProgressTime.Time)
		}
		out = append(out, pb)
	}
	return out
}

func step2PB(step *v1alpha1.Step) *regionalpb.UpgradeStep {
	if step == nil {
		return nil
	}
	pbStep := &regionalpb.UpgradeStep{
		Target:    string(step.Target),
		Version:   step.Version,
		ClusterId: step.Cluster,
		Options:   step.Options,
		Duration:  step.Duration,
		Phases:    stepPhases2PB(step.Phases),
	}
	if step.BeginTime != nil {
		pbStep.BeginTime = timestamppb.New(step.BeginTime.Time)
	}
	if step.EndTime != nil {
		pbStep.EndTime = timestamppb.New(step.EndTime.Time)
	}
	if step.LastProgressTime != nil {
		pbStep.LastProgressTime = timestamppb.New(step.LastProgressTime.Time)
	}
	return pbStep
}

func steps2PB(steps v1alpha1.Steps) []*regionalpb.UpgradeStep {
	if len(steps) == 0 {
		return nil
	}
	ret := make([]*regionalpb.UpgradeStep, 0, len(steps))
	for i := range steps {
		ret = append(ret, step2PB(&steps[i]))
	}
	return ret
}

func cloneStringMap(in map[string]string) map[string]string {
	if len(in) == 0 {
		return nil
	}
	out := make(map[string]string, len(in))
	for k, v := range in {
		out[k] = v
	}
	return out
}

func mergeAnnotations(existing, desired map[string]string) map[string]string {
	if len(desired) == 0 {
		return existing
	}
	out := cloneStringMap(existing)
	if out == nil {
		out = make(map[string]string, len(desired))
	}
	for k, v := range desired {
		out[k] = v
	}
	return out
}

func upgradeOperation2PB(uo *v1alpha1.UpgradeOperation) *regionalpb.UpgradeOperation {
	var begin, end *timestamppb.Timestamp
	if uo.Status.BeginTime != nil {
		begin = timestamppb.New(uo.Status.BeginTime.Time)
	}
	if uo.Status.EndTime != nil {
		end = timestamppb.New(uo.Status.EndTime.Time)
	}

	return &regionalpb.UpgradeOperation{
		Name:                  uo.Name,
		Schedule:              uo.Spec.Schedule,
		Duration:              uo.Spec.Duration.String(),
		Ttl:                   uo.Spec.TTL.String(),
		AppboxName:            uo.Spec.AppBoxName,
		MaintenancePolicyMode: uo.EffectiveMaintenancePolicyMode(),
		Annotations:           cloneStringMap(uo.Annotations),
		Status: &regionalpb.UpgradeOperationStatus{
			Phase:         string(uo.Status.Phase),
			Reason:        uo.Status.Reason,
			BeginTime:     begin,
			EndTime:       end,
			CurrentStep:   step2PB(uo.Status.CurrentStep),
			NextSteps:     steps2PB(uo.Status.NextSteps),
			FinishedSteps: steps2PB(uo.Status.FinishedSteps),
		},
	}
}
