Functions/Public/Show-OrionSmartMenu.ps1

function Show-OrionSmartMenu {
    <#
    .SYNOPSIS
        Displays a reusable interactive menu with smart input mode selection.
 
    .DESCRIPTION
        Show-OrionSmartMenu automatically chooses between arrow-key navigation and
        numeric input mode based on the number of options.
 
        Arrow mode is used when option count is less than or equal to ArrowThreshold.
        Numeric mode is used when option count is greater than ArrowThreshold.
 
        If a rich console host is unavailable for arrow-key input, the function
        falls back to numeric mode automatically.
 
    .PARAMETER Title
        Title displayed at the top of the menu.
 
    .PARAMETER Prompt
        Prompt text displayed above options.
 
    .PARAMETER Options
        Menu options to display.
 
    .PARAMETER ArrowThreshold
        Maximum option count for arrow mode. Default is 5.
 
    .PARAMETER ReturnObject
        Returns a rich object with SelectedIndex, SelectedOption, and Mode.
        By default, only the selected option text is returned.
 
    .PARAMETER Demo
        Renders a static demonstration of both Arrow mode and Numeric mode without
        requiring any user input. Useful for previewing the menu appearance.
 
    .OUTPUTS
        System.String by default.
        System.Management.Automation.PSCustomObject when -ReturnObject is specified.
 
    .EXAMPLE
        Show-OrionSmartMenu -Demo
 
    .EXAMPLE
        Show-OrionSmartMenu -Title 'Main Menu' -Prompt 'Choose an action:' -Options @('Run','Exit')
 
    .EXAMPLE
        Show-OrionSmartMenu -Title 'Tenant Explorer' -Prompt 'Choose an action:' -Options $menuOptions -ReturnObject
 
    .NOTES
        Author: Sune Alexandersen Narud
        Version: 1.0.0
        Date: February 2026
    #>


    [CmdletBinding(DefaultParameterSetName = 'Interactive')]
    [OutputType([string])]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Interactive')]
        [ValidateNotNullOrEmpty()]
        [string]$Title,

        [Parameter(Mandatory, ParameterSetName = 'Interactive')]
        [ValidateNotNullOrEmpty()]
        [string]$Prompt,

        [Parameter(Mandatory, ParameterSetName = 'Interactive')]
        [ValidateNotNullOrEmpty()]
        [string[]]$Options,

        [Parameter(ParameterSetName = 'Interactive')]
        [ValidateRange(1, 50)]
        [int]$ArrowThreshold = 5,

        [Parameter(ParameterSetName = 'Interactive')]
        [switch]$ReturnObject,

        [Parameter(Mandatory, ParameterSetName = 'Demo')]
        [switch]$Demo
    )

    if ($Demo) {
        $demoArrowOptions = @('Start Service', 'Stop Service', 'Restart Service')
        $demoNumericOptions = @('Create User', 'Delete User', 'Modify User', 'List Users', 'Reset Password', 'Assign Role', 'Revoke Role')

        Write-Host ''
        Write-Host ' Show-OrionSmartMenu Demo' -ForegroundColor Cyan
        Write-Host ' ========================' -ForegroundColor DarkGray
        Write-Host ''

        $demoCodeArrow = @(
            '$options = @(''Start Service'', ''Stop Service'', ''Restart Service'')'
            'Show-OrionSmartMenu -Title ''Service Manager'' -Prompt ''Choose an action:'' -Options $options'
        )
        $demoCodeNumeric = @(
            '$options = @(''Create User'', ''Delete User'', ''Modify User'', ''List Users'', ''Reset Password'', ''Assign Role'', ''Revoke Role'')'
            'Show-OrionSmartMenu -Title ''User Management'' -Prompt ''Choose an action:'' -Options $options'
        )

        $renderCodeBlock = {
            param([string[]]$Lines)
            $innerWidth = ($Lines | Measure-Object -Property Length -Maximum).Maximum + 2
            $bar = '─' * $innerWidth
            Write-Host ' # Code' -ForegroundColor DarkGray
            Write-Host " ┌$bar┐" -ForegroundColor DarkGray
            foreach ($line in $Lines) {
                $padded = " $line".PadRight($innerWidth)
                Write-Host " │" -ForegroundColor DarkGray -NoNewline
                Write-Host $padded -ForegroundColor Green -NoNewline
                Write-Host '│' -ForegroundColor DarkGray
            }
            Write-Host " └$bar┘" -ForegroundColor DarkGray
            Write-Host ''
        }

        # Arrow mode demo
        Write-Host ' [Arrow Mode] (triggered when options <= ArrowThreshold)' -ForegroundColor Yellow
        Write-Host ''
        & $renderCodeBlock -Lines $demoCodeArrow
        if (Get-Command -Name 'Write-Separator' -ErrorAction SilentlyContinue) {
            try { Write-Separator 'Service Manager' -Style Thick } catch { Write-Host '=== Service Manager ===' }
        } else {
            Write-Host '=== Service Manager ==='
        }
        Write-Host 'Choose an action:'
        Write-Host 'Use Up/Down arrows and Enter.' -ForegroundColor DarkGray
        Write-Host ''
        for ($i = 0; $i -lt $demoArrowOptions.Count; $i++) {
            if ($i -eq 1) {
                Write-Host "> $($demoArrowOptions[$i])" -ForegroundColor Cyan
            } else {
                Write-Host " $($demoArrowOptions[$i])"
            }
        }

        Write-Host ''
        Write-Host ' [Numeric Mode] (triggered when options > ArrowThreshold)' -ForegroundColor Yellow
        Write-Host ''
        & $renderCodeBlock -Lines $demoCodeNumeric
        if (Get-Command -Name 'Write-Separator' -ErrorAction SilentlyContinue) {
            try { Write-Separator 'User Management' -Style Thick } catch { Write-Host '=== User Management ===' }
        } else {
            Write-Host '=== User Management ==='
        }
        Write-Host 'Choose an action:'
        Write-Host ''
        for ($i = 0; $i -lt $demoNumericOptions.Count; $i++) {
            Write-Host ('{0}. {1}' -f ($i + 1), $demoNumericOptions[$i])
        }
        Write-Host "Choose 1-$($demoNumericOptions.Count): " -NoNewline -ForegroundColor Gray
        Write-Host '(demo - no input required)' -ForegroundColor DarkGray
        Write-Host ''
        return
    }

    $normalizedOptions = @($Options | ForEach-Object {
            if ($null -eq $_) { '' } else { $_.Trim() }
        })

    if ($normalizedOptions.Count -eq 0) {
        throw 'Options cannot be empty.'
    }

    if (($normalizedOptions | Where-Object { [string]::IsNullOrWhiteSpace($_) }).Count -gt 0) {
        throw 'Options cannot contain null, empty, or whitespace-only values.'
    }

    $canReadArrowKeys = $true
    try {
        $null = [Console]::KeyAvailable
    }
    catch {
        $canReadArrowKeys = $false
    }

    $mode = if ($normalizedOptions.Count -le $ArrowThreshold -and $canReadArrowKeys) { 'Arrow' } else { 'Numeric' }

    $renderHeader = {
        param(
            [string]$MenuTitle,
            [string]$MenuPrompt,
            [string]$CurrentMode
        )

        if (Get-Command -Name 'Write-Separator' -ErrorAction SilentlyContinue) {
            try {
                Write-Separator $MenuTitle -Style Thick
            }
            catch {
                Write-Host "=== $MenuTitle ==="
            }
        }
        else {
            Write-Host "=== $MenuTitle ==="
        }

        Write-Host $MenuPrompt
        if ($CurrentMode -eq 'Arrow') {
            Write-Host 'Use Up/Down arrows and Enter.' -ForegroundColor DarkGray
        }

        Write-Host ''
    }

    if ($mode -eq 'Arrow') {
        $selectedIndex = 0

        while ($true) {
            Clear-Host
            & $renderHeader -MenuTitle $Title -MenuPrompt $Prompt -CurrentMode $mode

            for ($optionIndex = 0; $optionIndex -lt $normalizedOptions.Count; $optionIndex++) {
                if ($optionIndex -eq $selectedIndex) {
                    Write-Host "> $($normalizedOptions[$optionIndex])" -ForegroundColor Cyan
                }
                else {
                    Write-Host " $($normalizedOptions[$optionIndex])"
                }
            }

            $keyInfo = [Console]::ReadKey($true)
            switch ($keyInfo.Key) {
                'UpArrow' {
                    if ($selectedIndex -le 0) {
                        $selectedIndex = $normalizedOptions.Count - 1
                    }
                    else {
                        $selectedIndex--
                    }
                }
                'DownArrow' {
                    if ($selectedIndex -ge ($normalizedOptions.Count - 1)) {
                        $selectedIndex = 0
                    }
                    else {
                        $selectedIndex++
                    }
                }
                'Enter' {
                    break
                }
            }
        }
    }
    else {
        $menuNumber = 0

        Clear-Host
        & $renderHeader -MenuTitle $Title -MenuPrompt $Prompt -CurrentMode $mode

        for ($optionIndex = 0; $optionIndex -lt $normalizedOptions.Count; $optionIndex++) {
            Write-Host ('{0}. {1}' -f ($optionIndex + 1), $normalizedOptions[$optionIndex])
        }

        do {
            $rawInput = Read-Host "Choose 1-$($normalizedOptions.Count)"
            $isNumber = [int]::TryParse($rawInput, [ref]$menuNumber)
            $isInRange = $isNumber -and $menuNumber -ge 1 -and $menuNumber -le $normalizedOptions.Count

            if (-not $isInRange) {
                Write-Host "Invalid selection. Enter a number from 1 to $($normalizedOptions.Count)." -ForegroundColor Yellow
            }
        }
        until ($isInRange)

        $selectedIndex = $menuNumber - 1
    }

    $selectedOption = $normalizedOptions[$selectedIndex]

    if ($ReturnObject) {
        [pscustomobject]@{
            SelectedIndex  = $selectedIndex
            SelectedOption = $selectedOption
            Mode           = $mode
        }
    }
    else {
        $selectedOption
    }
}