Deck.psm1

function ConvertFrom-DeckMarkdown {
    <#
    .SYNOPSIS
        Parses markdown file for slide presentation data.

    .DESCRIPTION
        Extracts YAML frontmatter settings and parses the markdown content into individual slides.
        Returns a structured object containing global settings and slide data.

    .PARAMETER Path
        Path to the markdown file to parse.

    .EXAMPLE
        ConvertFrom-DeckMarkdown -Path ".\presentation.md"
        Parses the markdown file and returns slide data.

    .OUTPUTS
        PSCustomObject with Settings and Slides properties.

    .NOTES
        Handles YAML frontmatter extraction and slide splitting by horizontal rules.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path $_ -PathType Leaf })]
        [string]$Path
    )

    begin {
        Write-Verbose "Starting markdown parsing for: $Path"
        
        # Default settings
        $defaultSettings = @{
            background      = 'black'
            foreground      = 'white'
            border          = 'magenta'
            header          = $null
            footer          = $null
            pagination      = $false
            paginationStyle = 'minimal'
            borderStyle     = 'rounded'
            titleFont       = 'default'
            sectionFont     = 'default'
            headerFont      = 'default'
        }
    }

    process {
        try {
            # Read the entire file
            $content = Get-Content -Path $Path -Raw
            
            # Extract YAML frontmatter
            $settings = $defaultSettings.Clone()
            $markdownContent = $content
            
            if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---\s*\r?\n(.*)$') {
                $yamlContent = $Matches[1]
                $markdownContent = $Matches[2]
                
                Write-Verbose "Found YAML frontmatter, parsing settings"
                
                # Parse YAML (simple key: value format)
                foreach ($line in ($yamlContent -split '\r?\n')) {
                    if ($line -match '^\s*([^:]+):\s*(.+?)\s*$') {
                        $key = $Matches[1].Trim()
                        $value = $Matches[2].Trim()
                        
                        # Remove quotes if present
                        $value = $value -replace '^["'']|["'']$', ''
                        
                        # Convert boolean strings
                        if ($value -eq 'true') {
                            $value = $true
                        }
                        elseif ($value -eq 'false') {
                            $value = $false
                        }
                        
                        # Store in settings
                        if ($settings.ContainsKey($key)) {
                            $settings[$key] = $value
                            Write-Verbose " Setting: $key = $value"
                        }
                        else {
                            Write-Warning "Unknown setting in frontmatter: $key"
                        }
                    }
                }
            }
            else {
                Write-Verbose "No YAML frontmatter found, using defaults"
            }
            
            # Split markdown into slides by horizontal rules (---, ***, ___)
            Write-Verbose "Splitting markdown into slides"
            $slidePattern = '(?m)^(?:---|___|\*\*\*)[ \t]*\r?$'
            $slideContents = $markdownContent -split $slidePattern
            
            # Check if any delimiters were found
            $noDelimiters = ($slideContents.Count -eq 1)
            if ($noDelimiters) {
                Write-Warning "No slide delimiters found. Treating entire content as single slide."
            }
            
            # Filter out empty slides and trim whitespace
            $slides = @()
            $slideNumber = 1
            
            foreach ($slideContent in $slideContents) {
                $trimmed = $slideContent.Trim()
                
                if ([string]::IsNullOrWhiteSpace($trimmed)) {
                    Write-Verbose " Skipping empty slide section"
                    continue
                }
                
                # Check for intentionally blank slides
                if ($trimmed -match '<!--\s*intentionally\s+blank\s*-->') {
                    Write-Verbose " Slide $slideNumber : Intentionally blank"
                    $slides += [PSCustomObject]@{
                        Number          = $slideNumber
                        Content         = $trimmed
                        IsBlank         = $true
                    }
                    $slideNumber++
                    continue
                }
                
                Write-Verbose " Slide $slideNumber : $(($trimmed -split '\r?\n')[0].Substring(0, [Math]::Min(50, ($trimmed -split '\r?\n')[0].Length)))..."
                
                $slides += [PSCustomObject]@{
                    Number          = $slideNumber
                    Content         = $trimmed
                    IsBlank         = $false
                }
                $slideNumber++
            }
            
            Write-Verbose "Found $($slides.Count) slides"
            
            # Return parsed data
            [PSCustomObject]@{
                Settings   = $settings
                Slides     = $slides
                SourcePath = $Path
            }
        }
        catch {
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $_.Exception,
                'MarkdownParsingFailed',
                [System.Management.Automation.ErrorCategory]::ParserError,
                $Path
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }
    }

    end {
        Write-Verbose 'Markdown parsing complete'
    }
}

