Public/Import-IntuneAppProtectionPolicy.ps1

function Import-IntuneAppProtectionPolicy {
    <#
    .SYNOPSIS
        Imports app protection (MAM) policies from templates
    .DESCRIPTION
        Reads app protection templates and upserts Android/iOS managed app protection policies via Graph.
    .PARAMETER TemplatePath
        Path to the app protection template directory (defaults to Templates/AppProtection)
    .PARAMETER Platform
        Filter templates by platform. Valid values: iOS, Android, All.
        Defaults to 'All' which imports all app protection templates regardless of platform.
        Note: App protection policies only apply to iOS and Android platforms.
    .EXAMPLE
        Import-IntuneAppProtectionPolicy
    .EXAMPLE
        Import-IntuneAppProtectionPolicy -Platform iOS
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter()]
        [string]$TemplatePath,

        [Parameter()]
        [ValidateSet('iOS', 'Android', 'All')]
        [string[]]$Platform = @('All'),

        [Parameter()]
        [switch]$RemoveExisting
    )

    if (-not $TemplatePath) {
        $TemplatePath = Join-Path -Path $script:TemplatesPath -ChildPath "AppProtection"
    }

    if (-not (Test-Path -Path $TemplatePath)) {
        Write-Warning "App protection template directory not found: $TemplatePath"
        return @()
    }

    $templateFiles = Get-FilteredTemplates -Path $TemplatePath -Platform $Platform -FilterMode 'Suffix' -Recurse -ResourceType "app protection template"

    if (-not $templateFiles -or $templateFiles.Count -eq 0) {
        Write-Warning "No app protection templates found in: $TemplatePath"
        return @()
    }

    $typeToEndpoint = @{
        '#microsoft.graph.androidManagedAppProtection' = 'beta/deviceAppManagement/androidManagedAppProtections'
        '#microsoft.graph.iosManagedAppProtection'     = 'beta/deviceAppManagement/iosManagedAppProtections'
    }

    $results = @()

    # Prefetch existing policies from both endpoints
    # Note: App protection policies don't support $select for description, so we fetch all properties
    $existingPolicies = @{}
    foreach ($endpoint in $typeToEndpoint.Values) {
        try {
            $listUri = $endpoint
            do {
                $existing = Invoke-MgGraphRequest -Method GET -Uri $listUri -ErrorAction Stop
                foreach ($policy in $existing.value) {
                    if ($policy.displayName) {
                        $isTagged = Test-HydrationKitObject -Description $policy.description
                        if (-not $existingPolicies.ContainsKey($policy.displayName)) {
                            $existingPolicies[$policy.displayName] = @{
                                Id          = $policy.id
                                Description = $policy.description
                                Endpoint    = $endpoint
                                IsTagged    = $isTagged
                            }
                        } elseif ($isTagged -and -not $existingPolicies[$policy.displayName].IsTagged) {
                            $existingPolicies[$policy.displayName] = @{
                                Id          = $policy.id
                                Description = $policy.description
                                Endpoint    = $endpoint
                                IsTagged    = $true
                            }
                        }
                    }
                }
                $listUri = $existing.'@odata.nextLink'
            } while ($listUri)
        } catch {
            Write-Warning "Could not retrieve existing policies from $endpoint`: $_"
        }
    }

    # Remove existing app protection policies if requested
    # SAFETY: Only delete policies that have "Imported by Intune Hydration Kit" in description
    if ($RemoveExisting) {
        # Load template names to scope deletes to only policies this kit would create
        $knownTemplateNames = Get-TemplateDisplayNames -Path $TemplatePath -Recurse

        # Group policies by endpoint for batch deletion
        $policiesByEndpoint = @{}
        foreach ($policyName in $existingPolicies.Keys) {
            $policyInfo = $existingPolicies[$policyName]

            if (-not (Test-HydrationKitObject -Description $policyInfo.Description -ObjectName $policyName)) {
                Write-Verbose "Skipping '$policyName' - not created by Intune Hydration Kit"
                continue
            }

            $escapedPrefix = [regex]::Escape($script:ImportPrefix)
            $nameForLookup = $policyName -replace "^$escapedPrefix", ''
            if (-not ($knownTemplateNames.Contains($policyName) -or $knownTemplateNames.Contains($nameForLookup))) {
                Write-Verbose "Skipping '$policyName' - not in this kit's templates (may be from another tool)"
                continue
            }

            $relativePath = $policyInfo.Endpoint -replace '^beta/', ''
            if (-not $policiesByEndpoint.ContainsKey($relativePath)) {
                $policiesByEndpoint[$relativePath] = @()
            }
            $policiesByEndpoint[$relativePath] += @{
                Name = $policyName
                Id   = $policyInfo.Id
            }
        }

        $totalPolicies = ($policiesByEndpoint.Values | ForEach-Object { $_.Count } | Measure-Object -Sum).Sum
        if ($totalPolicies -eq 0) {
            Write-Verbose "No app protection policies found to delete"
            return $results
        }

        if (-not $PSCmdlet.ShouldProcess("$totalPolicies app protection policy/policies", "Delete")) {
            if ($WhatIfPreference) {
                foreach ($endpoint in $policiesByEndpoint.Keys) {
                    foreach ($policy in $policiesByEndpoint[$endpoint]) {
                        Write-HydrationLog -Message " WouldDelete: $($policy.Name)" -Level Info
                        $results += New-HydrationResult -Name $policy.Name -Type 'AppProtection' -Action 'WouldDelete' -Status 'DryRun'
                    }
                }
            }
            return $results
        }

        foreach ($endpoint in $policiesByEndpoint.Keys) {
            $results += Invoke-GraphBatchOperation -Items $policiesByEndpoint[$endpoint] -Operation 'DELETE' -BaseUrl "/$endpoint" -ResultType 'AppProtection'
        }

        return $results
    }

    # Collect policies to create
    $policiesToCreate = @()
    foreach ($templateFile in $templateFiles) {
        try {
            $template = Get-Content -Path $templateFile.FullName -Raw -Encoding utf8 | ConvertFrom-Json
            $displayName = "$($script:ImportPrefix)$($template.displayName)"
            $odataType = $template.'@odata.type'

            if (-not $template.displayName -or -not $odataType) {
                Write-Warning "Template missing displayName or @odata.type: $($templateFile.FullName)"
                $results += New-HydrationResult -Name $templateFile.Name -Path $templateFile.FullName -Type 'AppProtection' -Action 'Failed' -Status 'Missing displayName or @odata.type'
                continue
            }

            $endpoint = $typeToEndpoint[$odataType]
            if (-not $endpoint) {
                Write-Warning "Unsupported @odata.type '$odataType' in $($templateFile.FullName) - skipping"
                $results += New-HydrationResult -Name $displayName -Path $templateFile.FullName -Type 'AppProtection' -Action 'Skipped' -Status "Unsupported @odata.type: $odataType"
                continue
            }

            # Check for existing policy using prefetched list - only skip if tagged by kit
            if ($existingPolicies.ContainsKey($displayName) -and $existingPolicies[$displayName].IsTagged) {
                Write-HydrationLog -Message " Skipped: $displayName" -Level Info
                $results += New-HydrationResult -Name $displayName -Id $existingPolicies[$displayName].Id -Path $templateFile.FullName -Type 'AppProtection' -Action 'Skipped' -Status 'Already exists'
                continue
            }

            # Prepare body (remove read-only properties)
            $importBody = Copy-DeepObject -InputObject $template
            Remove-ReadOnlyGraphProperties -InputObject $importBody -AdditionalProperties @(
                'apps',
                'assignments',
                'targetedAppManagementLevels'
            )

            # Add hydration kit tag to description
            $importBody.description = New-HydrationDescription -ExistingText $importBody.description

            # Apply import prefix to body
            if ($importBody.displayName) { $importBody.displayName = $displayName }

            # Remove empty manufacturer/model allowlists
            if ($importBody.allowedAndroidDeviceManufacturers -eq "") {
                $importBody.PSObject.Properties.Remove('allowedAndroidDeviceManufacturers') | Out-Null
            }
            if ($importBody.allowedIosDeviceModels -eq "") {
                $importBody.PSObject.Properties.Remove('allowedIosDeviceModels') | Out-Null
            }

            # Store as JSON string to avoid serialization issues
            $policiesToCreate += @{
                Name     = $displayName
                Path     = $templateFile.FullName
                Endpoint = $endpoint
                BodyJson = ($importBody | ConvertTo-Json -Depth 100 -Compress)
            }
        } catch {
            $errMessage = Get-GraphErrorMessage -ErrorRecord $_
            Write-HydrationLog -Message " Failed: $($templateFile.Name) - $errMessage" -Level Warning
            $results += New-HydrationResult -Name $templateFile.Name -Path $templateFile.FullName -Type 'AppProtection' -Action 'Failed' -Status $errMessage
        }
    }

    if (-not $PSCmdlet.ShouldProcess("$($policiesToCreate.Count) app protection policy/policies", "Create")) {
        if ($WhatIfPreference) {
            foreach ($policy in $policiesToCreate) {
                Write-HydrationLog -Message " WouldCreate: $($policy.Name)" -Level Info
                $results += New-HydrationResult -Name $policy.Name -Path $policy.Path -Type 'AppProtection' -Action 'WouldCreate' -Status 'DryRun'
            }
        }
        return $results
    }

    if ($policiesToCreate.Count -gt 0) {
        # Group policies by endpoint for batch creation
        $policiesByEndpoint = @{}
        foreach ($policy in $policiesToCreate) {
            $relativePath = $policy.Endpoint -replace '^beta/', ''
            if (-not $policiesByEndpoint.ContainsKey($relativePath)) {
                $policiesByEndpoint[$relativePath] = @()
            }
            $policiesByEndpoint[$relativePath] += $policy
        }

        foreach ($endpoint in $policiesByEndpoint.Keys) {
            $results += Invoke-GraphBatchOperation -Items $policiesByEndpoint[$endpoint] -Operation 'POST' -BaseUrl "/$endpoint" -ResultType 'AppProtection'
        }
    }

    return $results
}