// Copyright 2025 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 frontend

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"k8s.io/apimachinery/pkg/api/operation"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"

	azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
	"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"

	"github.com/Azure/ARO-HCP/internal/admission"
	"github.com/Azure/ARO-HCP/internal/api/coreapi"
	"github.com/Azure/ARO-HCP/internal/api/metadataapi"
	"github.com/Azure/ARO-HCP/internal/conversion"
	"github.com/Azure/ARO-HCP/internal/database/cosmosstorage/corecosmosstorage"
	"github.com/Azure/ARO-HCP/internal/database/cosmosstorage/cosmosstorageutils"
	"github.com/Azure/ARO-HCP/internal/utils"
	"github.com/Azure/ARO-HCP/internal/validation"
)

func (f *Frontend) GetExternalAuth(writer http.ResponseWriter, request *http.Request) error {
	ctx := request.Context()

	versionedInterface, err := VersionFromContext(ctx)
	if err != nil {
		return utils.TrackError(err)
	}
	resourceID, err := utils.ResourceIDFromContext(ctx) // used for error reporting
	if err != nil {
		return utils.TrackError(err)
	}

	resultingInternalExternalAuth, err := f.getInternalExternalAuthFromStorage(ctx, resourceID)
	if err != nil {
		return utils.TrackError(err)
	}
	responseBytes, err := coreapi.MarshalJSON(versionedInterface.NewHCPOpenShiftClusterExternalAuth(resultingInternalExternalAuth))
	if err != nil {
		return utils.TrackError(err)
	}
	_, err = coreapi.WriteJSONResponse(writer, http.StatusOK, responseBytes)
	if err != nil {
		return utils.TrackError(err)
	}

	return nil
}

func (f *Frontend) ArmResourceListExternalAuths(writer http.ResponseWriter, request *http.Request) error {
	ctx := request.Context()

	versionedInterface, err := VersionFromContext(ctx)
	if err != nil {
		return utils.TrackError(err)
	}

	subscriptionID := request.PathValue(PathSegmentSubscriptionID)
	resourceGroupName := request.PathValue(PathSegmentResourceGroupName)
	resourceName := request.PathValue(PathSegmentResourceName)

	// Verify the parent cluster exists so we return 404 instead of an empty list for a non-existent cluster (Cosmos List is prefix-based)
	_, err = f.resourcesDBClient.HCPClusters(subscriptionID, resourceGroupName).Get(ctx, resourceName)
	if err != nil {
		return utils.TrackError(err)
	}

	pagedResponse := coreapi.NewPagedResponse()

	internalExternalAuthIterator, err := f.resourcesDBClient.HCPClusters(subscriptionID, resourceGroupName).ExternalAuth(resourceName).List(ctx, dbListOptionsFromRequest(request))
	if err != nil {
		return utils.TrackError(err)
	}
	for _, externalAuth := range internalExternalAuthIterator.Items(ctx) {
		resultingExternalExternalAuth := versionedInterface.NewHCPOpenShiftClusterExternalAuth(externalAuth)
		jsonBytes, err := coreapi.MarshalJSON(resultingExternalExternalAuth)
		if err != nil {
			return utils.TrackError(err)
		}
		pagedResponse.AddValue(jsonBytes)
	}
	err = internalExternalAuthIterator.GetError()
	if err != nil {
		return utils.TrackError(err)
	}

	// MiddlewareReferer ensures Referer is present.
	err = pagedResponse.SetNextLink(request.Referer(), internalExternalAuthIterator.GetContinuationToken())
	if err != nil {
		return utils.TrackError(err)
	}

	_, err = coreapi.WriteJSONResponse(writer, http.StatusOK, pagedResponse)
	if err != nil {
		return utils.TrackError(err)
	}
	return nil
}

