package generator

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

	"github.com/go-logr/logr"
	"github.com/go-logr/zapr"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"go.uber.org/zap"

	entity "github.com/tidbcloud/devops/entity/k8supgrade"
	"github.com/tidbcloud/devops/pkg/infra/config"
	"github.com/tidbcloud/devops/pkg/infra/log"
)

// defaultTTL is the default time to live for an UpgradeOperation.
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
const defaultTTL = "2160h" // 90 days

const (
	o11yK8sNodes  = "o11y-k8s-nodes"
	o11yTiDBNodes = "o11y-tidb-nodes"
)

type gen struct {
	logger logr.Logger
	cfg    *config.RootConfig
}

func New(cfg *config.RootConfig) API {
	zapLog, _ := zap.NewDevelopment()
	return &gen{
		logger: zapr.NewLogger(zapLog),
		cfg:    cfg,
	}
}

func (g *gen) GenerateUpgradeOperationKey(info *entity.K8sInfo, task entity.ReadOnlyTask) (name, ns string) {
	name = fmt.Sprintf("%d-%s", task.ID(), info.K8sName)
	if info.Is3LayerArch {
		return name, info.AppboxNamespace
	}

	if parts := strings.Split(info.K8sName, "-"); len(parts) > 2 {
		if len(parts[1]) != 2 {
			// staging and prod
			ns = fmt.Sprintf("%s-%s", parts[0], parts[1])
		} else {
			// dev
			ns = fmt.Sprintf("%s-%s-%s", parts[0], parts[1], parts[2])
		}
	}
	return name, ns
}

// GenerateUpgradeOperation generates an UpgradeOperation CR based on k8s information.
func (g *gen) GenerateUpgradeOperation(ctx context.Context, info *entity.K8sInfo, name, ns string) (uo *entity.UpgradeOperation, desiredVersion string) {
	var steps []*entity.UpgradeStep
	steps, desiredVersion = g.generateUpgradeSteps(ctx, info)

	uo = &entity.UpgradeOperation{
		Name:                  name,
		Namespace:             ns,
		Steps:                 steps,
		TTL:                   defaultTTL,
		Duration:              estimateDuration(info, steps).String(),
		MaintenancePolicyMode: g.maintenancePolicyMode(info.TenantID, info.ProjectID),
		Annotations:           map[string]string{},
	}

	if info.Is3LayerArch {
		uo.AppboxName = info.AppboxName
	} else {
		uo.AppboxName = info.GetNetworkName()
	}

	return uo, desiredVersion
}

func (g *gen) maintenancePolicyMode(tenantID, projectID string) string {
	if g.cfg.Services.FeatureGates.K8sUpgradeClusterMaintenance.Enabled(tenantID, projectID) {
		return entity.MaintenancePolicyModeClusterMaintenance
	}
	return entity.MaintenancePolicyModeLegacy
}

