Private/Invoke-AzLocalUpdateRingTagWorker.ps1

function Get-AzLocalUpdateRingClusterRead {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true)][string]$ResourceId,
        [Parameter(Mandatory = $true)][hashtable]$Context
    )

    $uri = "https://management.azure.com$ResourceId`?api-version=2025-10-01"
    if ($ResourceId -notmatch '^/subscriptions/([^/]+)/resourceGroups/[^/]+/providers/Microsoft\.AzureStackHCI/clusters/[^/?#]+$') {
        return [PSCustomObject]@{ Ok = $false; Data = $null; Error = 'Invalid cluster resource ID for direct ARM read.' }
    }
    $subscriptionId = $Matches[1]
    $timer = [Diagnostics.Stopwatch]::StartNew()
    $headers = $null
    $tokenResponse = $null
    $rawToken = $null
    $cacheKey = $null
    try {
        if (-not $Context.ContainsKey('Accounts')) {
            $Context.Accounts = @{}
            $Context.Tokens = @{}
            $Context.DirectReads = 0
            $Context.FallbackReads = 0
            $Context.TokenRequests = 0
            $Context.ReadMilliseconds = 0L
            $rawAccounts = & az account list --output json --only-show-errors 2>$null
            if ($LASTEXITCODE -ne 0) { throw 'Account discovery failed.' }
            $accounts = ($rawAccounts -join "`n") | ConvertFrom-Json -ErrorAction Stop
            foreach ($account in $accounts) {
                if ($account.id -and $account.tenantId -and $account.user.name -and $account.user.type) {
                    $accountKey = '{0}|{1}|{2}' -f $account.tenantId, $account.user.type, $account.user.name
                    if ($Context.Accounts.ContainsKey([string]$account.id)) {
                        $Context.Accounts[[string]$account.id] = $null
                    }
                    else { $Context.Accounts[[string]$account.id] = $accountKey }
                }
            }
        }
        $cacheKey = $Context.Accounts[$subscriptionId]
        if (-not $cacheKey) { throw 'No unambiguous account context available.' }
        $cached = $Context.Tokens[$cacheKey]
        if (-not $cached -or $cached.ExpiresUtc -le [DateTimeOffset]::UtcNow.AddMinutes(2)) {
            $Context.TokenRequests++
            $rawToken = & az account get-access-token --subscription $subscriptionId --resource 'https://management.azure.com/' --output json --only-show-errors 2>$null
            if ($LASTEXITCODE -ne 0) { throw 'Token acquisition failed.' }
            $tokenResponse = ($rawToken -join "`n") | ConvertFrom-Json -ErrorAction Stop
            if (-not $tokenResponse.accessToken -or -not $tokenResponse.expires_on -or
                [string]$tokenResponse.subscription -ne $subscriptionId -or
                [string]$tokenResponse.tenant -ne ($cacheKey -split '\|')[0]) { throw 'Token metadata unavailable or inconsistent.' }
            $expiresUtc = [DateTimeOffset]::FromUnixTimeSeconds([long]$tokenResponse.expires_on)
            if ($expiresUtc -le [DateTimeOffset]::UtcNow.AddMinutes(2)) { throw 'Token expiry is too near.' }
            $cached = @{ AccessToken = [string]$tokenResponse.accessToken; ExpiresUtc = $expiresUtc }
            $Context.Tokens[$cacheKey] = $cached
        }
        $headers = @{ Authorization = 'Bearer ' + $cached.AccessToken }
        $data = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers -MaximumRedirection 0 -TimeoutSec 30 -ErrorAction Stop -Verbose:$false -Debug:$false
        if (-not $data -or [string]$data.id -ne $ResourceId) { throw 'Unexpected ARM resource response.' }
        $Context.DirectReads++
        return [PSCustomObject]@{ Ok = $true; Data = $data; Error = $null }
    }
    catch {
        if ($headers) { $headers.Clear() }
        if ($cacheKey -and $Context.ContainsKey('Tokens')) {
            if ($Context.Tokens[$cacheKey]) { $Context.Tokens[$cacheKey].Clear() }
            $Context.Tokens.Remove($cacheKey)
        }
        $Context.FallbackReads++
        Write-Verbose 'Direct ARM read unavailable; using the existing Azure CLI read and authentication recovery path.'
        return Invoke-AzRestJson -Uri $uri
    }
    finally {
        if ($headers) { $headers.Clear() }
        $cached = $null
        $rawToken = $null
        $tokenResponse = $null
        $timer.Stop()
        $Context.ReadMilliseconds += $timer.ElapsedMilliseconds
        Write-Verbose ("Config-2 cluster read completed: durationMs={0}; directReads={1}; fallbackReads={2}; tokenRequests={3}." -f $timer.ElapsedMilliseconds, $Context.DirectReads, $Context.FallbackReads, $Context.TokenRequests)
    }
}

