package opsservice

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"

	"github.com/google/uuid"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"

	o11yv1 "github.com/tidbcloud/observability/api/regionalserver/resourceworker/v1"
	"github.com/tidbcloud/observability/pkg/common/message"
	"github.com/tidbcloud/observability/pkg/resourcemanager"
	"github.com/tidbcloud/observability/pkg/resourceworker/controllers/clusterupgradeoperation"
	"github.com/tidbcloud/observability/pkg/resourceworker/controllers/clusterupgradeoperation/providers"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	defaultNamespace        = "observability"
	defaultVMStorageSTSName = "vmstorage-vmcluster"
	defaultVMStorageSTSNS   = defaultNamespace
)

// UpgradeOperationService provides operations for managing cluster upgrade operations
type UpgradeOperationService struct {
	rw resourcemanager.UpgradeOperationReaderWriter
}

var upgradeOpService *UpgradeOperationService
var upgradeOpServiceOnce sync.Once

// GetUpgradeOpService returns the singleton cluster upgrade service instance
func GetUpgradeOpService() *UpgradeOperationService {
	upgradeOpServiceOnce.Do(func() {
		upgradeOpService = &UpgradeOperationService{
			rw: resourcemanager.GetClusterUpgradeOperationReaderWriter(),
		}
	})
	return upgradeOpService
}

// CreateUpgrade creates a new cluster upgrade operation
func (s *UpgradeOperationService) CreateUpgrade(ctx context.Context, req message.CreateClusterUpgradeReq) (message.CreateClusterUpgradeResp, error) {
	operationID := uuid.New().String()

	// Build upgrade steps based on the request
	steps, err := s.buildUpgradeSteps(ctx, req)
	if err != nil {
		return message.CreateClusterUpgradeResp{}, fmt.Errorf("failed to build upgrade steps: %w", err)
	}

	// Determine schedule time
	scheduleTime := metav1.Now()
	if req.Schedule != nil {
		scheduleTime = metav1.Time{Time: *req.Schedule}
	}

	// Create the ClusterUpgradeOperation CR
	operation := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:      fmt.Sprintf("upgrade-%s", operationID[:8]),
			Namespace: defaultNamespace,
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			Paused:         false,
			Schedule:       scheduleTime,
			K8sClusterName: req.K8sClusterName,
			Region:         req.Region,
			TargetVersion:  req.TargetVersion,
			CloudProvider:  req.CloudProvider,
			Steps:          steps,
		},
	}

	if err := s.rw.Create(ctx, operation); err != nil {
		return message.CreateClusterUpgradeResp{}, fmt.Errorf("failed to create ClusterUpgradeOperation: %w", err)
	}

	return message.CreateClusterUpgradeResp{
		ID:      operation.Name,
		Status:  message.ClusterUpgradeStatusPending,
		Message: "Cluster upgrade operation created successfully",
	}, nil
}