function Get-SlideNavigation {
    <#
    .SYNOPSIS
        Processes keyboard input and returns navigation action.

    .DESCRIPTION
        Reads a ConsoleKeyInfo object and determines the appropriate navigation
        action (Next, Previous, Exit, or None).

    .PARAMETER KeyInfo
        The KeyInfo object from $Host.UI.RawUI.ReadKey().

    .OUTPUTS
        [string] One of: 'Next', 'Previous', 'Exit', 'None'

    .EXAMPLE
        $key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
        $action = Get-SlideNavigation -KeyInfo $key
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [System.Management.Automation.Host.KeyInfo]
        $KeyInfo
    )

    # Forward navigation
    if ($KeyInfo.VirtualKeyCode -in @(39, 40, 32, 13, 34) -or $KeyInfo.Character -eq 'n') {
        # 39=Right, 40=Down, 32=Space, 13=Enter, 34=PageDown
        return 'Next'
    }

    # Backward navigation
    if ($KeyInfo.VirtualKeyCode -in @(37, 38, 8, 33) -or $KeyInfo.Character -eq 'p') {
        # 37=Left, 38=Up, 8=Backspace, 33=PageUp
        return 'Previous'
    }

    # Exit
    if ($KeyInfo.VirtualKeyCode -eq 27 -or 
        ($KeyInfo.Character -eq 'c' -and $KeyInfo.ControlKeyState -match 'LeftCtrlPressed|RightCtrlPressed')) {
        # 27=Escape, c with Ctrl
        return 'Exit'
    }

    # Unhandled
    return 'None'
}

function Import-DeckDependency {
    <#
    .SYNOPSIS
        Imports PwshSpectreConsole module with automatic installation fallback.

    .DESCRIPTION
        Attempts to import the PwshSpectreConsole module. If not found, tries to install it
        using Install-PSResource. On failure, displays helpful ASCII art and installation
        instructions before terminating.

    .EXAMPLE
        Import-DeckDependency
        Attempts to load PwshSpectreConsole, installing if necessary.

    .OUTPUTS
        None. Terminates script on failure.

    .NOTES
        This function will exit the calling script if PwshSpectreConsole cannot be loaded.
    #>

    [CmdletBinding()]
    param()

    begin {
        Write-Verbose 'Checking for PwshSpectreConsole module'
    }

    process {
        try {
            # Try to import the module
            Import-Module PwshSpectreConsole -ErrorAction Stop
            Write-Verbose 'PwshSpectreConsole loaded successfully'
        }
        catch {
            Write-Warning 'PwshSpectreConsole module not found. Attempting to install...'
            
            try {
                # Try to install using Install-PSResource (PSResourceGet)
                Install-PSResource -Name PwshSpectreConsole -Repository PSGallery -TrustRepository -ErrorAction Stop
                Import-Module PwshSpectreConsole -ErrorAction Stop
                Write-Verbose 'PwshSpectreConsole installed and loaded successfully'
            }
            catch {
                # Installation failed - show sad face and exit
                Show-SadFace
                
                $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                    [System.Exception]::new('Failed to load PwshSpectreConsole module'),
                    'DependencyLoadFailure',
                    [System.Management.Automation.ErrorCategory]::NotInstalled,
                    'PwshSpectreConsole'
                )
                $PSCmdlet.ThrowTerminatingError($errorRecord)
            }
        }
    }

    end {
        Write-Verbose 'Dependency check complete'
    }
}

