GraphModuleStatus.psm1

##############################################################################
# GraphModuleStatus
# Shows the status of Microsoft Graph PowerShell modules on profile load
##############################################################################

# Path to the update script (bundled with module)
$script:UpdateScriptPath = Join-Path $PSScriptRoot "Update-MicrosoftGraph.ps1"

# Path to user preferences file (persists across module updates)
$script:PreferencesPath = Join-Path $env:APPDATA "GraphModuleStatus\preferences.json"

##############################################################################
# Get-GraphModulePreferences / Save-GraphModulePreferences
# Internal helpers for persistent user preferences
##############################################################################
function Get-GraphModulePreferences {
    if (Test-Path -Path $script:PreferencesPath) {
        try {
            $Json = Get-Content -Path $script:PreferencesPath -Raw -Encoding utf8 | ConvertFrom-Json
            return $Json
        }
        catch {
            return [PSCustomObject]@{ DismissedInstallOffers = @() }
        }
    }
    return [PSCustomObject]@{ DismissedInstallOffers = @() }
}

function Save-GraphModulePreferences {
    param([PSCustomObject]$Preferences)

    $Dir = Split-Path -Path $script:PreferencesPath -Parent
    if (-not (Test-Path -Path $Dir)) {
        New-Item -Path $Dir -ItemType Directory -Force | Out-Null
    }
    $Preferences | ConvertTo-Json -Depth 5 | Out-File -FilePath $script:PreferencesPath -Encoding utf8 -Force
}

##############################################################################
# Get-LatestPSGalleryVersion
# Fast version check via URL redirect — no download, 5-second timeout
# Adapted from Entra-PIM module (github.com/markdomansky/Entra-PIM)
##############################################################################
function Get-LatestPSGalleryVersion {
    param(
        [Parameter(Mandatory)]
        [string]$Name
    )
    try {
        $Url = "https://www.powershellgallery.com/packages/$Name"
        try {
            $null = Invoke-WebRequest -Uri $Url -UseBasicParsing -MaximumRedirection 0 -TimeoutSec 5 -ErrorAction Stop
        }
        catch {
            if ($_.Exception.Response -and $_.Exception.Response.Headers) {
                try {
                    $Location = $_.Exception.Response.Headers.GetValues('Location') | Select-Object -First 1
                    if ($Location) {
                        return [version](Split-Path -Path $Location -Leaf)
                    }
                }
                catch { }
            }
        }
    }
    catch { }
    return $null
}

