Private/Send-TCMNotification.ps1

function Send-TCMNotification {
    <#
    .SYNOPSIS
        Send drift notification to Teams via incoming webhook.
    .DESCRIPTION
        Internal helper. Posts an Adaptive Card to a Teams channel webhook
        with drift summary, monitor metadata, and admin portal links.
    .PARAMETER Drifts
        Array of drift objects from Get-TCMDrift.
    .PARAMETER Monitor
        Monitor object from Get-TCMMonitor.
    .PARAMETER WebhookUrl
        Teams incoming webhook URL.
    .PARAMETER CompareResult
        Optional Compare-TCMBaseline result for new/deleted resource counts.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [array]$Drifts,

        [Parameter(Mandatory)]
        [PSObject]$Monitor,

        [Parameter(Mandatory)]
        [string]$WebhookUrl,

        [PSObject]$CompareResult
    )

    # ── Build facts ─────────────────────────────────────────────────
    $timestamp = [DateTime]::UtcNow.ToString('yyyy-MM-dd HH:mm') + ' UTC'
    $driftCount = $Drifts.Count
    $newCount = if ($CompareResult) { $CompareResult.NewCount } else { 0 }
    $deletedCount = if ($CompareResult) { $CompareResult.DeletedCount } else { 0 }

    # Determine card theme
    if ($driftCount -gt 0) {
        $statusIcon = '⚠️'
        $statusText = "$driftCount configuration drift(s) detected"
        $accentColor = 'attention'
    }
    elseif ($newCount -gt 0 -or $deletedCount -gt 0) {
        $statusIcon = '🔶'
        $statusText = "$newCount new, $deletedCount deleted untracked resource(s)"
        $accentColor = 'warning'
    }
    else {
        $statusIcon = '✅'
        $statusText = 'No drift — configuration matches baseline'
        $accentColor = 'good'
    }

    # ── Build drift detail blocks ───────────────────────────────────
    $driftBlocks = @()

    if ($driftCount -gt 0) {
        $grouped = $Drifts | Group-Object -Property ResourceType
        foreach ($group in $grouped) {
            $shortType = ($group.Name -split '\.')[-1]
            $lines = @()
            foreach ($d in $group.Group | Select-Object -First 5) {
                $propSummary = ($d.DriftedProperties | Select-Object -First 3 | ForEach-Object {
                    "$($_.propertyName): ``$($_.baselineValue)`` → ``$($_.currentValue)``"
                }) -join '\n'
                $lines += "- **$($d.ResourceDisplay)** — $($d.DriftedPropertyCount) changed"
                if ($propSummary) { $lines += " $propSummary" }
            }
            if ($group.Count -gt 5) {
                $lines += "- *... and $($group.Count - 5) more*"
            }

            $driftBlocks += @{
                type = 'TextBlock'
                text = "**$shortType** ($($group.Count))"
                weight = 'Bolder'
                spacing = 'Medium'
            }
            $driftBlocks += @{
                type = 'TextBlock'
                text = ($lines -join '\n')
                wrap = $true
                spacing = 'Small'
            }
        }
    }

    # ── Baseline comparison block ───────────────────────────────────
    if ($CompareResult -and ($newCount -gt 0 -or $deletedCount -gt 0)) {
        $driftBlocks += @{
            type = 'TextBlock'
            text = "**Untracked resources:** $newCount new, $deletedCount deleted"
            weight = 'Bolder'
            spacing = 'Medium'
            color = 'warning'
        }
    }

    # ── Assemble Adaptive Card ──────────────────────────────────────
    $cardBody = @(
        @{
            type = 'TextBlock'
            size = 'Medium'
            weight = 'Bolder'
            text = "$statusIcon EasyTCM — $statusText"
            wrap = $true
            color = $accentColor
        }
        @{
            type = 'FactSet'
            facts = @(
                @{ title = 'Monitor'; value = $Monitor.DisplayName }
                @{ title = 'Resources'; value = "$($Monitor.ResourceCount) monitored" }
                @{ title = 'Checked'; value = $timestamp }
            )
        }
    )
    $cardBody += $driftBlocks

    # Action buttons
    $actions = @(
        @{
            type = 'Action.OpenUrl'
            title = 'Entra Portal'
            url = 'https://entra.microsoft.com'
        }
        @{
            type = 'Action.OpenUrl'
            title = 'Exchange Admin'
            url = 'https://admin.exchange.microsoft.com'
        }
    )

    $card = @{
        type = 'message'
        attachments = @(
            @{
                contentType = 'application/vnd.microsoft.card.adaptive'
                contentUrl = $null
                content = @{
                    '$schema' = 'http://adaptivecards.io/schemas/adaptive-card.json'
                    type = 'AdaptiveCard'
                    version = '1.4'
                    body = $cardBody
                    actions = $actions
                }
            }
        )
    }

    # ── Send ────────────────────────────────────────────────────────
    $json = $card | ConvertTo-Json -Depth 20 -Compress

    try {
        Invoke-RestMethod -Uri $WebhookUrl -Method Post -Body $json -ContentType 'application/json; charset=utf-8' -ErrorAction Stop | Out-Null
        Write-Host " 📨 Teams notification sent ($statusText)" -ForegroundColor Green
    }
    catch {
        Write-Warning "Failed to send Teams notification: $($_.Exception.Message)"
    }
}