Private/GraphHelpers.ps1

function Get-InTUIGraphConnectionDisplay {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object]$Context,

        [Parameter()]
        [string]$ClientId,

        [Parameter()]
        [switch]$ClientCredential
    )

    $connectionType = if ($ClientCredential -or [string]$Context.AuthType -eq 'AppOnly') { 'Service Principal' } else { 'User' }
    $account = if ($connectionType -eq 'Service Principal') {
        $Context.AppName ?? $ClientId ?? 'Service Principal'
    }
    else {
        $Context.Account ?? 'Unknown'
    }

    return [pscustomobject]@{
        Account        = $account
        ConnectionType = $connectionType
    }
}

function Show-InTUIGraphScopes {
    [CmdletBinding()]
    param()

    if (-not $script:Connected) {
        Show-InTUIWarning "Not connected to Microsoft Graph."
        Read-InTUIKey
        return
    }

    $context = Get-MgContext -ErrorAction SilentlyContinue
    if (-not $context) {
        Show-InTUIWarning "No active Microsoft Graph context found."
        Read-InTUIKey
        return
    }

    $connectionDisplay = Get-InTUIGraphConnectionDisplay -Context $context
    $scopes = @($context.Scopes | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | Sort-Object -Unique)

    if ($scopes.Count -eq 0) {
        Show-InTUIWarning "The current Microsoft Graph context does not report any scopes."
        Read-InTUIKey
        return
    }

    $content = @(
        "[grey]Tenant:[/] $($context.TenantId ?? 'Unknown')"
        "[grey]Account:[/] $($connectionDisplay.Account)"
        "[grey]Auth:[/] $($connectionDisplay.ConnectionType)"
        ''
        "[grey]Scopes:[/]"
    )

    foreach ($scope in $scopes) {
        $content += "- $scope"
    }

    Show-InTUIPanel -Title "[blue]Current Graph Scopes[/]" -Content ($content -join "`n") -BorderColor Blue
    Read-InTUIKey
}

function Assert-InTUIGraphDelegatedAuthMode {
    [CmdletBinding()]
    param(
        [Parameter()]
        [switch]$UseDeviceCode,

        [Parameter()]
        [switch]$UseBrowserAuth
    )

    if ($UseDeviceCode -and $UseBrowserAuth) {
        throw 'UseDeviceCode and UseBrowserAuth are mutually exclusive delegated authentication modes.'
    }
}

function Assert-InTUIGraphAuthMode {
    [CmdletBinding()]
    param(
        [Parameter()]
        [switch]$ClientCredential,

        [Parameter()]
        [switch]$UseDeviceCode,

        [Parameter()]
        [switch]$UseBrowserAuth
    )

    Assert-InTUIGraphDelegatedAuthMode -UseDeviceCode:$UseDeviceCode -UseBrowserAuth:$UseBrowserAuth
    if ($ClientCredential -and ($UseDeviceCode -or $UseBrowserAuth)) {
        throw 'Client credential authentication cannot be combined with delegated authentication switches.'
    }
}

function Resolve-InTUIGraphDelegatedAuthMode {
    [CmdletBinding()]
    param(
        [Parameter()]
        [switch]$UseDeviceCode,

        [Parameter()]
        [switch]$UseBrowserAuth
    )

    Assert-InTUIGraphAuthMode -UseDeviceCode:$UseDeviceCode -UseBrowserAuth:$UseBrowserAuth

    if ($UseDeviceCode) { return 'DeviceCode' }
    return 'BrowserAuth'
}