func (f *Frontend) CreateOrUpdateExternalAuth(writer http.ResponseWriter, request *http.Request) error {
	var err error

	// This handles both PUT and PATCH requests. PATCH requests will
	// never create a new resource. The only other notable difference
	// is the target struct that request bodies are overlayed onto:
	//
	// PUT requests overlay the request body onto a default resource
	// struct, which only has API-specified non-zero default values.
	// This means all required properties must be specified in the
	// request body, whether creating or updating a resource.
	//
	// PATCH requests overlay the request body onto a resource struct
	// that represents an existing resource to be updated.

	ctx := request.Context()

	resourceID, err := utils.ResourceIDFromContext(ctx)
	if err != nil {
		return utils.TrackError(err)
	}

	externalAuthCosmosClient := f.resourcesDBClient.HCPClusters(resourceID.SubscriptionID, resourceID.ResourceGroupName).ExternalAuth(resourceID.Parent.Name)
	oldInternalExternalAuth, err := externalAuthCosmosClient.Get(ctx, resourceID.Name)
	if err != nil && !cosmosstorageutils.IsNotFoundError(err) {
		return utils.TrackError(err)
	}

	updating := oldInternalExternalAuth != nil
	if updating {
		if err := checkForProvisioningStateConflict(ctx, f.resourcesDBClient, cosmosstorageutils.OperationRequestUpdate, oldInternalExternalAuth.ID, oldInternalExternalAuth.Properties.ProvisioningState); err != nil {
			return utils.TrackError(err)
		}
		switch request.Method {
		case http.MethodPut:
			return f.updateExternalAuth(writer, request, oldInternalExternalAuth)
		case http.MethodPatch:
			return f.patchExternalAuth(writer, request, oldInternalExternalAuth)
		default:
			return fmt.Errorf("unsupported method %s", request.Method)
		}
	}

	switch request.Method {
	case http.MethodPut:
		return f.createExternalAuth(writer, request)
	case http.MethodPatch:
		return coreapi.NewResourceNotFoundError(resourceID)
	default:
		return fmt.Errorf("unsupported method %s", request.Method)
	}
}

func decodeDesiredExternalAuthCreate(ctx context.Context) (*coreapi.HCPOpenShiftClusterExternalAuth, error) {
	versionedInterface, err := VersionFromContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	resourceID, err := utils.ResourceIDFromContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	body, err := BodyFromContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	systemData, err := SystemDataFromContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	// If for some reason systemData.CreatedAt is not set, we set it to the current time in UTC.
	if systemData.CreatedAt == nil {
		systemData.CreatedAt = ptr.To(time.Now().UTC())
	}

	externalExternalAuthFromRequest := versionedInterface.NewHCPOpenShiftClusterExternalAuth(nil)
	if err := json.Unmarshal(body, &externalExternalAuthFromRequest); err != nil {
		return nil, utils.TrackError(err)
	}
	newInternalExternalAuth, err := externalExternalAuthFromRequest.ConvertToInternal(nil)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	// Backstop for fields unknown to this API version's SetDefaultValues*.
	// See docs/api-version-defaults-and-storage.md.
	newInternalExternalAuth.EnsureDefaults()
	if len(newInternalExternalAuth.Name) > 0 && newInternalExternalAuth.Name != resourceID.Name {
		return nil, nameResourceIDMismatch(resourceID, newInternalExternalAuth.Name)
	}

	// ProxyResource info doesn't to come from the external resource information
	conversion.CopyReadOnlyProxyResourceValues(&newInternalExternalAuth.ProxyResource, ptr.To(coreapi.NewProxyResource(resourceID)))
	newInternalExternalAuth.SetResourceID(resourceID)
	newInternalExternalAuth.SetPartitionKey(resourceID.SubscriptionID)

	// set fields that were not included during the conversion, because the user does not provide them or because the
	// data is determined live on read.
	newInternalExternalAuth.SystemData = ensureSystemData(systemData, nil)

	return newInternalExternalAuth, nil
}

