PIMActivation.psm1

#Requires -Version 7.0
# Note: Required modules are declared in the manifest and handled by internal dependency management
# This allows for both PowerShell Gallery automatic installation and development scenarios

# Set strict mode for better error handling
Set-StrictMode -Version Latest

#region Module Setup

# Module-level variables
$script:ModuleRoot = $PSScriptRoot
$script:ModuleName = Split-Path -Path $script:ModuleRoot -Leaf

# Token storage variables
$script:CurrentAccessToken = $null
$script:TokenExpiry = $null

# User context variables
$script:CurrentUser = $null
$script:GraphContext = $null

# Configuration variables
$script:IncludeEntraRoles = $true
$script:IncludeGroups = $true
$script:IncludeAzureResources = $false

# Startup parameters (for restarts)
$script:StartupParameters = @{}

# Restart flag
$script:RestartRequested = $false

# Policy cache
if (-not (Test-Path Variable:script:PolicyCache)) {
    $script:PolicyCache = @{}
}

# Authentication context cache
if (-not (Test-Path Variable:script:AuthenticationContextCache)) {
    $script:AuthenticationContextCache = @{}
}

# Entra policies loaded flag
if (-not (Test-Path Variable:script:EntraPoliciesLoaded)) {
    $script:EntraPoliciesLoaded = $false
}

# Role data cache to avoid repeated API calls during refresh operations
if (-not (Test-Path Variable:script:CachedEligibleRoles)) {
    $script:CachedEligibleRoles = @()
}

if (-not (Test-Path Variable:script:CachedActiveRoles)) {
    $script:CachedActiveRoles = @()
}

if (-not (Test-Path Variable:script:LastRoleFetchTime)) {
    $script:LastRoleFetchTime = $null
}

if (-not (Test-Path Variable:script:RoleCacheValidityMinutes)) {
    $script:RoleCacheValidityMinutes = 5  # Cache roles for 5 minutes
}

# Authentication context variables - now supporting multiple contexts
$script:CurrentAuthContextToken = $null  # Deprecated - kept for backwards compatibility
$script:AuthContextTokens = @{}  # New: Hashtable of contextId -> token
$script:JustCompletedAuthContext = $null
$script:AuthContextCompletionTime = $null

# Module loading state for just-in-time loading
$script:ModuleLoadingState = @{}
$script:RequiredModuleVersions = @{
    'Microsoft.Graph.Authentication' = '2.29.0'
    'Microsoft.Graph.Users' = '2.29.0'
    'Microsoft.Graph.Identity.DirectoryManagement' = '2.29.0'
    'Microsoft.Graph.Identity.Governance' = '2.29.0'
    'Microsoft.Graph.Groups' = '2.29.0'
    'Microsoft.Graph.Identity.SignIns' = '2.29.0'
    'Az.Accounts' = '5.1.0'
}

#endregion Module Setup

#region Import Functions

# Import all functions from subdirectories
$functionFolders = [System.Collections.ArrayList]::new()
$null = $functionFolders.AddRange(@(
    'Authentication',
    'RoleManagement', 
    'UI',
    'Utilities'
))

# Note: Profiles folder contains placeholder functions for planned features
$null = $functionFolders.Add('Profiles')

# Import private functions from organized folders
# Temporarily suppress verbose output during function imports to reduce noise
$originalVerbosePreference = $VerbosePreference
$VerbosePreference = 'SilentlyContinue'

foreach ($folder in $functionFolders) {
    $folderPath = Join-Path -Path "$script:ModuleRoot\Private" -ChildPath $folder
    if (Test-Path -Path $folderPath) {
        $functions = Get-ChildItem -Path $folderPath -Filter '*.ps1' -File -ErrorAction SilentlyContinue
        
        foreach ($function in $functions) {
            try {
                . $function.FullName
            }
            catch {
                Write-Error -Message "Failed to import function $($function.FullName): $_"
            }
        }
    }
}

# Import remaining private functions from root Private folder
$privateRoot = Get-ChildItem -Path "$script:ModuleRoot\Private" -Filter '*.ps1' -File -ErrorAction SilentlyContinue
foreach ($import in $privateRoot) {
    try {
        . $import.FullName
    }
    catch {
        Write-Error -Message "Failed to import function $($import.FullName): $_"
    }
}

# Import public functions
$Public = @(Get-ChildItem -Path "$script:ModuleRoot\Public" -Filter '*.ps1' -File -ErrorAction SilentlyContinue)
foreach ($import in $Public) {
    try {
        . $import.FullName
    }
    catch {
        Write-Error -Message "Failed to import function $($import.FullName): $_"
    }
}

# Restore original verbose preference
$VerbosePreference = $originalVerbosePreference

#endregion Import Functions

#region Export Module Members

# Export public functions
if ($Public -and $Public.Count -gt 0) {
    Export-ModuleMember -Function $Public.BaseName -Alias *
}

#endregion Export Module Members

#region Module Initialization

# Smart dependency resolution - handles both development and production scenarios
# This allows the module to work regardless of how it's imported
$script:DependenciesValidated = $false