function Show-ContentSlide {
    <#
    .SYNOPSIS
        Renders a content slide with optional header and body text.

    .DESCRIPTION
        Displays a content slide that may contain a ### heading rendered as smaller
        figlet text followed by content. If no ### heading is present, only content is shown.

    .PARAMETER Slide
        The slide object containing the content to render.

    .PARAMETER Settings
        The presentation settings hashtable containing colors, fonts, and styling options.

    .PARAMETER VisibleBullets
        The number of progressive bullets (*) to show. If not specified, all bullets are shown.

    .EXAMPLE
        Show-ContentSlide -Slide $slideObject -Settings $settings

    .EXAMPLE
        Show-ContentSlide -Slide $slideObject -Settings $settings -VisibleBullets 2

    .NOTES
        Content slides are the most common slide type for displaying information.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$Slide,

        [Parameter(Mandatory = $true)]
        [hashtable]$Settings,

        [Parameter(Mandatory = $false)]
        [int]$VisibleBullets = [int]::MaxValue
    )

    begin {
        Write-Verbose "Rendering content slide #$($Slide.Number)"
    }

    process {
        try {
            # Clear the screen
            Clear-Host

            # Get terminal dimensions
            # Account for rendering behavior to prevent scrolling
            $windowWidth = $Host.UI.RawUI.WindowSize.Width
            $windowHeight = $Host.UI.RawUI.WindowSize.Height - 1

            # Determine if slide has a header and extract content
            $hasHeader = $false
            $headerText = $null
            $bodyContent = $null

            if ($Slide.Content -match '^###\s+(.+?)(?:\r?\n|$)') {
                $hasHeader = $true
                $headerText = $Matches[1].Trim()
                Write-Verbose " Header: $headerText"
                
                # Extract content after header
                $bodyContent = $Slide.Content -replace '^###\s+.+?(\r?\n|$)', ''
                $bodyContent = $bodyContent.Trim()
            }
            else {
                # No header, use all content
                $bodyContent = $Slide.Content.Trim()
            }

            # Parse and filter bullets based on visibility
            if ($bodyContent) {
                $lines = $bodyContent -split "`r?`n"
                $filteredLines = [System.Collections.Generic.List[string]]::new()
                $progressiveBulletCount = 0
                $visibleProgressiveBullets = 0
                
                foreach ($line in $lines) {
                    # Check if line is a progressive bullet (*)
                    if ($line -match '^\s*\*\s+') {
                        $progressiveBulletCount++
                        if ($visibleProgressiveBullets -lt $VisibleBullets) {
                            $filteredLines.Add($line)
                            $visibleProgressiveBullets++
                        }
                        else {
                            # Add blank line placeholder for hidden progressive bullets
                            $filteredLines.Add("")
                        }
                    }
                    # All other lines (including - bullets) are always shown
                    else {
                        $filteredLines.Add($line)
                    }
                }
                
                # Store total progressive bullet count on the slide object for navigation
                if (-not $Slide.PSObject.Properties['TotalProgressiveBullets']) {
                    Add-Member -InputObject $Slide -NotePropertyName 'TotalProgressiveBullets' -NotePropertyValue $progressiveBulletCount -Force
                }
                
                # Store the full content height for consistent vertical alignment
                # This ensures content doesn't jump as bullets are revealed
                if (-not $Slide.PSObject.Properties['FullContentHeight']) {
                    $fullContentText = $lines -join "`n"
                    $fullText = [Spectre.Console.Text]::new($fullContentText)
                    $fullSize = Get-SpectreRenderableSize -Renderable $fullText -ContainerWidth $windowWidth
                    Add-Member -InputObject $Slide -NotePropertyName 'FullContentHeight' -NotePropertyValue $fullSize.Height -Force
                }
                
                # Store the max line length of all content (including hidden bullets) for consistent horizontal alignment
                if (-not $Slide.PSObject.Properties['MaxLineLength']) {
                    $maxLength = ($lines | Measure-Object -Property Length -Maximum).Maximum
                    Add-Member -InputObject $Slide -NotePropertyName 'MaxLineLength' -NotePropertyValue $maxLength -Force
                }
                
                $bodyContent = $filteredLines -join "`n"
            }
            
            # Get border color and style
            $borderColor = $null
            if ($Settings.border) {
                $borderColorName = (Get-Culture).TextInfo.ToTitleCase($Settings.border.ToLower())
                Write-Verbose " Border color: $borderColorName"
                try {
                    $borderColor = [Spectre.Console.Color]::$borderColorName
                }
                catch {
                    Write-Warning "Invalid border color '$($Settings.border)', using default"
                }
            }
            
            $borderStyle = 'Rounded'
            if ($Settings.borderStyle) {
                $borderStyle = (Get-Culture).TextInfo.ToTitleCase($Settings.borderStyle.ToLower())
                Write-Verbose " Border style: $borderStyle"
            }

            # Build the renderable content
            $renderables = [System.Collections.Generic.List[object]]::new()
            
            # Add header figlet if present
            if ($hasHeader) {
                # Convert color name to Spectre.Console.Color
                $figletColor = $null
                if ($Settings.foreground) {
                    $colorName = (Get-Culture).TextInfo.ToTitleCase($Settings.foreground.ToLower())
                    Write-Verbose " Header color: $colorName"
                    try {
                        $figletColor = [Spectre.Console.Color]::$colorName
                    }
                    catch {
                        Write-Warning "Invalid color '$($Settings.foreground)', using default"
                    }
                }

                # Create figlet for header
                $miniFontPath = Join-Path $PSScriptRoot '../Fonts/mini.flf'
                if (Test-Path $miniFontPath) {
                    $font = [Spectre.Console.FigletFont]::Load($miniFontPath)
                    $figlet = [Spectre.Console.FigletText]::new($font, $headerText)
                }
                else {
                    $figlet = [Spectre.Console.FigletText]::new($headerText)
                }
                $figlet.Justification = [Spectre.Console.Justify]::Center
                if ($figletColor) {
                    $figlet.Color = $figletColor
                }
                $renderables.Add($figlet)
            }

            # Add body content as text
            if ($bodyContent) {
                # Manually pad each line to center the block
                $lines = $bodyContent -split "`r?`n"
                
                # Use stored max line length for consistent alignment during bullet reveal
                if ($Slide.PSObject.Properties['MaxLineLength']) {
                    $maxLineLength = $Slide.MaxLineLength
                }
                else {
                    $maxLineLength = ($lines | Measure-Object -Property Length -Maximum).Maximum
                }
                
                # Calculate padding to center the block within the panel
                $availableWidth = $windowWidth - 8  # Account for panel padding (4 left + 4 right)
                $leftPadding = [math]::Max(0, [math]::Floor(($availableWidth - $maxLineLength) / 2))
                
                # Rebuild content with padding
                $paddedLines = $lines | ForEach-Object {
                    (" " * $leftPadding) + $_
                }
                $paddedContent = $paddedLines -join "`n"
                
                # Create text with left justification (padding is already in the string)
                $text = [Spectre.Console.Text]::new($paddedContent)
                $text.Justification = [Spectre.Console.Justify]::Left
                $renderables.Add($text)
            }

            # Combine renderables into a Rows layout
            $rows = [Spectre.Console.Rows]::new([object[]]$renderables.ToArray())
            
            # Measure the actual height of the rendered content
            # (blank placeholder lines maintain consistent height for progressive bullets)
            $contentSize = Get-SpectreRenderableSize -Renderable $rows -ContainerWidth $windowWidth
            $actualContentHeight = $contentSize.Height
            
            # Calculate padding
            $borderHeight = 2
            $remainingSpace = $windowHeight - $actualContentHeight - $borderHeight
            $topPadding = [math]::Max(0, [math]::Ceiling($remainingSpace / 2.0))
            $bottomPadding = [math]::Max(0, $remainingSpace - $topPadding)
            
            Write-Verbose " Content height: $actualContentHeight, top padding: $topPadding, bottom padding: $bottomPadding"
            
            # Create panel with internal padding
            $panel = [Spectre.Console.Panel]::new($rows)
            $panel.Expand = $true
            $panel.Padding = [Spectre.Console.Padding]::new(4, $topPadding, 4, $bottomPadding)
            
            # Add border style
            if ($borderStyle) {
                $panel.Border = [Spectre.Console.BoxBorder]::$borderStyle
            }
            
            # Add border color
            if ($borderColor) {
                $panel.BorderStyle = [Spectre.Console.Style]::new($borderColor)
            }
            
            # Render panel
            Out-SpectreHost $panel
        }
        catch {
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $_.Exception,
                'ContentSlideRenderFailed',
                [System.Management.Automation.ErrorCategory]::InvalidOperation,
                $Slide
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }
    }

    end {
        Write-Verbose "Content slide rendered"
    }
}