function Connect-InTUIGraph {
    <#
    .SYNOPSIS
        Connects to Microsoft Graph with required scopes for Intune management.
    .PARAMETER Scopes
        Graph API permission scopes to request for delegated auth.
    .PARAMETER TenantId
        Optional tenant ID or domain.
    .PARAMETER ClientId
        Application (client) ID for service principal auth.
    .PARAMETER ClientSecret
        Client secret for service principal auth.
    .PARAMETER Environment
        Cloud environment: Global, USGov, USGovDoD, or China.
    .PARAMETER UseBrowserAuth
        Use InTUI's WAM-free PKCE loopback browser flow for delegated auth.
    .PARAMETER BrowserSuccessPurpose
        Browser auth success page purpose.
    .PARAMETER BrowserErrorPurpose
        Browser auth error page purpose.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'Microsoft.Graph.Authentication requires a PSCredential for client secret authentication.')]
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]]$Scopes = @(
            'DeviceManagementManagedDevices.ReadWrite.All',
            'DeviceManagementManagedDevices.PrivilegedOperations.All',
            'DeviceManagementApps.ReadWrite.All',
            'User.Read.All',
            'Group.Read.All',
            'GroupMember.Read.All',
            'DeviceManagementConfiguration.Read.All',
            'DeviceManagementServiceConfig.Read.All',
            'Directory.Read.All',
            'AuditLog.Read.All',
            'BitlockerKey.ReadBasic.All',
            'BitlockerKey.Read.All'
        ),

        [Parameter()]
        [string]$TenantId,

        [Parameter()]
        [string]$ClientId,

        [Parameter()]
        [string]$ClientSecret,

        [Parameter()]
        [ValidateSet('Global', 'USGov', 'USGovDoD', 'China')]
        [string]$Environment = 'Global',

        [Parameter()]
        [switch]$UseDeviceCode,

        [Parameter()]
        [switch]$UseBrowserAuth,

        [Parameter()]
        [string]$BrowserSuccessPurpose = 'Authentication',

        [Parameter()]
        [string]$BrowserErrorPurpose = 'Authentication'
    )

    $envConfig = $script:CloudEnvironments[$Environment]

    $useClientCredential = $ClientId -and $ClientSecret -and $TenantId

    try {
        $delegatedAuthMode = 'None'
        if ($useClientCredential) {
            Assert-InTUIGraphAuthMode -ClientCredential -UseDeviceCode:$UseDeviceCode -UseBrowserAuth:$UseBrowserAuth
        }
        else {
            $delegatedAuthMode = Resolve-InTUIGraphDelegatedAuthMode -UseDeviceCode:$UseDeviceCode -UseBrowserAuth:$UseBrowserAuth
        }

        $authMode = if ($useClientCredential) { 'ClientCredential' }
                    else { $delegatedAuthMode }

        Write-InTUILog -Message "Connecting to Microsoft Graph" -Context @{
            Environment  = $Environment
            GraphBaseUrl = $envConfig.GraphBaseUrl
            TenantId     = $TenantId
            AuthMode     = $authMode
        }

        if ($useClientCredential) {
            $secureSecret = ConvertTo-SecureString -String $ClientSecret -AsPlainText -Force
            $credential = [System.Management.Automation.PSCredential]::new($ClientId, $secureSecret)
            Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $credential -NoWelcome:$true -Environment $envConfig.MgEnvironment
        }
        else {
            if ($delegatedAuthMode -eq 'BrowserAuth') {
                Connect-InTUIBrowserGraph -Scopes $Scopes -EnvironmentConfig $envConfig -TenantId $TenantId -ClientId $ClientId -SuccessPurpose $BrowserSuccessPurpose -ErrorPurpose $BrowserErrorPurpose
            }
            else {
                $params = @{
                    Scopes       = $Scopes
                    ContextScope = 'Process'
                    NoWelcome    = $true
                    Environment  = $envConfig.MgEnvironment
                }
                if ($TenantId) { $params['TenantId'] = $TenantId }
                $params['UseDeviceCode'] = $true
                if ($ClientId) {
                    $params['ClientId'] = $ClientId
                }

                Connect-MgGraph @params
            }
        }

        $context = Get-MgContext
        if (-not $context) { return $false }

        $script:CloudEnvironment = $Environment
        $script:GraphBaseUrl = $envConfig.GraphBaseUrl
        $script:GraphBetaUrl = $envConfig.GraphBetaUrl
        $script:Connected = $true
        $script:UseDeviceCode = (-not $useClientCredential -and $delegatedAuthMode -eq 'DeviceCode')
        $script:UseBrowserAuth = (-not $useClientCredential -and $delegatedAuthMode -eq 'BrowserAuth')
        $script:TenantId = $context.TenantId
        $connectionDisplay = Get-InTUIGraphConnectionDisplay -Context $context -ClientId $ClientId -ClientCredential:$useClientCredential
        $script:Account = $connectionDisplay.Account
        $script:ConnectionType = $connectionDisplay.ConnectionType
        Write-InTUILog -Message "Connected to Microsoft Graph" -Context @{
            TenantId       = $context.TenantId
            Account        = $script:Account
            ConnectionType = $script:ConnectionType
            Environment    = $Environment
        }
        return $true
    }
    catch {
        Write-InTUILog -Level 'ERROR' -Message "Failed to connect to Microsoft Graph: $($_.Exception.Message)"
        Write-InTUIText "[red]Failed to connect to Microsoft Graph: $($_.Exception.Message)[/]"
        return $false
    }
}

