Private/NC-Hlp.ModuleUtils.ps1

#Requires -Version 5.0
using namespace System.Management.Automation

# Nebula.Core: (Private) Module's Utilities =========================================================================================================

function Format-OutputString {
    <#
    .SYNOPSIS
        Truncates a string to a maximum length.
    .DESCRIPTION
        Returns the original value when shorter than the specified length; otherwise appends ellipsis.
    .PARAMETER Value
        String to trim.
    .PARAMETER MaxLength
        Maximum allowed length (defaults to module configuration).
    #>

    [CmdletBinding()]
    param(
        [string]$Value,
        [ValidateRange(3, 260)]
        [int]$MaxLength = $NCVars.MaxFieldLength
    )

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

    if ($Value.Length -le $MaxLength) {
        return $Value
    }

    $length = [Math]::Max(3, $MaxLength)
    return $Value.Substring(0, $length - 3) + '...'
}

function Invoke-NCRetry {
    <#
    .SYNOPSIS
        Executes a scriptblock with retry logic.
    .DESCRIPTION
        Runs the provided block up to MaxAttempts, invoking OnError between retries.
        Throws the last error once all attempts are exhausted.
    .PARAMETER Action
        Script block to execute.
    .PARAMETER MaxAttempts
        Maximum number of attempts before throwing (default 3).
    .PARAMETER DelaySeconds
        Pause between attempts (default 5 seconds).
    .PARAMETER OperationDescription
        Friendly description used in log messages.
    .PARAMETER OnError
        Optional callback invoked after each failure.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [scriptblock]$Action,
        [ValidateRange(1, [int]::MaxValue)]
        [int]$MaxAttempts = 3,
        [ValidateRange(0, [int]::MaxValue)]
        [int]$DelaySeconds = 5,
        [string]$OperationDescription = 'operation',
        [scriptblock]$OnError
    )

    $attempt = 0
    while ($attempt -lt $MaxAttempts) {
        $attempt++
        try {
            return & $Action
        }
        catch {
            if ($OnError) {
                & $OnError -ArgumentList $attempt, $MaxAttempts, $_
            }
            else {
                Write-NCMessage "Operation '$OperationDescription' failed (attempt $attempt of $MaxAttempts). $($_.Exception.Message)" -Level ERROR
            }

            if ($attempt -ge $MaxAttempts) {
                throw
            }

            if ($DelaySeconds -gt 0) {
                Start-Sleep -Seconds $DelaySeconds
            }
        }
    }
}

function New-File {
    <#
    .SYNOPSIS
        Generates a non-colliding file path.
    .DESCRIPTION
        Given a desired path, appends _N before the extension until an unused name is found.
    .PARAMETER Path
        Desired output file path.
    #>

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

    $baseName = [System.IO.Path]::GetFileNameWithoutExtension($Path)
    $extension = [System.IO.Path]::GetExtension($Path)
    $directory = [System.IO.Path]::GetDirectoryName($Path)
    if (-not $directory) {
        $directory = (Get-Location).ProviderPath
    }

    $candidate = Join-Path -Path $directory -ChildPath ($baseName + $extension)
    $count = 1
    while (Test-Path -LiteralPath $candidate) {
        $fileName = "{0}_{1}{2}" -f $baseName, $count, $extension
        $candidate = Join-Path -Path $directory -ChildPath $fileName
        $count++
    }

    return $candidate
}

function Restore-ProgressAndInfoPreferences {
    <#
    .SYNOPSIS
        Restores Information/Progress preference variables.
    .DESCRIPTION
        Reverts preference variables previously captured by Set-ProgressAndInfoPreferences.
        No-ops if nothing was captured.
    #>

    [CmdletBinding()]
    param()

    if (-not $script:PreferencesCaptured) {
        return
    }

    if ($null -ne $script:PreviousInformationPreference) {
        Set-Variable -Name InformationPreference -Value $script:PreviousInformationPreference -Scope Global
    }

    if ($null -ne $script:PreviousProgressPreference) {
        Set-Variable -Name ProgressPreference -Value $script:PreviousProgressPreference -Scope Global
    }

    $script:PreferencesCaptured = $false
    $script:PreviousInformationPreference = $null
    $script:PreviousProgressPreference = $null
}

function Set-ProgressAndInfoPreferences {
    <#
    .SYNOPSIS
        Forces Information/Progress preference variables to Continue.
    .DESCRIPTION
        Saves current preference values (once per session) and sets global
        InformationPreference and ProgressPreference to Continue for verbose output.
    #>

    [CmdletBinding()]
    param()

    if (-not $script:PreferencesCaptured) {
        $script:PreviousInformationPreference = $InformationPreference
        $script:PreviousProgressPreference = $ProgressPreference
        $script:PreferencesCaptured = $true
    }

    Set-Variable -Name InformationPreference -Value Continue -Scope Global
    Set-Variable -Name ProgressPreference -Value Continue -Scope Global
}

function Show-Table {
    <#
    .SYNOPSIS
        Outputs a table of rows.
    .DESCRIPTION
        Outputs a table of rows with a title.
    .PARAMETER Rows
        Table rows.
    .PARAMETER AsTable
        Output as a table.
    #>

    [CmdletBinding()]
    param(
        [array]$Rows,
        [switch]$AsTable
    )

    if (-not $Rows -or $Rows.Count -eq 0) {
        Write-NCMessage "(none)" -Level INFO
        return
    }

    if ($AsTable) {
        $Rows | Format-Table -AutoSize
    }
    else {
        $Rows | Format-List
    }
}

function Test-Folder {
    <#
    .SYNOPSIS
        Normalizes and validates a folder path.
    .DESCRIPTION
        Returns the current directory when input is blank, trims trailing separators,
        and throws if the path is invalid.
    .PARAMETER Path
        Folder path to validate (optional).
    #>

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

    if ([string]::IsNullOrWhiteSpace($Path)) {
        return (Get-Location).ProviderPath
    }

    $normalized = $Path.TrimEnd('\')
    try {
        return [System.IO.Path]::GetFullPath($normalized)
    }
    catch {
        throw "Invalid folder path '$Path'. $($_.Exception.Message)"
    }
}