Private/Utilities/Install-RequiredModules.ps1

function Install-RequiredModules {
    <#
    .SYNOPSIS
        Installs required PowerShell modules for PIM activation.
     
    .DESCRIPTION
        Validates and installs necessary Microsoft Graph modules and optionally Azure PowerShell modules.
        Automatically handles NuGet provider setup, repository trust configuration, and module versioning.
        Falls back to CurrentUser scope if not running as administrator.
     
    .PARAMETER RequiredModules
        Array of hashtables containing module specifications with Name and MinVersion properties.
        If not provided, defaults to core Microsoft Graph modules required for PIM operations.
     
    .PARAMETER IncludeAzureModules
        Switch to include Azure PowerShell modules (Az.Accounts, Az.Resources) for Azure resource support.
     
    .EXAMPLE
        Install-RequiredModules
        Installs default Microsoft Graph modules for PIM operations.
     
    .EXAMPLE
        Install-RequiredModules -IncludeAzureModules
        Installs Microsoft Graph modules plus Azure PowerShell modules.
     
    .EXAMPLE
        $modules = @(@{Name='Microsoft.Graph.Users'; MinVersion='2.0.0'})
        Install-RequiredModules -RequiredModules $modules
        Installs only the specified modules.
     
    .OUTPUTS
        PSCustomObject
        Returns object with Success (boolean) and Error (string) properties indicating operation status.
     
    .NOTES
        Requires PowerShell 7 or later.
        Administrative privileges recommended for AllUsers scope installation.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [hashtable[]]$RequiredModules,
        
        [Parameter()]
        [switch]$IncludeAzureModules
    )
    
    $result = [PSCustomObject]@{
        Success = $true
        Error = $null
    }
    
    try {
        # Initialize module list with defaults if not provided
        if (-not $RequiredModules) {
            Write-Verbose "Using default Microsoft Graph module set"
            $RequiredModules = @(
                @{Name = 'Microsoft.Graph.Authentication'; MinVersion = '2.0.0'},
                @{Name = 'Microsoft.Graph.Users'; MinVersion = '2.0.0'},
                @{Name = 'Microsoft.Graph.Identity.DirectoryManagement'; MinVersion = '2.0.0'},
                @{Name = 'Microsoft.Graph.Identity.Governance'; MinVersion = '2.0.0'},
                @{Name = 'Az.Accounts'; MinVersion = '3.0.0'}
            )
            
            if ($IncludeAzureModules) {
                Write-Verbose "Including Azure PowerShell modules"
                $RequiredModules += @(
                    @{Name = 'Az.Resources'; MinVersion = '6.0.0'}
                )
            }
        }
        
        # Determine installation scope based on privileges
        $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")
        $installScope = if ($isAdmin) { 'AllUsers' } else { 'CurrentUser' }
        Write-Verbose "Installation scope: $installScope"
        
        # Process each required module
        foreach ($module in $RequiredModules) {
            Write-Verbose "Processing module: $($module.Name) (min version: $($module.MinVersion))"
            
            # Check if module is already loaded with sufficient version
            $loadedModule = Get-Module -Name $module.Name -ErrorAction SilentlyContinue
            if ($loadedModule -and $loadedModule.Version -ge $module.MinVersion) {
                Write-Verbose "✓ $($module.Name) v$($loadedModule.Version) already loaded"
                continue
            }
            
            # Check for suitable installed version
            $availableModules = Get-Module -ListAvailable -Name $module.Name -ErrorAction SilentlyContinue
            $suitableModule = $availableModules | 
                Where-Object { $_.Version -ge $module.MinVersion } | 
                Sort-Object Version -Descending | 
                Select-Object -First 1
            
            if ($suitableModule) {
                Write-Verbose "Found suitable version: $($module.Name) v$($suitableModule.Version)"
                try {
                    Import-Module -Name $module.Name -MinimumVersion $module.MinVersion -ErrorAction Stop
                    Write-Verbose "✓ $($module.Name) imported successfully"
                    continue
                }
                catch {
                    Write-Verbose "Import failed, proceeding with installation: $($_.Exception.Message)"
                }
            }
            
            # Install module if not available or insufficient version
            Write-Verbose "Installing $($module.Name)..."
            
            try {
                # Ensure NuGet provider is available
                $nugetProvider = Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue
                if (-not $nugetProvider -or $nugetProvider.Version -lt '2.8.5.201') {
                    Write-Verbose "Installing NuGet provider..."
                    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope $installScope -ErrorAction Stop
                }
                
                # Configure PSGallery as trusted repository
                $psGallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
                if ($psGallery.InstallationPolicy -ne 'Trusted') {
                    Write-Verbose "Configuring PSGallery as trusted repository"
                    Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop
                }
                
                # Install the module
                Install-Module -Name $module.Name `
                             -MinimumVersion $module.MinVersion `
                             -Scope $installScope `
                             -Force `
                             -AllowClobber `
                             -Repository PSGallery `
                             -ErrorAction Stop
                
                # Import the newly installed module
                Import-Module -Name $module.Name -MinimumVersion $module.MinVersion -ErrorAction Stop
                Write-Verbose "✓ $($module.Name) installed and imported successfully"
            }
            catch {
                # Fallback: retry with CurrentUser scope only
                try {
                    Write-Verbose "Retrying installation with CurrentUser scope..."
                    Install-Module -Name $module.Name `
                                 -MinimumVersion $module.MinVersion `
                                 -Scope CurrentUser `
                                 -Force `
                                 -AllowClobber `
                                 -ErrorAction Stop
                    
                    Import-Module -Name $module.Name -MinimumVersion $module.MinVersion -ErrorAction Stop
                    Write-Verbose "✓ $($module.Name) installed successfully (fallback)"
                }
                catch {
                    throw "Failed to install $($module.Name): $($_.Exception.Message)"
                }
            }
        }
        
        # Final validation of all required modules
        Write-Verbose "Validating module installation..."
        foreach ($module in $RequiredModules) {
            $loadedModule = Get-Module -Name $module.Name -ErrorAction SilentlyContinue
            if (-not $loadedModule) {
                throw "$($module.Name) failed to load after installation"
            }
            if ($loadedModule.Version -lt $module.MinVersion) {
                throw "$($module.Name) v$($loadedModule.Version) loaded but v$($module.MinVersion) required"
            }
        }
        
        Write-Verbose "All required modules validated successfully"
    }
    catch {
        $result.Success = $false
        $result.Error = $_.Exception.Message
        Write-Verbose "Installation failed: $($_.Exception.Message)"
    }
    
    return $result
}