func (f *Frontend) createExternalAuth(writer http.ResponseWriter, request *http.Request) error {
	ctx := request.Context()
	logger := utils.LoggerFromContext(ctx)

	versionedInterface, err := VersionFromContext(ctx)
	if err != nil {
		return utils.TrackError(err)
	}
	resourceID, err := utils.ResourceIDFromContext(ctx)
	if err != nil {
		return utils.TrackError(err)
	}
	correlationData, err := CorrelationDataFromContext(ctx)
	if err != nil {
		return utils.TrackError(err)
	}

	newInternalExternalAuth, err := decodeDesiredExternalAuthCreate(ctx)
	if err != nil {
		return utils.TrackError(err)
	}

	// We retrieve all external auths for the cluster. Used for admission validation.
	externalAuthIterator, err := f.resourcesDBClient.HCPClusters(resourceID.SubscriptionID, resourceID.ResourceGroupName).ExternalAuth(resourceID.Parent.Name).List(ctx, nil)
	if err != nil {
		return utils.TrackError(err)
	}
	clusterExternalAuths := make([]*coreapi.HCPOpenShiftClusterExternalAuth, 0)
	for _, clusterExternalAuth := range externalAuthIterator.Items(ctx) {
		clusterExternalAuths = append(clusterExternalAuths, clusterExternalAuth)
	}
	if err := externalAuthIterator.GetError(); err != nil {
		return utils.TrackError(err)
	}

	restOperation := operation.Operation{
		Type: operation.Create,
		// TODO set operations when a need appears for it.
	}
	externalAuthAdmissionContext := &admission.ExternalAuthAdmissionContext{
		ClusterExternalAuths: clusterExternalAuths,
	}

	validationErrs := validation.ValidateExternalAuthCreate(ctx, newInternalExternalAuth)
	validationErrs = append(validationErrs, admission.AdmitExternalAuth(ctx, externalAuthAdmissionContext, restOperation, newInternalExternalAuth, nil)...)
	if err := coreapi.CloudErrorFromFieldErrors(validationErrs); err != nil {
		return utils.TrackError(err)
	}

	logger.Info(fmt.Sprintf("creating resource %s", resourceID))
	cluster, err := f.getInternalClusterFromStorage(ctx, resourceID.Parent)
	if err != nil {
		return utils.TrackError(err)
	}
	if err := checkForProvisioningStateConflict(ctx, f.resourcesDBClient, cosmosstorageutils.OperationRequestCreate, newInternalExternalAuth.ID, newInternalExternalAuth.Properties.ProvisioningState); err != nil {
		return utils.TrackError(err)
	}
	if cluster.ServiceProviderProperties.ClusterServiceID == nil {
		return utils.TrackError(fmt.Errorf("cluster %s has no ClusterServiceID", cluster.ID))
	}

	operationRequest := cosmosstorageutils.OperationRequestCreate

	transaction := f.resourcesDBClient.NewTransaction(newInternalExternalAuth.ID.SubscriptionID)

	createExternalAuthOperation := cosmosstorageutils.NewOperation(
		operationRequest,
		newInternalExternalAuth.ID,
		metadataapi.InternalID{},
		f.azureLocation,
		request.Header.Get(coreapi.HeaderNameHomeTenantID),
		request.Header.Get(coreapi.HeaderNameClientObjectID),
		request.Header.Get(coreapi.HeaderNameAsyncNotificationURI),
		correlationData)
	transaction.OnSuccess(addOperationResponseHeaders(writer, request, createExternalAuthOperation.NotificationURI, createExternalAuthOperation.OperationID))
	_, err = f.resourcesDBClient.Operations(newInternalExternalAuth.ID.SubscriptionID).AddCreateToTransaction(ctx, transaction, createExternalAuthOperation, nil)
	if err != nil {
		return utils.TrackError(err)
	}

	// set fields that were not known until the operation doc instance was created.
	// TODO once we we have separate creation/validation of operation documents, this can be done ahead of time.
	newInternalExternalAuth.ServiceProviderProperties.ActiveOperationID = createExternalAuthOperation.ResourceID.Name
	newInternalExternalAuth.Properties.ProvisioningState = createExternalAuthOperation.Status

	externalAuthCosmosClient := f.resourcesDBClient.HCPClusters(resourceID.SubscriptionID, resourceID.ResourceGroupName).ExternalAuth(resourceID.Parent.Name)
	cosmosUID, err := externalAuthCosmosClient.AddCreateToTransaction(ctx, transaction, newInternalExternalAuth, nil)
	if err != nil {
		return utils.TrackError(err)
	}

	transactionResult, err := transaction.Execute(ctx, &azcosmos.TransactionalBatchOptions{
		EnableContentResponseOnWrite: true,
	})
	if err != nil {
		return utils.TrackError(err)
	}

	// Read back the resource document so the response body is accurate.
	resultingUncastInternalExternalAuth, err := transactionResult.GetItem(cosmosUID)
	if err != nil {
		return utils.TrackError(err)
	}
	resultingInternalExternalAuth, ok := resultingUncastInternalExternalAuth.(*coreapi.HCPOpenShiftClusterExternalAuth)
	if !ok {
		return fmt.Errorf("unexpected type %T", resultingUncastInternalExternalAuth)
	}
	responseBytes, err := coreapi.MarshalJSON(versionedInterface.NewHCPOpenShiftClusterExternalAuth(resultingInternalExternalAuth))
	if err != nil {
		return utils.TrackError(err)
	}

	_, err = coreapi.WriteJSONResponse(writer, http.StatusCreated, responseBytes)
	if err != nil {
		return utils.TrackError(err)
	}
	return nil
}

