// 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 engine

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strings"

	"github.com/go-logr/logr"

	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
	azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault"
	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8"
	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armlocks"
	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions"

	"github.com/Azure/ARO-HCP/tooling/cleanup-sweeper/pkg/engine/runner"
	armsteps "github.com/Azure/ARO-HCP/tooling/cleanup-sweeper/pkg/engine/steps/arm"
	dnssteps "github.com/Azure/ARO-HCP/tooling/cleanup-sweeper/pkg/engine/steps/dns"
	kvsteps "github.com/Azure/ARO-HCP/tooling/cleanup-sweeper/pkg/engine/steps/keyvault"
	netsteps "github.com/Azure/ARO-HCP/tooling/cleanup-sweeper/pkg/engine/steps/network"
	rgsteps "github.com/Azure/ARO-HCP/tooling/cleanup-sweeper/pkg/engine/steps/resourcegroup"
)

const (
	maxRetries                = 3
	dnsMaxRetries             = 3 // DNS zones need retries due to eventual consistency (matches bash script)
	cosmosMaxRetries          = 3 // Cosmos DB operations need retries due to eventual consistency and operation locks
	privateEndpointMaxRetries = 5 // Private endpoints may fail due to parent resources being deleted
	vnetLinkMaxRetries        = 3 // VNet links can have timing issues
)

// WorkflowOptions controls runner behavior for cleanup workflows.
type WorkflowOptions struct {
	Wait            bool
	DryRun          bool
	Parallelism     int
	ContinueOnError bool
	ClientOptions   *azcorearm.ClientOptions
}