##############################################################################
# Install-GraphModules
# Internal helper — scope selection, elevation, and installation
##############################################################################
function Install-GraphModules {
    param(
        [Parameter(Mandatory)]
        [array]$Modules
    )

    # If more than one module, let the user choose which to install
    $ModulesToInstall = $Modules
    if ($Modules.Count -gt 1) {
        Write-Host " Which modules would you like to install?" -ForegroundColor Cyan
        Write-Host ""
        $i = 1
        foreach ($Mod in $Modules) {
            Write-Host " [$i] $($Mod.Name)$(if ($Mod.AvailableVersion) { " v$($Mod.AvailableVersion)" })" -ForegroundColor White
            $i++
        }
        Write-Host " [A] All (default)" -ForegroundColor White
        Write-Host ""
        $ModChoice = Read-Host " Enter your choice"

        if ($ModChoice -match '^\d+$') {
            $Index = [int]$ModChoice - 1
            if ($Index -ge 0 -and $Index -lt $Modules.Count) {
                $ModulesToInstall = @($Modules[$Index])
            } else {
                Write-Host " Invalid choice. Installing all modules." -ForegroundColor DarkGray
            }
        }
        # Any other input (A, Enter, etc.) installs all — no action needed
        Write-Host ""
    }

    Write-Host " Install scope:" -ForegroundColor Cyan
    Write-Host " [1] All Users (Recommended)" -ForegroundColor White
    Write-Host " [2] Current User Only" -ForegroundColor White
    Write-Host ""
    $ScopeChoice = Read-Host " Enter your choice (1-2) [default: 1]"

    $InstallScope = if ($ScopeChoice -eq "2") { "CurrentUser" } else { "AllUsers" }

    # All Users requires elevation — auto-launch an elevated session if needed
    if ($InstallScope -eq "AllUsers") {
        $IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
        if (-not $IsAdmin) {
            Write-Host ""
            Write-Host " All Users requires Administrator rights." -ForegroundColor Yellow
            Write-Host " Launching elevated session to complete the installation..." -ForegroundColor Yellow
            Write-Host ""

            # Write install commands to a temp script — avoids -Command quoting issues
            $TempScript = [System.IO.Path]::GetTempFileName() -replace '\.tmp$', '.ps1'
            $ScriptLines = @(
                "Write-Host ''",
                "Write-Host ' Installing Microsoft Graph modules for All Users...' -ForegroundColor Cyan",
                "Write-Host ''"
            )
            foreach ($Mod in $ModulesToInstall) {
                $ScriptLines += "Write-Host ' Installing $($Mod.Name)...' -ForegroundColor Yellow"
                $ScriptLines += "Write-Host ' (This installs all sub-modules and may take several minutes)' -ForegroundColor Gray"
                $ScriptLines += "Write-Host ''"
                $ScriptLines += "Install-PSResource -Name '$($Mod.Name)' -Scope AllUsers -TrustRepository -AcceptLicense"
                $ScriptLines += "Write-Host ''"
                $ScriptLines += "Write-Host ' $($Mod.Name) installed successfully.' -ForegroundColor Green"
                $ScriptLines += "Write-Host ''"
            }
            $ScriptLines += "Write-Host ' Done. Open a new PowerShell window before using Graph commands.' -ForegroundColor Green"
            $ScriptLines += "Write-Host ''"
            $ScriptLines += "Read-Host ' Press Enter to close'"
            $ScriptLines += "Remove-Item -Path '$TempScript' -Force -ErrorAction SilentlyContinue"
            $ScriptLines | Out-File -FilePath $TempScript -Encoding utf8

            # Use the current process executable so elevation uses the same PS version
            $PwshExe = (Get-Process -Id $PID).MainModule.FileName
            try {
                Start-Process $PwshExe -Verb RunAs -ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $TempScript
                Write-Host " Elevated installer launched — check the new window for progress." -ForegroundColor Cyan
                Write-Host ""
            }
            catch {
                Remove-Item -Path $TempScript -Force -ErrorAction SilentlyContinue
                Write-Host " ERROR: Could not launch elevated session - $_" -ForegroundColor Red
                Write-Host " Run PowerShell as Administrator and call Get-GraphModuleStatus again." -ForegroundColor Yellow
                Write-Host ""
            }
            return
        }
    }

    $ScopeDisplay = if ($InstallScope -eq "AllUsers") { "All Users" } else { "Current User Only" }
    Write-Host ""
    Write-Host " Modules will be installed for: $ScopeDisplay" -ForegroundColor Gray
    Write-Host ""

    $InstallSuccess = 0
    $InstallFailed = 0

    foreach ($Mod in $ModulesToInstall) {
        Write-Host " Installing $($Mod.Name)$(if ($Mod.AvailableVersion) { " v$($Mod.AvailableVersion)" })..." -ForegroundColor Yellow
        Write-Host " (This installs all sub-modules and may take several minutes)" -ForegroundColor Gray
        Write-Host ""
        try {
            Install-PSResource -Name $Mod.Name -Scope $InstallScope -TrustRepository -AcceptLicense -ErrorAction Stop
            Write-Host " $($Mod.Name) installed successfully." -ForegroundColor Green
            $InstallSuccess++
        }
        catch {
            Write-Host " ERROR: Failed to install $($Mod.Name) - $_" -ForegroundColor Red
            $InstallFailed++
        }
        Write-Host ""
    }

    Write-Host " Install complete: $InstallSuccess succeeded, $InstallFailed failed." -ForegroundColor $(if ($InstallFailed -gt 0) { "Yellow" } else { "Green" })
    Write-Host ""
    if ($InstallSuccess -gt 0) {
        Write-Host " Open a new PowerShell window before running any Graph commands." -ForegroundColor Yellow
        Write-Host ""
    }
}