function Show-SadFace {
    <#
    .SYNOPSIS
        Displays sad ASCII art with installation instructions.

    .DESCRIPTION
        Shows a sad face ASCII art along with helpful instructions for manually installing
        PwshSpectreConsole. Called when automatic dependency loading fails.

    .EXAMPLE
        Show-SadFace
        Displays the sad face and installation help.

    .OUTPUTS
        None. Writes directly to host.

    .NOTES
        This function cannot use PwshSpectreConsole since it's called when that module fails to load.
    #>

    [CmdletBinding()]
    param()

    process {
        Write-Host ""
        Write-Host " ___________" -ForegroundColor Red
        Write-Host " / \" -ForegroundColor Red
        Write-Host " / O O \" -ForegroundColor Red
        Write-Host " | |" -ForegroundColor Red
        Write-Host " | ___ |" -ForegroundColor Red
        Write-Host " | / \ |" -ForegroundColor Red
        Write-Host " \ \___/ /" -ForegroundColor Red
        Write-Host " \___________/" -ForegroundColor Red
        Write-Host ""
        Write-Host " OH NO! Something went wrong!" -ForegroundColor Yellow
        Write-Host ""
        Write-Host " The Deck module requires PwshSpectreConsole to run." -ForegroundColor White
        Write-Host " Unfortunately, we couldn't install it automatically." -ForegroundColor White
        Write-Host ""
        Write-Host " To fix this, please run:" -ForegroundColor Cyan
        Write-Host ""
        Write-Host " Install-PSResource -Name PwshSpectreConsole -Repository PSGallery" -ForegroundColor Green
        Write-Host ""
        Write-Host " Or if you're using PowerShellGet v2:" -ForegroundColor Cyan
        Write-Host ""
        Write-Host " Install-Module -Name PwshSpectreConsole -Repository PSGallery" -ForegroundColor Green
        Write-Host ""
        Write-Host " Then try running Deck again!" -ForegroundColor White
        Write-Host ""
    }
}