function Install-MissingPIMModules {
    <#
    .SYNOPSIS
        Automatically installs missing required modules during import
    #>

    [CmdletBinding()]
    param()
    
    $missingModules = [System.Collections.ArrayList]::new()
    
    # Check each required module
    foreach ($moduleSpec in $script:RequiredModuleVersions.GetEnumerator()) {
        $moduleName = $moduleSpec.Key
        $requiredVersion = [version]$moduleSpec.Value
        
        # Check if suitable version is available
        $availableModule = Get-Module -ListAvailable -Name $moduleName -ErrorAction SilentlyContinue | 
            Where-Object { $_.Version -ge $requiredVersion } | 
            Sort-Object Version -Descending | 
            Select-Object -First 1
            
        if (-not $availableModule) {
            $null = $missingModules.Add(@{
                Name = $moduleName
                RequiredVersion = $requiredVersion
            })
        }
    }
    
    # Install missing modules if any
    if ($missingModules.Count -gt 0) {
        Write-Verbose "Installing missing dependencies..."
        
        # Ensure NuGet provider and PSGallery trust (silent setup)
        $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 CurrentUser -ErrorAction SilentlyContinue | Out-Null
        }
        
        $psGallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
        if ($psGallery.InstallationPolicy -ne 'Trusted') {
            Write-Verbose "Configuring PSGallery as trusted..."
            Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction SilentlyContinue
        }
        
        # Install each missing module
        foreach ($module in $missingModules) {
            try {
                Write-Verbose "Installing $($module.Name) v$($module.RequiredVersion)..."
                $originalInformationPreference = $InformationPreference
                $originalProgressPreference = $ProgressPreference
                $InformationPreference = 'SilentlyContinue'
                $ProgressPreference = 'SilentlyContinue'
                
                Install-Module -Name $module.Name -MinimumVersion $module.RequiredVersion -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop | Out-Null
                
                $InformationPreference = $originalInformationPreference
                $ProgressPreference = $originalProgressPreference
                Write-Verbose "Successfully installed $($module.Name)"
            }
            catch {
                $InformationPreference = $originalInformationPreference
                $ProgressPreference = $originalProgressPreference
                Write-Warning "Failed to install $($module.Name): $($_.Exception.Message)"
            }
        }
        
        Write-Verbose "Dependencies installation completed."
    }
}

function Test-PIMModuleDependencies {
    <#
    .SYNOPSIS
        Internal function to validate and import required module dependencies
    #>

    [CmdletBinding()]
    param()
    
    if ($script:DependenciesValidated) {
        return $true
    }
    
    # First, try to install any missing modules
    try {
        Install-MissingPIMModules
    }
    catch {
        Write-Verbose "Module installation failed: $($_.Exception.Message)"
    }
    
    # Now validate and import modules
    $failedModules = [System.Collections.ArrayList]::new()
    
    foreach ($moduleSpec in $script:RequiredModuleVersions.GetEnumerator()) {
        $moduleName = $moduleSpec.Key
        $requiredVersion = [version]$moduleSpec.Value
        
        $loadedModule = Get-Module -Name $moduleName -ErrorAction SilentlyContinue
        if (-not $loadedModule) {
            $availableModule = Get-Module -ListAvailable -Name $moduleName | 
                Where-Object { $_.Version -ge $requiredVersion } | 
                Sort-Object Version -Descending | 
                Select-Object -First 1
                
            if ($availableModule) {
                try {
                    Import-Module -Name $moduleName -MinimumVersion $requiredVersion -ErrorAction Stop -Force
                    Write-Verbose "Imported $moduleName v$($availableModule.Version)"
                }
                catch {
                    $null = $failedModules.Add("$moduleName (import failed: $($_.Exception.Message))")
                }
            }
            else {
                $null = $failedModules.Add("$moduleName v$requiredVersion+ (not available)")
            }
        }
        elseif ($loadedModule.Version -lt $requiredVersion) {
            $null = $failedModules.Add("$moduleName (loaded: v$($loadedModule.Version), required: v$requiredVersion+)")
        }
    }
    
    if ($failedModules.Count -gt 0) {
        $errorMessage = @"
Required module dependencies could not be resolved:
$($failedModules | ForEach-Object { " - $_" } | Out-String)
Try running: Start-PIMActivation -Force
"@

        Write-Warning $errorMessage
        return $false
    }
    
    $script:DependenciesValidated = $true
    return $true
}

# Attempt to resolve dependencies during module import (with error handling)
try {
    $null = Test-PIMModuleDependencies
    Write-Verbose "PIMActivation module dependencies resolved successfully"
}
catch {
    # Don't fail the import, just warn
    Write-Warning "Dependency resolution during import encountered issues: $($_.Exception.Message)"
    Write-Host "You can resolve this by running: Start-PIMActivation" -ForegroundColor Yellow
}

#endregion Module Initialization

#region Cleanup

# Clean up variables
Remove-Variable -Name Private, Public, functionFolders, folder, folderPath, functions, function, privateRoot, import -ErrorAction SilentlyContinue

#endregion Cleanup