##############################################################################
# Get-GraphModuleStatus
# Shows installed vs available versions of Graph modules
##############################################################################
Function Get-GraphModuleStatus {
    <#
    .SYNOPSIS
    Shows the status of Microsoft Graph modules (installed vs available versions)
 
    .DESCRIPTION
    Checks Microsoft.Graph and Microsoft.Graph.Beta modules and displays their
    current installed version compared to the latest available version in PSGallery.
    When updates are available, prompts to run the update script.
 
    .PARAMETER Silent
    If specified, suppresses output and returns objects instead.
 
    .PARAMETER NoPrompt
    If specified, does not prompt to run the update script when updates are available.
 
    .EXAMPLE
    Get-GraphModuleStatus
 
    .EXAMPLE
    Get-GraphModuleStatus -Silent
 
    .EXAMPLE
    Get-GraphModuleStatus -NoPrompt
 
    .LINK
    https://github.com/yourusername/GraphModuleStatus
    #>


    [CmdletBinding()]
    param(
        [switch]$Silent,
        [switch]$NoPrompt
    )

    $Modules = @(
        @{ Name = "Microsoft.Graph"; Display = "Microsoft.Graph" },
        @{ Name = "Microsoft.Graph.Beta"; Display = "Microsoft.Graph.Beta" }
    )

    $Results = @()

    if (-not $Silent) {
        Write-Host ""
    }

    foreach ($Module in $Modules) {
        # Check both CurrentUser and AllUsers scopes
        $Installed = @(
            Get-InstalledPSResource -Name $Module.Name -Scope CurrentUser -ErrorAction SilentlyContinue
            Get-InstalledPSResource -Name $Module.Name -Scope AllUsers -ErrorAction SilentlyContinue
        ) | Sort-Object Version -Descending | Select-Object -First 1

        $Status = [PSCustomObject]@{
            Name             = $Module.Name
            InstalledVersion = $null
            AvailableVersion = $null
            UpdateAvailable  = $false
            Installed        = $false
        }

        if ($Installed) {
            $Status.Installed = $true
            $Status.InstalledVersion = $Installed.Version.ToString()

            # Check for updates via fast URL redirect (no download needed)
            $LatestVersion = Get-LatestPSGalleryVersion -Name $Module.Name
            if ($LatestVersion) {
                $Status.AvailableVersion = $LatestVersion.ToString()
                $Status.UpdateAvailable = ($LatestVersion -gt [version]$Status.InstalledVersion)
            }

            if (-not $Silent) {
                if ($Status.UpdateAvailable) {
                    # Update available
                    Write-Host " [$($Module.Display)]" -ForegroundColor White -NoNewline
                    Write-Host " v$($Status.InstalledVersion) " -ForegroundColor Yellow -NoNewline
                    Write-Host "→" -ForegroundColor DarkGray -NoNewline
                    Write-Host " v$($Status.AvailableVersion)" -ForegroundColor Green
                } else {
                    # Up to date
                    Write-Host " [$($Module.Display)]" -ForegroundColor Cyan -NoNewline
                    Write-Host " v$($Status.InstalledVersion) " -NoNewline
                    Write-Host "●" -ForegroundColor Green -NoNewline
                    Write-Host " Current" -ForegroundColor DarkGray
                }
            }
        } else {
            # Not installed - check what's available on PSGallery via fast URL redirect
            $LatestVersion = Get-LatestPSGalleryVersion -Name $Module.Name
            if ($LatestVersion) {
                $Status.AvailableVersion = $LatestVersion.ToString()
            }

            if (-not $Silent) {
                Write-Host " [$($Module.Display)]" -ForegroundColor DarkGray -NoNewline
                Write-Host " ○ Not installed" -ForegroundColor Red -NoNewline
                if ($Status.AvailableVersion) {
                    Write-Host " (v$($Status.AvailableVersion) available on PSGallery)" -ForegroundColor DarkGray
                } else {
                    Write-Host ""
                }
            }
        }

        $Results += $Status
    }

    if (-not $Silent) {
        Write-Host ""
    }

    # Check if any updates are available and prompt user
    $UpdatesAvailable = $Results | Where-Object { $_.UpdateAvailable -eq $true }
    
    if ($UpdatesAvailable -and -not $Silent -and -not $NoPrompt) {
        if (Test-Path -Path $script:UpdateScriptPath) {
            $Response = Read-Host " Update available. Run Update-GraphModule now? (Y/N)"
            if ($Response -eq 'Y' -or $Response -eq 'y') {
                Write-Host ""
                Update-GraphModule
            } else {
                Write-Host ""
            }
        }
    }

    # If none are installed, offer to install them now
    $NoneInstalled = -not ($Results | Where-Object { $_.Installed -eq $true })
    $NotInstalledModules = @($Results | Where-Object { -not $_.Installed })

    if ($NoneInstalled -and $NotInstalledModules.Count -gt 0 -and -not $Silent -and -not $NoPrompt) {
        Write-Host " No Microsoft Graph modules are installed." -ForegroundColor Yellow
        $HasVersionInfo = @($NotInstalledModules | Where-Object { $null -ne $_.AvailableVersion })
        if ($HasVersionInfo.Count -gt 0) {
            Write-Host " The following versions are available from PSGallery:" -ForegroundColor Yellow
        }
        Write-Host ""
        foreach ($Mod in $NotInstalledModules) {
            Write-Host " [$($Mod.Name)]" -ForegroundColor White -NoNewline
            if ($null -ne $Mod.AvailableVersion) {
                Write-Host " v$($Mod.AvailableVersion)" -ForegroundColor Cyan
            } else {
                Write-Host " (latest)" -ForegroundColor DarkGray
            }
        }
        Write-Host ""

        $InstallPrompt = Read-Host " Would you like to install them now? (Y/N)"

        if ($InstallPrompt -match '^[Yy]') {
            Write-Host ""
            Install-GraphModules -Modules $NotInstalledModules
        }
    }

    # If some modules are installed but others are not, offer to install missing ones
    # Declining dismisses the offer permanently (stored in user preferences)
    $SomeInstalled = ($Results | Where-Object { $_.Installed }) -and $NotInstalledModules.Count -gt 0
    if ($SomeInstalled -and -not $Silent -and -not $NoPrompt) {
        $Prefs = Get-GraphModulePreferences
        $Dismissed = @($Prefs.DismissedInstallOffers)

        # Filter out modules the user has previously declined
        $OfferedModules = @($NotInstalledModules | Where-Object { $Dismissed -notcontains $_.Name })

        foreach ($Mod in $OfferedModules) {
            $VersionInfo = if ($Mod.AvailableVersion) { " (v$($Mod.AvailableVersion) available)" } else { "" }
            $Response = Read-Host " $($Mod.Name) is not installed$VersionInfo. Install it? (Y/N)"

            if ($Response -match '^[Yy]') {
                Write-Host ""
                Install-GraphModules -Modules @($Mod)
            }
            else {
                # Save the dismissal so we don't ask again
                $Dismissed += $Mod.Name
                $Prefs.DismissedInstallOffers = $Dismissed
                Save-GraphModulePreferences -Preferences $Prefs
                Write-Host ""
            }
        }
    }

    if ($Silent) {
        return $Results
    }
}

