Public/Edit-TCMMonitor.ps1

function Edit-TCMMonitor {
    <#
    .SYNOPSIS
        Interactively edit what resource types your TCM monitor watches.
    .DESCRIPTION
        Opens an HTML page in the browser where you can select/deselect resource types
        using checkboxes and profile presets. Click "Copy PowerShell Command" to get the
        command that applies your changes, then paste it in your terminal.

        Use -ResourceTypes for non-interactive (scripted) updates. This takes a new
        snapshot of added types, merges into the baseline, and updates the monitor.

    .PARAMETER MonitorId
        Target monitor ID. Defaults to the first monitor found.
    .PARAMETER ResourceTypes
        Apply a specific set of resource types non-interactively. This:
        1. Compares current vs new selection
        2. Snapshots any newly added types
        3. Merges new resources into the existing baseline
        4. Removes resources of dropped types from the baseline
        5. Updates the monitor with the new baseline
    .PARAMETER DisplayName
        Optional new display name for the monitor.
    .EXAMPLE
        Edit-TCMMonitor
        # Opens interactive HTML editor in browser
    .EXAMPLE
        Edit-TCMMonitor -ResourceTypes @('microsoft.entra.conditionalaccesspolicy','microsoft.entra.authenticationmethodpolicy')
        # Non-interactive update to exactly these types
    .EXAMPLE
        Edit-TCMMonitor -MonitorId 'eca21d95-...' -ResourceTypes (Get-TCMResourceTypeCatalog).Keys
        # Monitor all 62 types (Full profile equivalent)
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Browser')]
    param(
        [string]$MonitorId,

        [Parameter(ParameterSetName = 'Apply', Mandatory)]
        [string[]]$ResourceTypes,

        [string]$DisplayName,

        [Parameter(ParameterSetName = 'Apply')]
        [switch]$Force
    )

    # ── Get monitor ───────────────────────────────────────────────
    $needsBaseline = ($PSCmdlet.ParameterSetName -eq 'Apply')
    $monitors = if ($needsBaseline -and $MonitorId) {
        # Apply mode: fetch single monitor with full baseline
        @(Get-TCMMonitor -Id $MonitorId -IncludeBaseline)
    } elseif ($needsBaseline) {
        # Apply mode without ID: get list, then re-fetch first with baseline
        $list = @(Get-TCMMonitor)
        if ($list.Count -gt 0) {
            @(Get-TCMMonitor -Id $list[0].Id -IncludeBaseline)
        } else { @() }
    } else {
        @(Get-TCMMonitor)
    }

    if ($monitors.Count -eq 0) {
        Write-Host ''
        Write-Host ' No monitors found. Create one first:' -ForegroundColor Yellow
        Write-Host ' Start-TCMMonitoring -Profile Recommended' -ForegroundColor Cyan
        Write-Host ''
        return
    }

    $monitor = if ($MonitorId) {
        $monitors | Where-Object { $_.Id -eq $MonitorId }
    } else {
        $monitors[0]
    }

    if (-not $monitor) {
        Write-Warning "Monitor '$MonitorId' not found. Use Get-TCMMonitor to list available monitors."
        return
    }

    $catalog = Get-TCMResourceTypeCatalog

    # ── Browser mode (interactive HTML editor) ────────────────────
    if ($PSCmdlet.ParameterSetName -eq 'Browser') {
        $html = Get-TCMMonitorHtml -Catalog $catalog -MonitorId $monitor.Id `
            -MonitorDisplayName $monitor.DisplayName -MonitorStatus $monitor.Status `
            -ResourceCount $monitor.ResourceCount -MonitoredTypes @($monitor.MonitoredTypes) -Mode Edit

        $tempPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "EasyTCM-Edit-$(Get-Date -Format 'yyyyMMdd-HHmmss').html")
        $html | Set-Content -Path $tempPath -Encoding utf8
        Start-Process $tempPath

        Write-Host ''
        Write-Host ' Opened interactive editor in browser.' -ForegroundColor Green
        Write-Host ' Select resource types, then click "Copy PowerShell Command" and paste here.' -ForegroundColor Gray
        Write-Host ''
        return
    }

    # ── Apply mode (non-interactive) ──────────────────────────────
    $newTypes = [System.Collections.Generic.HashSet[string]]::new(
        [string[]]@($ResourceTypes),
        [StringComparer]::OrdinalIgnoreCase
    )

    # Validate resource types against catalog
    $invalidTypes = @($ResourceTypes | Where-Object { -not $catalog.ContainsKey($_) })
    if ($invalidTypes.Count -gt 0) {
        Write-Warning "Unknown resource types (not in catalog): $($invalidTypes -join ', ')"
        Write-Warning 'Use (Get-TCMResourceTypeCatalog).Keys to see valid types.'
        return
    }

    # Current types — exactly what the monitor watches (no profile expansion)
    $currentTypes = [System.Collections.Generic.HashSet[string]]::new(
        [string[]]@($monitor.MonitoredTypes),
        [StringComparer]::OrdinalIgnoreCase
    )

    # Compute diff
    $added = @($ResourceTypes | Where-Object { -not $currentTypes.Contains($_) })
    $removed = @($currentTypes | Where-Object { -not $newTypes.Contains($_) })

    if ($added.Count -eq 0 -and $removed.Count -eq 0 -and -not $DisplayName) {
        Write-Host ' No changes detected. Monitor already matches the requested configuration.' -ForegroundColor Green
        return
    }

    # Summary
    Write-Host ''
    Write-Host " Monitor: $($monitor.DisplayName) ($($monitor.Id))" -ForegroundColor Cyan
    Write-Host " Current: $($currentTypes.Count) types → New: $($newTypes.Count) types" -ForegroundColor Gray
    if ($added.Count -gt 0) {
        Write-Host " + Adding $($added.Count) types:" -ForegroundColor Green
        foreach ($t in $added) {
            $dn = if ($catalog.ContainsKey($t)) { $catalog[$t].DisplayName } else { $t }
            Write-Host " $dn" -ForegroundColor Green
        }
    }
    if ($removed.Count -gt 0) {
        Write-Host " - Removing $($removed.Count) types:" -ForegroundColor Red
        foreach ($t in $removed) {
            $dn = if ($catalog.ContainsKey($t)) { $catalog[$t].DisplayName } else { $t }
            Write-Host " $dn" -ForegroundColor Red
        }
    }
    if ($added.Count -gt 0) {
        # Quota pre-flight check
        $quota = Get-TCMQuota -PassThru 6>$null
        $dailyUsed = if ($quota) { $quota.DailyResourceUsage } else { 0 }
        $dailyLimit = 800
        $snapUsed = if ($quota) { $quota.SnapshotJobCount } else { 0 }
        $snapLimit = 12

        Write-Host ''
        Write-Host ' ⚠️ Snapshot required for new types:' -ForegroundColor Yellow
        Write-Host " Daily resources: $dailyUsed / $dailyLimit used ($([math]::Round($dailyUsed / $dailyLimit * 100))%)" -ForegroundColor Gray
        Write-Host " Snapshot jobs: $snapUsed / $snapLimit used" -ForegroundColor Gray

        if ($snapUsed -ge $snapLimit) {
            Write-Warning "Snapshot job limit reached ($snapUsed/$snapLimit). Cannot create snapshot for new types."
            Write-Warning 'Wait for existing snapshot jobs to complete or expire, then retry.'
            return
        }
        if ($dailyUsed -ge $dailyLimit) {
            Write-Warning "Daily resource quota exhausted ($dailyUsed/$dailyLimit). Snapshot will likely fail."
            Write-Warning 'Wait until tomorrow (UTC midnight reset) or reduce the number of new types.'
            return
        }
    }
    Write-Host ''

    # ── Drift warning (single confirmation point) ───────────────
    # -WhatIf: show what would happen and stop
    if (-not $PSCmdlet.ShouldProcess("Monitor '$($monitor.DisplayName)'", "Update baseline ($($added.Count) added, $($removed.Count) removed)")) {
        return
    }
    # -Force: skip interactive confirmation
    if (-not $Force) {
        $driftMsg = "This will update the baseline for '$($monitor.DisplayName)'.`n" +
                   " - Existing drift records will be reset (re-baselined).`n" +
                   " - Types added: $($added.Count), removed: $($removed.Count).`n" +
                   'Continue?'
        if (-not $PSCmdlet.ShouldContinue($driftMsg, 'Confirm baseline update')) {
            return
        }
    }

    # ── Step 1: Snapshot added types if any ───────────────────────
    $newResources = @()
    if ($added.Count -gt 0) {
        Write-Host ' Snapshotting newly added types...' -ForegroundColor Gray
        $snapshotName = "EasyTCM Edit $(Get-Date -Format 'yyyyMMdd HHmmss')"
        $snapshot = New-TCMSnapshot -DisplayName $snapshotName -Resources $added -Wait

        if (-not $snapshot) {
            Write-Warning 'Snapshot creation failed. Cannot add new types.'
            Write-Warning 'Monitor was NOT modified — no changes applied.'
            return
        }

        $snapshotStatus = if ($snapshot -is [System.Collections.IDictionary]) { $snapshot['status'] } else { $snapshot.status }
        $snapshotId = if ($snapshot -is [System.Collections.IDictionary]) { $snapshot['id'] } else { $snapshot.id }

        if ($snapshotStatus -notin @('succeeded', 'partiallySuccessful')) {
            $errDetail = if ($snapshot.errorDetails) { $snapshot.errorDetails -join '; ' } else { $snapshotStatus }
            Write-Warning "Snapshot failed ($errDetail). Cannot add new types."
            if ($snapshotStatus -eq 'quotaExceeded' -or $errDetail -match 'quota') {
                Write-Warning 'Daily resource quota exceeded. Wait until UTC midnight reset or reduce the number of new types.'
            }
            Write-Warning 'Monitor was NOT modified — no changes applied.'
            return
        }

        # Convert snapshot content to baseline resources
        $snapshotJob = Get-TCMSnapshot -Id $snapshotId -IncludeContent
        $snapContent = if ($snapshotJob -is [System.Collections.IDictionary]) { $snapshotJob['snapshotContent'] } else { $snapshotJob.snapshotContent }
        if (-not $snapContent) {
            Write-Warning 'Snapshot succeeded but content could not be retrieved.'
            Write-Warning 'Monitor was NOT modified — no changes applied.'
            return
        }
        $origWarnPref = $WarningPreference
        try {
            $WarningPreference = 'SilentlyContinue'
            $newBaselineTemp = ConvertTo-TCMBaseline -SnapshotContent $snapContent -Profile Full -DisplayName 'temp' 6>$null
        }
        finally { $WarningPreference = $origWarnPref }

        if ($newBaselineTemp -and $newBaselineTemp.Resources) {
            $newResources = @($newBaselineTemp.Resources)
        }

        # Clean up snapshot
        if ($snapshotId) {
            Remove-TCMSnapshot -Id $snapshotId -Confirm:$false -ErrorAction SilentlyContinue
        }

        if ($newResources.Count -eq 0) {
            Write-Warning "Snapshot succeeded but returned 0 resources for the $($added.Count) new types."
            Write-Warning 'This may mean those resource types have no instances in your tenant.'
            Write-Warning 'Monitor was NOT modified — no changes applied.'
            return
        }

        Write-Host " Captured $($newResources.Count) resources from $($added.Count) new types." -ForegroundColor Gray
    }

    # ── Step 2: Build updated baseline ────────────────────────────
    Write-Host ' Building updated baseline...' -ForegroundColor Gray

    # Get current baseline resources
    $currentBaseline = $monitor.Baseline
    $existingResources = @()
    if ($currentBaseline) {
        $bRes = if ($currentBaseline -is [System.Collections.IDictionary]) { $currentBaseline['resources'] } else { $currentBaseline.resources }
        if ($bRes) { $existingResources = @($bRes) }
    }

    # Filter out removed types from existing resources
    $keptResources = @($existingResources | Where-Object {
        $rt = if ($_ -is [System.Collections.IDictionary]) { $_['resourceType'] } else { $_.resourceType }
        $newTypes.Contains($rt)
    })

    # Merge kept + new resources
    $allResources = @($keptResources) + @($newResources)

    if ($allResources.Count -eq 0) {
        Write-Warning 'Resulting baseline would have 0 resources — the API requires at least one.'
        Write-Warning 'Monitor was NOT modified — no changes applied.'
        return
    }

    $currentName = if ($currentBaseline -is [System.Collections.IDictionary]) { $currentBaseline['displayName'] } else { $currentBaseline.displayName }
    $baselineName = if ($DisplayName) { $DisplayName } else { $currentName ?? $monitor.DisplayName }

    $newBaseline = @{
        DisplayName = $baselineName
        Resources   = $allResources
    }

    # ── Step 3: Update monitor ────────────────────────────────────
    Write-Host ' Updating monitor...' -ForegroundColor Gray

    $updateParams = @{ Id = $monitor.Id; Baseline = $newBaseline; Confirm = $false }
    if ($DisplayName) { $updateParams.DisplayName = $DisplayName }

    try {
        Update-TCMMonitor @updateParams -ErrorAction Stop
    }
    catch {
        Write-Warning "Failed to update monitor: $_"
        Write-Warning 'Monitor may not have been modified. Check with Get-TCMMonitor.'
        return
    }

    $quotaEst = $allResources.Count * 4
    Write-Host ''
    Write-Host " ✅ Monitor updated: $($allResources.Count) resources across $($newTypes.Count) types" -ForegroundColor Green
    Write-Host " Quota: ~$quotaEst / 800 resources per day ($([math]::Round($quotaEst / 800 * 100))%)" -ForegroundColor Gray

    if ($removed.Count -gt 0) {
        Write-Host ' ⚠️ Drift records for removed types will be cleared on next run.' -ForegroundColor Yellow
    }

    Write-Host ''
}