// generateUpgradeSteps generates the steps needed to upgrade a Kubernetes cluster and returns the desired version.
// It ensures that the control plane and all components are upgraded to the current control plane version
// plus 2 minor versions.
func (g *gen) generateUpgradeSteps(ctx context.Context, info *entity.K8sInfo) (steps []*entity.UpgradeStep, desiredVersion string) {
	logger := log.FromContextOrDef(ctx, g.logger, "k8sName", info.K8sName)
	var (
		versions      = make([]entity.Version, 0, 5)
		adminNodes    entity.Node
		hasAdminNodes bool
	)

	// Calculate the base version from the minimum version number across all nodes.
	controlPlaneVer := info.ControlPlaneVersion
	versions = append(versions, controlPlaneVer)
	for _, node := range info.AdminNodes {
		if node.Kind == string(cloudv1.AdminNodes) {
			adminNodes = *node
			hasAdminNodes = true
			versions = append(versions, adminNodes.Version)
			break
		}
	}
	for _, cluster := range info.TiDBClusters {
		for _, node := range cluster.ComponentNodes {
			versions = append(versions, node.Version)
		}
	}
	o11yK8sNode, hasO11yK8sNode := findO11yK8sNode(info)
	if hasO11yK8sNode {
		versions = append(versions, o11yK8sNode.Version)
	}

	// Check if there are invalid versions.
	for _, v := range versions {
		if v.Major <= 0 {
			logger.Error(nil, "should not have invalid version, will stop the generation", "version", v.String())
			return nil, ""
		}
	}
	baseVersion := entity.FindMinimalVersion(versions...)

	// All components must be upgraded to the current control plane version plus 2 minor versions.
	targetVersion := controlPlaneVer.IncrementMinor(2)
	logger = logger.WithValues("targetVersion", targetVersion.String())

	// needUpgrade reports whether a node should be upgraded to the given version:
	// either the node has reached the max allowed version skew, or the given version
	// is the final (target) version and the node is still behind it.
	needUpgrade := func(version entity.Version, node entity.Node) bool {
		return version.Sub(node.Version) == effectiveMaxSkewVersion(node.MaxSkewVersion) ||
			(version.Equal(targetVersion) && node.Version.LessThan(targetVersion))
	}

	// Add upgrade steps sequentially until all components reach the target version.
	steps = make([]*entity.UpgradeStep, 0, 10)
	hasOldNodepoolsLeft := false
	for version := baseVersion.IncrementMinor(1); version.LessThanOrEqual(targetVersion); version = version.IncrementMinor(1) {
		// If the control plane version is less than the current version, an upgrade is needed.
		if version.Sub(controlPlaneVer) == 1 {
			// If there has old nodepools left before this control plane version,
			// we need to upgrade the empty node pools first, because old nodepools
			// may cause the upgrade failure due to the version skew.
			if hasOldNodepoolsLeft {
				steps = append(steps, &entity.UpgradeStep{
					Target:  string(cloudv1.EmptyNodePools),
					Version: controlPlaneVer.String(),
					Options: map[string]string{
						"delay": "10m", // delay 10 minutes for old version nodepools to be scaled in automatically
					},
				})
				hasOldNodepoolsLeft = false
			}
			steps = append(steps, &entity.UpgradeStep{
				Target:  string(cloudv1.ControlPlane),
				Version: version.String(),
			})
			controlPlaneVer = version
		}

		// Only consider there is only one admin node group for now.
		if hasAdminNodes && needUpgrade(version, adminNodes) {
			steps = append(steps, &entity.UpgradeStep{
				Target:  string(cloudv1.AdminNodes),
				Version: version.String(),
			})
			adminNodes.Version = version
		}

		// o11y-k8s-nodes is a Kubernetes-cluster-level node group. Keep it at or
		// below the already-planned control plane version, same as component nodes.
		if hasO11yK8sNode && needUpgrade(version, o11yK8sNode) {
			steps = append(steps, &entity.UpgradeStep{
				Target:  o11yK8sNodes,
				Version: version.String(),
			})
			o11yK8sNode.Version = version
		}

		for _, cluster := range info.TiDBClusters {
			for _, node := range cluster.ComponentNodes {
				if node.Kind == "" {
					logger.Info("skip component node upgrade step because node kind is empty", "clusterID", cluster.ID, "node", fmt.Sprintf("%+v", *node))
					continue
				}
				if needUpgrade(version, *node) {
					if node.Kind == string(cloudv1.CDCNodes) || (node.Kind == string(cloudv1.TiDBNodes) && info.CloudProvider == cloudv1.ProviderGCP) {
						hasOldNodepoolsLeft = true
					}
					steps = append(steps, &entity.UpgradeStep{
						Target:    node.Kind,
						Version:   version.String(),
						ClusterId: cluster.ID,
					})
					node.Version = version
					logger.Info("added component nodes upgrade step", "kind", node.Kind, "cluster", cluster.ID)
				}
			}
		}
	}

	step := g.generateStepForEKSAddons(info, controlPlaneVer)
	if step != nil {
		steps = append(steps, step)
	}

	if hasOldNodepoolsLeft {
		steps = append(steps, &entity.UpgradeStep{
			Target:  string(cloudv1.EmptyNodePools),
			Version: controlPlaneVer.String(),
			Options: map[string]string{
				"delay": "10m",
			},
		})
	}
	return steps, controlPlaneVer.String()
}