function Reconnect-InTUIGraph {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '', Justification = 'Reconnect is the established internal verb for refreshing the active Graph session.')]
    [CmdletBinding()]
    param(
        [Parameter()]
        [string[]]$Scopes,

        [Parameter()]
        [string]$TenantId,

        [Parameter()]
        [ValidateSet('Global', 'USGov', 'USGovDoD', 'China')]
        [string]$Environment,

        [Parameter()]
        [switch]$UseDeviceCode,

        [Parameter()]
        [switch]$UseBrowserAuth,

        [Parameter()]
        [string]$BrowserSuccessPurpose = 'Authentication',

        [Parameter()]
        [string]$BrowserErrorPurpose = 'Authentication'
    )

    $context = Get-MgContext -ErrorAction SilentlyContinue
    $currentScopes = if ($context) {
        @($context.Scopes | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | Sort-Object -Unique)
    }
    else {
        @()
    }

    if (-not $PSBoundParameters.ContainsKey('TenantId') -and -not [string]::IsNullOrWhiteSpace([string]$script:TenantId)) {
        $TenantId = $script:TenantId
    }

    if (-not $PSBoundParameters.ContainsKey('Environment')) {
        $Environment = if ([string]::IsNullOrWhiteSpace([string]$script:CloudEnvironment)) { 'Global' } else { $script:CloudEnvironment }
    }

    if (-not $PSBoundParameters.ContainsKey('Scopes')) {
        $Scopes = $currentScopes
    }

    $scopeList = @($Scopes)

    if (-not $PSBoundParameters.ContainsKey('UseDeviceCode') -and $script:UseDeviceCode) {
        $UseDeviceCode = [switch]$true
    }

    if (-not $PSBoundParameters.ContainsKey('UseBrowserAuth') -and $script:UseBrowserAuth) {
        $UseBrowserAuth = [switch]$true
    }

    $delegatedAuthMode = Resolve-InTUIGraphDelegatedAuthMode -UseDeviceCode:$UseDeviceCode -UseBrowserAuth:$UseBrowserAuth

    Write-InTUILog -Message 'Reconnecting to Microsoft Graph' -Context @{
        TenantId      = $TenantId
        Environment   = $Environment
        ScopeCount    = $scopeList.Count
        AuthMode      = $delegatedAuthMode
    }

    $connectParams = @{ Environment = $Environment }
    if (-not [string]::IsNullOrWhiteSpace($TenantId)) {
        $connectParams['TenantId'] = $TenantId
    }
    if ($scopeList.Count -gt 0) {
        $connectParams['Scopes'] = $scopeList
    }
    if ($delegatedAuthMode -eq 'DeviceCode') {
        $connectParams['UseDeviceCode'] = $true
    }
    if ($delegatedAuthMode -eq 'BrowserAuth') {
        $connectParams['UseBrowserAuth'] = $true
        $connectParams['BrowserSuccessPurpose'] = $BrowserSuccessPurpose
        $connectParams['BrowserErrorPurpose'] = $BrowserErrorPurpose
    }

    if ($delegatedAuthMode -ne 'BrowserAuth') {
        Disconnect-MgGraph -ErrorAction SilentlyContinue
        $script:Connected = $false
    }

    return (Connect-InTUIGraph @connectParams)
}

