Private/UIHelpers.ps1

function Ensure-InTUIBufferSpace {
    <#
    .SYNOPSIS
        Scrolls the terminal buffer to ensure enough rows below the anchor for rendering.
    .DESCRIPTION
        When UI components render near the bottom of the terminal, SetCursorPosition
        calls past the buffer height silently fail or scroll content away. This function
        pre-scrolls the buffer to guarantee enough room, returning the adjusted anchor.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [int]$AnchorTop,

        [Parameter(Mandatory)]
        [int]$NeededRows
    )

    $bufferHeight = [Console]::BufferHeight
    $available = $bufferHeight - $AnchorTop

    if ($available -ge $NeededRows) { return $AnchorTop }

    # Scroll just enough, but keep the box title visible (3 rows above anchor)
    $scrollAmount = [math]::Min($NeededRows - $available, [math]::Max(0, $AnchorTop - 3))
    if ($scrollAmount -le 0) { return $AnchorTop }

    [Console]::SetCursorPosition(0, $bufferHeight - 1)
    for ($s = 0; $s -lt $scrollAmount; $s++) {
        [Console]::Write("`n")
    }
    return ($AnchorTop - $scrollAmount)
}

function Show-InTUIHeader {
    <#
    .SYNOPSIS
        Displays the InTUI header banner with gradient-bordered ASCII art.
    #>

    [CmdletBinding()]
    param(
        [string]$Subtitle
    )

    $palette = Get-InTUIColorPalette
    $reset = $palette.Reset

    $consoleWidth = try { [Console]::WindowWidth } catch { 80 }

    # Helper to center a plain string and return the left padding count
    $centerPad = {
        param([int]$TextWidth)
        [Math]::Max(0, [int](($consoleWidth - $TextWidth) / 2))
    }

    # Gradient-decorated top border
    $borderWidth = [Math]::Min(60, $consoleWidth - 4)
    $gradientTop = Get-InTUIGradientLine -Character ([char]0x2500) -Width $borderWidth
    Write-Host "$(' ' * (& $centerPad $borderWidth))$gradientTop"

    # ASCII art banner with gradient
    $bannerLines = @(
        '██╗███╗ ██╗████████╗██╗ ██╗██╗'
        '██║████╗ ██║╚══██╔══╝██║ ██║██║'
        '██║██╔██╗ ██║ ██║ ██║ ██║██║'
        '██║██║╚██╗██║ ██║ ██║ ██║██║'
        '██║██║ ╚████║ ██║ ╚██████╔╝██║'
        '╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝'
    )
    $bannerWidth = Measure-InTUIDisplayWidth -Text $bannerLines[0]
    $bannerPad = & $centerPad $bannerWidth
    foreach ($line in $bannerLines) {
        $gradientLine = Get-InTUIGradientString -Text $line
        Write-Host "$(' ' * $bannerPad)$gradientLine"
    }

    $tagline = 'Intune Terminal User Interface'
    $taglinePad = & $centerPad $tagline.Length
    Write-Host "$(' ' * $taglinePad)$($palette.Dim)$tagline$reset"
    Write-Host ""

    if ($script:Connected) {
        $tenant = if ($script:TenantId) {
            $tid = $script:TenantId
            if ($tid -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') {
                $tParts = $tid -split '-'
                '{0}-****-****-****-********{1}' -f $tParts[0], $tParts[4].Substring(8)
            }
            else {
                $tid
            }
        } else { 'Unknown' }
        $account = if ($script:Account) { $script:Account } else { 'Unknown' }
        $envLabel = if ($script:CloudEnvironments -and $script:CloudEnvironment) {
            $script:CloudEnvironments[$script:CloudEnvironment].Label
        } else { 'Global' }

        $infoLines = @(
            @{ Label = 'Env'; Value = $envLabel }
            @{ Label = 'Tenant'; Value = $tenant }
            @{ Label = 'Account'; Value = $account }
        )
        $maxLabel = ($infoLines | ForEach-Object { $_.Label.Length } | Measure-Object -Maximum).Maximum
        foreach ($info in $infoLines) {
            $plainLine = "$($info.Label.PadLeft($maxLabel)): $($info.Value)"
            $infoPad = & $centerPad $plainLine.Length
            Write-InTUIText "$(' ' * $infoPad)[grey]$($info.Label.PadLeft($maxLabel)):[/] [cyan]$($info.Value)[/]"
        }
    }

    if ($Subtitle) {
        $plainSub = Strip-InTUIMarkup -Text $Subtitle
        $subPad = & $centerPad $plainSub.Length
        Write-InTUIText "$(' ' * $subPad)[grey]$Subtitle[/]"
    }

    # Gradient bottom border
    $gradientBottom = Get-InTUIGradientLine -Character ([char]0x2500) -Width $borderWidth
    Write-Host "$(' ' * (& $centerPad $borderWidth))$gradientBottom"
    Write-Host ""
}