func decodeDesiredExternalAuthReplace(ctx context.Context, oldInternalExternalAuth *coreapi.HCPOpenShiftClusterExternalAuth) (*coreapi.HCPOpenShiftClusterExternalAuth, error) {
	versionedInterface, err := VersionFromContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}

	// Decoding for update has a series of semantics for determining the final desired update
	// 1. exact user request
	// 2. defaults for that version
	// 3. if not set, the values that the user doesn't necessary have to set but are not static defaults.  These from from the old value.
	// 4. values that are missing because the external type doesn't represent them
	// 5. values that might change because our machinery changes them.

	resourceID, err := utils.ResourceIDFromContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	body, err := BodyFromContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	systemData, err := SystemDataFromContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}

	// Initialize versionedRequestExternalAuth to include both
	// non-zero default values and current read-only values.
	// Exact user request
	externalExternalAuthFromRequest := versionedInterface.NewHCPOpenShiftClusterExternalAuth(nil)
	if err := json.Unmarshal(body, &externalExternalAuthFromRequest); err != nil {
		return nil, utils.TrackError(err)
	}

	newInternalExternalAuth, err := externalExternalAuthFromRequest.ConvertToInternal(oldInternalExternalAuth)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	if len(newInternalExternalAuth.Name) > 0 && newInternalExternalAuth.Name != resourceID.Name {
		return nil, nameResourceIDMismatch(resourceID, newInternalExternalAuth.Name)
	}

	// ServiceProviderProperties contains two types of information
	// 1. values that a user cannot change because the external type does not expose the information.
	//    We must overwrite those values with the oldInternalCluster values so the values don't change, because the user's input will always be empty.
	// 2. values that a user cannot change due to validation requirements, but the user *can* specify the values.
	//    We are overwriting these values that we consider to be status values.
	//    We do this because if a user has read a value, then modified it, then replaces it, we don't want to produce
	//    validation errors on status fields that the user isn't trying to modify.
	conversion.CopyReadOnlyExternalAuthValues(newInternalExternalAuth, oldInternalExternalAuth)
	newInternalExternalAuth.SystemData = ensureSystemData(systemData, oldInternalExternalAuth.SystemData)

	// Backstop for fields unknown to this API version's SetDefaultValues*.
	// See docs/api-version-defaults-and-storage.md.
	newInternalExternalAuth.EnsureDefaults()

	return newInternalExternalAuth, nil
}

func (f *Frontend) updateExternalAuth(writer http.ResponseWriter, request *http.Request, oldInternalExternalAuth *coreapi.HCPOpenShiftClusterExternalAuth) error {
	ctx := request.Context()

	newInternalExternalAuth, err := decodeDesiredExternalAuthReplace(ctx, oldInternalExternalAuth)
	if err != nil {
		return utils.TrackError(err)
	}

	return f.updateExternalAuthInCosmos(ctx, writer, request, http.StatusOK, newInternalExternalAuth, oldInternalExternalAuth)
}