function Get-InTUIGraphErrorRawDetails {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord
    )

    $rawDetails = $ErrorRecord.ErrorDetails.Message
    if (-not [string]::IsNullOrWhiteSpace($rawDetails)) {
        return $rawDetails
    }

    $responseContent = $ErrorRecord.Exception.Response?.Content
    if ($null -eq $responseContent) {
        return $null
    }

    try {
        return $responseContent.ReadAsStringAsync().GetAwaiter().GetResult()
    }
    catch {
        return $null
    }
}

function Get-InTUIIntuneNestedErrorMessage {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$Text
    )

    if ([string]::IsNullOrWhiteSpace($Text)) {
        return $null
    }

    $nestedCodeMatch = [regex]::Match($Text, '(?:\\"|")ErrorCode(?:\\"|")\s*:\s*(?:\\"|")(?<code>[^"\\]+)')
    $activityIdMatch = [regex]::Match($Text, 'Activity ID:\s*(?<activityId>[0-9a-fA-F-]{36})')
    if (-not $nestedCodeMatch.Success -or -not $activityIdMatch.Success) {
        return $null
    }

    return "$($nestedCodeMatch.Groups['code'].Value): Intune service rejected the request. Activity ID: $($activityIdMatch.Groups['activityId'].Value)"
}

function ConvertFrom-InTUIGraphErrorJson {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$RawDetails
    )

    if ([string]::IsNullOrWhiteSpace($RawDetails)) {
        return $null
    }

    try {
        $errorDetail = $RawDetails | ConvertFrom-Json
    }
    catch {
        return $null
    }

    $intuneMessage = Get-InTUIIntuneNestedErrorMessage -Text $RawDetails
    if ($intuneMessage) {
        return $intuneMessage
    }

    if ($errorDetail.error) {
        $code = [string]$errorDetail.error.code
        $message = [string]$errorDetail.error.message
        if ([string]::IsNullOrWhiteSpace($code)) {
            return $message
        }

        return "$code`: $message"
    }

    if ($errorDetail.message) {
        return [string]$errorDetail.message
    }

    return $RawDetails
}

function Get-InTUIGraphErrorMessage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.ErrorRecord]$ErrorRecord
    )

    $errorMessage = $ErrorRecord.Exception.Message
    $rawDetails = Get-InTUIGraphErrorRawDetails -ErrorRecord $ErrorRecord
    $errorText = $rawDetails

    if ([string]::IsNullOrWhiteSpace($errorText) -and $errorMessage -match '\{"error"') {
        $jsonStart = $errorMessage.IndexOf('{"error"')
        if ($jsonStart -ge 0) {
            $rawDetails = $errorMessage.Substring($jsonStart)
            $errorText = $rawDetails
        }
    }

    $parsedErrorMessage = (ConvertFrom-InTUIGraphErrorJson -RawDetails $errorText) ??
        (Get-InTUIIntuneNestedErrorMessage -Text $errorText) ??
        (Get-InTUIIntuneNestedErrorMessage -Text $errorMessage)

    if ($parsedErrorMessage) {
        $errorMessage = $parsedErrorMessage
    }

    if ([string]::IsNullOrWhiteSpace($errorMessage)) {
        $errorMessage = "Request failed (HTTP $($ErrorRecord.Exception.Response.StatusCode))"
        if ($ErrorRecord.Exception.Response.ReasonPhrase) {
            $errorMessage += " - $($ErrorRecord.Exception.Response.ReasonPhrase)"
        }
    }

    return [pscustomobject]@{
        Message = $errorMessage
        RawBody = $rawDetails
    }
}