// buildUpgradeSteps builds the upgrade steps based on the request
// It ensures that control plane upgrades are done one minor version at a time,
// and checks node group compatibility at each step
func (s *UpgradeOperationService) buildUpgradeSteps(ctx context.Context, req message.CreateClusterUpgradeReq) (o11yv1.UpgradeSteps, error) {
	var steps o11yv1.UpgradeSteps

	// Get cloud provider to check node group versions
	var k8sClient client.Client
	if s.rw != nil {
		k8sClient = s.rw.GetClient()
	}
	providerRegistry, err := providers.NewCloudProviderRegistry(req.CloudProvider, req.Region, k8sClient)
	if err != nil {
		return nil, fmt.Errorf("failed to create cloud provider registry: %w", err)
	}

	provider, err := providerRegistry.Get(req.CloudProvider)
	if err != nil {
		return nil, fmt.Errorf("failed to get cloud provider: %w", err)
	}

	if len(req.ClusterAddons) > 0 {
		if req.CloudProvider != o11yv1.CloudProviderEKS && req.CloudProvider != o11yv1.CloudProviderACK && req.CloudProvider != o11yv1.CloudProviderMock {
			return nil, fmt.Errorf("cluster addons are only supported for EKS or ACK")
		}
	}

	// Get current control plane version from cluster
	currentVersion, err := provider.GetControlPlaneVersion(ctx, req.K8sClusterName)
	if err != nil {
		return nil, fmt.Errorf("failed to get control plane version: %w", err)
	}

	// Get all node groups to check their versions
	nodeGroupsToCheck, err := provider.GetNodeGroupsByLabels(ctx, req.K8sClusterName, map[string]string{})
	if err != nil {
		return nil, fmt.Errorf("failed to get all node groups: %w", err)
	}

	if err := checkNodeGroupsCompatible(req, nodeGroupsToCheck, req.TargetVersion); err != nil {
		return nil, fmt.Errorf("couldn't upgrade cluster: %w", err)
	}

	// Calculate intermediate versions (one minor version at a time)
	intermediateVersions, err := calculateIntermediateVersions(currentVersion, req.TargetVersion)
	if err != nil {
		return nil, fmt.Errorf("failed to calculate intermediate versions: %w", err)
	}

	if req.CloudProvider == o11yv1.CloudProviderACK && len(intermediateVersions) > 1 {
		return nil, fmt.Errorf("couldn't upgrade cluster, the maxVersionDiff is 1 for ACK")
	}

	// Step: Upgrade control plane to specified version one by one
	for _, version := range intermediateVersions {
		opts := o11yv1.ControlPlaneUpgradeOptions{
			TargetVersion: version,
		}
		optsRaw, err := toRawExtension(opts)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal control plane options: %w", err)
		}
		steps.Push(o11yv1.UpgradeStep{
			Processor: o11yv1.ProcessorUpgradeControlPlane,
			Options:   optsRaw,
		})
	}

	// Step: Upgrade cluster addons after the control plane reaches the target version.
	if len(req.ClusterAddons) > 0 {
		for _, addon := range req.ClusterAddons {
			addonUpgrade := o11yv1.AddonUpgrade{
				Name:                  addon.Name,
				Version:               addon.Version,
				ConfigurationValues:   addon.ConfigurationValues,
				ResolveConflicts:      addon.ResolveConflicts,
				ServiceAccountRoleArn: addon.ServiceAccountRoleArn,
			}
			addonsOptions := o11yv1.ClusterAddonsUpgradeOptions{
				Addons: []o11yv1.AddonUpgrade{addonUpgrade},
			}
			addonsOptionsRaw, err := toRawExtension(addonsOptions)
			if err != nil {
				return nil, fmt.Errorf("failed to marshal addons options: %w", err)
			}
			steps.Push(o11yv1.UpgradeStep{
				Processor: o11yv1.ProcessorUpgradeClusterAddons,
				Options:   addonsOptionsRaw,
			})
		}
	}

	// Step: Upgrade node groups to target version (if node group upgrade config specified)
	// This ensures node groups are upgraded to the final target version after all control plane upgrades
	if len(req.NodeGroupUpgrades) > 0 {
		options, err := buildNodeGroupUpgradeOptions(req)
		if err != nil {
			return nil, fmt.Errorf("failed to build node group upgrade options: %w", err)
		}
		for _, option := range options {
			steps.Push(o11yv1.UpgradeStep{
				Processor: o11yv1.ProcessorUpgradeNodeGroup,
				Options:   option,
			})
		}
	}

	return steps, nil
}

// GetUpgrade retrieves details of a specific cluster upgrade operation
func (s *UpgradeOperationService) GetUpgrade(ctx context.Context, req message.GetClusterUpgradeReq) (message.GetClusterUpgradeResp, error) {
	op, err := s.rw.Get(ctx, defaultNamespace, req.ID) // TODO: make namespace configurable
	if err != nil {
		return message.GetClusterUpgradeResp{}, err
	}

	return message.GetClusterUpgradeResp{
		ID:             op.Name, // Using Name as ID for now
		Name:           op.Name,
		K8sClusterName: op.Spec.K8sClusterName,
		Region:         op.Spec.Region,
		CloudProvider:  string(op.Spec.CloudProvider),
		TargetVersion:  op.Spec.TargetVersion,
		Status:         op.Status.Phase,
	}, nil
}