function Show-InTUIBreadcrumb {
    <#
    .SYNOPSIS
        Displays a breadcrumb navigation bar with visual separator.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$Path
    )

    $separator = " [grey]>[/] "

    $pathItems = @()
    for ($i = 0; $i -lt $Path.Count; $i++) {
        $pathItems += "[blue]$($Path[$i])[/]"
    }

    $breadcrumb = $pathItems -join $separator
    Write-InTUIText $breadcrumb
    Write-Host ""
}

function Show-InTUIStatusBar {
    <#
    .SYNOPSIS
        Displays a status bar with counts.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [int]$Total = 0,

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

        [Parameter()]
        [string]$FilterText
    )

    $status = "[grey]Showing [white]$Showing[/] of [white]$Total[/] items[/]"
    if ($FilterText) {
        $status += " [grey]| Filter: [yellow]$FilterText[/][/]"
    }
    Write-InTUIText $status
}

function Read-InTUIKey {
    <#
    .SYNOPSIS
        Reads a key press and returns the key info.
    #>

    Write-InTUIText "[grey]Press any key to continue...[/]"
    $null = [Console]::ReadKey($true)
}

function Show-InTUIMenu {
    <#
    .SYNOPSIS
        Displays a selection menu and returns the selected option string.
        Routes to arrow-key or classic menu based on capability.
        Returns the original choice string for backward-compatible switch matching.
    #>

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

        [Parameter(Mandatory)]
        [string[]]$Choices,

        [Parameter()]
        [string]$Color = 'Blue',

        [Parameter()]
        [int]$PageSize = 15
    )

    if ($script:HasArrowKeySupport) {
        $result = Show-InTUIMenuArrowSingle -Title $Title -Choices $Choices -PageSize $PageSize
    }
    else {
        $result = Show-InTUIMenuClassic -Title $Title -Choices $Choices
    }

    if ($result -eq 'Back') {
        # Escape pressed: find a Back/Cancel choice so the caller's switch exits naturally
        $backChoice = $Choices | Where-Object { $_ -match '^Back' } | Select-Object -Last 1
        return $backChoice  # $null if no back choice exists (e.g. main menu)
    }
    if ($result -is [int] -and $result -ge 0 -and $result -lt $Choices.Count) {
        return $Choices[$result]
    }
    return $null
}

function Show-InTUIMultiSelect {
    <#
    .SYNOPSIS
        Multi-selection menu wrapper. Returns selected choice strings.
        Replaces Read-SpectreMultiSelection.
    #>

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

        [Parameter(Mandatory)]
        [string[]]$Choices,

        [Parameter()]
        [int]$PageSize = 15
    )

    if ($script:HasArrowKeySupport) {
        $indices = Show-InTUIMenuArrowMulti -Title $Title -Choices $Choices -PageSize $PageSize
    }
    else {
        $indices = Show-InTUIMenuClassic -Title $Title -Choices $Choices -MultiSelect
    }

    if (-not $indices -or $indices.Count -eq 0) { return @() }

    $selected = @()
    foreach ($idx in $indices) {
        if ($idx -ge 0 -and $idx -lt $Choices.Count) {
            $selected += $Choices[$idx]
        }
    }
    return $selected
}