function Invoke-AzLocalUpdateRingTagPlanBatch {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true)][object[]]$Batch,
        [Parameter(Mandatory = $true)]$Options
    )

    $readContext = @{ DirectReads = 0; FallbackReads = 0; TokenRequests = 0; ReadMilliseconds = 0L }
    $batchTimer = [Diagnostics.Stopwatch]::StartNew()
    try {
        foreach ($item in $Batch) {
            New-AzLocalUpdateRingTagPlan -ClusterEntry $item -ClusterTagFilters @($Options.ClusterTagFilters) `
                -Force ([bool]$Options.Force) -CaptureVerbose ([bool]$Options.CaptureVerbose) -ReadContext $readContext
        }
    }
    finally {
        $batchTimer.Stop()
        try {
            Write-Verbose ("Config-2 planning batch completed: clusters={0}; durationMs={1}; directReads={2}; fallbackReads={3}; tokenRequests={4}; readDurationMs={5}." -f $Batch.Count, $batchTimer.ElapsedMilliseconds, $readContext.DirectReads, $readContext.FallbackReads, $readContext.TokenRequests, $readContext.ReadMilliseconds)
        }
        finally {
            if ($readContext.ContainsKey('Tokens')) {
                foreach ($token in $readContext.Tokens.Values) { $token.Clear() }
                $readContext.Tokens.Clear()
            }
            $readContext.Clear()
        }
    }
}

function New-AzLocalUpdateRingTagPlan {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $ClusterEntry,

        [Parameter(Mandatory = $false)]
        [AllowNull()]
        [object[]]$ClusterTagFilters,

        [Parameter(Mandatory = $false)]
        [bool]$Force = $false,

        [Parameter(Mandatory = $false)]
        [bool]$CaptureVerbose = $false,

        [Parameter(Mandatory = $false)]
        [hashtable]$ReadContext
    )

    $logEntries = [System.Collections.Generic.List[object]]::new()
    $verboseMessages = [System.Collections.Generic.List[string]]::new()

    if ($ClusterEntry -is [System.Collections.IDictionary]) {
        $ClusterEntry = [PSCustomObject]$ClusterEntry
    }

    $inputIndex = if ($ClusterEntry.PSObject.Properties['InputIndex']) { [int]$ClusterEntry.InputIndex } else { 0 }
    $resourceId = if ($ClusterEntry.PSObject.Properties['ResourceId']) { [string]$ClusterEntry.ResourceId } else { '' }
    $currentUpdateRingValue = if ($ClusterEntry.PSObject.Properties['UpdateRingValue']) { [string]$ClusterEntry.UpdateRingValue } else { '' }
    $updateStartWindowValue = if ($ClusterEntry.PSObject.Properties['UpdateStartWindowValue']) { [string]$ClusterEntry.UpdateStartWindowValue } else { '' }
    $updateExclusionsWindowValue = if ($ClusterEntry.PSObject.Properties['UpdateExclusionsWindowValue']) { [string]$ClusterEntry.UpdateExclusionsWindowValue } else { '' }
    $updateExcludedValue = if ($ClusterEntry.PSObject.Properties['UpdateExcludedValue']) { [string]$ClusterEntry.UpdateExcludedValue } else { '' }
    $updateAuthAccountIdValue = if ($ClusterEntry.PSObject.Properties['UpdateAuthAccountIdValue']) { [string]$ClusterEntry.UpdateAuthAccountIdValue } else { '' }

    $headerClusterName = ($resourceId -split '/')[-1]
    if ([string]::IsNullOrWhiteSpace($headerClusterName)) { $headerClusterName = $resourceId }

    [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = '' })
    [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = '----------------------------------------' })
    [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = "Processing: $headerClusterName" })
    [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = "ARM Resource ID: $resourceId" })
    [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = "Target UpdateRing: $currentUpdateRingValue" })
    [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = '----------------------------------------' })

    $clusterName = ''
    $resourceGroup = ''
    $subscriptionId = ''
    $previousTagValue = ''

    $newResult = {
        param(
            [string]$Action,
            [string]$Status,
            [string]$Message
        )
        [PSCustomObject]@{
            ClusterName      = $clusterName
            ResourceGroup    = $resourceGroup
            SubscriptionId   = $subscriptionId
            ResourceId       = $resourceId
            Action           = $Action
            PreviousTagValue = $previousTagValue
            NewTagValue      = $currentUpdateRingValue
            Status           = $Status
            Message          = $Message
        }
    }

    $newEnvelope = {
        param($Result, $Plan)
        [PSCustomObject]@{
            InputIndex      = $inputIndex
            ResourceId      = $resourceId
            Result          = $Result
            Plan            = $Plan
            LogEntries      = $logEntries.ToArray()
            VerboseMessages = $verboseMessages.ToArray()
        }
    }

    try {
        if ($resourceId -notmatch '/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/([^/]+)/([^/]+)/([^/]+)') {
            $message = 'Invalid Resource ID format'
            [void]$logEntries.Add([PSCustomObject]@{ Level = 'Error'; Message = "Invalid Resource ID format: $resourceId" })
            return & $newEnvelope (& $newResult 'Skipped' 'Failed' $message) $null
        }

        $subscriptionId = $Matches[1]
        $resourceGroup = $Matches[2]
        $providerNamespace = $Matches[3]
        $resourceType = $Matches[4]
        $clusterName = $Matches[5]
        $actualType = "$providerNamespace/$resourceType"

        if ($actualType -notlike 'Microsoft.AzureStackHCI/clusters') {
            $message = "Invalid resource type: $actualType (expected Microsoft.AzureStackHCI/clusters)"
            [void]$logEntries.Add([PSCustomObject]@{ Level = 'Error'; Message = "Resource is not an Azure Local cluster. Type: $actualType" })
            return & $newEnvelope (& $newResult 'Skipped' 'Failed' $message) $null
        }

        [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = "Cluster: $clusterName" })
        [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = "Resource Group: $resourceGroup" })
        [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = "Subscription: $subscriptionId" })
        [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = 'Verifying cluster exists and retrieving current tags...' })

        $uri = "https://management.azure.com$resourceId`?api-version=2025-10-01"
        $restOutput = if ($null -ne $ReadContext) {
            @(Get-AzLocalUpdateRingClusterRead -ResourceId $resourceId -Context $ReadContext -Verbose:$CaptureVerbose 4>&1)
        }
        else {
            @(Invoke-AzRestJson -Uri $uri -Verbose:$CaptureVerbose 4>&1)
        }
        $clusterResponse = $null
        foreach ($outputItem in $restOutput) {
            if ($outputItem -is [System.Management.Automation.VerboseRecord]) {
                [void]$verboseMessages.Add([string]$outputItem.Message)
            }
            elseif ($outputItem -and $outputItem.PSObject.Properties['Ok']) {
                $clusterResponse = $outputItem
            }
        }

        if (-not $clusterResponse -or -not $clusterResponse.Ok -or -not $clusterResponse.Data) {
            $detail = if ($clusterResponse -and $clusterResponse.Error) { ": $($clusterResponse.Error)" } else { '' }
            $message = "Cluster not found or access denied$detail"
            [void]$logEntries.Add([PSCustomObject]@{ Level = 'Error'; Message = "Failed to retrieve cluster. It may not exist or you don't have access.$detail" })
            return & $newEnvelope (& $newResult 'Skipped' 'Failed' $message) $null
        }
        $clusterInfo = $clusterResponse.Data

        if (-not (Test-AzLocalClusterMatchesTagFilter -Tags $clusterInfo.tags -ClusterTagFilters $ClusterTagFilters)) {
            $message = 'Cluster does not match the configured scope.clusterTagFilters policy. No tags were changed.'
            [void]$logEntries.Add([PSCustomObject]@{ Level = 'Warning'; Message = "$message ResourceId: $resourceId" })
            return & $newEnvelope (& $newResult 'Skipped' 'GlobalFilterMismatch' $message) $null
        }

        if ($clusterInfo.type -notlike 'Microsoft.AzureStackHCI/clusters') {
            $message = "Resource type mismatch: $($clusterInfo.type)"
            [void]$logEntries.Add([PSCustomObject]@{ Level = 'Error'; Message = "Resource type mismatch. Expected Azure Local cluster, got: $($clusterInfo.type)" })
            return & $newEnvelope (& $newResult 'Skipped' 'Failed' $message) $null
        }

        [void]$logEntries.Add([PSCustomObject]@{ Level = 'Success'; Message = "Cluster verified: $($clusterInfo.name)" })

        $currentTags = if ($clusterInfo.tags) { $clusterInfo.tags } else { [PSCustomObject]@{} }
        if ($currentTags -is [System.Collections.IDictionary]) {
            $currentTags = [PSCustomObject]$currentTags
        }

        $action = ''
        $hasUpdateRing = [bool]$currentTags.PSObject.Properties['UpdateRing']
        if ($hasUpdateRing) {
            $previousTagValue = [string]$currentTags.UpdateRing
            if ($previousTagValue -eq $currentUpdateRingValue) {
                [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = "Existing UpdateRing tag found with value: '$previousTagValue' (matches target)" })
            }
            else {
                [void]$logEntries.Add([PSCustomObject]@{ Level = 'Warning'; Message = "Existing UpdateRing tag found with value: '$previousTagValue' (differs from target '$currentUpdateRingValue')" })
            }

            $needsExcludedDefaultStamp = (-not $currentTags.PSObject.Properties[$script:UpdateExcludedTagName]) -and (-not $updateExcludedValue)
            $hasNewScheduleTags = ($updateStartWindowValue -and (-not $currentTags.PSObject.Properties[$script:UpdateStartWindowTagName] -or $currentTags.$($script:UpdateStartWindowTagName) -ne $updateStartWindowValue)) -or
                                  ($updateExclusionsWindowValue -and (-not $currentTags.PSObject.Properties[$script:UpdateExclusionsWindowTagName] -or $currentTags.$($script:UpdateExclusionsWindowTagName) -ne $updateExclusionsWindowValue)) -or
                                  ($updateExcludedValue -and (-not $currentTags.PSObject.Properties[$script:UpdateExcludedTagName] -or $currentTags.$($script:UpdateExcludedTagName) -ne $updateExcludedValue)) -or
                                  ($updateAuthAccountIdValue -and (-not $currentTags.PSObject.Properties[$script:UpdateAuthAccountIdTagName] -or $currentTags.$($script:UpdateAuthAccountIdTagName) -ne $updateAuthAccountIdValue)) -or
                                  $needsExcludedDefaultStamp

            if (-not $Force -and -not $hasNewScheduleTags) {
                if ($previousTagValue -eq $currentUpdateRingValue) {
                    $message = 'All managed tags (UpdateRing, UpdateStartWindow, UpdateExclusionsWindow, UpdateExcluded) already match desired state.'
                    [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = 'All managed tags already match desired state - no action needed' })
                    return & $newEnvelope (& $newResult 'NoChange' 'AlreadyInSync' $message) $null
                }

                $message = "Existing UpdateRing tag (value: $previousTagValue) differs from target ($currentUpdateRingValue). Use -Force to overwrite."
                [void]$logEntries.Add([PSCustomObject]@{ Level = 'Warning'; Message = 'Skipping cluster - UpdateRing differs from target; use -Force to overwrite existing tag' })
                return & $newEnvelope (& $newResult 'Skipped' 'Skipped' $message) $null
            }

            if (-not $Force) {
                [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = 'UpdateRing unchanged but new schedule tags to apply - proceeding' })
                $action = 'Updated'
            }
            elseif ($previousTagValue -eq $currentUpdateRingValue -and -not $hasNewScheduleTags) {
                $message = 'All managed tags already match desired state; -Force PATCH skipped (no-op).'
                [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = 'Force mode enabled but all managed tags already match desired state - no PATCH needed' })
                return & $newEnvelope (& $newResult 'NoChange' 'AlreadyInSync' $message) $null
            }
            else {
                [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = 'Force mode enabled - will update existing tag' })
                $action = 'Updated'
            }
        }
        else {
            [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = 'No existing UpdateRing tag - will create new tag' })
            $action = 'Created'
        }

        $tagsToMerge = [ordered]@{ UpdateRing = $currentUpdateRingValue }
        if ($updateStartWindowValue) {
            $tagsToMerge[$script:UpdateStartWindowTagName] = $updateStartWindowValue
            [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = " Will also set $($script:UpdateStartWindowTagName) tag: $updateStartWindowValue" })
        }
        if ($updateExclusionsWindowValue) {
            $tagsToMerge[$script:UpdateExclusionsWindowTagName] = $updateExclusionsWindowValue
            [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = " Will also set $($script:UpdateExclusionsWindowTagName) tag: $updateExclusionsWindowValue" })
        }
        if ($updateExcludedValue) {
            $tagsToMerge[$script:UpdateExcludedTagName] = $updateExcludedValue
            [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = " Will also set $($script:UpdateExcludedTagName) tag: $updateExcludedValue" })
        }
        elseif (-not $currentTags.PSObject.Properties[$script:UpdateExcludedTagName]) {
            $tagsToMerge[$script:UpdateExcludedTagName] = 'False'
            [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = " Will default-stamp $($script:UpdateExcludedTagName) tag: 'False' (tag absent on cluster)" })
        }

        if ($updateAuthAccountIdValue) {
            $authIdToWrite = $updateAuthAccountIdValue.Trim()
            if ($authIdToWrite -notmatch '^\d{1,3}$') {
                throw "Invalid UpdateAuthAccountId '$authIdToWrite' for cluster '$clusterName' - must be numeric (1-3 digits, e.g. 001)."
            }
            $tagsToMerge[$script:UpdateAuthAccountIdTagName] = $authIdToWrite
            [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = " Will also set $($script:UpdateAuthAccountIdTagName) tag: $authIdToWrite" })
        }

        $tagDeltas = [System.Collections.Generic.List[string]]::new()
        foreach ($tagName in $tagsToMerge.Keys) {
            $newValue = [string]$tagsToMerge[$tagName]
            $oldValue = if ($currentTags.PSObject.Properties[$tagName]) { [string]$currentTags.$tagName } else { '<absent>' }
            if ($oldValue -ne $newValue) {
                [void]$tagDeltas.Add(("{0}: '{1}' -> '{2}'" -f $tagName, $oldValue, $newValue))
            }
        }

        $patchBodyObject = [PSCustomObject]@{
            operation  = 'Merge'
            properties = [PSCustomObject]@{ tags = [PSCustomObject]$tagsToMerge }
        }
        $successMessage = if ($tagDeltas.Count -gt 0) {
            "Tags $($action.ToLower()): " + ($tagDeltas -join '; ')
        }
        else {
            "UpdateRing tag $($action.ToLower()) successfully"
        }
        $whatIfMessage = if ($tagDeltas.Count -gt 0) {
            "Would $($action.ToLower()) tags: " + ($tagDeltas -join '; ')
        }
        else {
            "Would $($action.ToLower()) UpdateRing tag"
        }

        $plan = [PSCustomObject]@{
            InputIndex       = $inputIndex
            ClusterName      = $clusterName
            ResourceGroup    = $resourceGroup
            SubscriptionId   = $subscriptionId
            ResourceId       = $resourceId
            Action           = $action
            PreviousTagValue = $previousTagValue
            NewTagValue      = $currentUpdateRingValue
            PatchBody        = ($patchBodyObject | ConvertTo-Json -Compress -Depth 10)
            SuccessMessage   = $successMessage
            WhatIfMessage    = $whatIfMessage
        }
        return & $newEnvelope $null $plan
    }
    catch {
        $message = $_.Exception.Message
        [void]$logEntries.Add([PSCustomObject]@{ Level = 'Error'; Message = "Error processing cluster: $message" })
        return & $newEnvelope (& $newResult 'Error' 'Failed' $message) $null
    }
}