// ResourceGroupOrderedCleanupWorkflow builds an ordered cleanup workflow for a
// single resource group, following the historical delete.sh behavior.
//
// Deletes all resources in a resource group except those with locks.
// Handles dependencies by deleting resources in the proper order:
//
//  1. Remove NSP associations first (with force deletion)
//
//  2. Delete private endpoints and DNS components (in dependency order):
//     a. Private DNS zone groups
//     b. Private endpoint connections
//     c. Private endpoints
//     d. Private DNS zone virtual network links
//     e. Private link services
//     f. Private DNS zones (with verification)
//
//  3. Delete public DNS zones and clean up NS delegation records
//
//  4. Delete compute roots (VMs/VMSS)
//
//  5. Delete remaining application and infrastructure resources
//
//  6. Delete public IP addresses (after AKS clusters to avoid load balancer conflicts)
//
//  7. Delete monitoring resources (Data Collection Rules and Endpoints)
//
//  8. Delete core networking (Virtual Networks and Network Security Groups)
//
//  9. Purge soft-deleted Key Vaults
//
// 10. Attempt to delete the resource group itself (with retries and warnings)
func ResourceGroupOrderedCleanupWorkflow(
	ctx context.Context,
	resourceGroupName string,
	subscriptionID string,
	credential azcore.TokenCredential,
	opts WorkflowOptions,
) (*runner.Engine, error) {
	if strings.TrimSpace(resourceGroupName) == "" {
		return nil, fmt.Errorf("resource group name is required")
	}
	if strings.TrimSpace(subscriptionID) == "" {
		return nil, fmt.Errorf("subscription ID is required")
	}
	if credential == nil {
		return nil, fmt.Errorf("azure credential is required")
	}

	clientOptions := normalizeARMClientOptions(opts.ClientOptions)

	rgClient, err := armresources.NewResourceGroupsClient(subscriptionID, credential, clientOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to create resource groups client: %w", err)
	}

	_, err = rgClient.Get(ctx, resourceGroupName, nil)
	if err != nil {
		var respErr *azcore.ResponseError
		if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound {
			logger, loggerErr := logr.FromContext(ctx)
			if loggerErr != nil {
				panic(loggerErr)
			}
			logger.Info(
				"Resource group already deleted; skipping ordered cleanup workflow",
				"resourceGroup", resourceGroupName,
				"subscriptionID", subscriptionID,
			)
			return &runner.Engine{
				Parallelism: opts.Parallelism,
				DryRun:      opts.DryRun,
				Wait:        opts.Wait,
			}, nil
		}
		return nil, fmt.Errorf("failed to get resource group %q: %w", resourceGroupName, err)
	}

	resourcesClient, err := armresources.NewClient(subscriptionID, credential, clientOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to create resources client: %w", err)
	}
	locksClient, err := armlocks.NewManagementLocksClient(subscriptionID, credential, clientOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to create management locks client: %w", err)
	}
	providersClient, err := armresources.NewProvidersClient(subscriptionID, credential, clientOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to create providers client: %w", err)
	}
	apiVersionCache := armsteps.NewAPIVersionCache(providersClient)

	clientFactory, err := armnetwork.NewClientFactory(subscriptionID, credential, clientOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to create network client factory: %w", err)
	}
	nspClient := clientFactory.NewSecurityPerimetersClient()
	vaultsClient, err := armkeyvault.NewVaultsClient(subscriptionID, credential, clientOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to create vaults client: %w", err)
	}
	subsClient, err := armsubscriptions.NewClient(credential, clientOptions)
	if err != nil {
		return nil, fmt.Errorf("failed to create subscriptions client: %w", err)
	}

	return &runner.Engine{
		Parallelism: opts.Parallelism,
		DryRun:      opts.DryRun,
		Wait:        opts.Wait,
		PostRunFn:   resourceGroupSummaryFn(resourcesClient, resourceGroupName, opts),
		Steps: []runner.Step{
			netsteps.MustNewNSPForceDeleteStep(netsteps.NSPForceDeleteStepConfig{
				ResourceGroupName: resourceGroupName,
				ResourcesClient:   resourcesClient,
				LocksClient:       locksClient,
				NSPClient:         nspClient,
				Name:              "Delete network security perimeters",
				Retries:           maxRetries,
				ContinueOnError:   opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector: armsteps.ResourceSelector{IncludedResourceTypes: []string{
					"Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
				}},
				Name:            "Delete private DNS zone groups",
				ContinueOnError: opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector: armsteps.ResourceSelector{IncludedResourceTypes: []string{
					"Microsoft.Network/privateEndpointConnections",
				}},
				Name:            "Delete private endpoint connections",
				ContinueOnError: opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector: armsteps.ResourceSelector{IncludedResourceTypes: []string{
					"Microsoft.Network/privateEndpoints",
				}},
				Name:            "Delete private endpoints",
				Retries:         privateEndpointMaxRetries,
				ContinueOnError: opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector: armsteps.ResourceSelector{IncludedResourceTypes: []string{
					"Microsoft.Network/privateDnsZones/virtualNetworkLinks",
				}},
				Name:            "Delete private DNS zone virtual network links",
				Retries:         vnetLinkMaxRetries,
				ContinueOnError: opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector: armsteps.ResourceSelector{IncludedResourceTypes: []string{
					"Microsoft.Network/privateLinkServices",
				}},
				Name:            "Delete private link services",
				ContinueOnError: opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector: armsteps.ResourceSelector{IncludedResourceTypes: []string{
					"Microsoft.Network/privateDnsZones",
				}},
				Name:            "Delete private DNS zones",
				Retries:         dnsMaxRetries,
				ContinueOnError: opts.ContinueOnError,
				Verify: func(ctx context.Context) error {
					return dnssteps.VerifyPrivateDNSZonesDeleted(ctx, resourcesClient, resourceGroupName)
				},
			}),
			dnssteps.MustNewDeleteNSDelegationRecordsStep(dnssteps.DeleteNSDelegationRecordsStepConfig{
				ResourceGroupName: resourceGroupName,
				Credential:        credential,
				ClientOptions:     clientOptions,
				LocksClient:       locksClient,
				ResourcesClient:   resourcesClient,
				SubsClient:        subsClient,
				Name:              "Delete parent NS delegations",
				Retries:           1,
				ContinueOnError:   opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector: armsteps.ResourceSelector{IncludedResourceTypes: []string{
					"Microsoft.Network/dnszones",
				}},
				Name:            "Delete public DNS zones",
				Retries:         dnsMaxRetries,
				ContinueOnError: opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector: armsteps.ResourceSelector{IncludedResourceTypes: []string{
					"Microsoft.Compute/virtualMachines",
					"Microsoft.Compute/virtualMachineScaleSets",
				}},
				Name:            "Delete compute roots",
				Retries:         maxRetries,
				ContinueOnError: opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector: armsteps.ResourceSelector{ExcludedResourceTypes: []string{
					// Networking resources
					"Microsoft.Network/networkSecurityPerimeters",
					"Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
					"Microsoft.Network/privateEndpointConnections",
					"Microsoft.Network/privateEndpoints",
					"Microsoft.Network/privateDnsZones/virtualNetworkLinks",
					"Microsoft.Network/privateLinkServices",
					"Microsoft.Network/privateDnsZones",
					"Microsoft.Network/dnszones",
					"Microsoft.Network/virtualNetworks",
					"Microsoft.Network/networkSecurityGroups",
					"Microsoft.Network/publicIPAddresses", // Deleted after AKS to avoid load balancer attachment conflicts
					// Monitoring resources
					"Microsoft.Insights/dataCollectionRules",
					"Microsoft.Insights/dataCollectionEndpoints",
					// Container instances (excluded to avoid disruption)
					"Microsoft.ContainerInstance/containerGroups",
					// Cosmos DB handled in its own step with extra retries
					"Microsoft.DocumentDB/databaseAccounts",
				}},
				Name:            "Delete non-networking resources",
				Retries:         1,
				ContinueOnError: opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector:          armsteps.ResourceSelector{IncludedResourceTypes: []string{"Microsoft.DocumentDB/databaseAccounts"}},
				Name:              "Delete Cosmos DB accounts",
				Retries:           cosmosMaxRetries,
				ContinueOnError:   opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector:          armsteps.ResourceSelector{IncludedResourceTypes: []string{"Microsoft.Network/publicIPAddresses"}},
				Name:              "Delete public IP addresses",
				Retries:           3,
				ContinueOnError:   opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector:          armsteps.ResourceSelector{IncludedResourceTypes: []string{"Microsoft.Insights/dataCollectionRules"}},
				Name:              "Delete data collection rules",
				Retries:           maxRetries,
				ContinueOnError:   opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector:          armsteps.ResourceSelector{IncludedResourceTypes: []string{"Microsoft.Insights/dataCollectionEndpoints"}},
				Name:              "Delete data collection endpoints",
				Retries:           maxRetries,
				ContinueOnError:   opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector:          armsteps.ResourceSelector{IncludedResourceTypes: []string{"Microsoft.Network/virtualNetworks"}},
				Name:              "Delete virtual networks",
				ContinueOnError:   opts.ContinueOnError,
			}),
			armsteps.MustNewDeletionStep(armsteps.DeletionStepConfig{
				ResourceGroupName: resourceGroupName,
				Client:            resourcesClient,
				LocksClient:       locksClient,
				APIVersionCache:   apiVersionCache,
				Selector:          armsteps.ResourceSelector{IncludedResourceTypes: []string{"Microsoft.Network/networkSecurityGroups"}},
				Name:              "Delete network security groups",
				ContinueOnError:   opts.ContinueOnError,
			}),
			kvsteps.MustNewPurgeDeletedStep(kvsteps.PurgeDeletedStepConfig{
				ResourceGroupName: resourceGroupName,
				VaultsClient:      vaultsClient,
				Retries:           maxRetries,
				ContinueOnError:   opts.ContinueOnError,
			}),
			rgsteps.MustNewDeleteStep(rgsteps.DeleteStepConfig{
				ResourceGroupName: resourceGroupName,
				RGClient:          rgClient,
				LocksClient:       locksClient,
				Retries:           5,
				ContinueOnError:   opts.ContinueOnError,
			}),
		},
	}, nil
}

