modules/shared/Errors.ps1

# Errors.ps1 — rich, sanitized error helpers for orchestrator + wrappers.
#
# Companion to New-InstallerError / Write-InstallerError in Installer.ps1.
# Whereas InstallerError covers prerequisite-installation failures, FindingError
# covers the broader "validation / runtime / external-call" surface used by the
# orchestrator and shared modules. Both shapes guarantee:
# - sanitized Details (Remove-Credentials applied to free-text fields)
# - a Category enum so consumers can branch on failure class
# - an optional Remediation string so users see a next action, never a bare
# "Failed to X."
#
# Use Format-FindingErrorMessage when you need to `throw` a string-style
# exception (the most common pattern in PowerShell). Use Write-FindingError
# when emitting a warning/log line is enough.

if (-not (Get-Command Remove-Credentials -ErrorAction SilentlyContinue)) {
    function Remove-Credentials { param ([string]$Text) return $Text }
}

$script:FindingErrorCategories = @(
    'InvalidParameter',
    'MissingDependency',
    'AuthenticationFailed',
    'AuthorizationFailed',
    'NotFound',
    'TransientFailure',
    'TimeoutExceeded',
    'ConfigurationError',
    'IOFailure',
    'UnexpectedFailure'
)

function New-FindingError {
    <#
    .SYNOPSIS
        Build a sanitized, structured error describing an orchestrator or
        wrapper failure.
    .DESCRIPTION
        Returns a PSCustomObject with Source, Category, Reason, Remediation,
        and Details. All free-text fields pass through Remove-Credentials so
        the object is safe to log or serialize.
    .PARAMETER Source
        The component raising the error (e.g. 'orchestrator',
        'wrapper:azqr', 'shared:RemoteClone').
    .PARAMETER Category
        One of the FindingErrorCategories enum values.
    .PARAMETER Reason
        Short human-readable explanation of what failed.
    .PARAMETER Remediation
        Concrete next action the user should take. Strongly encouraged.
    .PARAMETER Details
        Free-text additional context (stderr snippets, exception messages).
        Will be sanitized before being attached.
    #>

    param (
        [Parameter(Mandatory)][string] $Source,
        [Parameter(Mandatory)][string] $Category,
        [Parameter(Mandatory)][string] $Reason,
        [string] $Remediation,
        [string] $Details
    )
    if ($Category -notin $script:FindingErrorCategories) {
        $valid = $script:FindingErrorCategories -join ', '
        throw "New-FindingError: invalid Category '$Category'. Valid values: $valid"
    }
    return [PSCustomObject]@{
        Source       = $Source
        Category     = $Category
        Reason       = Remove-Credentials ([string]$Reason)
        Remediation  = Remove-Credentials ([string]$Remediation)
        Details      = Remove-Credentials ([string]$Details)
        TimestampUtc = (Get-Date).ToUniversalTime().ToString('o')
    }
}

function Format-FindingErrorMessage {
    <#
    .SYNOPSIS
        Render a FindingError object as a single-line message suitable for
        `throw` or Write-Warning.
    .EXAMPLE
        throw (Format-FindingErrorMessage (New-FindingError -Source 'orchestrator' `
            -Category 'InvalidParameter' -Reason '-Foo and -Bar are mutually exclusive.' `
            -Remediation 'Pass only one of -Foo or -Bar.'))
    #>

    param ([Parameter(Mandatory)] $FindingError)
    $line = "[{0}] {1}: {2}" -f $FindingError.Source, $FindingError.Category, $FindingError.Reason
    if ($FindingError.Remediation) { $line += " Action: $($FindingError.Remediation)" }
    return $line
}

function Write-FindingError {
    <#
    .SYNOPSIS
        Emit a FindingError as a Write-Warning (with Details routed through
        Write-Verbose so they don't pollute warning output).
    #>

    param ([Parameter(Mandatory)] $FindingError)
    Write-Warning (Format-FindingErrorMessage $FindingError)
    if ($FindingError.Details) { Write-Verbose $FindingError.Details }
}