func decodeDesiredExternalAuthPatch(ctx context.Context, oldInternalExternalAuth *coreapi.HCPOpenShiftClusterExternalAuth) (*coreapi.HCPOpenShiftClusterExternalAuth, error) {
	versionedInterface, err := VersionFromContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	resourceID, err := utils.ResourceIDFromContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	body, err := BodyFromContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	systemData, err := SystemDataFromContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}

	// TODO find a way to represent the desired change without starting from internal state here (very confusing)
	// TODO we appear to lack a test, but this seems to take an original, apply the patch and unmarshal the result, meaning the above patch step is just incorrect.
	newExternalExternalAuth := versionedInterface.NewHCPOpenShiftClusterExternalAuth(oldInternalExternalAuth)
	if err := coreapi.ApplyRequestBody(http.MethodPatch, body, newExternalExternalAuth); err != nil {
		return nil, utils.TrackError(err)
	}
	newInternalExternalAuth, err := newExternalExternalAuth.ConvertToInternal(oldInternalExternalAuth)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	if len(newInternalExternalAuth.Name) > 0 && newInternalExternalAuth.Name != resourceID.Name {
		return nil, nameResourceIDMismatch(resourceID, newInternalExternalAuth.Name)
	}

	conversion.CopyReadOnlyExternalAuthValues(newInternalExternalAuth, oldInternalExternalAuth)
	newInternalExternalAuth.SystemData = ensureSystemData(systemData, oldInternalExternalAuth.SystemData)

	// Backstop for fields unknown to this API version's SetDefaultValues*.
	// See docs/api-version-defaults-and-storage.md.
	newInternalExternalAuth.EnsureDefaults()

	return newInternalExternalAuth, nil
}

func (f *Frontend) patchExternalAuth(writer http.ResponseWriter, request *http.Request, oldInternalExternalAuth *coreapi.HCPOpenShiftClusterExternalAuth) error {
	// PATCH requests overlay the request body onto a resource struct
	// that represents an existing resource to be updated.
	ctx := request.Context()

	newInternalExternalAuth, err := decodeDesiredExternalAuthPatch(ctx, oldInternalExternalAuth)
	if err != nil {
		return utils.TrackError(err)
	}

	return f.updateExternalAuthInCosmos(ctx, writer, request, http.StatusAccepted, newInternalExternalAuth, oldInternalExternalAuth)
}

func (f *Frontend) updateExternalAuthInCosmos(ctx context.Context, writer http.ResponseWriter, request *http.Request, httpStatusCode int, newInternalExternalAuth, oldInternalExternalAuth *coreapi.HCPOpenShiftClusterExternalAuth) error {
	logger := utils.LoggerFromContext(ctx)

	versionedInterface, err := VersionFromContext(ctx)
	if err != nil {
		return utils.TrackError(err)
	}
	correlationData, err := CorrelationDataFromContext(ctx)
	if err != nil {
		return utils.TrackError(err)
	}

	validationErrs := validation.ValidateExternalAuthUpdate(ctx, newInternalExternalAuth, oldInternalExternalAuth)
	if err := coreapi.CloudErrorFromFieldErrors(validationErrs); err != nil {
		return utils.TrackError(err)
	}

	logger.Info(fmt.Sprintf("updating resource %s", oldInternalExternalAuth.ID))

	transaction := f.resourcesDBClient.NewTransaction(oldInternalExternalAuth.ID.SubscriptionID)

	externalAuthUpdateOperation := cosmosstorageutils.NewOperation(
		cosmosstorageutils.OperationRequestUpdate,
		newInternalExternalAuth.ID,
		ptr.Deref(newInternalExternalAuth.ServiceProviderProperties.ClusterServiceID, metadataapi.InternalID{}),
		f.azureLocation,
		request.Header.Get(coreapi.HeaderNameHomeTenantID),
		request.Header.Get(coreapi.HeaderNameClientObjectID),
		request.Header.Get(coreapi.HeaderNameAsyncNotificationURI),
		correlationData)
	transaction.OnSuccess(addOperationResponseHeaders(writer, request, externalAuthUpdateOperation.NotificationURI, externalAuthUpdateOperation.OperationID))
	_, err = f.resourcesDBClient.Operations(newInternalExternalAuth.ID.SubscriptionID).AddCreateToTransaction(ctx, transaction, externalAuthUpdateOperation, nil)
	if err != nil {
		return utils.TrackError(err)
	}

	// set fields that were not known until the operation doc instance was created.
	// TODO once we we have separate creation/validation of operation documents, this can be done ahead of time.
	newInternalExternalAuth.ServiceProviderProperties.ActiveOperationID = externalAuthUpdateOperation.ResourceID.Name
	newInternalExternalAuth.Properties.ProvisioningState = externalAuthUpdateOperation.Status

	_, err = f.resourcesDBClient.HCPClusters(newInternalExternalAuth.ID.SubscriptionID, newInternalExternalAuth.ID.ResourceGroupName).
		ExternalAuth(newInternalExternalAuth.ID.Parent.Name).
		AddReplaceToTransaction(ctx, transaction, newInternalExternalAuth, nil)
	if err != nil {
		return utils.TrackError(err)
	}

	transactionResult, err := transaction.Execute(ctx, &azcosmos.TransactionalBatchOptions{
		EnableContentResponseOnWrite: true,
	})
	if err != nil {
		return utils.TrackError(err)
	}

	// Read back the resource document so the response body is accurate.
	resultingUncastInternalExternalAuth, err := transactionResult.GetItem(oldInternalExternalAuth.GetCosmosData().GetCosmosUID())
	if err != nil {
		return utils.TrackError(err)
	}
	resultingInternalExternalAuth, ok := resultingUncastInternalExternalAuth.(*coreapi.HCPOpenShiftClusterExternalAuth)
	if !ok {
		return fmt.Errorf("unexpected type %T", resultingUncastInternalExternalAuth)
	}
	responseBytes, err := coreapi.MarshalJSON(versionedInterface.NewHCPOpenShiftClusterExternalAuth(resultingInternalExternalAuth))
	if err != nil {
		return utils.TrackError(err)
	}

	_, err = coreapi.WriteJSONResponse(writer, httpStatusCode, responseBytes)
	if err != nil {
		return utils.TrackError(err)
	}
	return nil
}