##############################################################################
# Update-GraphModule
# Runs the update script to clean install/reinstall Graph modules
##############################################################################
Function Update-GraphModule {
    <#
    .SYNOPSIS
    Runs the Microsoft Graph module update script
 
    .DESCRIPTION
    Invokes the Update-MicrosoftGraph.ps1 script which performs a clean
    uninstall and reinstall of Microsoft Graph modules.
 
    .EXAMPLE
    Update-GraphModule
 
    .LINK
    https://github.com/yourusername/GraphModuleStatus
    #>


    [CmdletBinding()]
    param()

    if (Test-Path -Path $script:UpdateScriptPath) {
        Write-Host "Running Microsoft Graph update script..." -ForegroundColor Cyan
        Write-Host ""
        & $script:UpdateScriptPath
    } else {
        Write-Host "Update script not found at: $script:UpdateScriptPath" -ForegroundColor Red
        Write-Host "Please ensure the script exists or update the path in the module." -ForegroundColor Yellow
    }
}

##############################################################################
# Add-GraphModuleStatusToProfile
# Adds GraphModuleStatus check to PowerShell profile
##############################################################################
Function Add-GraphModuleStatusToProfile {
    <#
    .SYNOPSIS
    Add GraphModuleStatus check to PowerShell Profile
 
    .DESCRIPTION
    Adds the GraphModuleStatus module import and status check to your PowerShell profile
    so it runs automatically when you start PowerShell.
 
    Needs to be executed separately for PowerShell v5 and v7.
 
    .EXAMPLE
    Add-GraphModuleStatusToProfile
 
    .LINK
    https://github.com/yourusername/GraphModuleStatus
    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param()

    $ProfileContent = @"
 
# GraphModuleStatus: Check Microsoft Graph module status on startup
Import-Module -Name GraphModuleStatus -ErrorAction SilentlyContinue
Get-GraphModuleStatus
"@


    if (-not (Test-Path -Path $Profile)) {
        # No Profile found
        Write-Host "No PowerShell Profile exists. Creating new Profile with GraphModuleStatus setup." -ForegroundColor Yellow
        $ProfileContent | Out-File -FilePath $Profile -Encoding utf8 -Force
        Write-Host "Profile created at: $Profile" -ForegroundColor Green
    } else {
        # Profile found
        $ExistingContent = Get-Content -Path $Profile -Encoding utf8 -Raw
        $Match = $ExistingContent | Where-Object { $_ -match "GraphModuleStatus" }

        if ($Match) {
            # GraphModuleStatus already in Profile
            Write-Host "GraphModuleStatus is already in your PowerShell Profile." -ForegroundColor Yellow
        } else {
            # GraphModuleStatus not in Profile
            Write-Host "Adding GraphModuleStatus to existing PowerShell Profile..." -ForegroundColor Yellow
            Add-Content -Path $Profile -Value $ProfileContent -Encoding utf8
            Write-Host "GraphModuleStatus added to: $Profile" -ForegroundColor Green
        }
    }
}