function Show-SectionSlide {
    <#
    .SYNOPSIS
        Renders a section slide with medium figlet text.

    .DESCRIPTION
        Displays a full-screen section slide containing a ## heading rendered as medium
        figlet text. Section slides are centered and use the configured sectionFont setting.

    .PARAMETER Slide
        The slide object containing the content to render.

    .PARAMETER Settings
        The presentation settings hashtable containing colors, fonts, and styling options.

    .EXAMPLE
        Show-SectionSlide -Slide $slideObject -Settings $settings

    .NOTES
        Section slides should contain only a single ## heading with no other content.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$Slide,

        [Parameter(Mandatory = $true)]
        [hashtable]$Settings
    )

    begin {
        Write-Verbose "Rendering section slide #$($Slide.Number)"
    }

    process {
        try {
            # Extract the ## heading text
            if ($Slide.Content -match '^##\s+(.+)$') {
                $sectionText = $Matches[1].Trim()
                Write-Verbose " Section: $sectionText"
            }
            else {
                throw "Section slide does not contain a valid ## heading"
            }

            # Clear the screen
            Clear-Host

            # Convert colors to Spectre.Console.Color
            $figletColor = $null
            if ($Settings.foreground) {
                $colorName = (Get-Culture).TextInfo.ToTitleCase($Settings.foreground.ToLower())
                Write-Verbose " Figlet color: $colorName"
                try {
                    $figletColor = [Spectre.Console.Color]::$colorName
                }
                catch {
                    Write-Warning "Invalid color '$($Settings.foreground)', using default"
                }
            }

            $borderColor = $null
            if ($Settings.border) {
                $borderColorName = (Get-Culture).TextInfo.ToTitleCase($Settings.border.ToLower())
                Write-Verbose " Border color: $borderColorName"
                try {
                    $borderColor = [Spectre.Console.Color]::$borderColorName
                }
                catch {
                    Write-Warning "Invalid border color '$($Settings.border)', using default"
                }
            }

            # Determine border style
            $borderStyle = 'Rounded'
            if ($Settings.borderStyle) {
                $borderStyle = (Get-Culture).TextInfo.ToTitleCase($Settings.borderStyle.ToLower())
                Write-Verbose " Border style: $borderStyle"
            }

            # Create figlet text object with small font if available
            $smallFontPath = Join-Path $PSScriptRoot '../Fonts/small.flf'
            if (Test-Path $smallFontPath) {
                $figlet = [Spectre.Console.FigletText]::new([Spectre.Console.FigletFont]::Load($smallFontPath), $sectionText)
            }
            else {
                $figlet = [Spectre.Console.FigletText]::new($sectionText)
            }
            $figlet.Justification = [Spectre.Console.Justify]::Center
            if ($figletColor) {
                $figlet.Color = $figletColor
            }

            # Create panel with internal padding calculated to fill terminal height
            # Account for rendering behavior to prevent scrolling
            $windowHeight = $Host.UI.RawUI.WindowSize.Height - 1
            $windowWidth = $Host.UI.RawUI.WindowSize.Width
            
            # Set panel to expand and measure what we need to fill
            $panel = [Spectre.Console.Panel]::new($figlet)
            $panel.Expand = $true
            
            # Add border style first
            if ($borderStyle) {
                $panel.Border = [Spectre.Console.BoxBorder]::$borderStyle
            }
            
            # Measure figlet with horizontal padding already applied
            # Horizontal padding is 4 on each side = 8 total
            $contentWidth = $windowWidth - 8
            $figletSize = Get-SpectreRenderableSize -Renderable $figlet -ContainerWidth $contentWidth
            $actualFigletHeight = $figletSize.Height
            
            # Calculate vertical padding needed
            # Total height = border (2) + top padding + content + bottom padding
            $borderHeight = 2
            $remainingSpace = $windowHeight - $actualFigletHeight - $borderHeight
            $topPadding = [math]::Max(0, [math]::Ceiling($remainingSpace / 2.0))
            $bottomPadding = [math]::Max(0, $remainingSpace - $topPadding)
            
            $panel.Padding = [Spectre.Console.Padding]::new(4, $topPadding, 4, $bottomPadding)
            
            # Border style already added above
            if ($borderStyle) {
                $panel.Border = [Spectre.Console.BoxBorder]::$borderStyle
            }
            
            # Add border color
            if ($borderColor) {
                $panel.BorderStyle = [Spectre.Console.Style]::new($borderColor)
            }
            
            # Render panel
            Out-SpectreHost $panel
        }
        catch {
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $_.Exception,
                'SectionSlideRenderFailed',
                [System.Management.Automation.ErrorCategory]::InvalidOperation,
                $Slide
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }
    }

    end {
        Write-Verbose "Section slide rendered"
    }
}