// ListUpgrades lists all cluster upgrade operations
func (s *UpgradeOperationService) ListUpgrades(ctx context.Context, req message.ListClusterUpgradesReq) (message.ListClusterUpgradesResp, error) {
	opList, err := s.rw.List(ctx, defaultNamespace, req.ContinueRequest) // TODO: make namespace configurable
	if err != nil {
		return message.ListClusterUpgradesResp{}, err
	}

	summaries := make([]message.ClusterUpgradeSummary, len(opList.Items))
	for i, op := range opList.Items {
		summaries[i] = message.ClusterUpgradeSummary{
			ID:             op.Name,
			Name:           op.Name,
			K8sClusterName: op.Spec.K8sClusterName,
			Status:         op.Status.Phase,
			TargetVersion:  op.Spec.TargetVersion,
		}
	}

	return message.ListClusterUpgradesResp{
		ClusterUpgrades: summaries,
		ContinueResponse: message.ContinueResponse{
			ContinueToken: opList.Continue,
		},
	}, nil
}

// PauseUpgrade pauses a running cluster upgrade operation
func (s *UpgradeOperationService) PauseUpgrade(ctx context.Context, req message.PauseClusterUpgradeReq) (message.PauseClusterUpgradeResp, error) {
	op, err := s.rw.Get(ctx, defaultNamespace, req.ID)
	if err != nil {
		return message.PauseClusterUpgradeResp{}, err
	}

	op.Spec.Paused = true
	if err := s.rw.Update(ctx, op); err != nil {
		return message.PauseClusterUpgradeResp{}, err
	}

	return message.PauseClusterUpgradeResp{
		Message: fmt.Sprintf("Cluster upgrade %s paused", req.ID),
	}, nil
}

// ResumeUpgrade resumes a paused cluster upgrade operation
func (s *UpgradeOperationService) ResumeUpgrade(ctx context.Context, req message.ResumeClusterUpgradeReq) (message.ResumeClusterUpgradeResp, error) {
	op, err := s.rw.Get(ctx, defaultNamespace, req.ID)
	if err != nil {
		return message.ResumeClusterUpgradeResp{}, err
	}

	op.Spec.Paused = false
	if err := s.rw.Update(ctx, op); err != nil {
		return message.ResumeClusterUpgradeResp{}, err
	}

	return message.ResumeClusterUpgradeResp{
		Message: fmt.Sprintf("Cluster upgrade %s resumed", req.ID),
	}, nil
}

// DeleteUpgrade deletes a cluster upgrade operation
func (s *UpgradeOperationService) DeleteUpgrade(ctx context.Context, req message.DeleteClusterUpgradeReq) (message.DeleteClusterUpgradeResp, error) {
	if err := s.rw.Delete(ctx, defaultNamespace, req.ID); err != nil {
		return message.DeleteClusterUpgradeResp{}, err
	}

	return message.DeleteClusterUpgradeResp{
		Message: fmt.Sprintf("Cluster upgrade %s deletion initiated", req.ID),
	}, nil
}

