modules/Azure/Discovery/Private/InvokeCIEMDiscoveryPhase.ps1

function InvokeCIEMDiscoveryPhase {
    <#
    .SYNOPSIS
        Runs one named phase of Start-CIEMAzureDiscovery with uniform
        stopwatch + try/catch + logging semantics.

    .DESCRIPTION
        Every collection/persistence step of discovery follows the same shape:
        start a stopwatch, run an action, log elapsed time, and route any
        exceptions into a shared error accumulator so the run can complete as
        Partial only when the phase declares a non-fatal failure mode.

        This helper centralizes that shell so individual phase Actions contain
        only the domain logic.

        On success, returns the Action's output. On failure, returns $null and
        mutates $ErrorMessages with a phase-prefixed message. The caller can
        then decide whether to set a per-phase success flag.

    .PARAMETER Name
        Human-readable phase label used in logs and error messages.

    .PARAMETER Action
        Scriptblock executing the phase. May return any value; its output is
        forwarded to the caller.

    .PARAMETER ErrorMessages
        Shared error accumulator list. Failed phases append
        "<Name> failed: <message>" entries.

    .PARAMETER WarningCounter
        Ref to an integer counter. Failed phases increment it.

    .PARAMETER FailureMode
        FailRun throws after recording the phase failure. RecordUnsupported records
        the failure and returns a failed phase result.

    .PARAMETER OnSuccess
        Optional scriptblock invoked after a successful Action with the
        Action's output as $args[0]. Use this to update row counts or set
        per-phase success flags.

    .PARAMETER DetailBuilder
        Optional scriptblock invoked (with the Action output as $args[0])
        to produce a log-detail suffix like "123 rows".
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory)]
        [scriptblock]$Action,

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [System.Collections.Generic.List[string]]$ErrorMessages,

        [Parameter(Mandatory)]
        [ref]$WarningCounter,

        [Parameter(Mandatory)]
        [ValidateSet('FailRun', 'RecordUnsupported')]
        [string]$FailureMode,

        [Parameter()]
        [scriptblock]$OnSuccess,

        [Parameter()]
        [scriptblock]$DetailBuilder
    )

    $ErrorActionPreference = 'Stop'

    $stopwatch = [Diagnostics.Stopwatch]::StartNew()
    $result = $null
    $succeeded = $true
    $phaseException = $null

    try {
        $result = & $Action
    }
    catch {
        $succeeded = $false
        $WarningCounter.Value++
        $failMessage = "${Name} failed: $($_.Exception.Message)"
        $ErrorMessages.Add($failMessage)
        Write-Warning $failMessage
        $phaseException = [System.InvalidOperationException]::new($failMessage, $_.Exception)
    }

    $stopwatch.Stop()
    $elapsed = [math]::Round($stopwatch.Elapsed.TotalSeconds, 2)

    if ($succeeded) {
        $detail = if ($DetailBuilder) {
            & $DetailBuilder $result
        }
        else { $null }

        $logMessage = "Phase ${Name} completed in ${elapsed}s"
        if ($detail) { $logMessage += " - $detail" }
        Write-CIEMLog -Message $logMessage -Component 'Discovery'

        if ($OnSuccess) {
            & $OnSuccess $result
        }
    }
    else {
        Write-CIEMLog -Message "Phase ${Name} failed after ${elapsed}s" -Severity WARNING -Component 'Discovery'
        if ($FailureMode -eq 'FailRun') {
            throw $phaseException
        }
    }

    [pscustomobject]@{
        Name = $Name
        Succeeded = $succeeded
        Result = $result
        ElapsedSeconds = $elapsed
    }
}