function Show-TitleSlide {
    <#
    .SYNOPSIS
        Renders a title slide with large figlet text.

    .DESCRIPTION
        Displays a full-screen title slide containing a # heading rendered as large
        figlet text. Title slides are centered and use the configured titleFont setting.

    .PARAMETER Slide
        The slide object containing the content to render.

    .PARAMETER Settings
        The presentation settings hashtable containing colors, fonts, and styling options.

    .EXAMPLE
        Show-TitleSlide -Slide $slideObject -Settings $settings

    .NOTES
        Title slides should contain only a single # heading with no other content.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$Slide,

        [Parameter(Mandatory = $true)]
        [hashtable]$Settings,

        [Parameter()]
        [switch]$IsFirstSlide
    )

    begin {
        Write-Verbose "Rendering title slide #$($Slide.Number)"
    }

    process {
        try {
            # Extract the # heading text
            if ($Slide.Content -match '^#\s+(.+)$') {
                $titleText = $Matches[1].Trim()
                Write-Verbose " Title: $titleText"
            }
            else {
                throw "Title slide does not contain a valid # heading"
            }

            # Clear the screen
            Clear-Host

            # Convert colors to Spectre.Console.Color
            $figletColor = $null
            if ($Settings.foreground) {
                $colorName = (Get-Culture).TextInfo.ToTitleCase($Settings.foreground.ToLower())
                Write-Verbose " Figlet color: $colorName"
                try {
                    $figletColor = [Spectre.Console.Color]::$colorName
                }
                catch {
                    Write-Warning "Invalid color '$($Settings.foreground)', using default"
                }
            }

            $borderColor = $null
            if ($Settings.border) {
                $borderColorName = (Get-Culture).TextInfo.ToTitleCase($Settings.border.ToLower())
                Write-Verbose " Border color: $borderColorName"
                try {
                    $borderColor = [Spectre.Console.Color]::$borderColorName
                }
                catch {
                    Write-Warning "Invalid border color '$($Settings.border)', using default"
                }
            }

            # Determine border style
            $borderStyle = 'Rounded'
            if ($Settings.borderStyle) {
                $borderStyle = (Get-Culture).TextInfo.ToTitleCase($Settings.borderStyle.ToLower())
                Write-Verbose " Border style: $borderStyle"
            }

            # Create figlet text object
            $figlet = [Spectre.Console.FigletText]::new($titleText)
            $figlet.Justification = [Spectre.Console.Justify]::Center
            if ($figletColor) {
                $figlet.Color = $figletColor
            }

            # Create panel with internal padding calculated to fill terminal height
            # Account for rendering behavior to prevent scrolling
            $windowHeight = $Host.UI.RawUI.WindowSize.Height - 1
            $windowWidth = $Host.UI.RawUI.WindowSize.Width
            
            # Set panel to expand and measure what we need to fill
            $panel = [Spectre.Console.Panel]::new($figlet)
            $panel.Expand = $true
            
            # Add border style first
            if ($borderStyle) {
                $panel.Border = [Spectre.Console.BoxBorder]::$borderStyle
            }
            
            # Measure figlet with horizontal padding already applied
            # Horizontal padding is 4 on each side = 8 total
            $contentWidth = $windowWidth - 8
            $figletSize = Get-SpectreRenderableSize -Renderable $figlet -ContainerWidth $contentWidth
            $actualFigletHeight = $figletSize.Height
            
            # Calculate vertical padding needed
            # Total height = border (2) + top padding + content + bottom padding
            $borderHeight = 2
            $remainingSpace = $windowHeight - $actualFigletHeight - $borderHeight
            $topPadding = [math]::Max(0, [math]::Ceiling($remainingSpace / 2.0))
            $bottomPadding = [math]::Max(0, $remainingSpace - $topPadding)
            
            $panel.Padding = [Spectre.Console.Padding]::new(4, $topPadding, 4, $bottomPadding)
            
            # Border style already added above
            if ($borderStyle) {
                $panel.Border = [Spectre.Console.BoxBorder]::$borderStyle
            }
            
            # Add border color
            if ($borderColor) {
                $panel.BorderStyle = [Spectre.Console.Style]::new($borderColor)
            }
            
            # Add help text title for first slide
            if ($IsFirstSlide) {
                $panel.Header = [Spectre.Console.PanelHeader]::new("[grey39]press ? for help[/]")
            }
            
            # Render panel
            Out-SpectreHost $panel
        }
        catch {
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $_.Exception,
                'TitleSlideRenderFailed',
                [System.Management.Automation.ErrorCategory]::InvalidOperation,
                $Slide
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }
    }

    end {
        Write-Verbose "Title slide rendered"
    }
}

