src/Write-ScoutProgress.ps1

#Requires -Version 7.0
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

<#
.SYNOPSIS
    Uniform phase-level progress reporting for the Scout collect/assess/report
    pipeline (AB#405).

.DESCRIPTION
    A single call site every long-running phase (a collect query, an ingestor, an
    assessment, a report renderer) calls into instead of hand-rolling its own
    Write-Progress / Write-Host calls.

    Renders through PwshSpectreConsole ONLY when that module is ALREADY installed
    and discoverable (`Get-Module -ListAvailable`) — a genuinely optional, soft
    dependency. Scout never installs PwshSpectreConsole, never requires it, and a
    run never fails because it is missing: this file, and every caller of it, is
    fully functional with nothing beyond core PowerShell 7 + the Az modules Scout
    already requires.

    Falls back to the native `Write-Progress` cmdlet, which is automatically
    silenced whenever the caller has set `$ProgressPreference = 'SilentlyContinue'`
    (exactly what Invoke-ScoutPipeline does for unattended/CI runs) — no extra
    detection logic needed for that case. When progress is silenced this way (a
    headless/CI host), a single-line, log-friendly status line is written instead
    via Write-Information, so scrollback still shows phase-by-phase progress
    without a live bar that would otherwise just spam a log file with carriage
    returns. That line is colored with raw ANSI escape sequences when the host
    reports virtual-terminal support and the NO_COLOR convention is not set;
    plain text otherwise.

    Every rendering path is wrapped in its own try/catch so a failure in ANY of
    them (an unexpected PwshSpectreConsole cmdlet signature, a host that doesn't
    support Write-Progress the way expected, etc.) degrades to a plain
    Write-Verbose line rather than ever being able to break the collect/assess/
    report run that is asking for progress UX.

.PARAMETER Activity
    Top-level activity name (e.g. 'Scout Collect', 'Scout Assessment', 'Scout Report').

.PARAMETER Status
    Current phase/status text (e.g. 'Collecting: Networking (4/12)').

.PARAMETER PercentComplete
    0-100. Omit (or pass -1, the default) for an indeterminate phase.

.PARAMETER Id
    Progress record id — matches Write-Progress -Id. Lets independent phases
    (e.g. collect vs. report) run their own progress record without colliding.

.PARAMETER ParentId
    Parent progress record id, for a nested progress record (Write-Progress -ParentId).

.PARAMETER Completed
    Marks the activity's progress record complete/removed (Write-Progress -Completed).

.EXAMPLE
    Write-ScoutProgress -Activity 'Scout Collect' -Status 'Querying: virtualNetworks' -PercentComplete 40 -Id 1

.EXAMPLE
    Write-ScoutProgress -Activity 'Scout Collect' -Id 1 -Completed

.NOTES
    Tracks ADO AB#405. Soft dependency only — PwshSpectreConsole is optional and
    Scout does not install it; every caller guards the call with
    `Get-Command Write-ScoutProgress -ErrorAction SilentlyContinue` so a session
    that never dot-sourced/imported this file behaves exactly as it did before
    this file existed.
#>

function Write-ScoutProgress {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Activity,
        [string] $Status = 'Working...',
        [int]    $PercentComplete = -1,
        [int]    $Id = 1,
        [int]    $ParentId = -1,
        [switch] $Completed
    )

    # ---- optional Spectre rendering (soft dependency -- Scout never installs it) ----
    # The availability probe is cached per-session (Get-Module -ListAvailable is not
    # free) rather than re-run on every single progress call.
    if (-not (Test-Path Variable:script:ScoutSpectreChecked)) {
        $script:ScoutSpectreChecked = $true
        try { $script:ScoutSpectreAvailable = [bool](Get-Module -ListAvailable -Name PwshSpectreConsole -ErrorAction SilentlyContinue) }
        catch { $script:ScoutSpectreAvailable = $false }
    }

    $useSpectre = $script:ScoutSpectreAvailable -and ($ProgressPreference -ne 'SilentlyContinue') -and (-not $Completed)
    if ($useSpectre) {
        try {
            Import-Module PwshSpectreConsole -ErrorAction Stop
            $pctText = if ($PercentComplete -ge 0) { "[$PercentComplete%] " } else { '' }
            Write-SpectreHost "[bold cyan]$Activity[/] $pctText[grey]$Status[/]"
            return
        }
        catch {
            # Any Spectre failure (an older/newer cmdlet signature, a non-VT host,
            # etc.) must never break progress reporting -- fall through to the
            # native fallback below instead of propagating.
            Write-Verbose "Write-ScoutProgress: PwshSpectreConsole rendering failed, falling back to Write-Progress: $($_.Exception.Message)"
        }
    }

    # ---- native Write-Progress (auto-silenced by $ProgressPreference = 'SilentlyContinue') ----
    try {
        $progressParams = @{ Activity = $Activity; Status = $Status; Id = $Id }
        if ($ParentId -ge 0) { $progressParams.ParentId = $ParentId }
        if ($Completed) { $progressParams.Completed = $true }
        elseif ($PercentComplete -ge 0) { $progressParams.PercentComplete = [Math]::Min(100, $PercentComplete) }
        Write-Progress @progressParams
    }
    catch {
        Write-Verbose "Write-ScoutProgress: Write-Progress failed, continuing without a progress bar: $($_.Exception.Message)"
    }

    # ---- log-friendly single-line fallback for headless/CI hosts ----
    # Only emitted when the interactive bar above is suppressed
    # ($ProgressPreference = 'SilentlyContinue', e.g. Invoke-ScoutPipeline's
    # non-interactive mode) -- an interactive console gets the live bar only, not
    # both a bar AND a scrolling duplicate line per call.
    if (-not $Completed -and $ProgressPreference -eq 'SilentlyContinue') {
        try {
            $pctText = if ($PercentComplete -ge 0) { "$PercentComplete%" } else { '...' }
            $line = "[$Activity] $pctText $Status"
            $supportsAnsi = $false
            try { $supportsAnsi = $Host.UI.SupportsVirtualTerminal -and -not $env:NO_COLOR } catch { $supportsAnsi = $false }
            if ($supportsAnsi) {
                $esc = [char]27
                Write-Information "$esc[36m$line$esc[0m" -InformationAction Continue
            }
            else {
                Write-Information $line -InformationAction Continue
            }
        }
        catch {
            Write-Verbose "Write-ScoutProgress: log-line fallback failed: $($_.Exception.Message)"
        }
    }
}