function Get-InTUIChoiceMap {
    <#
    .SYNOPSIS
        Ensures menu choices are unique and returns an index map.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$Choices
    )

    $counts = @{}
    $uniqueChoices = [System.Collections.Generic.List[string]]::new()
    $indexMap = @{}

    for ($i = 0; $i -lt $Choices.Count; $i++) {
        $choice = $Choices[$i]
        if (-not $counts.ContainsKey($choice)) {
            $counts[$choice] = 0
        }
        $counts[$choice]++

        $suffix = if ($counts[$choice] -gt 1) { " [grey](#$($counts[$choice]))[/]" } else { '' }
        $uniqueChoice = "$choice$suffix"
        $uniqueChoices.Add($uniqueChoice)
        $indexMap[$uniqueChoice] = $i
    }

    return @{ Choices = $uniqueChoices.ToArray(); IndexMap = $indexMap }
}

function Show-InTUIConfirm {
    <#
    .SYNOPSIS
        Shows a confirmation prompt.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Message
    )

    Read-InTUIConfirmInput -Message $Message
}

function Show-InTUIPanel {
    <#
    .SYNOPSIS
        Displays content in a bordered panel.
    #>

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

        [Parameter(Mandatory)]
        [string]$Content,

        [Parameter()]
        [string]$BorderColor = 'Blue'
    )

    Render-InTUIPanel -Content $Content -Title $Title -BorderColor $BorderColor
}

function Show-InTUITable {
    <#
    .SYNOPSIS
        Creates and displays a formatted table.
    #>

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

        [Parameter(Mandatory)]
        [string[]]$Columns,

        [Parameter(Mandatory)]
        [array]$Rows,

        [Parameter()]
        [string]$BorderColor = 'Blue'
    )

    Render-InTUITable -Title $Title -Columns $Columns -Rows $Rows -BorderColor $BorderColor
}

function Show-InTUILoading {
    <#
    .SYNOPSIS
        Shows a loading spinner while executing a script block.
    #>

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

        [Parameter(Mandatory)]
        [scriptblock]$ScriptBlock
    )

    Invoke-InTUIWithSpinner -Title $Title -ScriptBlock $ScriptBlock
}

function Show-InTUIError {
    <#
    .SYNOPSIS
        Displays an error message in a styled panel.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Message
    )

    Render-InTUIPanel -Content "[red]$Message[/]" -Title "[red]Error[/]" -BorderColor 'Red'
}

function Show-InTUISuccess {
    <#
    .SYNOPSIS
        Displays a success message.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Message
    )

    Write-InTUIText "[green]+[/] $Message"
}

function Show-InTUIWarning {
    <#
    .SYNOPSIS
        Displays a warning message.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Message
    )

    Write-InTUIText "[yellow]![/] $Message"
}

function Show-InTUIInfo {
    <#
    .SYNOPSIS
        Displays an info message.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Message
    )

    Write-InTUIText "[blue]*[/] $Message"
}

function Get-InTUIProgressBar {
    <#
    .SYNOPSIS
        Returns a text-based progress bar with markup.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [double]$Percentage,

        [Parameter()]
        [int]$Width = 20,

        [Parameter()]
        [string]$FilledColor = 'green',

        [Parameter()]
        [string]$EmptyColor = 'grey'
    )

    $percentage = [Math]::Max(0, [Math]::Min(100, $Percentage))
    $filled = [int][Math]::Floor(($percentage / 100) * $Width)
    $empty = [int]($Width - $filled)

    $filledChar = [string][char]0x2588  # Full block
    $emptyChar = [string][char]0x2591   # Light shade

    $bar = "[$FilledColor]$($filledChar * $filled)[/][$EmptyColor]$($emptyChar * $empty)[/]"
    return $bar
}