function Show-Deck {
    <#
    .SYNOPSIS
        Displays a Markdown file as an interactive terminal presentation.

    .DESCRIPTION
        Converts a Markdown file into a live terminal-based presentation with rich
        formatting, colors, and ASCII art. Navigate through slides using arrow keys,
        space, or enter.

    .PARAMETER Path
        Path to the Markdown file containing the presentation.

    .PARAMETER Background
        Override the background color from the Markdown frontmatter.
        Accepts Spectre.Console.Color values (e.g., 'Black', 'DarkBlue', 'Grey15').

    .PARAMETER Foreground
        Override the foreground color from the Markdown frontmatter.
        Accepts Spectre.Console.Color values (e.g., 'White', 'Cyan1', 'Yellow').

    .PARAMETER Border
        Override the border color from the Markdown frontmatter.
        Accepts Spectre.Console.Color values (e.g., 'Blue', 'Magenta1', 'Green').

    .EXAMPLE
        Show-Deck -Path ./presentation.md

        Displays the presentation from the specified Markdown file.

    .EXAMPLE
        Show-Deck -Path ./presentation.md -Foreground Cyan1 -Background Black

        Displays the presentation with custom colors.

    .NOTES
        Requires PwshSpectreConsole module for terminal rendering.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateScript({ Test-Path $_ -PathType Leaf })]
        [string]$Path,

        [Parameter()]
        [string]$Background,

        [Parameter()]
        [string]$Foreground,

        [Parameter()]
        [string]$Border
    )

    begin {
        Write-Verbose "Starting presentation from: $Path"
        
        # Ensure PwshSpectreConsole is loaded
        Import-DeckDependency
    }

    process {
        try {
            # Parse the markdown file
            $presentation = ConvertFrom-DeckMarkdown -Path $Path
            Write-Verbose "Loaded $($presentation.Slides.Count) slides"

            # Apply parameter overrides to settings
            if ($PSBoundParameters.ContainsKey('Background')) {
                $presentation.Settings.background = $Background
            }
            if ($PSBoundParameters.ContainsKey('Foreground')) {
                $presentation.Settings.foreground = $Foreground
            }
            if ($PSBoundParameters.ContainsKey('Border')) {
                $presentation.Settings.border = $Border
            }

            # Hide cursor during presentation using ANSI escape codes
            Write-Host "`e[?25l" -NoNewline  # Hide cursor

            try {
                # Full navigation loop (Phase 6)
                $currentSlide = 0
                $totalSlides = $presentation.Slides.Count
                $shouldExit = $false
                
                # Track visible bullets per slide
                $visibleBullets = @{}

                while ($true) {
                    # Move cursor to top-left and redraw (no clear to reduce flicker)
                    Write-Host "`e[H" -NoNewline

                $slide = $presentation.Slides[$currentSlide]
                
                # Initialize visible bullets for this slide if not set
                if (-not $visibleBullets.ContainsKey($currentSlide)) {
                    $visibleBullets[$currentSlide] = 0
                }
                
                # Detect slide type based on content
                if ($slide.Content -match '^\s*#\s+.+$' -and $slide.Content -notmatch '\n[^#]') {
                    # Title slide: Only has # heading, no other content
                    Write-Verbose "Rendering title slide $($currentSlide + 1)/$totalSlides"
                    Show-TitleSlide -Slide $slide -Settings $presentation.Settings -IsFirstSlide:($currentSlide -eq 0)
                }
                elseif ($slide.Content -match '^\s*##\s+.+$' -and $slide.Content -notmatch '\n[^#]') {
                    # Section slide: Only has ## heading, no other content
                    Write-Verbose "Rendering section slide $($currentSlide + 1)/$totalSlides"
                    Show-SectionSlide -Slide $slide -Settings $presentation.Settings
                }
                else {
                    # Content slide: May have ### heading or just content
                    Write-Verbose "Rendering content slide $($currentSlide + 1)/$totalSlides with $($visibleBullets[$currentSlide]) bullets"
                    Show-ContentSlide -Slide $slide -Settings $presentation.Settings -VisibleBullets $visibleBullets[$currentSlide]
                }

                # Get user input
                $key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
                $action = Get-SlideNavigation -KeyInfo $key

                # Handle help key
                if ($key.Character -eq '?') {
                    Write-Host "`e[H" -NoNewline
                    
                    # Fill screen with blank lines to clear previous content
                    $windowHeight = $Host.UI.RawUI.WindowSize.Height
                    $windowWidth = $Host.UI.RawUI.WindowSize.Width
                    for ($i = 0; $i -lt $windowHeight; $i++) {
                        Write-Host (" " * $windowWidth)
                    }
                    
                    # Move cursor back to top and render help text
                    Write-Host "`e[H" -NoNewline
                    Write-Host "`n Navigation Controls`n" -ForegroundColor Cyan
                    Write-Host " Forward: " -ForegroundColor Gray -NoNewline
                    Write-Host "Right, Down, Space, Enter, n, Page Down" -ForegroundColor White
                    Write-Host " Backward: " -ForegroundColor Gray -NoNewline
                    Write-Host "Left, Up, Backspace, p, Page Up" -ForegroundColor White
                    Write-Host " Exit: " -ForegroundColor Gray -NoNewline
                    Write-Host "Esc, Ctrl+C" -ForegroundColor White
                    Write-Host "`n Press any key to return to presentation..." -ForegroundColor DarkGray
                    $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
                    continue
                }

                # Handle navigation
                switch ($action) {
                    'Next' {
                        # Check if current slide has hidden bullets
                        if ($slide.PSObject.Properties['TotalProgressiveBullets'] -and 
                            $visibleBullets[$currentSlide] -lt $slide.TotalProgressiveBullets) {
                            # Reveal next bullet
                            $visibleBullets[$currentSlide]++
                            Write-Verbose "Revealed bullet $($visibleBullets[$currentSlide])/$($slide.TotalProgressiveBullets)"
                        }
                        elseif ($currentSlide -lt $totalSlides - 1) {
                            # Move to next slide and reset bullets to 0
                            $currentSlide++
                            $visibleBullets[$currentSlide] = 0
                            Write-Verbose "Advanced to slide $($currentSlide + 1)"
                        }
                        else {
                            # On last slide, trying to go forward shows end screen
                            Write-Host "`e[H" -NoNewline
                            
                            # Center text vertically and horizontally
                            $windowHeight = $Host.UI.RawUI.WindowSize.Height
                            $windowWidth = $Host.UI.RawUI.WindowSize.Width
                            
                            $line1 = "End of Deck"
                            $line2 = "Press ESC to Exit"
                            
                            # Calculate vertical position (center)
                            $verticalPadding = [math]::Floor($windowHeight / 2) - 1
                            
                            # Fill screen with blank lines to clear previous content
                            for ($i = 0; $i -lt $windowHeight; $i++) {
                                Write-Host (" " * $windowWidth)
                            }
                            
                            # Move cursor back to top and render centered text
                            Write-Host "`e[H" -NoNewline
                            
                            # Print vertical padding
                            Write-Host ("`n" * $verticalPadding) -NoNewline
                            
                            # Print first line centered
                            $padding1 = [math]::Max(0, [math]::Floor(($windowWidth - $line1.Length) / 2))
                            Write-Host (" " * $padding1) -NoNewline
                            Write-Host $line1 -ForegroundColor White
                            
                            # Blank line between
                            Write-Host ""
                            
                            # Print second line centered
                            $padding2 = [math]::Max(0, [math]::Floor(($windowWidth - $line2.Length) / 2))
                            Write-Host (" " * $padding2) -NoNewline
                            Write-Host $line2 -ForegroundColor Gray
                            
                            # Wait for ESC or backward navigation
                            do {
                                $exitKey = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
                                $exitAction = Get-SlideNavigation -KeyInfo $exitKey
                                
                                if ($exitAction -eq 'Previous') {
                                    # Go back to last slide
                                    Write-Verbose "Returning to last slide from end screen"
                                    break
                                }
                            } while ($exitKey.VirtualKeyCode -ne 27)  # 27 = ESC
                            
                            # If ESC was pressed, exit; if backward nav, continue loop
                            if ($exitKey.VirtualKeyCode -eq 27) {
                                Write-Verbose "User exited from end screen"
                                $shouldExit = $true
                                break
                            }
                        }
                    }
                    'Previous' {
                        # Check if current slide has revealed bullets that can be hidden
                        if ($slide.PSObject.Properties['TotalProgressiveBullets'] -and 
                            $visibleBullets[$currentSlide] -gt 0) {
                            # Hide last bullet
                            $visibleBullets[$currentSlide]--
                            Write-Verbose "Hid bullet, now showing $($visibleBullets[$currentSlide])/$($slide.TotalProgressiveBullets)"
                        }
                        elseif ($currentSlide -gt 0) {
                            # Move to previous slide and show all bullets
                            $currentSlide--
                            $prevSlide = $presentation.Slides[$currentSlide]
                            if ($prevSlide.PSObject.Properties['TotalProgressiveBullets']) {
                                $visibleBullets[$currentSlide] = $prevSlide.TotalProgressiveBullets
                            }
                            else {
                                $visibleBullets[$currentSlide] = 0
                            }
                            Write-Verbose "Moved back to slide $($currentSlide + 1)"
                        }
                    }
                    'Exit' {
                        Write-Verbose "User requested exit"
                        $shouldExit = $true
                        break
                    }
                    'None' {
                        # Unhandled key, ignore
                        Write-Verbose "Unhandled key: $($key.Key)"
                    }
                }

                # Check if we should exit
                if ($shouldExit) {
                    break
                }
            }

            Write-Verbose "Presentation ended"
            
            # Show goodbye message
            Clear-Host
            $windowHeight = $Host.UI.RawUI.WindowSize.Height
            $windowWidth = $Host.UI.RawUI.WindowSize.Width
            $message = "Goodbye! <3"
            
            # Center vertically and horizontally
            $verticalPadding = [math]::Floor($windowHeight / 2)
            $horizontalPadding = [math]::Max(0, [math]::Floor(($windowWidth - $message.Length) / 2))
            
            Write-Host ("`n" * $verticalPadding) -NoNewline
            Write-Host (" " * $horizontalPadding) -NoNewline
            Write-Host $message -ForegroundColor Magenta
            
            Start-Sleep -Milliseconds 800
            Clear-Host
            }
            finally {
                # Show cursor again
                Write-Host "`e[?25h" -NoNewline  # Show cursor
            }
        }
        catch {
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                $_.Exception,
                'PresentationFailed',
                [System.Management.Automation.ErrorCategory]::InvalidOperation,
                $Path
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        }
    }

    end {
        Write-Verbose "Show-Deck complete"
    }
}



# Export functions and aliases as required
Export-ModuleMember -Function @('Show-Deck') -Alias @()