// Copyright 2026 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package version

import (
	"context"
	"fmt"
	"time"

	"github.com/blang/semver/v4"

	arohcpv1alpha1 "github.com/openshift-online/ocm-sdk-go/arohcp/v1alpha1"

	"github.com/Azure/ARO-HCP/backend/pkg/utils/controllerutils"
	"github.com/Azure/ARO-HCP/internal/api/metadataapi"
	"github.com/Azure/ARO-HCP/internal/database/cosmosstorage/corecosmosstorage"
	"github.com/Azure/ARO-HCP/internal/database/cosmosstorage/cosmosstorageutils"
	"github.com/Azure/ARO-HCP/internal/database/informers/coreinformers"
	"github.com/Azure/ARO-HCP/internal/database/listers/corelisters"
	unionkubeapplierinformers "github.com/Azure/ARO-HCP/internal/database/unioninformers/kubeapplier"
	"github.com/Azure/ARO-HCP/internal/ocm"
	"github.com/Azure/ARO-HCP/internal/utils"
)

// triggerNodePoolUpgradeSyncer is a NodePool syncer that triggers node pool upgrades
type triggerNodePoolUpgradeSyncer struct {
	resourcesDBClient             corecosmosstorage.ResourcesDBClient
	clusterServiceClient          ocm.ClusterServiceClientSpec
	serviceProviderNodePoolLister corelisters.ServiceProviderNodePoolLister
}

var _ controllerutils.NodePoolSyncer = (*triggerNodePoolUpgradeSyncer)(nil)

// NewTriggerNodePoolUpgradeController creates a new controller that triggers node pool upgrades.
// It monitors node pools where the desired version differs from the actual version and creates
// a NodePoolUpgradePolicy in Cluster Service to initiate the upgrade.
func NewTriggerNodePoolUpgradeController(
	resourcesDBClient corecosmosstorage.ResourcesDBClient,
	clusterServiceClient ocm.ClusterServiceClientSpec,
	serviceProviderNodePoolLister corelisters.ServiceProviderNodePoolLister,
	informers coreinformers.BackendInformers,
	kubeApplierInformers *unionkubeapplierinformers.UnionKubeApplierInformers,
) controllerutils.Controller {
	syncer := &triggerNodePoolUpgradeSyncer{
		resourcesDBClient:             resourcesDBClient,
		clusterServiceClient:          clusterServiceClient,
		serviceProviderNodePoolLister: serviceProviderNodePoolLister,
	}

	controller := controllerutils.NewNodePoolWatchingController(
		"TriggerNodePoolUpgrade",
		resourcesDBClient,
		informers,
		kubeApplierInformers,
		5*time.Minute,
		syncer,
	)

	return controller
}

// SyncOnce performs a single reconciliation to trigger a node pool upgrade if needed.
//
// High-level flow:
//  1. Fetch the node pool and service provider node pool state
//  2. Skips upgrade trigger if no active versions exist yet (during installation)
//  3. Check if desiredVersion differs from latest actual version
//  4. If different, create a NodePoolUpgradePolicy to trigger upgrade
func (c *triggerNodePoolUpgradeSyncer) SyncOnce(ctx context.Context, key controllerutils.HCPNodePoolKey) error {
	existingNodePool, err := c.resourcesDBClient.HCPClusters(key.SubscriptionID, key.ResourceGroupName).
		NodePools(key.HCPClusterName).Get(ctx, key.HCPNodePoolName)
	if cosmosstorageutils.IsNotFoundError(err) {
		return nil // node pool doesn't exist, no work to do
	}
	if err != nil {
		return utils.TrackError(fmt.Errorf("failed to get NodePool: %w", err))
	}
	if existingNodePool.ServiceProviderProperties.DeletionTimestamp != nil {
		return nil
	}
	// if we have no clusterservice nodepool, we have nothing to trigger.
	if existingNodePool.ServiceProviderProperties.ClusterServiceID == nil || len(existingNodePool.ServiceProviderProperties.ClusterServiceID.String()) == 0 {
		return nil
	}

	existingServiceProviderNodePool, err := c.serviceProviderNodePoolLister.Get(ctx, key.SubscriptionID, key.ResourceGroupName, key.HCPClusterName, key.HCPNodePoolName)
	if cosmosstorageutils.IsNotFoundError(err) {
		// CreateServiceProviderNodePool will populate it; we'll be re-enqueued via the ServiceProviderNodePool informer.
		return nil
	}
	if err != nil {
		return utils.TrackError(fmt.Errorf("failed to get ServiceProviderNodePool: %w", err))
	}

	desiredVersion := existingServiceProviderNodePool.Spec.NodePoolVersion.DesiredVersion
	if desiredVersion == nil {
		return nil // No desired version set
	}

	// No active version yet (installation ongoing); skip upgrade trigger.
	if len(existingServiceProviderNodePool.Status.NodePoolVersion.ActiveVersions) == 0 {
		return nil
	}

	// Get latest actual version from active versions
	actualLatestVersion := existingServiceProviderNodePool.Status.NodePoolVersion.ActiveVersions[0].Version

	// If desired version matches latest actual version, nothing to do
	if actualLatestVersion != nil && desiredVersion.EQ(*actualLatestVersion) {
		return nil
	}

	return c.createUpgradePolicyIfNeeded(ctx, desiredVersion, *existingNodePool.ServiceProviderProperties.ClusterServiceID)
}

// createUpgradePolicyIfNeeded ensures a node pool upgrade policy exists for the desired version.
// It creates a new policy only if the most recently created policy does not match the desired version.
//
// The method:
//  1. Queries existing upgrade policies from Cluster Service (sorted by creation_timestamp desc)
//  2. Checks if the latest policy matches the desired version - returns nil if it does
//  3. Otherwise, creates a new upgrade policy with the desired version
func (c *triggerNodePoolUpgradeSyncer) createUpgradePolicyIfNeeded(ctx context.Context, desiredVersion *semver.Version, nodePoolServiceID metadataapi.InternalID) error {
	logger := utils.LoggerFromContext(ctx)

	// Query existing node pool upgrade policies from Cluster Service
	iterator := c.clusterServiceClient.ListNodePoolUpgradePolicies(nodePoolServiceID, "creation_timestamp desc")

	// Only create a new upgrade policy if the latest created policy doesn't match the desired version
	for policy := range iterator.Items(ctx) {
		// Only check the first (latest) policy
		if latestPolicyVersion, ok := policy.GetVersion(); ok {
			if latestPolicyVersion == desiredVersion.String() {
				return nil
			}
		}
		break // Only need to check the first policy
	}

	if err := iterator.GetError(); err != nil {
		return utils.TrackError(fmt.Errorf("failed to list node pool upgrade policies: %w", err))
	}

	// Create a new node pool upgrade policy for the desired version
	logger.Info("Creating node pool upgrade policy", "desiredVersion", desiredVersion)

	_, policyErr := c.clusterServiceClient.PostNodePoolUpgradePolicy(ctx, nodePoolServiceID, arohcpv1alpha1.NewNodePoolUpgradePolicy().Version(desiredVersion.String()))
	if policyErr != nil {
		return utils.TrackError(fmt.Errorf("failed to create node pool upgrade policy: %w", policyErr))
	}

	logger.Info("Successfully created node pool upgrade policy", "desiredVersion", desiredVersion)

	return nil
}
