Microsoft.AVS.CDR.psm1

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

class DependencyGraphNode {
    [string]$Name
    [string]$Version
    [System.Collections.ArrayList]$Dependencies
    [bool]$NotFound
    [string]$Repository
    [string]$InstalledLocation

    DependencyGraphNode(
        [string]$Name,
        [string]$Version,
        [System.Collections.IList]$Dependencies,
        [bool]$NotFound,
        [string]$Repository,
        [string]$InstalledLocation
    ) {
        $this.Name = $Name
        $this.Version = $Version
        $this.Dependencies = [System.Collections.ArrayList]::new($Dependencies)
        $this.NotFound = $NotFound
        $this.Repository = $Repository
        $this.InstalledLocation = $InstalledLocation
    }
}

$script:defaultRedirectMap = @{
}

$script:moduleMapCache = @{
}

<#
.SYNOPSIS
    Finds and validates a redirect for a dependency version.
    
.PARAMETER RedirectMap
    Map entries: "Name@Version" -> "NewVersion", "Name" -> "Version",
    "Name@Version" -> "*" or "Name" -> "*" (retain version, normalize casing).
    
.OUTPUTS
    Hashtable with ResolvedVersion, ResolvedName, and IsRedirected.
#>

function Find-DependencyRedirect {
    param(
        [Parameter(Mandatory = $true)]
        [string]$DependencyName,
        
        [Parameter(Mandatory = $false)]
        [AllowEmptyString()]
        [string]$DependencyVersion,
        
        [Parameter(Mandatory = $true)]
        [hashtable]$RedirectMap,
        
        [Parameter(Mandatory = $false)]
        [string]$Indent = ""
    )
    
    if ([string]::IsNullOrWhiteSpace($DependencyVersion)) {
        if ($RedirectMap.ContainsKey($DependencyName)) {
            $depVersion = $RedirectMap[$DependencyName]
            Write-Verbose "${Indent}Resolved unversioned dependency: $DependencyName -> $depVersion (from redirect map)"
            
            $resolvedName = $DependencyName
            foreach ($entry in $RedirectMap.GetEnumerator()) {
                if ($entry.Key -eq $DependencyName) {
                    $resolvedName = $entry.Key
                    break
                }
            }
            
            return @{
                ResolvedVersion = $depVersion
                ResolvedName = $resolvedName
                IsRedirected = $true
            }
        }
        else {
            throw "${Indent}Cannot conservatively resolve version for dependency '$DependencyName'. Please add a redirect mapping for this module."
        }
    }
    
    $isExactVersion = $true
    $normalizedDepVersion = $DependencyVersion
    
    # Version range like "[1.0, 1.0]", "[1.0, )", "(, 2.0]"
    if ($DependencyVersion -match '^(\[|\()([^,]*),\s*([^\]\)]*)(\]|\))$') {
        $openBracket = $matches[1]
        $minVer = $matches[2]
        $maxVer = $matches[3]
        $closeBracket = $matches[4]
        
        if ($minVer -and $maxVer -and ($minVer -eq $maxVer) -and ($openBracket -eq '[') -and ($closeBracket -eq ']')) {
            $normalizedDepVersion = $minVer  # exact: [1.0, 1.0]
        }
        elseif ($maxVer -and (-not $minVer)) {
            $isExactVersion = $false
            $normalizedDepVersion = $maxVer  # open-ended: (, 2.0]
        }
        elseif ($minVer -and (-not $maxVer)) {
            $isExactVersion = $false
            $normalizedDepVersion = $minVer  # open-ended: [1.0, )
        }
        else {
            $isExactVersion = $false
            $normalizedDepVersion = $minVer
        }
    }
    
    # Check name@version first, then name-only fallback
    $depKeyPattern = "${DependencyName}@${normalizedDepVersion}"
    $versionSpecificEntry = $null
    $nameOnlyEntry = $null
    
    foreach ($entry in $RedirectMap.GetEnumerator()) {
        if ($entry.Key -eq $depKeyPattern) {
            $versionSpecificEntry = $entry
            break
        }
        elseif ($null -eq $nameOnlyEntry -and $entry.Key -eq $DependencyName) {
            $nameOnlyEntry = $entry
        }
    }
    
    $matchedEntry = if ($versionSpecificEntry) { $versionSpecificEntry } else { $nameOnlyEntry }
    $isNameOnlyMatch = $null -eq $versionSpecificEntry -and $null -ne $nameOnlyEntry
    
    if ($matchedEntry) {
        $resolvedVersion = $matchedEntry.Value
        
        # "*" retains version but normalizes dependency name casing
        if ($resolvedVersion -eq "*") {
            $resolvedVersion = $normalizedDepVersion
            
            if ($isNameOnlyMatch) {
                $resolvedName = $matchedEntry.Key
            }
            else {
                $resolvedName = $matchedEntry.Key -replace '@.*$', ''
            }
            
            Write-Verbose "${Indent}Normalizing dependency name: $DependencyName -> $resolvedName (version $normalizedDepVersion retained)"
            
            return @{
                ResolvedVersion = $resolvedVersion
                ResolvedName = $resolvedName
                IsRedirected = $true
            }
        }
        
        if ($isExactVersion -and $resolvedVersion -ne $normalizedDepVersion) {
            throw "${Indent}Cannot redirect exact version dependency '$DependencyName' from version $normalizedDepVersion to $resolvedVersion. Exact version specifications must redirect to the same version or have no redirect."
        }
        
        if ($isNameOnlyMatch) {
            $resolvedName = $matchedEntry.Key
            Write-Verbose "${Indent}Redirecting dependency: $DependencyName $DependencyVersion -> $resolvedVersion (from name-only redirect)"
        }
        else {
            $resolvedName = $matchedEntry.Key -replace '@.*$', ''
            Write-Verbose "${Indent}Redirecting dependency: $DependencyName $DependencyVersion -> $resolvedVersion (from redirect map)"
        }
        
        return @{
            ResolvedVersion = $resolvedVersion
            ResolvedName = $resolvedName
            IsRedirected = $true
        }
    }
    else {
        return @{
            ResolvedVersion = $normalizedDepVersion
            ResolvedName = $DependencyName
            IsRedirected = $false
        }
    }
}

<#
.SYNOPSIS
    Merges redirect maps — OuterMap takes precedence. Loads module-specific map files from maps/ dir.
#>

function Get-MergedRedirectMap {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$OuterMap,
        
        [Parameter(Mandatory = $true)]
        [string]$Name,
        
        [Parameter(Mandatory = $true)]
        [string]$Version
    )
    
    $redirectMap = $OuterMap
    
    if ([string]::IsNullOrWhiteSpace($Version)) {
        $versionPatterns = @()
    }
    else {
        $baseVersion = $Version
        if ($Version -match '[\[\(]([0-9][0-9a-zA-Z.\-]*)') {
            $baseVersion = $matches[1]
        }
        
        $versionParts = $baseVersion -split '[.\-]'
        $major = $versionParts[0]
        $minor = if ($versionParts.Count -gt 1) { $versionParts[1] } else { "0" }
        $versionPatterns = @(
            $baseVersion,  # Full version (e.g., 1.4.0.15939652 or 1.0.0-preview)
            "$major.$minor",  # Major.Minor (e.g., 1.4)
            "$major"  # Major only (e.g., 1)
        )
    }
    
    $moduleMap = $null
    foreach ($versionPattern in $versionPatterns) {
        $cacheKey = "$Name@$versionPattern"
        
        if ($script:moduleMapCache.ContainsKey($cacheKey)) {
            Write-Verbose "Using cached redirect map for: $cacheKey"
            $moduleMap = $script:moduleMapCache[$cacheKey]
            break
        }
        
        $testPath = Join-Path $PSScriptRoot "maps" "$Name@$versionPattern.json"
        if (Test-Path $testPath) {
            Write-Verbose "Loading module-specific redirect map from: $testPath"
            $moduleMap = Get-Content $testPath -Raw | ConvertFrom-Json -AsHashtable
            $script:moduleMapCache[$cacheKey] = $moduleMap
            break
        }
    }
    
    if ($moduleMap) {
        $mergedMap = @{}
        foreach ($key in $moduleMap.Keys) {
            $mergedMap[$key] = $moduleMap[$key]
        }
        foreach ($key in $redirectMap.Keys) {
            $mergedMap[$key] = $redirectMap[$key]  # Outer map wins
        }
        $redirectMap = $mergedMap
    }
    
    return $redirectMap
}