// DeleteExternalAuth implements the deletion API contract for ARM
// * 200 if a deletion is successful
// * 202 if an asynchronous delete is initiated
// * 204 if a well-formed request attempts to delete a nonexistent resource
func (f *Frontend) DeleteExternalAuth(writer http.ResponseWriter, request *http.Request) error {
	ctx := request.Context()
	logger := utils.LoggerFromContext(ctx)

	resourceID, err := utils.ResourceIDFromContext(ctx)
	if err != nil {
		return utils.TrackError(err)
	}

	externalAuth, err := f.resourcesDBClient.HCPClusters(resourceID.SubscriptionID, resourceID.ResourceGroupName).ExternalAuth(resourceID.Parent.Name).Get(ctx, resourceID.Name)
	if cosmosstorageutils.IsNotFoundError(err) {
		// For resource not found errors on deletion, ARM requires
		writer.WriteHeader(http.StatusNoContent)
		return nil
	}
	if err != nil {
		return utils.TrackError(err)
	}

	if err := checkForProvisioningStateConflict(ctx, f.resourcesDBClient, cosmosstorageutils.OperationRequestDelete, externalAuth.ID, externalAuth.Properties.ProvisioningState); err != nil {
		return utils.TrackError(err)
	}

	logger.Info(fmt.Sprintf("deleting resource %s", externalAuth.ID))

	transaction := f.resourcesDBClient.NewTransaction(externalAuth.ID.SubscriptionID)
	if err := f.addDeleteExternalAuthToTransaction(ctx, writer, request, transaction, externalAuth); err != nil {
		return utils.TrackError(err)
	}
	_, err = transaction.Execute(ctx, nil)
	if err != nil {
		return utils.TrackError(err)
	}

	writer.WriteHeader(http.StatusAccepted)
	return nil
}

