Public/Import-IntuneMobileApp.ps1

function Import-IntuneMobileApp {
    <#
    .SYNOPSIS
        Imports mobile apps from JSON templates
    .DESCRIPTION
        Reads JSON templates from Templates/MobileApps and creates mobile apps via Graph API.
    .PARAMETER TemplatePath
        Path to the mobile apps template directory (defaults to Templates/MobileApps)
    .PARAMETER RemoveExisting
        If specified, removes existing mobile apps that were created by Intune Hydration Kit
    .PARAMETER Platform
        Filter templates by platform. Valid values: Windows, macOS, All.
        Defaults to 'All' which imports all mobile app templates regardless of platform.
        Note: Mobile app templates are organized by Windows and macOS directories.
    .EXAMPLE
        Import-IntuneMobileApp
    .EXAMPLE
        Import-IntuneMobileApp -RemoveExisting
    .EXAMPLE
        Import-IntuneMobileApp -Platform Windows
    #>

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

        [Parameter()]
        [ValidateSet('Windows', 'macOS', 'All')]
        [string[]]$Platform = @('All'),

        [Parameter()]
        [switch]$RemoveExisting
    )

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

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

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

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

    # Prefetch existing mobile apps (paged)
    $existingApps = @{}
    $listUri = "beta/deviceAppManagement/mobileApps?`$select=id,displayName,notes"
    try {
        do {
            $existingResponse = Invoke-MgGraphRequest -Method GET -Uri $listUri -ErrorAction Stop
            foreach ($app in $existingResponse.value) {
                $appName = $app.displayName
                if ($appName) {
                    $isTagged = Test-HydrationKitObject -Notes $app.notes
                    if (-not $existingApps.ContainsKey($appName)) {
                        $existingApps[$appName] = @{
                            Id       = $app.id
                            Notes    = $app.notes
                            IsTagged = $isTagged
                        }
                    } elseif ($isTagged -and -not $existingApps[$appName].IsTagged) {
                        # Prefer the tagged (kit-created) version
                        $existingApps[$appName] = @{
                            Id       = $app.id
                            Notes    = $app.notes
                            IsTagged = $true
                        }
                    }
                }
            }
            $listUri = $existingResponse.'@odata.nextLink'
        } while ($listUri)
    } catch {
        Write-Warning "Failed to list existing mobile apps: $($_.Exception.Message)"
    }

    $results = @()

    # Remove existing apps if requested
    if ($RemoveExisting) {
        # Load template names to scope deletes to only apps this kit would create
        $knownTemplateNames = Get-TemplateDisplayNames -Path $TemplatePath -Recurse

        $appsToDelete = @()
        foreach ($appName in $existingApps.Keys) {
            $appInfo = $existingApps[$appName]

            if (-not (Test-HydrationKitObject -Notes $appInfo.Notes -ObjectName $appName)) {
                Write-Verbose "Skipping '$appName' - not created by Intune Hydration Kit"
                continue
            }

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

            $appsToDelete += @{
                Name = $appName
                Id   = $appInfo.Id
            }
        }

        if ($appsToDelete.Count -eq 0) {
            Write-Verbose "No mobile apps found to delete"
            return $results
        }

        if (-not $PSCmdlet.ShouldProcess("$($appsToDelete.Count) mobile app(s)", "Delete")) {
            if ($WhatIfPreference) {
                foreach ($app in $appsToDelete) {
                    Write-HydrationLog -Message " WouldDelete: $($app.Name)" -Level Info
                    $results += New-HydrationResult -Name $app.Name -Type 'MobileApp' -Action 'WouldDelete' -Status 'DryRun'
                }
            }
            return $results
        }

        return Invoke-GraphBatchOperation -Items $appsToDelete -Operation 'DELETE' -BaseUrl '/deviceAppManagement/mobileApps' -ResultType 'MobileApp'
    }

    # Collect apps to create
    $appsToCreate = @()
    foreach ($templateFile in $templateFiles) {
        try {
            $template = Get-Content -Path $templateFile.FullName -Raw -Encoding utf8 | ConvertFrom-Json
            $displayName = "$($script:ImportPrefix)$($template.displayName)"
            if (-not $template.displayName) {
                Write-Warning "Template missing displayName: $($templateFile.FullName)"
                $results += New-HydrationResult -Name $templateFile.Name -Path $templateFile.FullName -Type 'MobileApp' -Action 'Failed' -Status 'Missing displayName'
                continue
            }

            # Check both prefixed and unprefixed names (backward compat with pre-prefix apps)
            $originalName = $template.displayName
            $hasLegacyMatch = -not [string]::IsNullOrWhiteSpace($originalName) -and $originalName -ne $displayName -and
                              $existingApps.ContainsKey($originalName) -and $existingApps[$originalName].IsTagged
            if (($existingApps.ContainsKey($displayName) -and $existingApps[$displayName].IsTagged) -or $hasLegacyMatch) {
                $matchedName = if ($existingApps.ContainsKey($displayName) -and $existingApps[$displayName].IsTagged) { $displayName } else { $originalName }
                Write-HydrationLog -Message " Skipped: $displayName" -Level Info
                $results += New-HydrationResult -Name $displayName -Id $existingApps[$matchedName].Id -Path $templateFile.FullName -Type 'MobileApp' -Action 'Skipped' -Status 'Already exists'
                continue
            }

            $importBody = Copy-DeepObject -InputObject $template
            Remove-ReadOnlyGraphProperties -InputObject $importBody

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

            # Add hydration kit tag to notes field (mobile apps use notes instead of description for this)
            $newNotes = New-HydrationDescription -ExistingText $(if ($importBody.PSObject.Properties['notes']) { $importBody.notes } else { '' })
            if ($importBody.PSObject.Properties['notes']) {
                $importBody.notes = $newNotes
            } else {
                $importBody | Add-Member -NotePropertyName 'notes' -NotePropertyValue $newNotes
            }

            # Store as JSON string to avoid serialization issues
            $appsToCreate += @{
                Name     = $displayName
                Path     = $templateFile.FullName
                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 'MobileApp' -Action 'Failed' -Status $errMessage
        }
    }

    if (-not $PSCmdlet.ShouldProcess("$($appsToCreate.Count) mobile app(s)", "Create")) {
        if ($WhatIfPreference) {
            foreach ($app in $appsToCreate) {
                Write-HydrationLog -Message " WouldCreate: $($app.Name)" -Level Info
                $results += New-HydrationResult -Name $app.Name -Path $app.Path -Type 'MobileApp' -Action 'WouldCreate' -Status 'DryRun'
            }
        }
        return $results
    }

    if ($appsToCreate.Count -gt 0) {
        $results += Invoke-GraphBatchOperation -Items $appsToCreate -Operation 'POST' -BaseUrl '/deviceAppManagement/mobileApps' -ResultType 'MobileApp'
    }

    return $results
}