function Invoke-InTUIGraphRequest {
    <#
    .SYNOPSIS
        Wrapper around Invoke-MgGraphRequest with error handling and pagination support.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Uri,

        [Parameter()]
        [ValidateSet('GET', 'POST', 'PATCH', 'DELETE')]
        [string]$Method = 'GET',

        [Parameter()]
        [hashtable]$Body,

        [Parameter()]
        [switch]$Beta,

        [Parameter()]
        [switch]$All,

        [Parameter()]
        [int]$Top = 0,

        [Parameter()]
        [hashtable]$Headers,

        [Parameter()]
        [ValidateRange(1, 10000)]
        [int]$MaxPages = 500,

        [Parameter()]
        [switch]$NoCache,

        [Parameter()]
        [switch]$SuppressErrorOutput
    )

    if (-not $script:Connected) {
        Write-InTUILog -Level 'WARN' -Message "Graph request attempted while not connected" -Context @{ Uri = $Uri }
        Write-InTUIText "[red]Not connected to Microsoft Graph. Run Connect-InTUI first.[/]"
        return $null
    }

    $baseUrl = if ($Beta) { $script:GraphBetaUrl } else { $script:GraphBaseUrl }

    if ($Uri -notmatch '^https://') {
        $fullUri = "$baseUrl/$($Uri.TrimStart('/'))"
    }
    else {
        $fullUri = $Uri
    }

    if ($Top -gt 0 -and $Method -eq 'GET') {
        $separator = if ($fullUri -match '\?') { '&' } else { '?' }
        $fullUri = "$fullUri$separator`$top=$Top"
    }

    # Check cache for GET requests
    if ($Method -eq 'GET' -and $script:CacheEnabled -and -not $NoCache) {
        $cached = Get-InTUICachedResponse -Uri $fullUri -Method $Method -Beta:$Beta
        if ($null -ne $cached) {
            return $cached
        }
    }

    Write-InTUILog -Message "Graph API request" -Context @{
        Method = $Method
        Uri = $fullUri
        Beta = [bool]$Beta
        All = [bool]$All
        Environment = $script:CloudEnvironment
    }

    $params = @{
        Uri    = $fullUri
        Method = $Method
        OutputType = 'PSObject'
    }

    if ($Headers) {
        $params['Headers'] = $Headers
    }

    if ($Body) {
        $params['Body'] = $Body | ConvertTo-Json -Depth 10
        $params['ContentType'] = 'application/json'
    }

    try {
        $script:LastGraphError = $null
        $response = Invoke-MgGraphRequest @params

        if ($All -and $Method -eq 'GET') {
            $allResults = [System.Collections.Generic.List[object]]::new()
            if ($response.value) {
                $allResults.AddRange(@($response.value))
            }

            $pageCount = 1
            while ($response.'@odata.nextLink') {
                $pageCount++
                if ($pageCount -gt $MaxPages) {
                    throw "Graph pagination exceeded max page limit of $MaxPages for '$fullUri'."
                }

                Write-InTUILog -Message "Fetching pagination page $pageCount" -Context @{ NextLink = $response.'@odata.nextLink' }
                $response = Invoke-MgGraphRequest -Uri $response.'@odata.nextLink' -Method GET -OutputType PSObject
                if ($response.value) {
                    $allResults.AddRange(@($response.value))
                }
            }

            Write-InTUILog -Message "Graph API request completed" -Context @{ TotalResults = $allResults.Count; Pages = $pageCount }

            # Cache the paginated results
            if ($script:CacheEnabled -and -not $NoCache) {
                Set-InTUICachedResponse -Uri $fullUri -Data $allResults -Method $Method -Beta:$Beta
            }

            return $allResults
        }

        $resultCount = if ($response.value) { @($response.value).Count } else { 1 }
        Write-InTUILog -Message "Graph API request completed" -Context @{ ResultCount = $resultCount }

        # Cache single-page response
        if ($Method -eq 'GET' -and $script:CacheEnabled -and -not $NoCache) {
            Set-InTUICachedResponse -Uri $fullUri -Data $response -Method $Method -Beta:$Beta
        }

        # Return $true for no-content success (e.g., 204) so $null exclusively means error
        return ($response ?? $true)
    }
    catch {
        $graphError = Get-InTUIGraphErrorMessage -ErrorRecord $_
        $errorMessage = $graphError.Message
        Write-InTUILog -Level 'ERROR' -Message "Graph API Error: $errorMessage" -Context @{ Uri = $fullUri; Method = $Method }
        $script:LastGraphError = [pscustomobject]@{
            Message    = $errorMessage
            Uri        = $fullUri
            Method     = $Method
            StatusCode = $_.Exception.Response.StatusCode
            RawBody    = $graphError.RawBody
        }
        if (-not $SuppressErrorOutput) {
            Write-InTUIText "[red]Graph API Error: $errorMessage[/]"
        }
        return $null
    }
}