<#
.SYNOPSIS
    Builds a dependency graph by recursively querying a remote repository via Find-PSResource.
#>

function Build-RemoteDependencyGraph {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ModuleName,
        
        [Parameter(Mandatory = $true)]
        [string]$ModuleVersion,
        
        [Parameter(Mandatory = $true)]
        [hashtable]$Graph,
        
        [Parameter(Mandatory = $true)]
        [hashtable]$RedirectMap,
        
        [Parameter(Mandatory = $false)]
        [string]$Repository,
        
        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential,
        
        [Parameter(Mandatory = $false)]
        [switch]$Prerelease,
        
        [Parameter(Mandatory = $false)]
        [int]$Depth = 0
    )
    
    $indent = " " * $Depth
    $moduleKey = "${ModuleName}@${ModuleVersion}"
    
    # Skip if already processed
    if ($Graph.ContainsKey($moduleKey)) {
        Write-Verbose "${indent}Already in graph: $moduleKey"
        return
    }
    
    $findParams = @{
        Name = $ModuleName
        Version = $ModuleVersion
    }
    if ($Repository) {
        $findParams['Repository'] = $Repository
    }
    if ($Credential) {
        $findParams['Credential'] = $Credential
    }
    if ($Prerelease) {
        $findParams['Prerelease'] = $Prerelease
    }
    
    Write-Verbose "Looking for dependencies: $ModuleName version $ModuleVersion"
    $moduleInfo = Find-PSResource @findParams -ErrorAction SilentlyContinue | Select-Object -First 1
    
    $notFound = $false
    if (-not $moduleInfo) {
        Write-Verbose "${indent}Module not found in repository: $ModuleName version $ModuleVersion (will validate after resolution)"
        $notFound = $true
    }
    
    Write-Verbose "${indent}Building graph for: $ModuleName version $ModuleVersion"
    
    $graphNode = [DependencyGraphNode]::new(
        $ModuleName,
        $ModuleVersion,
        [System.Collections.ArrayList]@(),
        $notFound,
        $(if ($moduleInfo) { $moduleInfo.Repository } else { $null }),
        $null
    )
    $Graph[$moduleKey] = $graphNode
    
    if ($notFound) {
        return
    }
    
    $deps = $moduleInfo.Dependencies
    if (-not $deps -or $deps.Count -eq 0) {
        Write-Verbose "${indent}No dependencies for $ModuleName"
        return
    }
    
    Write-Verbose "${indent}Found $($deps.Count) dependency(ies)"
    
    foreach ($dep in $deps) {
        $depName = $dep.Name
        $depVersion = $dep.VersionRange
        
        $redirectResult = Find-DependencyRedirect -DependencyName $depName -DependencyVersion $depVersion `
            -RedirectMap $RedirectMap -Indent $indent
        
        $resolvedDepVersion = $redirectResult.ResolvedVersion
        $resolvedDepName = $redirectResult.ResolvedName
        
        $depKey = "${resolvedDepName}@${resolvedDepVersion}"
        Write-Verbose "${indent} Dependency: $depKey"
        
        [void]$graphNode.Dependencies.Add($depKey)
        
        $depRedirectMap = Get-MergedRedirectMap -OuterMap $RedirectMap -Name $resolvedDepName -Version $resolvedDepVersion
        
        Build-RemoteDependencyGraph -ModuleName $resolvedDepName -ModuleVersion $resolvedDepVersion `
            -Graph $Graph -RedirectMap $depRedirectMap -Repository $Repository -Credential $Credential -Prerelease:$Prerelease -Depth ($Depth + 1)
    }
}

<#
.SYNOPSIS
    Compares two semver strings including prerelease labels.
    Returns -1, 0, or 1. Prerelease < release (1.0.0-alpha < 1.0.0).
#>

function Compare-SemVer {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Version1,
        
        [Parameter(Mandatory = $true)]
        [string]$Version2
    )
    
    $v1Parts = $Version1 -split '-', 2
    $v2Parts = $Version2 -split '-', 2
    
    $v1Base = $v1Parts[0]
    $v2Base = $v2Parts[0]
    $v1Prerelease = if ($v1Parts.Count -gt 1) { $v1Parts[1] } else { $null }
    $v2Prerelease = if ($v2Parts.Count -gt 1) { $v2Parts[1] } else { $null }
    
    try {
        $v1Ver = [System.Version]$v1Base
        $v2Ver = [System.Version]$v2Base
        $baseCompare = $v1Ver.CompareTo($v2Ver)
    }
    catch {
        $baseCompare = [string]::Compare($v1Base, $v2Base, [StringComparison]::OrdinalIgnoreCase)
    }
    
    if ($baseCompare -ne 0) {
        return $baseCompare
    }
    
    # No prerelease > any prerelease (1.0.0 > 1.0.0-alpha)
    if ($null -eq $v1Prerelease -and $null -eq $v2Prerelease) {
        return 0
    }
    if ($null -eq $v1Prerelease) {
        return 1  # v1 is release, v2 is prerelease
    }
    if ($null -eq $v2Prerelease) {
        return -1  # v1 is prerelease, v2 is release
    }
    
    # Both have prerelease — lexicographic: alpha < beta < dev < rc
    return [string]::Compare($v1Prerelease, $v2Prerelease, [StringComparison]::OrdinalIgnoreCase)
}

<#
.SYNOPSIS
    Resolves diamond dependencies — selects the highest found version and updates all graph references.
#>

function Resolve-DiamondDependencies {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$Graph
    )
    
    # Group nodes by module name
    $moduleVersions = @{}
    foreach ($nodeKey in $Graph.Keys) {
        $node = $Graph[$nodeKey]
        $moduleName = $node.Name
        if (-not $moduleVersions.ContainsKey($moduleName)) {
            $moduleVersions[$moduleName] = @()
        }
        $moduleVersions[$moduleName] += @{
            Key = $nodeKey
            VersionString = $node.Version
            Node = $node
            NotFound = $node.NotFound
        }
    }
    
    # Resolve conflicts — prefer found versions, then highest semver
    foreach ($moduleName in $moduleVersions.Keys) {
        $versions = $moduleVersions[$moduleName]
        if ($versions.Count -gt 1) {
            $sorted = $versions | Sort-Object -Property @{
                Expression = {
                    if ($_.NotFound) { "1" } else { "0" }
                }
            }, @{
                Expression = {
                    $ver = $_.VersionString
                    $parts = $ver -split '-', 2
                    $base = $parts[0]
                    $prerelease = if ($parts.Count -gt 1) { $parts[1] } else { $null }
                    
                    $verParts = $base -split '\.'
                    $paddedBase = ($verParts | ForEach-Object { $_.PadLeft(10, '0') }) -join '.'
                    
                    # Release versions sort after prereleases
                    $prereleaseKey = if ($null -eq $prerelease) { 'zzzzzzzzzz' } else { $prerelease }
                    
                    "$paddedBase|$prereleaseKey"
                }
                Descending = $true
            }
            
            $highest = $sorted[0]
            $conflicts = $sorted | Select-Object -Skip 1
            
            $discardedNotFound = $conflicts | Where-Object { $_.NotFound }
            if ($discardedNotFound) {
                Write-Verbose " Discarding unavailable version(s): $($discardedNotFound.VersionString -join ', ') (higher version available)"
            }
            
            Write-Warning "Diamond dependency detected for '$moduleName': versions $($versions.VersionString -join ', '). Using highest available: $($highest.VersionString)"
            
            foreach ($conflict in $conflicts) {
                $oldKey = $conflict.Key
                $newKey = $highest.Key
                
                Write-Verbose " Redirecting $oldKey -> $newKey"
                
                # Update all references from old version to new
                foreach ($nodeKey in $Graph.Keys) {
                    $node = $Graph[$nodeKey]
                    for ($i = 0; $i -lt $node.Dependencies.Count; $i++) {
                        if ($node.Dependencies[$i] -eq $oldKey) {
                            $node.Dependencies[$i] = $newKey
                        }
                    }
                }
                
                $Graph.Remove($oldKey)
            }
        }
    }
    
    # Validate all remaining nodes were found
    foreach ($nodeKey in $Graph.Keys) {
        $node = $Graph[$nodeKey]
        if ($node.NotFound) {
            throw "Module not found: $($node.Name) version $($node.Version). No alternative version available to satisfy the dependency."
        }
    }
}