// GetNodeGroups returns all node groups and their versions for a cluster
func (s *UpgradeOperationService) GetNodeGroups(ctx context.Context, req message.GetNodeGroupsReq) (message.GetNodeGroupsResp, error) {
	var k8sClient client.Client
	if s.rw != nil {
		k8sClient = s.rw.GetClient()
	}
	providerRegistry, err := providers.NewCloudProviderRegistry(req.CloudProvider, req.Region, k8sClient)
	if err != nil {
		return message.GetNodeGroupsResp{}, fmt.Errorf("failed to create cloud provider registry: %w", err)
	}

	provider, err := providerRegistry.Get(req.CloudProvider)
	if err != nil {
		return message.GetNodeGroupsResp{}, fmt.Errorf("failed to get cloud provider: %w", err)
	}

	nodeGroups, err := provider.GetNodeGroupsByLabels(ctx, req.K8sClusterName, map[string]string{})
	if err != nil {
		return message.GetNodeGroupsResp{}, fmt.Errorf("failed to get node groups: %w", err)
	}

	result := make([]message.NodeGroupInfo, 0, len(nodeGroups))
	for _, ng := range nodeGroups {
		name := ng.Name
		if name == "" {
			name = ng.ID
		}
		version := ng.Version
		if version == "" && s.rw != nil {
			// Fallback: get version from K8s nodes when cloud API does not provide it (e.g. ACK NodeComponents empty).
			// Only returns version when all nodes have the same version.
			version = clusterupgradeoperation.GetNodeGroupVersionFromNodes(ctx, s.rw.GetClient(), provider, req.K8sClusterName, &ng)
		}
		result = append(result, message.NodeGroupInfo{
			ID:                    ng.ID,
			Name:                  name,
			Version:               version,
			RuntimeVersion:        ng.RuntimeVersion,
			Status:                string(ng.Status),
			NodeCount:             ng.NodeCount,
			LaunchTemplateVersion: ng.LaunchTemplateVersion,
			LaunchTemplateId:      ng.LaunchTemplateId,
			LaunchTemplateName:    ng.LaunchTemplateName,
		})
	}

	return message.GetNodeGroupsResp{
		NodeGroups: result,
	}, nil
}

// ListAddons returns all addons installed in the cluster
func (s *UpgradeOperationService) ListAddons(ctx context.Context, req message.ListAddonsReq) (message.ListAddonsResp, error) {
	var k8sClient client.Client
	if s.rw != nil {
		k8sClient = s.rw.GetClient()
	}
	providerRegistry, err := providers.NewCloudProviderRegistry(req.CloudProvider, req.Region, k8sClient)
	if err != nil {
		return message.ListAddonsResp{}, fmt.Errorf("failed to create cloud provider registry: %w", err)
	}

	provider, err := providerRegistry.Get(req.CloudProvider)
	if err != nil {
		return message.ListAddonsResp{}, fmt.Errorf("failed to get cloud provider: %w", err)
	}

	addons, err := provider.ListClusterAddons(ctx, req.K8sClusterName)
	if err != nil {
		return message.ListAddonsResp{}, fmt.Errorf("failed to list addons: %w", err)
	}

	result := make([]message.AddonInfo, 0, len(addons))
	for _, a := range addons {
		result = append(result, message.AddonInfo{
			Name:    a.Name,
			Version: a.Version,
			Status:  a.Status,
		})
	}

	return message.ListAddonsResp{
		Addons: result,
	}, nil
}

// DryRunUpgrade dry-runs a cluster upgrade request and returns the ClusterUpgradeOperation CR
// that would be created without actually creating it
func (s *UpgradeOperationService) DryRunUpgrade(ctx context.Context, req message.CreateClusterUpgradeReq) (message.DryRunClusterUpgradeResp, error) {
	operationID := uuid.New().String()

	// Build upgrade steps based on the request
	steps, err := s.buildUpgradeSteps(ctx, req)
	if err != nil {
		return message.DryRunClusterUpgradeResp{}, fmt.Errorf("failed to build upgrade steps: %w", err)
	}

	// Determine schedule time
	scheduleTime := metav1.Now()
	if req.Schedule != nil {
		scheduleTime = metav1.Time{Time: *req.Schedule}
	}

	// Build the ClusterUpgradeOperation CR (without creating it)
	operation := &o11yv1.ClusterUpgradeOperation{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "observability.pingcap.com/v1",
			Kind:       "ClusterUpgradeOperation",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      fmt.Sprintf("upgrade-%s", operationID[:8]),
			Namespace: defaultNamespace,
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			Paused:         false,
			Schedule:       scheduleTime,
			K8sClusterName: req.K8sClusterName,
			Region:         req.Region,
			TargetVersion:  req.TargetVersion,
			CloudProvider:  req.CloudProvider,
			Steps:          steps,
		},
	}

	return message.DryRunClusterUpgradeResp{
		ClusterUpgradeOperation: operation,
	}, nil
}