function Get-InTUIPagedResults {
    <#
    .SYNOPSIS
        Gets paged results from Graph API with navigation support.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Uri,

        [Parameter()]
        [switch]$Beta,

        [Parameter()]
        [int]$PageSize = $script:PageSize,

        [Parameter()]
        [string]$Filter,

        [Parameter()]
        [string]$Search,

        [Parameter()]
        [string]$Select,

        [Parameter()]
        [string]$OrderBy,

        [Parameter()]
        [string]$Expand,

        [Parameter()]
        [hashtable]$Headers,

        [Parameter()]
        [switch]$IncludeCount
    )

    $queryParams = @()

    if ($PageSize -gt 0) {
        $queryParams += "`$top=$PageSize"
    }
    if ($Filter) {
        $queryParams += "`$filter=$Filter"
    }
    if ($Search) {
        $searchValue = if ($Search.StartsWith('"')) { $Search } else { "`"$Search`"" }
        $queryParams += "`$search=$searchValue"
    }
    if ($Select) {
        $queryParams += "`$select=$Select"
    }
    if ($OrderBy) {
        $queryParams += "`$orderby=$OrderBy"
    }
    if ($Expand) {
        $queryParams += "`$expand=$Expand"
    }
    if ($IncludeCount) {
        $queryParams += "`$count=true"
    }

    $fullUri = $Uri
    if ($queryParams.Count -gt 0) {
        $fullUri = "$Uri`?$($queryParams -join '&')"
    }

    $params = @{ Uri = $fullUri }
    if ($Beta) { $params['Beta'] = $true }
    if ($Headers) { $params['Headers'] = $Headers }

    $response = Invoke-InTUIGraphRequest @params

    # Guard: null or non-object response (e.g. $true from 204 No Content)
    if ($null -eq $response -or $response -is [bool]) {
        return @{ Results = @(); NextLink = $null; TotalCount = 0 }
    }

    $results = if ($response.value) { @($response.value) }
               elseif ($response -is [array]) { $response }
               else { @() }

    $resultCount = @($results).Count
    $odataCount = $response.'@odata.count'
    # Use @odata.count only when present and sensible (>= page results); otherwise use actual count
    $totalCount = if ($null -ne $odataCount -and $odataCount -ge $resultCount) { $odataCount } else { $resultCount }

    return @{
        Results    = $results
        NextLink   = $response.'@odata.nextLink'
        TotalCount = $totalCount
    }
}

function ConvertTo-InTUISafeFilterValue {
    <#
    .SYNOPSIS
        Escapes a string for safe use inside an OData $filter expression.
    #>

    param([string]$Value)

    if ([string]::IsNullOrEmpty($Value)) { return $Value }
    return $Value -replace "'", "''"
}

function Format-InTUIDate {
    <#
    .SYNOPSIS
        Formats a date string for display.
    #>

    param([string]$DateString)

    if ([string]::IsNullOrEmpty($DateString)) { return 'N/A' }

    try {
        $date = [DateTime]::Parse($DateString)
        $now = [DateTime]::UtcNow
        $diff = $now - $date

        if ($diff.TotalMinutes -lt 60) {
            return "$([math]::Floor($diff.TotalMinutes))m ago"
        }
        elseif ($diff.TotalHours -lt 24) {
            return "$([math]::Floor($diff.TotalHours))h ago"
        }
        elseif ($diff.TotalDays -lt 7) {
            return "$([math]::Floor($diff.TotalDays))d ago"
        }
        else {
            return $date.ToString('yyyy-MM-dd HH:mm')
        }
    }
    catch {
        return $DateString
    }
}

