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

import (
	"context"
	"crypto/sha256"
	"encoding/base32"
	"fmt"
	"strings"

	"github.com/go-logr/logr"

	"k8s.io/utils/ptr"

	"github.com/Azure/ARO-Tools/pipelines/graph"
	"github.com/Azure/ARO-Tools/pipelines/types"
	"github.com/Azure/ARO-Tools/tools/cmdutils"
	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeploymentstacks"
	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
)

// generateDeploymentStackName creates a deployment stack name based on:
// service group / resource group / step name / cloud / env / region / stamp
// This ensures the name is stable over time while being unique across deployment contexts.
// The name is always hashed using base32(sha224(inputs)) to guarantee it fits within Azure's
// 64-character deployment name limit. The resulting name is 45 characters.
// The original inputs should be stored as tags on the deployment stack for traceability.
func generateDeploymentStackName(serviceGroup, resourceGroup, stepName, cloud, environment, region, stamp string) string {
	parts := []string{
		serviceGroup,
		resourceGroup,
		stepName,
		cloud,
		environment,
		region,
		stamp,
	}
	fullName := strings.Join(parts, "-")
	hash := sha256.Sum224([]byte(fullName))
	return strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hash[:]))
}

// runArmStackStep transforms a .bicep + .bicepparam into an ARM deployment stack. The deployment stack name is
// generated from: service group / resource group / step name / cloud / env / region / stamp. This ensures the
// name remains stable over time (required for deployment stacks) while being unique across environments and regions.
// This logic is a transliteration of the equivalent logic in the `az` CLI:
// https://github.com/Azure/azure-cli/blob/cf11272c36d2680a65bd775e10d338afa3a8b902/src/azure-cli/azure/cli/command_modules/resource/custom.py#L1396-L1405
func runArmStackStep(
	ctx context.Context,
	options *StepRunOptions,
	executionTarget ExecutionTarget,
	id graph.Identifier,
	step *types.ARMStackStep,
	state *ExecutionState,
	environment string,
	stamp string,
) (Output, DetailsProducer, error) {
	logger := logr.FromContextOrDiscard(ctx)

	cred, err := cmdutils.GetAzureTokenCredentials()
	if err != nil {
		return nil, nil, fmt.Errorf("failed to get credentials: %w", err)
	}
	resourceGroupClient, err := armresources.NewResourceGroupsClient(executionTarget.GetSubscriptionID(), cred, nil)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create resource group client: %w", err)
	}
	stackClient, err := armdeploymentstacks.NewClient(executionTarget.GetSubscriptionID(), cred, nil)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create deployment stack client: %w", err)
	}
	operationsClient, err := armresources.NewDeploymentOperationsClient(executionTarget.GetSubscriptionID(), cred, nil)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create deployment operations client: %w", err)
	}
	getOperationsClient := NewCachedOperationsClientGetter(executionTarget.GetSubscriptionID(), operationsClient, cred, nil)

	if err := ensureResourceGroupExists(ctx, resourceGroupClient, executionTarget.GetRegion(), executionTarget.GetResourceGroup(), !options.NoPersist); err != nil {
		return nil, nil, fmt.Errorf("failed to ensure resource group exists: %w", err)
	}

	// Generate deployment stack name using the full context
	deploymentStackName := generateDeploymentStackName(
		id.ServiceGroup,
		id.ResourceGroup,
		step.StepName(),
		options.Cloud,
		environment,
		executionTarget.GetRegion(),
		stamp,
	)

	state.RLock()
	inputValues, err := getInputValues(id.ServiceGroup, step.Variables, options.Configuration, state.GetOutputs(id.Stamp))
	state.RUnlock()
	if err != nil {
		return nil, nil, fmt.Errorf("failed to get input values: %w", err)
	}

	template, params, err := transformParameters(ctx, options.BicepClient, options.Configuration, inputValues, step.Parameters, options.PipelineDirectory, options.SkipBicepparamValidation)
	if err != nil {
		return nil, nil, err
	}

	adaptedParams := map[string]*armdeploymentstacks.DeploymentParameter{}
	for k, v := range params {
		asMap, ok := v.(map[string]any)
		if !ok {
			return nil, nil, fmt.Errorf("failed to convert parameter %s to map, got %T", k, v)
		}
		adaptedParams[k] = &armdeploymentstacks.DeploymentParameter{
			Value: asMap["value"],
		}
	}

	stack := armdeploymentstacks.DeploymentStack{
		Properties: &armdeploymentstacks.DeploymentStackProperties{
			ActionOnUnmanage: &armdeploymentstacks.ActionOnUnmanage{
				Resources:        ptr.To(armdeploymentstacks.DeploymentStacksDeleteDetachEnum(step.ActionOnUnmanage)),
				ResourceGroups:   ptr.To(armdeploymentstacks.DeploymentStacksDeleteDetachEnum(step.ActionOnUnmanage)),
				ManagementGroups: ptr.To(armdeploymentstacks.DeploymentStacksDeleteDetachEnum(step.ActionOnUnmanage)),
			},
			BypassStackOutOfSyncError: ptr.To(step.BypassStackOutOfSyncError),
			DenySettings: &armdeploymentstacks.DenySettings{
				Mode:               ptr.To(armdeploymentstacks.DenySettingsModeNone),
				ApplyToChildScopes: ptr.To(false),
				ExcludedActions:    []*string{},
				ExcludedPrincipals: []*string{},
			},
			Parameters: adaptedParams,
			Template:   template,
		},
		Tags: map[string]*string{
			"serviceGroup":  ptr.To(id.ServiceGroup),
			"resourceGroup": ptr.To(id.ResourceGroup),
			"stepName":      ptr.To(step.StepName()),
			"cloud":         ptr.To(options.Cloud),
			"environment":   ptr.To(environment),
			"region":        ptr.To(executionTarget.GetRegion()),
			"stamp":         ptr.To(stamp),
		},
	}

	inputs := stackInputs{
		Stack:           &stack,
		DeploymentLevel: step.DeploymentLevel,
		ResourceGroup:   executionTarget.GetResourceGroup(),
		StepName:        deploymentStackName,
	}

	_, skip, commit, err := checkCachedOutput[ArmOutput](logger, inputs, options.StepCacheDir)
	if err != nil {
		return nil, nil, err
	}
	if skip != nil {
		return skip, nil, nil
	}

	var output armdeploymentstacks.DeploymentStack
	switch step.DeploymentLevel {
	case "Subscription":
		stack.Location = ptr.To(executionTarget.GetRegion())
		poller, err := stackClient.BeginCreateOrUpdateAtSubscription(ctx, deploymentStackName, stack, nil)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to create or update deployment stack at subscription scope: %w", err)
		}
		result, err := poller.PollUntilDone(ctx, nil)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to wait for deployment stack at subscription scope: %w", err)
		}
		output = result.DeploymentStack
	case "ResourceGroup":
		poller, err := stackClient.BeginCreateOrUpdateAtResourceGroup(ctx, executionTarget.GetResourceGroup(), deploymentStackName, stack, nil)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to create or update deployment stack at resource group scope: %w", err)
		}
		result, err := poller.PollUntilDone(ctx, nil)
		if err != nil {
			return nil, nil, fmt.Errorf("failed to wait for deployment stack at resource group scope: %w", err)
		}
		output = result.DeploymentStack
	default:
		return nil, nil, fmt.Errorf("invalid deployment level: %s", step.DeploymentLevel)
	}

	// The deployment stack creates an underlying deployment whose name differs from the stack name.
	// Extract the actual deployment name from the stack response to fetch operation details.
	var details DetailsProducer
	if output.Properties != nil && output.Properties.DeploymentID != nil {
		underlyingDeploymentName := (*output.Properties.DeploymentID)[strings.LastIndex(*output.Properties.DeploymentID, "/")+1:]
		switch step.DeploymentLevel {
		case "Subscription":
			details = DetermineOperationsForSubscriptionDeployment(getOperationsClient, executionTarget.GetSubscriptionID(), underlyingDeploymentName)
		case "ResourceGroup":
			details = DetermineOperationsForResourceGroupDeployment(getOperationsClient, executionTarget.GetSubscriptionID(), executionTarget.GetResourceGroup(), underlyingDeploymentName)
		}
	}

	if output.Properties.Outputs != nil {
		if outputMap, ok := output.Properties.Outputs.(map[string]any); ok {
			returnMap := ArmOutput{}
			for k, v := range outputMap {
				returnMap[k] = v
			}
			return returnMap, details, commit(returnMap)
		}
	}
	return nil, details, nil
}

type stackInputs struct {
	Stack           *armdeploymentstacks.DeploymentStack
	DeploymentLevel string
	ResourceGroup   string
	StepName        string
}