// estimateDuration estimates the duration of the upgrade operation based on the steps.
func estimateDuration(info *entity.K8sInfo, steps []*entity.UpgradeStep) time.Duration {
	var duration time.Duration

	var adminNodeCount int

	for _, adminNode := range info.AdminNodes {
		adminNodeCount += adminNode.Count
	}

	tidbClusterNodeCountMap := make(map[string]int)
	dmNodeCount := make(map[string]int)
	cdcNodeCount := make(map[string]int)
	for _, cluster := range info.TiDBClusters {
		for _, cnt := range cluster.Topology {
			tidbClusterNodeCountMap[cluster.ID] += cnt
		}
		for _, node := range cluster.ComponentNodes {
			switch node.Kind {
			case string(cloudv1.DMNodes):
				dmNodeCount[cluster.ID] += node.Count
			case string(cloudv1.CDCNodes):
				cdcNodeCount[cluster.ID] += node.Count
			}
		}
	}

	for _, step := range steps {
		switch cloudv1.UpgradeOperationTarget(step.Target) {
		case cloudv1.ControlPlane:
			switch info.CloudProvider {
			case cloudv1.ProviderAWS:
				duration += 15 * time.Minute
			case cloudv1.ProviderGCP:
				duration += 35 * time.Minute
			}

		case cloudv1.AdminNodes:
			duration += 5 * time.Minute * time.Duration(adminNodeCount)

		case cloudv1.TiDBNodes:
			cnt := tidbClusterNodeCountMap[step.ClusterId]
			if cnt > 0 {
				// extra tidb
				duration += 5*time.Minute*time.Duration(cnt) + 5*time.Minute
			}

		case cloudv1.DMNodes:
			duration += 5 * time.Minute * time.Duration(dmNodeCount[step.ClusterId])

		case cloudv1.CDCNodes:
			duration += 5 * time.Minute * time.Duration(cdcNodeCount[step.ClusterId])

		case cloudv1.EmptyNodePools:
			delay := 10 * time.Minute
			if step.Options != nil {
				if d, err := time.ParseDuration(step.Options["delay"]); err == nil {
					delay += d
				}
			}
			duration += delay

		case "addons", cloudv1.AddOns:
			duration += 10 * time.Minute

		case o11yK8sNodes, o11yTiDBNodes:
			duration += 10 * time.Minute
		}
	}

	return duration
}

func GetDesiredVersion(uo *entity.UpgradeOperation) string {
	if uo == nil || len(uo.Steps) == 0 {
		return ""
	}
	return uo.Steps[len(uo.Steps)-1].Version
}

func findO11yK8sNode(info *entity.K8sInfo) (entity.Node, bool) {
	for _, node := range info.O11YK8SNodes {
		if node.Kind == o11yK8sNodes {
			return *node, true
		}
	}
	return entity.Node{}, false
}

func effectiveMaxSkewVersion(v int) int {
	if v <= 0 {
		return 2
	}
	return v
}

// generateStepForEKSAddons generates the step for EKS addons upgrade.
func (g *gen) generateStepForEKSAddons(info *entity.K8sInfo, controlPlaneVer entity.Version) *entity.UpgradeStep {
	// Only EKS addons need to be upgraded on AWS.
	if info.CloudProvider != cloudv1.CloudProviderAWS {
		return nil
	}

	if info.Is3LayerArch {
		return &entity.UpgradeStep{
			Target:  "addons",
			Version: controlPlaneVer.String(),
		}
	}

	addons, ok := g.cfg.Services.K8sUpgrade.GeneratorConfig.EKSAddonsConfig[controlPlaneVer.String()]
	if ok && len(addons) > 0 {
		step := &entity.UpgradeStep{
			Target:  string(cloudv1.AddOns),
			Version: controlPlaneVer.String(),
			Options: make(map[string]string, len(addons)),
		}
		for _, addon := range addons {
			step.Options[addon.Name] = addon.Version
		}
		return step
	}
	return nil
}