##############################################################################
# Remove-GraphModuleStatusFromProfile
# Removes GraphModuleStatus from PowerShell profile
##############################################################################
Function Remove-GraphModuleStatusFromProfile {
    <#
    .SYNOPSIS
    Remove GraphModuleStatus from PowerShell Profile
 
    .DESCRIPTION
    Removes the GraphModuleStatus module import and status check from your PowerShell profile.
 
    .EXAMPLE
    Remove-GraphModuleStatusFromProfile
 
    .LINK
    https://github.com/yourusername/GraphModuleStatus
    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param()

    if (-not (Test-Path -Path $Profile)) {
        Write-Host "No PowerShell Profile exists." -ForegroundColor Yellow
        return
    }

    $Content = Get-Content -Path $Profile -Encoding utf8
    $NewContent = $Content | Where-Object { 
        $_ -notmatch "GraphModuleStatus" -and 
        $_ -notmatch "# GraphModuleStatus:" 
    }

    if ($Content.Count -ne $NewContent.Count) {
        $NewContent | Out-File -FilePath $Profile -Encoding utf8 -Force
        Write-Host "GraphModuleStatus removed from PowerShell Profile." -ForegroundColor Green
    } else {
        Write-Host "GraphModuleStatus was not found in your PowerShell Profile." -ForegroundColor Yellow
    }
}

##############################################################################
# Module Load Message
##############################################################################
if (-not (Test-Path -Path $Profile)) {
    Write-Host "Tip: Run Add-GraphModuleStatusToProfile to check Graph module status on startup." -ForegroundColor DarkGray
} else {
    $Content = Get-Content -Path $Profile -Encoding utf8 -Raw -ErrorAction SilentlyContinue
    if ($Content -notmatch "GraphModuleStatus") {
        Write-Host "Tip: Run Add-GraphModuleStatusToProfile to check Graph module status on startup." -ForegroundColor DarkGray
    }
}