source/Private/Common.ps1

function Invoke-AzCommandWithNotFoundHandling {
    <#
    .SYNOPSIS
    Executes an Az command and returns a default value on "not found" errors instead of throwing.
 
    .DESCRIPTION
    Wraps a scriptblock in try/catch. If the Az cmdlet throws with FullyQualifiedErrorId
    containing 'Request_ResourceNotFound', returns DefaultValue. All other errors propagate.
 
    .PARAMETER ScriptBlock
    The Az command to execute. Must use -ErrorAction Stop internally.
 
    .PARAMETER DefaultValue
    Value to return when the resource is not found. Defaults to $null.
 
    .OUTPUTS
    The output of the scriptblock, or DefaultValue on not-found errors.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [scriptblock]$ScriptBlock,

        $DefaultValue = $null
    )

    try {
        & $ScriptBlock
    }
    catch {
        if ($_.FullyQualifiedErrorId -match 'Request_ResourceNotFound') {
            return $DefaultValue
        }
        throw
    }
}

function Get-ConflictingModuleList {

    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param (
        [Parameter(Mandatory = $true)]
        [System.Collections.IDictionary]$RequiredModules
    )

    $conflicts = @()

    foreach ($moduleName in $RequiredModules.Keys) {
        $requiredVersion = $RequiredModules[$moduleName]
        $loadedModules = Get-Module -Name $moduleName -ErrorAction SilentlyContinue

        if ($loadedModules) {
            $conflictingVersions = $loadedModules | Where-Object { $_.Version -ne $requiredVersion }

            if ($conflictingVersions) {
                $conflicts += [PSCustomObject]@{
                    ModuleName = $moduleName
                    LoadedVersions = $conflictingVersions.Version
                    RequiredVersion = $requiredVersion
                }
            }
        }
    }

    return $conflicts
}

function Show-ModuleConflictInstructions {
    <#
    .SYNOPSIS
    Shows user-friendly instructions to resolve module version conflicts.
 
    .PARAMETER ConflictingModules
    Array of conflicting module information from Get-ConflictingModuleList.
    #>

    [CmdletBinding()]
    [OutputType([void])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', 'Show-ModuleConflictInstructions')]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject[]]$ConflictingModules
    )

    Write-Host "Module version conflicts detected - initialization cannot proceed in current session" -ForegroundColor Red
    Write-Host ""
    Write-Host "PowerShell cannot unload assemblies from the current session." -ForegroundColor Yellow
    Write-Host "To resolve this issue, please follow these steps:" -ForegroundColor Yellow
    Write-Host ""
    Write-Host "1. Close this PowerShell session" -ForegroundColor Cyan
    Write-Host "2. Open a new PowerShell session" -ForegroundColor Cyan
    Write-Host "3. Re-run your command" -ForegroundColor Cyan
    Write-Host ""
    Write-Host "Module version conflicts:" -ForegroundColor Yellow

    foreach ($conflict in $ConflictingModules) {
        $loadedVersions = $conflict.LoadedVersions -join ', '
        Write-Host " - $($conflict.ModuleName): Loaded=$loadedVersions, Required=$($conflict.RequiredVersion)" -ForegroundColor Yellow
    }

    Write-Host ""
}

function Initialize-RequiredModule {
    <#
    .SYNOPSIS
    Initializes required PowerShell modules with version pinning.
 
    .DESCRIPTION
    Checks for module version conflicts, installs missing modules, and imports required modules.
    If version conflicts are detected, provides user instructions and returns false.
 
    .PARAMETER RequiredModules
    Hashtable of module names to required versions. Defaults to RequiredAzModules.
 
    .OUTPUTS
    [bool] True if initialization succeeded, False if conflicts prevent initialization.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param(
        [System.Collections.IDictionary]$RequiredModules = $RequiredAzModules
    )

    Write-Verbose "Initializing required PowerShell modules..."

    # Check for module version conflicts (wrong version already loaded = unrecoverable in this session)
    $conflictingModules = Get-ConflictingModuleList -RequiredModules $RequiredModules
    if ($conflictingModules.Count -gt 0) {
        Show-ModuleConflictInstructions -ConflictingModules $conflictingModules
        return $false
    }

    # Determine what needs installation vs import
    $modulesToInstall = [ordered]@{}
    $modulesToImport = [ordered]@{}

    foreach ($moduleName in $RequiredModules.Keys) {
        $requiredVersion = $RequiredModules[$moduleName]

        $loadedModule = Get-Module -Name $moduleName | Where-Object { $_.Version -eq $requiredVersion }
        if ($loadedModule) {
            Write-Verbose "Module $moduleName version $requiredVersion is already loaded"
            continue
        }

        $installedModule = Get-Module -FullyQualifiedName @{
            ModuleName      = $moduleName
            RequiredVersion = $requiredVersion
        } -ListAvailable -ErrorAction SilentlyContinue

        if (-not $installedModule) {
            $modulesToInstall[$moduleName] = $requiredVersion
        }

        $modulesToImport[$moduleName] = $requiredVersion
    }

    # Pass 1: Install all missing modules BEFORE importing any (avoids file-lock warnings)
    foreach ($moduleName in $modulesToInstall.Keys) {
        $requiredVersion = $modulesToInstall[$moduleName]
        Write-Verbose "Installing module $moduleName version $requiredVersion..."
        try {
            Install-Module -Name $moduleName -RequiredVersion $requiredVersion -Force -AllowClobber -Scope CurrentUser -ErrorAction Stop
            Write-Information "Successfully installed $moduleName version $requiredVersion"
        }
        catch {
            Write-Error "Failed to install required module $moduleName version $requiredVersion. Error: $($_.Exception.Message)"
            return $false
        }
    }

    # Pass 2: Import all modules (no -Force; dependencies are already loaded in order)
    foreach ($moduleName in $modulesToImport.Keys) {
        $requiredVersion = $modulesToImport[$moduleName]
        Write-Verbose "Importing module $moduleName version $requiredVersion..."
        try {
            Import-Module -Name $moduleName -RequiredVersion $requiredVersion -ErrorAction Stop -InformationAction SilentlyContinue -Verbose:$false
            Write-Verbose "Successfully imported $moduleName version $requiredVersion"
        }
        catch {
            Write-Error "Failed to import required module $moduleName version $requiredVersion. Error: $($_.Exception.Message)"
            return $false
        }
    }

    Write-Verbose "All required PowerShell modules are initialized and ready to use"
    return $true
}