function Show-InTUISectionHeader {
    <#
    .SYNOPSIS
        Displays a gradient-decorated section divider.
    #>

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

        [Parameter()]
        [string]$Color = 'blue',

        [Parameter()]
        [string]$Icon
    )

    $iconDisplay = if ($Icon) { "$Icon " } else { "" }
    $fullTitle = "$iconDisplay$Title"

    Write-Host ""
    $gradientLine = Get-InTUIGradientString -Text "--- $fullTitle ---"
    Write-Host $gradientLine
    Write-Host ""
}

function Get-InTUIStatusBadge {
    <#
    .SYNOPSIS
        Returns a colored status badge markup string.
    #>

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

        [Parameter()]
        [string]$Color
    )

    $badgeColor = if ($Color) { $Color } else {
        switch -Wildcard ($Status.ToLower()) {
            '*success*'    { 'green' }
            '*compli*'     { 'green' }
            '*enabled*'    { 'green' }
            '*active*'     { 'green' }
            '*installed*'  { 'green' }
            '*fail*'       { 'red' }
            '*error*'      { 'red' }
            '*disabled*'   { 'red' }
            '*noncompliant*' { 'red' }
            '*warning*'    { 'yellow' }
            '*pending*'    { 'yellow' }
            '*grace*'      { 'yellow' }
            '*processing*' { 'cyan' }
            default        { 'grey' }
        }
    }

    return "[$badgeColor]* $Status[/]"
}

function Get-InTUIAppIcon {
    <#
    .SYNOPSIS
        Returns an icon based on app type.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$AppType
    )

    switch -Wildcard ($AppType) {
        '*win32*'           { return '[blue]W[/]' }
        '*msi*'             { return '[blue]M[/]' }
        '*ios*'             { return '[grey]i[/]' }
        '*android*'         { return '[green]A[/]' }
        '*web*'             { return '[cyan]w[/]' }
        '*office*'          { return '[orange]O[/]' }
        '*microsoft*'       { return '[blue]M[/]' }
        default             { return '[grey]-[/]' }
    }
}

function Get-InTUIUserIcon {
    <#
    .SYNOPSIS
        Returns a user icon with optional status.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$AccountEnabled = 'true',

        [Parameter()]
        [switch]$IsAdmin
    )

    if ($IsAdmin) {
        return '[yellow]*[/]'
    }
    elseif ($AccountEnabled -eq 'true') {
        return '[green]+[/]'
    }
    else {
        return '[red]-[/]'
    }
}

function Get-InTUIGroupIcon {
    <#
    .SYNOPSIS
        Returns a group icon based on type.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$GroupType,

        [Parameter()]
        [string]$SecurityEnabled,

        [Parameter()]
        [string]$MailEnabled
    )

    if ($SecurityEnabled -eq 'true' -and $MailEnabled -eq 'true') {
        return '[cyan]SM[/]'
    }
    elseif ($SecurityEnabled -eq 'true') {
        return '[blue]S[/]'
    }
    elseif ($MailEnabled -eq 'true') {
        return '[cyan]@[/]'
    }
    elseif ($GroupType -match 'DynamicMembership') {
        return '[yellow]D[/]'
    }
    else {
        return '[grey]G[/]'
    }
}