function Invoke-AzLocalUpdateRingTagPatch {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        $Plan,

        [Parameter(Mandatory = $false)]
        [bool]$CaptureVerbose = $false
    )

    if ($Plan -is [System.Collections.IDictionary]) {
        $Plan = [PSCustomObject]$Plan
    }

    $logEntries = [System.Collections.Generic.List[object]]::new()
    $verboseMessages = [System.Collections.Generic.List[string]]::new()
    $result = $null

    try {
        [void]$logEntries.Add([PSCustomObject]@{ Level = 'Info'; Message = "Applying UpdateRing tag with value: '$($Plan.NewTagValue)'..." })
        $tagsUri = "https://management.azure.com$($Plan.ResourceId)/providers/Microsoft.Resources/tags/default?api-version=2021-04-01"
        $restOutput = @(Invoke-AzRestJson -Uri $tagsUri -Method PATCH -Body ([string]$Plan.PatchBody) -Headers @('Content-Type=application/json') -Verbose:$CaptureVerbose 4>&1)
        $patchResponse = $null
        foreach ($outputItem in $restOutput) {
            if ($outputItem -is [System.Management.Automation.VerboseRecord]) {
                [void]$verboseMessages.Add([string]$outputItem.Message)
            }
            elseif ($outputItem -and $outputItem.PSObject.Properties['Ok']) {
                $patchResponse = $outputItem
            }
        }

        if ($patchResponse -and $patchResponse.Ok) {
            [void]$logEntries.Add([PSCustomObject]@{ Level = 'Success'; Message = "Successfully $(([string]$Plan.Action).ToLower()) UpdateRing tag" })
            $result = [PSCustomObject]@{
                ClusterName      = [string]$Plan.ClusterName
                ResourceGroup    = [string]$Plan.ResourceGroup
                SubscriptionId   = [string]$Plan.SubscriptionId
                ResourceId       = [string]$Plan.ResourceId
                Action           = [string]$Plan.Action
                PreviousTagValue = [string]$Plan.PreviousTagValue
                NewTagValue      = [string]$Plan.NewTagValue
                Status           = 'Success'
                Message          = [string]$Plan.SuccessMessage
            }
        }
        else {
            $errorMessage = if ($patchResponse -and $patchResponse.Error) { [string]$patchResponse.Error } else { 'No response returned by ARM.' }
            [void]$logEntries.Add([PSCustomObject]@{ Level = 'Error'; Message = "Failed to apply tag: $errorMessage" })
            $result = [PSCustomObject]@{
                ClusterName      = [string]$Plan.ClusterName
                ResourceGroup    = [string]$Plan.ResourceGroup
                SubscriptionId   = [string]$Plan.SubscriptionId
                ResourceId       = [string]$Plan.ResourceId
                Action           = [string]$Plan.Action
                PreviousTagValue = [string]$Plan.PreviousTagValue
                NewTagValue      = [string]$Plan.NewTagValue
                Status           = 'Failed'
                Message          = "Failed to apply tag: $errorMessage"
            }
        }
    }
    catch {
        $message = $_.Exception.Message
        [void]$logEntries.Add([PSCustomObject]@{ Level = 'Error'; Message = "Error applying tag: $message" })
        $result = [PSCustomObject]@{
            ClusterName      = [string]$Plan.ClusterName
            ResourceGroup    = [string]$Plan.ResourceGroup
            SubscriptionId   = [string]$Plan.SubscriptionId
            ResourceId       = [string]$Plan.ResourceId
            Action           = 'Error'
            PreviousTagValue = [string]$Plan.PreviousTagValue
            NewTagValue      = [string]$Plan.NewTagValue
            Status           = 'Failed'
            Message          = $message
        }
    }

    return [PSCustomObject]@{
        InputIndex      = [int]$Plan.InputIndex
        ResourceId      = [string]$Plan.ResourceId
        Result          = $result
        LogEntries      = $logEntries.ToArray()
        VerboseMessages = $verboseMessages.ToArray()
    }
}