function Get-InTUIComplianceColor {
    <#
    .SYNOPSIS
        Returns markup color name based on compliance state.
    #>

    param([string]$State)

    switch ($State) {
        'compliant'     { return 'green' }
        'noncompliant'  { return 'red' }
        'error'         { return 'red' }
        'inGracePeriod' { return 'yellow' }
        'configManager' { return 'blue' }
        'conflict'      { return 'orange1' }
        default         { return 'grey' }
    }
}

function Get-InTUIInstallStateColor {
    <#
    .SYNOPSIS
        Returns markup color name based on app install state.
    #>

    param([string]$State)

    switch ($State) {
        'installed'       { return 'green' }
        'failed'          { return 'red' }
        'uninstallFailed' { return 'red' }
        'notInstalled'    { return 'grey' }
        'notApplicable'   { return 'grey' }
        default           { return 'yellow' }
    }
}

function Get-InTUIDeviceIcon {
    <#
    .SYNOPSIS
        Returns an icon character based on OS type.
    #>

    param([string]$OperatingSystem)

    switch -Wildcard ($OperatingSystem) {
        '*Windows*' { return '[blue]W[/]' }
        '*iOS*'     { return '[grey]i[/]' }
        '*iPadOS*'  { return '[grey]P[/]' }
        '*macOS*'   { return '[grey]m[/]' }
        '*Android*' { return '[green]A[/]' }
        '*Linux*'   { return '[yellow]L[/]' }
        default     { return '[grey]-[/]' }
    }
}

function Get-InTUIAppTypeIcon {
    <#
    .SYNOPSIS
        Returns an icon based on application type.
    #>

    param([string]$AppType)

    switch -Wildcard ($AppType) {
        '*win32*'           { return '[blue]W[/]' }
        '*msi*'             { return '[blue]M[/]' }
        '*ios*'             { return '[grey]i[/]' }
        '*android*'         { return '[green]A[/]' }
        '*webApp*'          { return '[cyan]w[/]' }
        '*office*'          { return '[orange1]O[/]' }
        '*microsoft*'       { return '[blue]M[/]' }
        '*store*'           { return '[cyan]S[/]' }
        '*managed*'         { return '[yellow]m[/]' }
        default             { return '[grey]-[/]' }
    }
}

function Get-InTUIPolicyIcon {
    <#
    .SYNOPSIS
        Returns an icon based on policy type.
    #>

    param([string]$PolicyType)

    switch -Wildcard ($PolicyType) {
        '*compliance*'      { return '[green]+[/]' }
        '*configuration*'   { return '[blue]*[/]' }
        '*conditional*'     { return '[yellow]![/]' }
        '*security*'        { return '[red]#[/]' }
        '*update*'          { return '[cyan]~[/]' }
        default             { return '[grey]-[/]' }
    }
}

function Get-InTUISecurityIcon {
    <#
    .SYNOPSIS
        Returns security-related icons.
    #>

    param(
        [Parameter(Mandatory)]
        [ValidateSet('Shield', 'Lock', 'Unlock', 'Key', 'Warning', 'Error', 'Check', 'Cross')]
        [string]$Type
    )

    switch ($Type) {
        'Shield'  { return '[blue]#[/]' }
        'Lock'    { return '[green]#[/]' }
        'Unlock'  { return '[yellow]-[/]' }
        'Key'     { return '[yellow]k[/]' }
        'Warning' { return '[yellow]![/]' }
        'Error'   { return '[red]x[/]' }
        'Check'   { return '[green]+[/]' }
        'Cross'   { return '[red]x[/]' }
    }
}