// buildNodeGroupUpgradeOptions builds NodeGroupUpgradeOptions from the request
func buildNodeGroupUpgradeOptions(req message.CreateClusterUpgradeReq) ([]runtime.RawExtension, error) {
	options := make([]runtime.RawExtension, 0, len(req.NodeGroupUpgrades))
	if len(req.NodeGroupUpgrades) == 0 {
		// This should not happen as we check before calling this function
		return options, nil
	}

	for _, ngUpgrade := range req.NodeGroupUpgrades {
		if req.CloudProvider == o11yv1.CloudProviderACK && ngUpgrade.Strategy == o11yv1.NodeGroupUpgradeStrategyBlueGreen {
			return nil, fmt.Errorf("blue-green strategy is not supported for ACK node group upgrades")
		}
		if !ngUpgrade.RequiresVMStorageRollout && ngUpgrade.VMClusterHelmValues != nil {
			return nil, fmt.Errorf("vmClusterHelmValues requires requiresVMStorageRollout to be true")
		}
		if ngUpgrade.Strategy == o11yv1.NodeGroupUpgradeStrategyInPlace &&
			(ngUpgrade.RequiresVMStorageRollout || len(ngUpgrade.VMClusterHelmValues) > 0) {
			return nil, fmt.Errorf("vmstorage rollout is only supported for blue-green node group upgrades")
		}
		ngOptions := o11yv1.NodeGroupUpgradeOptions{
			NodeGroupName:            ngUpgrade.NodeGroupName,
			RuntimeType:              ngUpgrade.RuntimeType,
			Strategy:                 ngUpgrade.Strategy,
			RequiresVMStorageRollout: ngUpgrade.RequiresVMStorageRollout,
			LaunchTemplateVersion:    ngUpgrade.LaunchTemplateVersion,
			LaunchTemplateId:         ngUpgrade.LaunchTemplateId,
			LaunchTemplateName:       ngUpgrade.LaunchTemplateName,
			NewNodeGroupName:         ngUpgrade.NewNodeGroupName,
		}
		if ngUpgrade.RuntimeType != "" && ngUpgrade.TargetRuntimeVersion != "" {
			ngOptions.TargetRuntimeVersion = ngUpgrade.TargetRuntimeVersion
			ngOptions.RuntimeType = ngUpgrade.RuntimeType
		}
		if ngUpgrade.RequiresVMStorageRollout {
			if ngUpgrade.Strategy != o11yv1.NodeGroupUpgradeStrategyBlueGreen {
				return nil, fmt.Errorf("vmstorage rollout is only supported for blue-green node group upgrades")
			}
			if ngUpgrade.VMClusterHelmValues == nil {
				return nil, fmt.Errorf("vmClusterHelmValues is required when requiresVMStorageRollout is true")
			}
			helmValuesJSON, err := json.Marshal(ngUpgrade.VMClusterHelmValues)
			if err != nil {
				return nil, fmt.Errorf("failed to marshal VMCluster spec overlay: %w", err)
			}
			ngOptions.VMClusterHelmValues = &apiextensionsv1.JSON{Raw: helmValuesJSON}
			ngOptions.VMStorageSTSName = ngUpgrade.VMStorageSTSName
			if ngOptions.VMStorageSTSName == "" {
				ngOptions.VMStorageSTSName = defaultVMStorageSTSName
			}
			ngOptions.VMStorageSTSNamespace = ngUpgrade.VMStorageSTSNamespace
			if ngOptions.VMStorageSTSNamespace == "" {
				ngOptions.VMStorageSTSNamespace = defaultVMStorageSTSNS
			}
		}
		ngOptionsRaw, err := toRawExtension(ngOptions)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal nodegroup options: %w", err)
		}
		options = append(options, ngOptionsRaw)
	}

	return options, nil
}

// toRawExtension converts a value to runtime.RawExtension
func toRawExtension(v interface{}) (runtime.RawExtension, error) {
	raw, err := json.Marshal(v)
	if err != nil {
		return runtime.RawExtension{}, err
	}
	return runtime.RawExtension{Raw: raw}, nil
}