function Show-InTUIBoxedText {
    <#
    .SYNOPSIS
        Displays text in a decorative Unicode box.
    #>

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

        [Parameter()]
        [string]$Color = 'blue'
    )

    $palette = Get-InTUIColorPalette
    $reset = $palette.Reset

    $colorAnsi = switch ($Color.ToLower()) {
        'blue'   { $palette.Blue }
        'green'  { $palette.Green }
        'red'    { $palette.Red }
        'yellow' { $palette.Yellow }
        'cyan'   { $palette.Cyan }
        default  { $palette.Blue }
    }

    $topLeft = [char]0x256D
    $topRight = [char]0x256E
    $bottomLeft = [char]0x2570
    $bottomRight = [char]0x256F
    $horizontal = [char]0x2500
    $vertical = [char]0x2502

    $padding = 2
    $textLength = Measure-InTUIDisplayWidth -Text (Strip-InTUIMarkup -Text $Text)
    $boxWidth = $textLength + ($padding * 2)

    $ansiText = ConvertFrom-InTUIMarkup -Text $Text
    Write-Host "$colorAnsi$topLeft$([string]::new($horizontal, $boxWidth))$topRight$reset"
    Write-Host "$colorAnsi$vertical$reset$(' ' * $padding)$ansiText$(' ' * $padding)$colorAnsi$vertical$reset"
    Write-Host "$colorAnsi$bottomLeft$([string]::new($horizontal, $boxWidth))$bottomRight$reset"
}

function Show-InTUISparkline {
    <#
    .SYNOPSIS
        Returns a sparkline chart markup string from an array of values.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [double[]]$Values,

        [Parameter()]
        [string]$Color = 'cyan'
    )

    $blocks = @([char]0x2581, [char]0x2582, [char]0x2583, [char]0x2584, [char]0x2585, [char]0x2586, [char]0x2587, [char]0x2588)

    $min = ($Values | Measure-Object -Minimum).Minimum
    $max = ($Values | Measure-Object -Maximum).Maximum
    $range = $max - $min

    if ($range -eq 0) { $range = 1 }

    $sparkline = ""
    foreach ($value in $Values) {
        $normalized = [Math]::Floor((($value - $min) / $range) * 7)
        $sparkline += $blocks[$normalized]
    }

    return "[$Color]$sparkline[/]"
}

function Get-InTUIConfigProfileType {
    <#
    .SYNOPSIS
        Maps a device configuration @odata.type to a friendly name and platform.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$ODataType
    )

    if ([string]::IsNullOrEmpty($ODataType)) {
        return @{ Platform = 'Unknown'; FriendlyName = 'Unknown' }
    }

    switch -Wildcard ($ODataType) {
        '*windows10General*'            { return @{ Platform = 'Windows'; FriendlyName = 'General' } }
        '*windows10Custom*'             { return @{ Platform = 'Windows'; FriendlyName = 'Custom' } }
        '*windows10EndpointProtection*' { return @{ Platform = 'Windows'; FriendlyName = 'Endpoint Protection' } }
        '*windowsUpdateForBusiness*'    { return @{ Platform = 'Windows'; FriendlyName = 'Update Ring' } }
        '*iosGeneral*'                  { return @{ Platform = 'iOS'; FriendlyName = 'General' } }
        '*iosCustom*'                   { return @{ Platform = 'iOS'; FriendlyName = 'Custom' } }
        '*macOSGeneral*'                { return @{ Platform = 'macOS'; FriendlyName = 'General' } }
        '*macOSCustom*'                 { return @{ Platform = 'macOS'; FriendlyName = 'Custom' } }
        '*androidGeneral*'              { return @{ Platform = 'Android'; FriendlyName = 'General' } }
        '*androidCustom*'               { return @{ Platform = 'Android'; FriendlyName = 'Custom' } }
        default {
            $rawType = $ODataType -replace '#microsoft\.graph\.', ''
            return @{ Platform = 'Unknown'; FriendlyName = $rawType }
        }
    }
}

function Protect-InTUIMarkup {
    <#
    .SYNOPSIS
        Escapes brackets so they are displayed literally instead of being
        interpreted as markup tags.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$Text
    )

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

    return $Text -replace '\[', '[[' -replace '\]', ']]'
}

# Keep backward-compatible alias
Set-Alias -Name ConvertTo-InTUISafeMarkup -Value Protect-InTUIMarkup