func (f *Frontend) addDeleteExternalAuthToTransaction(ctx context.Context, writer http.ResponseWriter, request *http.Request, transaction cosmosstorageutils.DBTransaction, externalAuth *coreapi.HCPOpenShiftClusterExternalAuth) error {
	correlationData, err := CorrelationDataFromContext(ctx)
	if err != nil {
		return utils.TrackError(err)
	}

	// Cluster Service will take care of canceling any ongoing operations
	// on the resource or child resources, but we need to do some database
	// bookkeeping to reflect that.
	_, err = corecosmosstorage.CancelActiveOperations(ctx, f.resourcesDBClient, transaction, &corecosmosstorage.ResourcesDBClientListActiveOperationDocsOptions{
		ExternalID:             externalAuth.ID,
		IncludeNestedResources: true,
	})
	if err != nil {
		return utils.TrackError(err)
	}

	operationDoc := cosmosstorageutils.NewOperation(
		cosmosstorageutils.OperationRequestDelete,
		externalAuth.ID,
		metadataapi.InternalID{},
		f.azureLocation,
		"",
		"",
		"",
		correlationData)
	// TODO remove this once migration of the new external auth deletion from frontend to backend approach is fully completed in all ARO-HCP
	// permanent environments, for all regions.
	operationDoc.UsesNewExternalAuthDeletionApproach = true
	if request != nil {
		// these are optional because when this is triggered via the subscription deletion flow, there is no
		// deletion request containing these headers so these operations cannot be directly tracked.
		operationDoc.TenantID = request.Header.Get(coreapi.HeaderNameHomeTenantID)
		operationDoc.ClientID = request.Header.Get(coreapi.HeaderNameClientObjectID)
		operationDoc.NotificationURI = request.Header.Get(coreapi.HeaderNameAsyncNotificationURI)
		transaction.OnSuccess(addOperationResponseHeaders(writer, request, operationDoc.NotificationURI, operationDoc.OperationID))
	}
	_, err = f.resourcesDBClient.Operations(operationDoc.OperationID.SubscriptionID).AddCreateToTransaction(ctx, transaction, operationDoc, nil)
	if err != nil {
		return utils.TrackError(err)
	}

	if externalAuth.ServiceProviderProperties.DeletionTimestamp == nil {
		externalAuth.ServiceProviderProperties.DeletionTimestamp = &metav1.Time{Time: time.Now().UTC()}
	}
	externalAuth.ServiceProviderProperties.ActiveOperationID = operationDoc.ResourceID.Name
	externalAuth.Properties.ProvisioningState = operationDoc.Status
	// TODO remove this once migration of the new external auth deletion from frontend to backend approach is fully completed in all ARO-HCP
	// permanent environments, for all regions.
	externalAuth.ServiceProviderProperties.UsesNewExternalAuthDeletionApproach = true
	_, err = f.resourcesDBClient.HCPClusters(externalAuth.ID.SubscriptionID, externalAuth.ID.ResourceGroupName).ExternalAuth(externalAuth.ID.Parent.Name).
		AddReplaceToTransaction(ctx, transaction, externalAuth, nil)
	if err != nil {
		return utils.TrackError(err)
	}

	return nil
}

func (f *Frontend) getInternalExternalAuthFromStorage(ctx context.Context, resourceID *azcorearm.ResourceID) (*coreapi.HCPOpenShiftClusterExternalAuth, error) {
	internalExternalAuth, err := f.resourcesDBClient.HCPClusters(resourceID.SubscriptionID, resourceID.ResourceGroupName).ExternalAuth(resourceID.Parent.Name).Get(ctx, resourceID.Name)
	if cosmosstorageutils.IsNotFoundError(err) {
		return nil, coreapi.NewResourceNotFoundError(resourceID)
	}
	if err != nil {
		return nil, utils.TrackError(err)
	}

	// Replace the ID field from Cosmos with the given resourceID,
	// which typically comes from the URL. This helps preserve the
	// casing of the resource group and resource name from the URL
	// to meet RPC requirements:
	//
	// Put Resource | Arguments
	//
	// The resource group names and resource names should be matched
	// case insensitively. ... Additionally, the Resource Provider must
	// preserve the casing provided by the user. The service must return
	// the most recently specified casing to the client and must not
	// normalize or return a toupper or tolower form of the resource
	// group or resource name. The resource group name and resource
	// name must come from the URL and not the request body.
	if internalExternalAuth.ResourceID == nil {
		return nil, fmt.Errorf("stored externalauth document is missing cosmosMetadata.resourceID")
	}
	if !strings.EqualFold(internalExternalAuth.ResourceID.String(), resourceID.String()) {
		return nil, fmt.Errorf("unexpected resourceID: %s", internalExternalAuth.ResourceID.String())
	}
	internalExternalAuth.ID = resourceID

	return internalExternalAuth, nil
}
