Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoIterationNodes/Get-AzDoIterationNodes.ps1

<#
.SYNOPSIS
    Retrieves and processes Azure DevOps Iteration Nodes for a specified project.
 
.DESCRIPTION
    The Get-AzDoIterationNodes function retrieves and processes Azure DevOps Iteration Nodes for a specified project.
    It compares the current state of iteration nodes with the desired state and returns the differences.
 
.PARAMETER ProjectName
    The name of the Azure DevOps project. This parameter is mandatory.
 
.PARAMETER IterationAttributes
    An optional hashtable array specifying the attributes of the iterations, such as Path, StartDate, and EndDate.
 
.PARAMETER LookupResult
    An optional hashtable for lookup results.
 
.PARAMETER Ensure
    An optional parameter to specify the desired state of the iteration nodes.
    Possible values are 'Present' or 'Absent'.
 
.PARAMETER Force
    A switch parameter to force the execution of the function.
 
.EXAMPLE
    Get-AzDoIterationNodes -ProjectName "MyProject" -IterationAttributes @(@{Path="Iteration1"; StartDate="2023-01-01"; EndDate="2023-01-31"})
 
    Retrieves and processes the iteration nodes for the project "MyProject" with the specified iteration attributes.
 
.EXAMPLE
    Get-AzDoIterationNodes -ProjectName "MyProject" -Ensure Present
 
    Ensures that the iteration nodes for the project "MyProject" are present.
 
.NOTES
    This function requires the global variable (Get-AzDoOrganizationName) to be set with the organization name.
    The function uses cached iteration nodes and compares them with the desired state to determine the necessary changes.
 
#>