<#
.SYNOPSIS
    Returns module keys in topological order (dependencies first). Warns on cycles.
#>

function Get-TopologicalOrder {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$Graph
    )
    
    $visited = @{}
    $visiting = @{}  # For cycle detection
    $order = [System.Collections.ArrayList]@()
    
    function Visit {
        param([string]$NodeKey)
        
        if ($visited.ContainsKey($NodeKey)) {
            return
        }
        
        if ($visiting.ContainsKey($NodeKey)) {
            Write-Warning "Circular dependency detected involving: $NodeKey"
            return
        }
        
        $visiting[$NodeKey] = $true
        
        if ($Graph.ContainsKey($NodeKey)) {
            $node = $Graph[$NodeKey]
            foreach ($depKey in $node.Dependencies) {
                Visit -NodeKey $depKey
            }
        }
        
        $visiting.Remove($NodeKey)
        $visited[$NodeKey] = $true
        [void]$order.Add($NodeKey)
    }
    # Visit all nodes
    foreach ($nodeKey in $Graph.Keys) {
        Visit -NodeKey $nodeKey
    }
    
    return $order.ToArray()
}

<#
.SYNOPSIS
    Builds a dependency graph for installed modules via Get-PSResource.
#>

function Build-InstalledDependencyGraph {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ModuleName,
        
        [Parameter(Mandatory = $true)]
        [string]$ModuleVersion,
        
        [Parameter(Mandatory = $true)]
        [hashtable]$Graph,
        
        [Parameter(Mandatory = $true)]
        [hashtable]$RedirectMap,
        
        [Parameter(Mandatory = $false)]
        [int]$Depth = 0
    )
    
    $indent = " " * $Depth
    $moduleKey = "${ModuleName}@${ModuleVersion}"
    
    # Skip if already processed
    if ($Graph.ContainsKey($moduleKey)) {
        Write-Verbose "${indent}Already in graph: $moduleKey"
        return
    }
    
    # Find the installed module
    $installedModule = Get-PSResource -Name $ModuleName -Version $ModuleVersion -ErrorAction SilentlyContinue | Select-Object -First 1
    
    $notFound = $false
    if (-not $installedModule) {
        Write-Verbose "${indent}Module not installed: $ModuleName version $ModuleVersion (will validate after resolution)"
        $notFound = $true
    }
    
    $actualVersion = if ($installedModule) { $installedModule.Version.ToString() } else { $ModuleVersion }
    Write-Verbose "${indent}Building graph for: $ModuleName version $actualVersion"
    
    # InstalledLocation is the base modules folder; append ModuleName/Version
    $moduleVersionPath = if ($installedModule) { Join-Path $installedModule.InstalledLocation $ModuleName $actualVersion } else { $null }
    
    $graphNode = [DependencyGraphNode]::new(
        $ModuleName,
        $actualVersion,
        [System.Collections.ArrayList]@(),
        $notFound,
        $null,
        $moduleVersionPath
    )
    $Graph[$moduleKey] = $graphNode
    
    if ($notFound) {
        return
    }
    
    $deps = $installedModule.Dependencies
    if (-not $deps -or $deps.Count -eq 0) {
        Write-Verbose "${indent}No dependencies for $ModuleName"
        return
    }
    
    Write-Verbose "${indent}Found $($deps.Count) dependency(ies)"
    
    foreach ($dep in $deps) {
        $depName = $dep.Name
        $depVersion = $dep.VersionRange
        
        $redirectResult = Find-DependencyRedirect -DependencyName $depName -DependencyVersion $depVersion `
            -RedirectMap $RedirectMap -Indent $indent
        
        $resolvedDepVersion = $redirectResult.ResolvedVersion
        $resolvedDepName = $redirectResult.ResolvedName
        
        $depKey = "${resolvedDepName}@${resolvedDepVersion}"
        Write-Verbose "${indent} Dependency: $depKey"
        
        [void]$graphNode.Dependencies.Add($depKey)
        
        $depRedirectMap = Get-MergedRedirectMap -OuterMap $RedirectMap -Name $resolvedDepName -Version $resolvedDepVersion
        
        Build-InstalledDependencyGraph -ModuleName $resolvedDepName -ModuleVersion $resolvedDepVersion `
            -Graph $Graph -RedirectMap $depRedirectMap -Depth ($Depth + 1)
    }
}