// resourceGroupSummaryFn returns a PostRunFn that lists remaining resources in
// a resource group and logs a summary. Errors are informational only.
func resourceGroupSummaryFn(
	client *armresources.Client,
	resourceGroupName string,
	opts WorkflowOptions,
) func(ctx context.Context) error {
	return func(ctx context.Context) error {
		logger, err := logr.FromContext(ctx)
		if err != nil {
			panic(err)
		}
		if opts.DryRun {
			logger.Info("Dry-run workflow complete; collecting final state")
		} else {
			logger.Info("Ordered cleanup workflow complete; collecting final state")
		}

		pager := client.NewListByResourceGroupPager(resourceGroupName, nil)
		var remaining []*armresources.GenericResourceExpanded
		for pager.More() {
			page, err := pager.NextPage(ctx)
			if err != nil {
				var respErr *azcore.ResponseError
				if errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound {
					logger.Info("Resource group has been deleted")
					return nil
				}
				logger.Info("Could not verify remaining resources", "error", err)
				return nil
			}
			remaining = append(remaining, page.Value...)
		}

		if len(remaining) == 0 {
			logger.Info("All resources have been deleted from the resource group")
			return nil
		}

		byType := make(map[string][]string)
		for _, res := range remaining {
			if res.Type != nil && res.Name != nil {
				byType[*res.Type] = append(byType[*res.Type], *res.Name)
			}
		}

		if opts.DryRun {
			logger.Info(fmt.Sprintf("Resource group cleanup preview completed. %d resources would be deleted", len(remaining)))
		} else {
			logger.Info(fmt.Sprintf("Resource group cleanup completed with %d resources remaining", len(remaining)))
			if !opts.Wait {
				logger.Info("Cleanup ran with wait=false, so asynchronous deletes may still be in progress")
			}
			for resType, names := range byType {
				logger.Info("Remaining resources",
					"type", resType,
					"count", len(names),
					"names", strings.Join(names, ", "),
				)
			}
		}
		return nil
	}
}