Function Get-AzDoIterationNodes {
    [CmdletBinding()]
    param (
        # Mandatory parameter for the project name
        [Parameter(Mandatory = $true)]
        [string]$ProjectName,

        # Optional parameter for specifying Iteration paths
        [Parameter()]
        [object[]]$IterationAttributes,

        # Optional hashtable for lookup results
        [Parameter()]
        [HashTable]$LookupResult,

        # Optional parameter to ensure state
        [Parameter()]
        [Ensure]$Ensure,

        # Switch parameter to force execution
        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $Force
    )

    # Retrieve the global organization name
    $OrganizationName = (Get-AzDoOrganizationName)

    # Log the start of function execution with verbose output
    Write-Verbose "[Get-AzDoIterationNodes] Start function execution"
    Write-Verbose "[Get-AzDoIterationNodes] ProjectName: $ProjectName"
    Write-Verbose "[Get-AzDoIterationNodes] IterationAttributes: $($IterationAttributes | Out-String)"

    <#
        Iteration Attributes
 
        @{
            Path = ''
            StartDate = ''
            EndDate = ''
        }
    #>


    $FormattedIterationAttributes = Format-AzDoIterationNodes -IterationAttributes $IterationAttributes -ProjectName $ProjectName
    Write-Verbose "[Get-AzDoIterationNodes] FormattedIterationAttributesPath $($FormattedIterationAttributes | ConvertTo-Json)"

    # Initialize the result object with default values
    $getIterationResult = @{
        Ensure = [Ensure]::Absent
        propertiesChanged = @{
            toAdd = @()
            toRemove = @()
            toUpdate = @()
        }
        ProjectName = $ProjectName
        IterationAttributes = $FormattedIterationAttributes
        CachedIterationNodes = $null
        Status = [DSCGetSummaryState]::Unchanged
        Reason = $null
    }

    # Retrieve cached Iteration nodes from cache
    Write-Verbose "[Get-AzDoIterationNodes] Retrieving cached Iteration nodes"
    $cachedIterationNodes = (Get-CacheObject -CacheType 'LiveIterations' | Where-Object { $_.Key -like "\$ProjectName\Iteration*" }).Value
    $cachedIterationNodesPath = $cachedIterationNodes.Path

    # Set the cached Iteration nodes in the result object for use by other functions
    $getIterationResult.CachedIterationNodes = $cachedIterationNodes

    # Check if the cached Iteration nodes contain a top-level node
    $isTopLevel = $cachedIterationNodesPath | Where-Object { $_ -eq "\$ProjectName\Iteration" }

    # Handle case where no Iteration paths are specified and only top-level node exists
    if ($FormattedIterationAttributes.Count -eq 0 -and $cachedIterationNodesPath.Count -eq 1 -and $isTopLevel) {
        Write-Verbose "[Get-AzDoIterationNodes] IterationAttributesPath is not specified and no Iteration Nodes exist in cache"

        $getIterationResult.status = [DSCGetSummaryState]::Unchanged
        $getIterationResult.reason = 'Iteration Node does not exist and only the top level Iteration Node exists'

        return $getIterationResult
    }

    # Handle case where no Iteration paths are specified
    if ($FormattedIterationAttributes.Count -eq 0) {

        Write-Verbose "[Get-AzDoIterationNodes] IterationAttributesPath is not specified"
        $getIterationResult.status = [DSCGetSummaryState]::Missing
        $getIterationResult.reason = 'Desired State Iterations Node does not exist'

        if ($Ensure -eq [Ensure]::Absent) {
            $getIterationResult.status = [DSCGetSummaryState]::NotFound
            $getIterationResult.propertiesChanged.toAdd = $cachedIterationNodes | Where-Object { $_.Path -ne "\$ProjectName\Iteration" }
        } else {
            $getIterationResult.propertiesChanged.toRemove = $cachedIterationNodes | Where-Object { $_.Path -ne "\$ProjectName\Iteration" }
        }

        return $getIterationResult

    }

    # Handle case where only top-level node exists
    if ($cachedIterationNodes.Count -eq 1 -and $isTopLevel) {

        Write-Verbose "[Get-AzDoIterationNodes] Cached Iteration Nodes does not exist"
        $getIterationResult.status = [DSCGetSummaryState]::NotFound

        if ($Ensure -eq [Ensure]::Absent) {
            $getIterationResult.status = [DSCGetSummaryState]::Missing
            $getIterationResult.propertiesChanged.toRemove = $FormattedIterationAttributes | Where-Object { $_.Path -ne "\$ProjectName\Iteration" }
        } else {
            $getIterationResult.propertiesChanged.toAdd = $FormattedIterationAttributes | Where-Object { $_.Path -ne "\$ProjectName\Iteration" }
        }

        $getIterationResult.reason = 'Iteration Node does not exist'

        return $getIterationResult
    }

    $formattedCachedIterationNodes = $cachedIterationNodes | Where-Object { $_.Path -ne "\$ProjectName\Iteration" }
    $formattedIterationNodes = $FormattedIterationAttributes | Where-Object { $_.Path -ne "\$ProjectName\Iteration" }

    # Iterate Through the Cached Iteration Nodes
    ForEach ($node in $formattedCachedIterationNodes) {
        Write-Verbose "[Get-AzDoIterationNodes] Processing cached node: $($node.Path)"

        # Attempt to perform a lookup and find the corresponding path in FormattedIterationAttributes
        $matched = @($formattedIterationNodes | Where-Object { $_.Path -eq $node.Path })
        # Test if the result has been matched.

        if ($matched.Count -eq 1) {
            Write-Verbose "[Get-AzDoIterationNodes] Found matching node: $($matched.Path)"

            # If ensure is absent and it's present in the source list. Delete!
            if ($Ensure -eq [Ensure]::Absent) {
                $getIterationResult.propertiesChanged.toRemove += $node
                continue
            }

            # Only process the following if ensure is set to present.
            # Azure DevOps API stores dates nested under attributes.startDate/finishDate.
            $nodeStartDate = if ($node.attributes) { $node.attributes.startDate } else { $null }
            $nodeEndDate   = if ($node.attributes) { $node.attributes.finishDate } else { $null }
            $matchedNode   = $matched[0]

            # Normalize dates to a common format before comparing to avoid type mismatches
            # (e.g. Deserialized.System.DateTime from cache vs string from desired state).
            $cachedStartDate  = Format-Date $nodeStartDate
            $cachedEndDate    = Format-Date $nodeEndDate
            $matchedStartDate = Format-Date $matchedNode.StartDate
            $matchedEndDate   = Format-Date $matchedNode.EndDate

            Write-Verbose "[Get-AzDoIterationNodes] Comparing dates: cached=[$cachedStartDate/$cachedEndDate] desired=[$matchedStartDate/$matchedEndDate]"

            if (($cachedStartDate -ne $matchedStartDate) -or ($cachedEndDate -ne $matchedEndDate)) {
                Write-Verbose "[Get-AzDoIterationNodes] DateTimes differ, updating node: $($node.Path)"
                $getIterationResult.propertiesChanged.ToUpdate += @{
                    StartDate = $matchedNode.StartDate
                    EndDate   = $matchedNode.EndDate
                    Path      = $matchedNode.Path
                }
            }
            # Move to the next item
            continue
        }

        # Depending on what ensure is doing will depend on the outcome
        if ($Ensure -eq [Ensure]::Absent) {
            # It's not present in the source list so it's unchanged
            continue
        } else {
            # It's not present in the source list but it's active - delete it.
            Write-Verbose "[Get-AzDoIterationNodes] Node not in source list, deleting: $($node.Path)"
            $getIterationResult.propertiesChanged.toRemove += $node
            continue
        }
    }

    # Switch and compare the Desired Input list with the cache.
    ForEach ($node in $formattedIterationNodes) {
        Write-Verbose "[Get-AzDoIterationNodes] Processing formatted node: $($node.Path)"

        # Attempt to perform a lookup and find the corresponding path in FormattedIterationAttributes
        $matched = @($formattedCachedIterationNodes | Where-Object { $_.Path -eq $node.Path })
        # Test if the result has been matched.
        if ($matched.Count -eq 1) {
            # We don't need to test the properties since they were tested in the earlier look. Continue
            continue
        }

        # Depending on what ensure is doing will depend on the outcome
        if ($Ensure -eq [Ensure]::Absent) {
            # It's present in the source list but not online
            continue
        } else {
            # It's not present online but defined in the source list. Add it.
            Write-Verbose "[Get-AzDoIterationNodes] Node not online, adding: $($node.Path)"
            $getIterationResult.propertiesChanged.toAdd += $node
            continue
        }
    }

    # Update status based on differences between current and desired states
    if ($getIterationResult.propertiesChanged.ToUpdate.count -ne 0) {
        Write-Verbose "[Get-AzDoIterationNodes] Changes detected, status set to Changed."
        $getIterationResult.status = [DSCGetSummaryState]::Changed
    }
    elseif (($getIterationResult.propertiesChanged.toRemove.count -ne 0) -and ($getIterationResult.propertiesChanged.toAdd.count -ne 0)) {
        Write-Verbose "[Get-AzDoIterationNodes] Both ToAdd to ToRemove properties contain values."
        $getIterationResult.status = [DSCGetSummaryState]::Changed
    }
    elseif ($getIterationResult.propertiesChanged.toRemove.count -ne 0) {
        Write-Verbose "[Get-AzDoIterationNodes] Some nodes missing, status set to Missing."
        $getIterationResult.status = [DSCGetSummaryState]::Missing
    }
    elseif ($getIterationResult.propertiesChanged.toAdd.count -ne 0) {
        Write-Verbose "[Get-AzDoIterationNodes] Some nodes not found, status set to NotFound."
        $getIterationResult.status = [DSCGetSummaryState]::NotFound
    }

    # Return the result object with all computed information
    Write-Verbose "[Get-AzDoIterationNodes] Function execution completed"
    return $getIterationResult


}