function Install-PSResourcePinned {
    <#
    .SYNOPSIS
        Installs a module with pinned dependency versions.
        Works around PowerCLI not following semver (13.4 breaks backward-compat).
        
    .EXAMPLE
        Install-PSResourcePinned -Name "VMware.PowerCLI" -RequiredVersion "13.3.0"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,
        
        [Parameter(Mandatory = $true)]
        [string]$RequiredVersion,
        
        [Parameter(Mandatory = $false)]
        [string]$RedirectMapPath,
        
        [Parameter(Mandatory = $false)]
        [ValidateSet('CurrentUser', 'AllUsers')]
        [string]$Scope = 'CurrentUser',
        
        [Parameter(Mandatory = $false)]
        [string]$Repository,
        
        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential,
        
        [Parameter(Mandatory = $false)]
        [switch]$Prerelease,
        
        [Parameter(Mandatory = $false)]
        [switch]$Force
    )
    
    # Load redirect map
    if ($RedirectMapPath) {
        if (-not (Test-Path $RedirectMapPath)) {
            throw "Redirect map file not found: $RedirectMapPath"
        }
        Write-Verbose "Loading redirect map from: $RedirectMapPath"
        $redirectMap = Get-Content $RedirectMapPath -Raw | ConvertFrom-Json -AsHashtable
    }
    else {
        Write-Verbose "Using default redirect map"
        $redirectMap = $script:defaultRedirectMap
    }
    
    $redirectMap = Get-MergedRedirectMap -OuterMap $redirectMap -Name $Name -Version $RequiredVersion
    
    Write-Verbose "Building dependency graph for $Name version $RequiredVersion"
    $dependencyGraph = @{}
    
    Build-RemoteDependencyGraph -ModuleName $Name -ModuleVersion $RequiredVersion `
        -Graph $dependencyGraph -RedirectMap $redirectMap -Repository $Repository -Credential $Credential -Prerelease:$Prerelease
    
    Resolve-DiamondDependencies -Graph $dependencyGraph
    
    Write-Verbose "Computing topological order"
    $topologicalOrder = @(Get-TopologicalOrder -Graph $dependencyGraph)
    
    Write-Verbose "Install order ($($topologicalOrder.Count) modules):"
    for ($i = 0; $i -lt $topologicalOrder.Count; $i++) {
        Write-Verbose " $($i + 1). $($topologicalOrder[$i])"
    }
    
    # Install modules in topological order
    foreach ($moduleKey in $topologicalOrder) {
        $node = $dependencyGraph[$moduleKey]
        $modName = $node.Name
        $modVersion = $node.Version
        
        $installed = $null
        if (-not $Force) {
            $installed = Get-PSResource -Name $modName -ErrorAction SilentlyContinue | 
                Where-Object {
                    if (-not $_) { return $false }
                    $installedVersion = $_.Version.ToString()
                    if ($_.Prerelease) {
                        $installedVersion = "$installedVersion-$($_.Prerelease)"
                    }
                    $installedVersion -eq $modVersion
                }
        }
        
        if (-not $installed) {
            Write-Verbose "Installing: $modName version $modVersion"
            $installParams = @{
                Name = $modName
                Version = $modVersion
                Scope = $Scope
                Prerelease = $Prerelease
                TrustRepository = $true
                SkipDependencyCheck = $true
            }
            if ($Repository) {
                $installParams['Repository'] = $Repository
            }
            if ($Credential) {
                $installParams['Credential'] = $Credential
            }
            if ($Force) {
                $installParams['Reinstall'] = $true
            }
            
            Install-PSResource @installParams
        }
        else {
            Write-Verbose "Already installed: $modName version $modVersion"
        }
    }
    
    $mainModuleKey = "${Name}@${RequiredVersion}"
    $mainNode = $dependencyGraph[$mainModuleKey]
    Write-Host "Successfully installed $Name version $($mainNode.Version)"
}

function Save-PSResourcePinned {
    <#
    .SYNOPSIS
        Downloads a module and its dependencies with pinned versions.
        Saves as expanded module folders by default; pass -AsNupkg to save as NuGet packages.
        
    .EXAMPLE
        Save-PSResourcePinned -Name "VMware.PowerCLI" -RequiredVersion "13.3.0" -Path "./packages"

    .EXAMPLE
        Save-PSResourcePinned -Name "VMware.PowerCLI" -RequiredVersion "13.3.0" -Path "./packages" -AsNupkg
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,
        
        [Parameter(Mandatory = $true)]
        [string]$RequiredVersion,
        
        [Parameter(Mandatory = $true)]
        [string]$Path,
        
        [Parameter(Mandatory = $false)]
        [string]$RedirectMapPath,
        
        [Parameter(Mandatory = $false)]
        [string]$Repository,
        
        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential,
        
        [Parameter(Mandatory = $false)]
        [switch]$AsNupkg,
        
        [Parameter(Mandatory = $false)]
        [switch]$Prerelease
    )
    
    # Validate and create destination path
    if (-not (Test-Path $Path)) {
        Write-Verbose "Creating destination directory: $Path"
        New-Item -ItemType Directory -Path $Path -Force | Out-Null
    }
    
    $resolvedPath = Resolve-Path $Path
    Write-Verbose "Saving packages to: $resolvedPath"
    
    # Load redirect map
    if ($RedirectMapPath) {
        if (-not (Test-Path $RedirectMapPath)) {
            throw "Redirect map file not found: $RedirectMapPath"
        }
        Write-Verbose "Loading redirect map from: $RedirectMapPath"
        $redirectMap = Get-Content $RedirectMapPath -Raw | ConvertFrom-Json -AsHashtable
    }
    else {
        Write-Verbose "Using default redirect map"
        $redirectMap = $script:defaultRedirectMap
    }
    
    $redirectMap = Get-MergedRedirectMap -OuterMap $redirectMap -Name $Name -Version $RequiredVersion
    
    Write-Verbose "Building dependency graph for $Name version $RequiredVersion"
    $dependencyGraph = @{}
    
    Build-RemoteDependencyGraph -ModuleName $Name -ModuleVersion $RequiredVersion `
        -Graph $dependencyGraph -RedirectMap $redirectMap -Repository $Repository -Credential $Credential -Prerelease:$Prerelease
    
    Resolve-DiamondDependencies -Graph $dependencyGraph
    
    Write-Verbose "Computing topological order"
    $topologicalOrder = @(Get-TopologicalOrder -Graph $dependencyGraph)
    
    Write-Verbose "Save order ($($topologicalOrder.Count) modules):"
    for ($i = 0; $i -lt $topologicalOrder.Count; $i++) {
        Write-Verbose " $($i + 1). $($topologicalOrder[$i])"
    }
    
    # Save modules in topological order
    foreach ($moduleKey in $topologicalOrder) {
        $node = $dependencyGraph[$moduleKey]
        $modName = $node.Name
        $modVersion = $node.Version
        
        # Existing-output detection depends on output format:
        # -AsNupkg -> $Path/$modName.$modVersion.nupkg
        # (default) -> $Path/$modName/$modVersion (expanded module folder)
        if ($AsNupkg) {
            $expectedPath = Join-Path $resolvedPath.Path "$modName.$modVersion.nupkg"
        }
        else {
            $expectedPath = Join-Path $resolvedPath.Path $modName $modVersion
        }
        
        if (-not (Test-Path $expectedPath)) {
            Write-Verbose "Saving: $modName version $modVersion"
            $saveParams = @{
                Name = $modName
                Version = $modVersion
                Path = $resolvedPath.Path
                Prerelease = $Prerelease
                TrustRepository = $true
                SkipDependencyCheck = $true
            }
            if ($AsNupkg) {
                $saveParams['AsNupkg'] = $true
            }
            if ($Repository) {
                $saveParams['Repository'] = $Repository
            }
            if ($Credential) {
                $saveParams['Credential'] = $Credential
            }
            
            Save-PSResource @saveParams
        }
        else {
            Write-Verbose "Already saved: $modName version $modVersion"
        }
    }
    
    $mainModuleKey = "${Name}@${RequiredVersion}"
    $mainNode = $dependencyGraph[$mainModuleKey]
    Write-Host "Successfully saved $Name version $($mainNode.Version) and dependencies to $resolvedPath"
}

<#
.SYNOPSIS
    Extracts RequiredModules and ModuleList from a manifest, deduped (RequiredModules wins).
    NuGet feeds package both as dependencies; reading both keeps the local graph consistent
    with remote graphs and prevents "assembly already loaded" errors during pre-loading.

.OUTPUTS
    Array of @{ Name; Version } hashtables.
#>

function Get-ManifestModuleDependencies {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$Manifest
    )

    $hasRequired = $Manifest.ContainsKey('RequiredModules') -and $Manifest.RequiredModules -and $Manifest.RequiredModules.Count -gt 0
    $hasModuleList = $Manifest.ContainsKey('ModuleList') -and $Manifest.ModuleList -and $Manifest.ModuleList.Count -gt 0

    if (-not $hasRequired -and -not $hasModuleList) {
        return @()
    }

    # Parse a single manifest entry into @{ Name; Version }
    function ParseEntry {
        param([object]$Entry, [string]$Source)

        if ($Entry -is [string]) {
            throw "$Source entry '$Entry' has no version. All entries must specify a version (RequiredVersion or ModuleVersion)."
        }
        elseif ($Entry -is [hashtable]) {
            $name = if ($Entry.ContainsKey('ModuleName')) { $Entry.ModuleName } else { $null }
            if (-not $name) {
                throw "$Source entry has no module name: $($Entry | ConvertTo-Json -Compress)"
            }
            $version = $null
            if ($Entry.ContainsKey('RequiredVersion')) {
                $version = $Entry.RequiredVersion.ToString()
            }
            elseif ($Entry.ContainsKey('ModuleVersion')) {
                $version = "[$($Entry.ModuleVersion), )"
            }
            if (-not $version) {
                throw "$Source entry '$name' has no version. All entries must specify a version (RequiredVersion or ModuleVersion)."
            }
            return @{ Name = $name; Version = $version }
        }
        else {
            throw "Unrecognized $Source format in manifest: $Entry. Expected string or hashtable."
        }
    }

    # RequiredModules take precedence
    $seen = @{}
    $results = [System.Collections.ArrayList]@()

    if ($hasRequired) {
        Write-Verbose "Found $($Manifest.RequiredModules.Count) module(s) in RequiredModules"
        foreach ($entry in $Manifest.RequiredModules) {
            $parsed = ParseEntry -Entry $entry -Source 'RequiredModules'
            $key = $parsed.Name.ToLowerInvariant()
            if (-not $seen.ContainsKey($key)) {
                $seen[$key] = $true
                [void]$results.Add($parsed)
            }
        }
    }

    if ($hasModuleList) {
        Write-Verbose "Found $($Manifest.ModuleList.Count) module(s) in ModuleList"
        foreach ($entry in $Manifest.ModuleList) {
            $parsed = ParseEntry -Entry $entry -Source 'ModuleList'
            $key = $parsed.Name.ToLowerInvariant()
            if (-not $seen.ContainsKey($key)) {
                $seen[$key] = $true
                [void]$results.Add($parsed)
            }
            else {
                Write-Verbose "Skipping ModuleList entry '$($parsed.Name)' — already declared in RequiredModules"
            }
        }
    }

    return $results.ToArray()
}

