modules/AWS/Infrastructure/Public/Invoke-AWSAPI.ps1

function Invoke-AWSAPI {
    <#
    .SYNOPSIS
        Invokes AWS CLI commands with standardized error handling.

    .DESCRIPTION
        Single point of entry for all AWS CLI calls. Reads auth context
        (profile/region) from $script:AWSAuthContext set by Connect-CIEMAWS.

        By default, non-success responses result in warnings (silent failure).
        Use -ErrorAction Stop to throw terminating errors.

    .PARAMETER Service
        The AWS service name (e.g., 'iam', 's3', 'sts').

    .PARAMETER Command
        The AWS CLI command (e.g., 'list-users', 'get-account-password-policy').

    .PARAMETER Arguments
        Additional CLI arguments as an array.

    .PARAMETER ResourceName
        A friendly name for the resource being loaded, used in verbose/warning messages.

    .PARAMETER RawOutput
        Return the raw string output instead of parsing JSON.

    .OUTPUTS
        [PSObject] The parsed JSON response. Returns nothing on error
        (unless -ErrorAction Stop is specified).

    .EXAMPLE
        Invoke-AWSAPI -Service iam -Command list-users -ResourceName 'IAM Users'

    .EXAMPLE
        Invoke-AWSAPI -Service iam -Command get-credential-report -ResourceName 'Credential Report' -ErrorAction Stop
    #>

    [CmdletBinding()]
    [OutputType([PSObject])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Service,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Command,

        [Parameter()]
        [string[]]$Arguments = @(),

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ResourceName,

        [Parameter()]
        [switch]$RawOutput
    )

    # Capture caller's ErrorAction before we override
    $shouldThrow = $ErrorActionPreference -eq 'Stop'

    $ErrorActionPreference = 'Stop'

    Write-Verbose "Loading $ResourceName..."

    # Build AWS CLI arguments
    $awsArgs = @($Service, $Command) + $Arguments + @('--output', 'json')

    # Add profile/region from auth context if available
    $awsContext = $script:AWSAuthContext
    if ($awsContext) {
        if ($awsContext.Profile) {
            $awsArgs += @('--profile', $awsContext.Profile)
        }
        if ($awsContext.Region) {
            $awsArgs += @('--region', $awsContext.Region)
        }
    }

    Write-CIEMLog -Message "AWS CLI: aws $Service $Command $($Arguments -join ' ')" -Severity DEBUG -Component 'Invoke-AWSAPI'

    # Execute AWS CLI (outside try/catch so exit-code handling is clean)
    $result = & aws @awsArgs 2>&1
    $exitCode = $LASTEXITCODE

    if ($exitCode -ne 0) {
        $errorText = ($result | Out-String).Trim()

        # Classify common AWS errors
        $msg = if ($errorText -match 'AccessDenied|not authorized') {
            "Access denied loading $ResourceName - missing permissions: $errorText"
        } elseif ($errorText -match 'ExpiredToken|SecurityTokenExpired') {
            "Token expired loading $ResourceName - re-authenticate: $errorText"
        } elseif ($errorText -match 'NoSuchEntity') {
            "Resource not found: $ResourceName"
        } elseif ($errorText -match 'could not be found|does not exist') {
            "Resource not found: $ResourceName"
        } else {
            "Failed to load $ResourceName (exit code $exitCode): $errorText"
        }

        Write-CIEMLog -Message $msg -Severity WARNING -Component 'Invoke-AWSAPI'

        if ($shouldThrow) { throw $msg }
        Write-Warning $msg
        return
    }

    # Parse the successful response (try/catch only around JSON parsing)
    $rawString = ($result | Out-String).Trim()

    if ($RawOutput) {
        return $rawString
    }

    if (-not $rawString) {
        Write-Verbose "Empty response for $ResourceName"
        return $null
    }

    try {
        $parsed = $rawString | ConvertFrom-Json
        Write-CIEMLog -Message "Loaded $ResourceName successfully" -Severity DEBUG -Component 'Invoke-AWSAPI'
        $parsed
    }
    catch {
        $msg = "Failed to parse $ResourceName response: $($_.Exception.Message)"
        Write-CIEMLog -Message $msg -Severity ERROR -Component 'Invoke-AWSAPI'

        if ($shouldThrow) { throw $msg }
        Write-Warning $msg
    }
}