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 instead of exploding on the first failure.

        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 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)]
        [System.Collections.Generic.List[string]]$ErrorMessages,

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

        [Parameter()]
        [scriptblock]$OnSuccess,

        [Parameter()]
        [scriptblock]$DetailBuilder
    )

    $ErrorActionPreference = 'Stop'

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

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

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

    if ($succeeded) {
        $detail = if ($DetailBuilder) {
            try { & $DetailBuilder $result } catch { $null }
        }
        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'
    }

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