function Find-PSResourceDependencies {
    <#
    .SYNOPSIS
        Resolves all dependencies (RequiredModules + ModuleList) from a .psd1 manifest via remote repository.
        
    .EXAMPLE
        Find-PSResourceDependencies -ManifestPath "./MyModule/MyModule.psd1"
        
    .OUTPUTS
        Array of PSCustomObject with Name, Version, Repository, and IsRedirected properties.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ManifestPath,
        
        [Parameter(Mandatory = $false)]
        [string]$RedirectMapPath,
        
        [Parameter(Mandatory = $false)]
        [string]$Repository,
        
        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential,
        
        [Parameter(Mandatory = $false)]
        [switch]$Prerelease
    )
    
    if (-not (Test-Path $ManifestPath)) {
        throw "Manifest file not found: $ManifestPath"
    }
    
    $resolvedPath = Resolve-Path $ManifestPath
    if (-not $resolvedPath.Path.EndsWith('.psd1')) {
        throw "File must be a PowerShell module manifest (.psd1): $ManifestPath"
    }
    
    Write-Verbose "Reading manifest from: $resolvedPath"
    
    $manifest = Import-PowerShellDataFile -Path $resolvedPath
    
    $moduleDependencies = @(Get-ManifestModuleDependencies -Manifest $manifest)
    if ($moduleDependencies.Count -eq 0) {
        Write-Verbose "No module dependencies found in manifest (RequiredModules or ModuleList)"
        return @()
    }
    
    $manifestModuleName = [System.IO.Path]::GetFileNameWithoutExtension($resolvedPath.Path)
    $manifestModuleVersion = if ($manifest.ModuleVersion) { $manifest.ModuleVersion.ToString() } else { "" }
    
    if ($RedirectMapPath) {
        if (-not (Test-Path $RedirectMapPath)) {
            throw "Redirect map file not found: $RedirectMapPath"
        }
        Write-Verbose "Loading redirect map from: $RedirectMapPath"
        $redirectMap = Get-Content $RedirectMapPath -Raw | ConvertFrom-Json -AsHashtable
    }
    else {
        Write-Verbose "Looking for redirect map based on manifest: $manifestModuleName version $manifestModuleVersion"
        $redirectMap = Get-MergedRedirectMap -OuterMap $script:defaultRedirectMap -Name $manifestModuleName -Version $manifestModuleVersion
    }
    
    Write-Verbose "Found $($moduleDependencies.Count) module dependency(ies) in manifest"
    
    $dependencyGraph = @{}
    
    foreach ($depEntry in $moduleDependencies) {
        $moduleName = $depEntry.Name
        $moduleVersion = $depEntry.Version
        
        $mergedRedirectMap = Get-MergedRedirectMap -OuterMap $redirectMap -Name $moduleName -Version ($moduleVersion ?? "")
        $redirectResult = Find-DependencyRedirect -DependencyName $moduleName -DependencyVersion $moduleVersion `
            -RedirectMap $mergedRedirectMap -Indent ""
        
        Build-RemoteDependencyGraph -ModuleName $redirectResult.ResolvedName -ModuleVersion $redirectResult.ResolvedVersion `
            -Graph $dependencyGraph -RedirectMap $mergedRedirectMap -Repository $Repository -Credential $Credential -Prerelease:$Prerelease
    }
    
    Resolve-DiamondDependencies -Graph $dependencyGraph
    
    $topologicalOrder = @(Get-TopologicalOrder -Graph $dependencyGraph)
    
    $resolvedDependencies = [System.Collections.ArrayList]@()
    
    foreach ($moduleKey in $topologicalOrder) {
        $node = $dependencyGraph[$moduleKey]
        
        [void]$resolvedDependencies.Add([PSCustomObject]@{
            Name = $node.Name
            Version = $node.Version
            Repository = $node.Repository
            IsRedirected = $false  # Graph already has redirected versions applied
        })
    }
    
    Write-Verbose "Resolved $($resolvedDependencies.Count) module(s) (including transitive dependencies)"
    
    return $resolvedDependencies.ToArray()
}

function Install-PSResourceDependencies {
    <#
    .SYNOPSIS
        Installs all manifest dependencies using Find-PSResourceDependencies.
        
    .EXAMPLE
        Install-PSResourceDependencies -ManifestPath "./MyModule/MyModule.psd1"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ManifestPath,
        
        [Parameter(Mandatory = $false)]
        [string]$RedirectMapPath,
        
        [Parameter(Mandatory = $false)]
        [ValidateSet('CurrentUser', 'AllUsers')]
        [string]$Scope = 'CurrentUser',
        
        [Parameter(Mandatory = $false)]
        [string]$Repository,
        
        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential,
        
        [Parameter(Mandatory = $false)]
        [switch]$Force
    )
    
    $findParams = @{
        ManifestPath = $ManifestPath
    }
    if ($RedirectMapPath) {
        $findParams['RedirectMapPath'] = $RedirectMapPath
    }
    if ($Repository) {
        $findParams['Repository'] = $Repository
    }
    if ($Credential) {
        $findParams['Credential'] = $Credential
    }
    
    $resolvedDependencies = Find-PSResourceDependencies @findParams
    
    if (-not $resolvedDependencies -or $resolvedDependencies.Count -eq 0) {
        Write-Verbose "No dependencies to install"
        return
    }
    
    Write-Verbose "Installing $($resolvedDependencies.Count) resolved dependency(ies)"
    
    foreach ($dependency in $resolvedDependencies) {
        $installed = $null
        if (-not $Force) {
            $installed = Get-PSResource -Name $dependency.Name -ErrorAction SilentlyContinue | 
                Where-Object { $_.Version.ToString() -eq $dependency.Version }
        }
        
        if (-not $installed) {
            Write-Host "Installing dependency: $($dependency.Name) version $($dependency.Version)"
            
            $installParams = @{
                Name = $dependency.Name
                Version = $dependency.Version
                Scope = $Scope
                TrustRepository = $true
                SkipDependencyCheck = $true
            }
            if ($Repository) {
                $installParams['Repository'] = $Repository
            }
            if ($Credential) {
                $installParams['Credential'] = $Credential
            }
            if ($Force) {
                $installParams['Reinstall'] = $true
            }
            
            Install-PSResource @installParams
        }
        else {
            Write-Verbose "Already installed: $($dependency.Name) version $($dependency.Version)"
        }
    }
    
    Write-Host "Successfully installed all dependencies from manifest"
}

function Import-PSResourceDependencies {
    <#
    .SYNOPSIS
        Imports all manifest dependencies (RequiredModules + ModuleList) in topological order
        with pinned versions. Prevents "assembly already loaded" errors from incomplete graphs.
        
    .EXAMPLE
        Import-PSResourceDependencies -ManifestPath "./MyModule/MyModule.psd1"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ManifestPath,
        
        [Parameter(Mandatory = $false)]
        [string]$RedirectMapPath,
        
        [Parameter(Mandatory = $false)]
        [switch]$Force,
        
        [Parameter(Mandatory = $false)]
        [switch]$PassThru
    )
    
    # Validate manifest path
    if (-not (Test-Path $ManifestPath)) {
        throw "Manifest file not found: $ManifestPath"
    }
    
    $resolvedPath = Resolve-Path $ManifestPath
    if (-not $resolvedPath.Path.EndsWith('.psd1')) {
        throw "File must be a PowerShell module manifest (.psd1): $ManifestPath"
    }
    
    Write-Verbose "Reading manifest from: $resolvedPath"
    
    # Parse the manifest
    $manifest = Import-PowerShellDataFile -Path $resolvedPath
    
    # Extract module dependencies from both RequiredModules and ModuleList
    $moduleDependencies = @(Get-ManifestModuleDependencies -Manifest $manifest)
    if ($moduleDependencies.Count -eq 0) {
        Write-Verbose "No module dependencies found in manifest (RequiredModules or ModuleList)"
        return
    }
    
    $manifestModuleName = [System.IO.Path]::GetFileNameWithoutExtension($resolvedPath.Path)
    $manifestModuleVersion = if ($manifest.ModuleVersion) { $manifest.ModuleVersion.ToString() } else { "" }
    
    if ($RedirectMapPath) {
        if (-not (Test-Path $RedirectMapPath)) {
            throw "Redirect map file not found: $RedirectMapPath"
        }
        Write-Verbose "Loading redirect map from: $RedirectMapPath"
        $redirectMap = Get-Content $RedirectMapPath -Raw | ConvertFrom-Json -AsHashtable
    }
    else {
        Write-Verbose "Looking for redirect map based on manifest: $manifestModuleName version $manifestModuleVersion"
        $redirectMap = Get-MergedRedirectMap -OuterMap $script:defaultRedirectMap -Name $manifestModuleName -Version $manifestModuleVersion
    }
    
    Write-Verbose "Found $($moduleDependencies.Count) module dependency(ies) in manifest"
    
    $dependencyGraph = @{}
    
    foreach ($depEntry in $moduleDependencies) {
        $moduleName = $depEntry.Name
        $moduleVersion = $depEntry.Version
        
        $mergedRedirectMap = Get-MergedRedirectMap -OuterMap $redirectMap -Name $moduleName -Version ($moduleVersion ?? "")
        $redirectResult = Find-DependencyRedirect -DependencyName $moduleName -DependencyVersion $moduleVersion `
            -RedirectMap $mergedRedirectMap -Indent ""
        
        Build-InstalledDependencyGraph -ModuleName $redirectResult.ResolvedName -ModuleVersion $redirectResult.ResolvedVersion `
            -Graph $dependencyGraph -RedirectMap $mergedRedirectMap
    }
    
    Resolve-DiamondDependencies -Graph $dependencyGraph
    
    # Compute topological order
    Write-Verbose "Computing topological order"
    $topologicalOrder = @(Get-TopologicalOrder -Graph $dependencyGraph)
    
    Write-Verbose "Import order ($($topologicalOrder.Count) modules):"
    for ($i = 0; $i -lt $topologicalOrder.Count; $i++) {
        Write-Verbose " $($i + 1). $($topologicalOrder[$i])"
    }
    
    Write-Verbose "Pre-loading all modules in topological order"
    
    $importedModules = @{}
    
    foreach ($moduleKey in $topologicalOrder) {
        $node = $dependencyGraph[$moduleKey]
        $modName = $node.Name
        $modVersion = $node.Version
        
        $loadedModule = Get-Module -Name $modName | Where-Object { $_.Version.ToString() -eq $modVersion }
        
        if ($loadedModule -and -not $Force) {
            Write-Verbose "Already loaded: $modName version $modVersion"
            $importedModules[$moduleKey] = $loadedModule
            continue
        }
        
        # -Global ensures modules persist after this function returns
        $importParams = @{
            Name = $modName
            RequiredVersion = $modVersion
            ErrorAction = 'Stop'
            DisableNameChecking = $true
            Global = $true
        }
        
        if ($Force) {
            $importParams['Force'] = $true
        }
        
        try {
            Write-Verbose "Importing: $modName version $modVersion"
            $imported = Import-Module @importParams -PassThru
            $importedModules[$moduleKey] = $imported
        }
        catch {
            throw "Failed to import $modName version $($modVersion): $_"
        }
    }
    
    Write-Verbose "Successfully imported $($importedModules.Count) module(s) from manifest"
    
    if ($PassThru) {
        return $importedModules.Values
    }
}

function Import-ModulePinned {
    <#
    .SYNOPSIS
        Imports a module after pre-loading ALL transitive dependencies at exact versions.
        Prevents PowerShell from loading wrong versions via minimum-version semantics.
        
    .EXAMPLE
        Import-ModulePinned -Name "VMware.PowerCLI" -RequiredVersion "13.3.0"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$Name,
        
        [Parameter(Mandatory = $true, Position = 1)]
        [string]$RequiredVersion,
        
        [Parameter(Mandatory = $false)]
        [string]$RedirectMapPath,
        
        [Parameter(Mandatory = $false)]
        [switch]$Force,
        
        [Parameter(Mandatory = $false)]
        [string]$Prefix,
        
        [Parameter(Mandatory = $false)]
        [switch]$PassThru
    )
    
    # Load redirect map
    if ($RedirectMapPath) {
        if (-not (Test-Path $RedirectMapPath)) {
            throw "Redirect map file not found: $RedirectMapPath"
        }
        Write-Verbose "Loading redirect map from: $RedirectMapPath"
        $redirectMap = Get-Content $RedirectMapPath -Raw | ConvertFrom-Json -AsHashtable
    }
    else {
        Write-Verbose "Using default redirect map"
        $redirectMap = $script:defaultRedirectMap
    }
    
    $redirectMap = Get-MergedRedirectMap -OuterMap $redirectMap -Name $Name -Version $RequiredVersion
    
    Write-Verbose "Building dependency graph for $Name version $RequiredVersion"
    $dependencyGraph = @{}
    
    Build-InstalledDependencyGraph -ModuleName $Name -ModuleVersion $RequiredVersion `
        -Graph $dependencyGraph -RedirectMap $redirectMap
    
    Resolve-DiamondDependencies -Graph $dependencyGraph
    
    Write-Verbose "Dependency graph contains $($dependencyGraph.Count) modules:"
    
    foreach ($nodeKey in $dependencyGraph.Keys | Sort-Object) {
        $node = $dependencyGraph[$nodeKey]
        Write-Verbose " $nodeKey"
        Write-Verbose " Location: $($node.InstalledLocation)"
        if ($node.Dependencies.Count -gt 0) {
            Write-Verbose " Dependencies:"
            foreach ($dep in $node.Dependencies) {
                Write-Verbose " -> $dep"
            }
        }
        else {
            Write-Verbose " Dependencies: (none)"
        }
    }
    
    # Compute topological order
    Write-Verbose "Computing topological order"
    $topologicalOrder = @(Get-TopologicalOrder -Graph $dependencyGraph)
    
    Write-Verbose "Import order ($($topologicalOrder.Count) modules):"
    for ($i = 0; $i -lt $topologicalOrder.Count; $i++) {
        Write-Verbose " $($i + 1). $($topologicalOrder[$i])"
    }
    Write-Verbose "Pre-loading all modules in topological order"
    
    $importedModules = @{}
    
    foreach ($moduleKey in $topologicalOrder) {
        $node = $dependencyGraph[$moduleKey]
        $modName = $node.Name
        $modVersion = $node.Version
        
        $loadedModule = Get-Module -Name $modName | Where-Object { $_.Version.ToString() -eq $modVersion }
        
        if ($loadedModule -and -not $Force) {
            Write-Verbose "Already loaded: $modName version $modVersion"
            $importedModules[$moduleKey] = $loadedModule
            continue
        }
        
        # -Global ensures modules persist after this function returns
        $importParams = @{
            Name = $modName
            RequiredVersion = $modVersion
            ErrorAction = 'Stop'
            DisableNameChecking = $true
            Global = $true
        }
        
        if ($Force) {
            $importParams['Force'] = $true
        }
        
        try {
            Write-Verbose "Importing: $modName version $modVersion"
            $imported = Import-Module @importParams -PassThru
            $importedModules[$moduleKey] = $imported
        }
        catch {
            throw "Failed to import $modName version $($modVersion): $_"
        }
    }
    
    Write-Verbose "Returning main module"
    
    $mainModuleKey = "${Name}@${RequiredVersion}"
    $mainModule = $importedModules[$mainModuleKey]
    
    if (-not $mainModule) {
        $mainModule = Get-Module -Name $Name | Where-Object { $_.Version.ToString() -eq $RequiredVersion }
    }
    
    Write-Verbose "Successfully imported $Name version $RequiredVersion (and $($importedModules.Count - 1) dependencies)"
    
    if ($PassThru) {
        return $mainModule
    }
}

function Find-PSResourcesPinned {
    <#
    .SYNOPSIS
        Resolves a module and all dependencies with pinned versions. Returns results in topological order.
        
    .EXAMPLE
        Find-PSResourcesPinned -Name "VMware.PowerCLI" -RequiredVersion "13.3.0"
        
    .OUTPUTS
        Array of objects with Name, Version, Repository, and Dependencies properties.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,
        
        [Parameter(Mandatory = $true)]
        [string]$RequiredVersion,
        
        [Parameter(Mandatory = $false)]
        [string]$RedirectMapPath,
        
        [Parameter(Mandatory = $false)]
        [string]$Repository,
        
        [Parameter(Mandatory = $false)]
        [PSCredential]$Credential,
        
        [Parameter(Mandatory = $false)]
        [switch]$Prerelease
    )
    
    # Load redirect map
    if ($RedirectMapPath) {
        if (-not (Test-Path $RedirectMapPath)) {
            throw "Redirect map file not found: $RedirectMapPath"
        }
        Write-Verbose "Loading redirect map from: $RedirectMapPath"
        $redirectMap = Get-Content $RedirectMapPath -Raw | ConvertFrom-Json -AsHashtable
    }
    else {
        Write-Verbose "Using default redirect map"
        $redirectMap = $script:defaultRedirectMap
    }
    
    $redirectMap = Get-MergedRedirectMap -OuterMap $redirectMap -Name $Name -Version $RequiredVersion
    
    Write-Verbose "Building dependency graph for $Name version $RequiredVersion"
    $dependencyGraph = @{}
    
    Build-RemoteDependencyGraph -ModuleName $Name -ModuleVersion $RequiredVersion `
        -Graph $dependencyGraph -RedirectMap $redirectMap -Repository $Repository -Credential $Credential -Prerelease:$Prerelease
    
    Resolve-DiamondDependencies -Graph $dependencyGraph
    
    Write-Verbose "Computing topological order"
    $topologicalOrder = @(Get-TopologicalOrder -Graph $dependencyGraph)
    
    $resolvedModules = [System.Collections.ArrayList]@()
    
    foreach ($moduleKey in $topologicalOrder) {
        $node = $dependencyGraph[$moduleKey]
        
        [void]$resolvedModules.Add([PSCustomObject]@{
            Name = $node.Name
            Version = $node.Version
            Repository = $node.Repository
            Dependencies = $node.Dependencies
        })
    }
    
    Write-Verbose "Found $($resolvedModules.Count) module(s) (including main module and all dependencies)"
    
    return $resolvedModules.ToArray()
}
# SIG # Begin signature block
# MIIncQYJKoZIhvcNAQcCoIInYjCCJ14CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCClPUYI3amuSDUy
# 5Qku568MOQ61BfA/2vvnfH+KzZ54fKCCDMkwggYEMIID7KADAgECAhMzAAACHPrN
# xZvoL37EAAAAAAIcMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD
# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQxWhcNMjcwNDE1MTg1
# OTQxWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD
# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDVsZfgOKmM31HPfoWOoNEiw0SlCiIxUMC0I9NMWbucKOw/e9lP
# oAoehQVu6SG65V4EPzrYsnBnFPNoi4/HoOdjhz1qkrEt4I6tEcxXU6oOeY9zGveC
# /3iBeuhLYxM3M/PkcUoebF+Nednm8OkdSPoDu8imViHPQq/8CQUu0WRR4rE+dMRf
# rpVqfmNi2qWCX94T4MsepijGVkwE//tJg0ryAiYdHT34LSnlG/RSBZmQRGWZ5g8j
# qnKjRParSqMft1gvjuUTVgtWNZfgcLFSK5Wa0myrq8OPcgTGGsRgun+tnSS+IxDT
# xVsAPH1OzvPjwomguByhUe/OcvUN0D5Wmp7xAgMBAAGjggGqMIIBpjAOBgNVHQ8B
# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O
# BBYEFNoH7a2YDjOSwpkp6DHcmUS7J+0yMFQGA1UdEQRNMEukSTBHMS0wKwYDVQQL
# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxFjAUBgNVBAUT
# DTIzMDAxMis1MDc1NjkwHwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEw
# YAYDVR0fBFkwVzBVoFOgUYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w
# cy9jcmwvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy
# bDBtBggrBgEFBQcBAQRhMF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9z
# b2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmcl
# MjBQQ0ElMjAyMDI0LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IC
# AQAUnEqhaRXe0T3hIJjvdQErEkrA/7bByjn6t5IArODkkRjzkYwtKMc2yYj2quaN
# rLutWw2YZcngKPy1b71YyDJQTy4NDRwaSh9Tw5thrk3NmcPrAHia5vtcBJ1CgtKK
# 7mQbIcQ22d/N3813ayCDDFewu1+jsZmX+r/aTEqaOM4TVxVtRSkuCy8nAXKuChOK
# Li/zA4XuH8iEYqIsj2YoNaeSxVmeGiERXpKdo3dDmYi0kO5w2D8VS4c3+9h6gElY
# BaAAg/dYErBg27qT3vv0zRDJhJufvCNylA8S7/+8H5E/PV5cng6na9VV/w9OV3qu
# uND6zdGa2EX38Glp50F9AIQk3p2xXmcvorDeM4XJ7UlWYBi6g80J1SSOQnInCYFE
# msfUNn3+1AaTJKSJL83quKArTac2pKhu0Yzzzrzo6HrsRiQKzpnRBb1/dMa6P3hz
# 75XbMRBctNsFhZC07WCmjExdLg2eHW5uV0TY8D5+6wozJf7vF3+WHkYPO85Z+BC6
# U4FkNbYNycZ9cE4j1tXRdyDCfml6c0HWPHjNVDObrv9lKt3qUqFpX38VCqVCyNOO
# 1UcXfQiVjJw32U2WUKZjt/neJKHEBsm9kFsLuWzkQ53+qcaSaytmsCnk2gOglrlD
# 5d3kKyvvAw+rzm0lT8K38P6PLxfZQHhu4W8dV7Av8N2ZmDCCBr0wggSloAMCAQIC
# EzMAAAA5O7Y3Gb8GHWcAAAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS
# b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoX
# DTM2MDMyMjIyMTMwNFowVzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQ
# Q0EgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeq
# lRYHNa265v4IY9fH8TKhemHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo
# 0dtS/EW6I/yEL/bLSY8hKpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATv
# QVL4tcf03aTycsz8QeCdM0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a
# 1uv1zerOYMnsneRRwCbpyW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1
# FyQfK0fVkaya8SmVHQ/tOf23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfO
# GSWHIIV4YrTJTT6PNty5REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7
# ttOu1bVnXfHaqPYl2rPs20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJ
# uz2MXMCt7iw7lFPG9LXKGjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxS
# CwyoGIq0PhaA7Y+VPct5pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOm
# VQop36wUVUYklUy++vDWeEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3
# SkE/xIkgpfl22MM1itkZ35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8E
# BAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPX
# LQaUEggxMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMB
# Af8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBP
# oE2gS4ZJaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv
# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAw
# TgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv
# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOC
# AgEAFJQfOChP7onn6fLIMKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D
# 5W4wMwYeLystcEqfkjz4NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBY
# nbu0+THSuVHTe0VTTPVhily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSI
# vgn0JksVBVMYVI5QFu/qhnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6
# aR9y34aiM1qmxaxBi6OUnyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4w
# PKC5OmHm1DQIt/MNokbbH3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7
# RTX8AdBPo0I6OEojf39zuFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK
# /fg8B2qjW88MT/WF5V5uvZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSK
# YBv0VisCzfxgeU+dquXW9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkw
# YTu/9dLeH2pDqeJZAABVDWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVT
# Ql0v4q8J/AUmQN5W4n101cY2L4A7GTQG1h32HHAvfQESWP0xghn+MIIZ+gIBATBu
# MFcxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# KDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIc
# +s3Fm+gvfsQAAAAAAhwwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwG
# CisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZI
# hvcNAQkEMSIEIDYKto4bhQZFVWLCB2PEp4wQCmp8kBp26HqVegSZIl1pMEIGCisG
# AQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAyh6/zL8WlyWPsgbnivlJ
# aIULErR5mx4vzkVznNyUtDF0eiHnGzJkG7TRFm2UlTdST4/sg3NfF26mMhUYlqal
# ds+FWpqFT4IT9GgLgv3ArMBzEL0AVZvyHjGA86cCDMtDrTTVrJHTLRP9HbHeGpty
# Zb9HhcOK9vfz83gvwYQe3kdR6RjrUZjNCi1BVLNbKhTm8lawPe6s2gM0IUK/0KzM
# l+TAJRrt+Hn4nUl+D14G5/tQkBA6r++5rbTEzSP/Pe+KcXL3D5q/2bpQaCQGI56B
# vOKsB/Hp1z1I90DWM1vClEyHrv3wXhnw8JN0TYZYox5vVENevNI2PBJ+yQbRE1zV
# BaGCF7AwghesBgorBgEEAYI3AwMBMYIXnDCCF5gGCSqGSIb3DQEHAqCCF4kwgheF
# AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFFMIIB
# QQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCCl502lC4sgoQLwzBer
# Voe7GQQIf9JpSUfUEofOB7entgIGaeuLMYcPGBMyMDI2MDUyMTIxMDMwNy40MzVa
# MASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0
# ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjoyRDFBLTA1RTAtRDk0NzElMCMG
# A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEf4wggcoMIIFEKAD
# AgECAhMzAAACEtEIBjzKGE+qAAEAAAISMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNV
# BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w
# HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m
# dCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgxNVoXDTI2MTExMzE4
# NDgxNVowgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD
# VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTAr
# BgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEnMCUG
# A1UECxMeblNoaWVsZCBUU1MgRVNOOjJEMUEtMDVFMC1EOTQ3MSUwIwYDVQQDExxN
# aWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEFAAOC
# Ag8AMIICCgKCAgEAr0zToDkpWQtsZekS0cV0quDdKSTGkovvBaZH0OAIEi0O3CcO
# 77JiX8c4Epq9uibHVZZ1W/LoufE172vkRXO+QYNtWWorECJ2AcZQ10bpAltkhZNi
# XlVJ8L3QzhKgrXrmMkm2J+/g81U23JPcO4wXHEftonT3wpd//936rjmwxMm7Nkbs
# ygbJf+4AVBMNr4aMPQhBd76od0KMB6WrvyEGOOU0893OFufS5EDey4n44WgaxJE0
# Vnv3/OOvuOw5Kp1KPqjjYJ+L9ywLuBMtcDfLpNQO/h1eFEoMrbiEM67TOfNlXfxb
# Dz4MlsYvLioxgd2Xzey1QxrV1+i+JyVDJMiSe9gKOuzpiQQFE19DUPgsidyjLTzX
# EhSVLBlRor0eCVf7gC6Rfk8NY3rO2sggOL79vU5FuDKTh/sIOtcUHeHC42jBGB+t
# fdKC1KOBR+UlN9aOzg8mpUNI2FgqQvirVP9ppbeMUfvp2wA9voyTiRWvDgzCxo8x
# lJ1nscYTHIQrmkF9j/Ca0IDmt8fvOn64nnlJOGUYZYHMC1l0xtgkYTE1ESUqqkaw
# Kk7iqbxdnLyycS+dR+zaxPudMDLrQFz8lgfy9obk0D8HC2dzhWpYNn5hdkoPEzgC
# qQUOp8v3Qj/sd4anyupe5KoCkjABOP3yhSQ4W9Z+DrJnhM/rbsXC7oTv26cCAwEA
# AaOCAUkwggFFMB0GA1UdDgQWBBRSBblSxb5cYKYOwvd/VfoXOfu33jAfBgNVHSME
# GDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5odHRw
# Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUaW1l
# LVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwGCCsG
# AQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01p
# Y3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNVHRMB
# Af8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIHgDAN
# BgkqhkiG9w0BAQsFAAOCAgEAXnSAkmX79Rc7lxS1wOozXJ7V0ou5DntVcOJplIkD
# jvEN8BIQph4U+gSOLZuVReP/z9YdUiUkcPwL1PM245/kEX1EegpxNc8HDA6hKCHg
# 0ALNEcuxnGOlgKLokXfUer1D5hiW8PABM9R+neiteTgPaaRlJFvGTYvotc0uqGiE
# S5hMQhL8RNFhpS9RcIWHtnQGEnrdOUvCAhs4FeViawcmLTKv+1870c/MeTHi0QDd
# eR+7/Wg4qhkJ2k1iEHJdmYf8rIV0NRBZcdRTTdHee35SXP5neNCfAkjDIuZycRud
# 6jzPLCNLiNYzGXBswzJygj4EeSORT7wMvaFuKeRAXoXC3wwYvgIsI1zn3DGY625Y
# +yZSi8UNSNHuri36Zv9a+Q4vJwDpYK36S0TB2pf7xLiiH32nk7YK73Rg98W6fZ2I
# NuzYzZ7Ghgvfffkj4EUXg1E0EffY1pEqkbpDTP7h/DBqtzoPXsyw2MUh+7yvWcq2
# BGZSuca6CY6X4ioMuc5PWpsmvOOli7ARNA7Ab8kKdCc2gNDLacglsweZEc9/VQB6
# hls/b6Kk32nkwuHExKlaeoSVrKB5U9xlp1+c8J/7GJj4Rw7AiQ8tcp+WmfyD8KxX
# 2QlKbDi4SUjnglv4617R8+a/cDWJyaMt8279Wn7f2yMedN7kfGIQ5SZj66RdhdlZ
# Oq8wggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3DQEB
# CwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYD
# VQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAe
# Fw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0
# YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5OGm
# TOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/XE/H
# ZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1hlDc
# wUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7M62A
# W36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3KNi1w
# jjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy1cCG
# MFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF803RKJ
# 1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQcNIIP
# 8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahhaYQFz
# ymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkLiWHz
# NgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV2xo3
# xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIGCSsG
# AQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUpzxD/
# LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBTMFEG
# DCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29m
# dC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYIKwYB
# BQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8G
# A1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQw
# VgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9j
# cmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUF
# BwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3Br
# aS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcNAQEL
# BQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1OdfC
# cTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYAA7AF
# vonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbzaN9l
# 9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6LGYnn
# 8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3mSj5m
# O0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0SCyx
# TkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxkoJLo4
# S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFmPWn9
# y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC4822rpM
# +Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7vzhw
# RNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDWTCCAkEC
# AQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0
# ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjoyRDFBLTA1RTAtRDk0NzElMCMG
# A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIa
# AxUA5VHBr4h00EN7jUdQ33SE+qbk/8CggYMwgYCkfjB8MQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1T
# dGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIFAO25oDgwIhgPMjAyNjA1MjEx
# NTE2MDhaGA8yMDI2MDUyMjE1MTYwOFowdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA
# 7bmgOAIBADAKAgEAAgIdOgIB/zAHAgEAAgISnjAKAgUA7brxuAIBADA2BgorBgEE
# AYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYag
# MA0GCSqGSIb3DQEBCwUAA4IBAQA0iIbhMu4oDtcy7JlKbJyW7o1Iz8Pmq9Y3f3/Q
# PkS68XE/ab3NAgHB3Y+r90UoXBM6AYC9lfx3X9uwgPaJi1g3kwamBSjWzhMuzI7w
# ZsygXB06QRm/MqAOHStDmMYe4LnoYOfm+IafEWcOuHjwWOKkOO0avffn4Cuj/P4C
# Ju0oIL9OytSJ5BcIy/W8hBDzp1PDBG2PVBaRqzOKsYxnKzMIAHcjFOL47plWLmIG
# VgqMY9qdw+9naxe1JD4f1++1SERCzgzQD7KNqTYfh4d2hYtlOskZV1G9bvL60+Lg
# Lr4X8o2mJik2KbciERPQ3rOC0J/j5dCt1pvsnAFBl3tmN7Y7MYIEDTCCBAkCAQEw
# gZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT
# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE
# AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIS0QgGPMoYT6oA
# AQAAAhIwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0B
# CRABBDAvBgkqhkiG9w0BCQQxIgQgbNXlchNRkMt1Pbjh+nORzeXV5XTwiv3ll5xB
# 8LtVM1UwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCBz+X5GvO7WngknH4BZ
# eYU+BzBL1Jy5oJ8wVlTNIxfYgzCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwAhMzAAACEtEIBjzKGE+qAAEAAAISMCIEIBrOq4Vc0OrwzfnoFIc3
# rgaGJq0TiQTWnWJQmaeCpDSkMA0GCSqGSIb3DQEBCwUABIICAEX1W5Jo4VayagdG
# Gb4wfDwwW3Hft3cpmrPbQH/4qe21mouDl0zejucFbb4SBHRvhatfdybpd6YMTJyf
# RlSh5ww2XuELAazD/T4TcYJMCF21YVwdIHueoLBG2BqsJCIWS9+FIZFXcb/6+Nvb
# /Jhcj9+wvRWCUR4kDUzsYs/bfBNhPRRo1gGhn47gWNuKEyX6Zw2dBORYsuOlcRpN
# t0Q4vIH8PbIisoOvMHaYjqtJmsxf6hKDe1a+fTsbAnWXrdzGkN9kOeCJ3fjMW9zw
# mw1Cl875kVUkcAfFDbtLDzl5q1KZC7spzhyn+L7COkwjb2heZDT0SHFzhZ71AFjN
# tQC41vNpz34AxQWZsZeLFqjlUbNYwB71grnqqvYe9kW1Cp1CWdyxU1Ud7A9W8NQt
# 2tMMl0aEKwUQjEUTjDcHfyxDpAkmcjlJUw8BMPWiLnDOirAGPPi2ah95sQeP2nIj
# CbL6jh07hypWqiI2X5mVgDLFK33mz2FCW5iWht2o+IqVjyY6o1+ygtPi8F+RFNU5
# W+7WkFGbjR28lfs3z6G1iqelbNu2IZu+ZZgfer98SSsxVjSAWw4+O9ZCzO21swN/
# n+GjCJFdU/tCh8FBpK3FPkcARYN/8ROSh21PYmrjJWlsjPcgJ0M7N7nqZWKtfF7h
# iarCBLBbL25kw92GkbTZhvKoGw5y
# SIG # End signature block