internal/functions/resolve/Resolve-Application.ps1

function Resolve-Application {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string[]] $InputReference,
        [switch] $DontFailIfNotExisting,
        [switch] $SearchInDesiredConfiguration,
        [switch] $Expand, # When set return object with appId, servicePrincipalId, applicationObjectId, displayName
        [switch] $ReturnObjectId, # When set (and not Expand) return the application (app registration) object id instead of appId
        [switch] $DisplayName, # Return displayName if not -Expand
        [System.Management.Automation.PSCmdlet]
        $Cmdlet = $PSCmdlet
    )

    begin {
        if ($InputReference.Count -gt 1) {
            $InputReference = $InputReference | ForEach-Object { Resolve-String -Text $_ }
        } else {
            $InputReference = Resolve-String -Text $InputReference[0]
        }
        if (-not $script:applicationDetailCache) {
            $script:applicationDetailCache = @{}
        }
    }
    process {
        if ($InputReference -is [array] -and $InputReference.Count -gt 1) {
            if (Test-TmfInputsCached -CacheName 'applicationDetailCache' -Inputs $InputReference -SkipValues @('All', 'Office365', 'MicrosoftAdminPortals')) {
                $results = foreach ($i in $InputReference) {
                    if ($i -in @('All', 'Office365', 'MicrosoftAdminPortals')) {
                        $i
                    } elseif ($script:applicationDetailCache.ContainsKey($i)) {
                        $detail = $script:applicationDetailCache[$i]
                        if ($Expand) {
                            $detail
                        } elseif ($DisplayName) {
                            $detail.displayName
                        } elseif ($ReturnObjectId) {
                            ($detail.applicationObjectId)
                        } else {
                            $detail.appId
                        }
                    } else {
                        $i
                    }
                }
                return , $results
            }
            $prefetch = {
                param($all)
                $requests = @(); $idMap = @{}; $rid = 0
                # GUID-like inputs: could be servicePrincipal id, application object id, or appId
                $guidish = $all | Where-Object { $_ -match $script:guidRegex }
                if ($guidish.Count -gt 0) {
                    foreach ($g in $guidish | Where-Object { -not $script:applicationDetailCache.ContainsKey($_) }) {
                        $rid++; $reqId = "g$rid"
                        $requests += @{ id = $reqId; method = 'GET'; url = "/servicePrincipals/$g?`$select=id,appId,displayName" }
                        $requests += @{ id = "$reqId-app"; method = 'GET'; url = "/applications/$g?`$select=id,appId,displayName" }
                        $idMap[$reqId] = $g; $idMap["$reqId-app"] = $g
                    }
                }
                # Non-GUID inputs (displayName)
                $toResolve = $all | Where-Object { -not $script:applicationDetailCache.ContainsKey($_) -and ($_ -notmatch $script:guidRegex) }
                foreach ($dn in $toResolve) {
                    $rid++; $reqId = "d$rid"
                    $escaped = $dn -replace "'", "''"
                    $requests += @{ id = "$reqId-appdn"; method = 'GET'; url = "/applications?`$filter=displayName eq '$escaped'&`$select=id,appId,displayName" }
                    $requests += @{ id = "$reqId-spdn"; method = 'GET'; url = "/servicePrincipals?`$filter=(displayName eq '$escaped') and (servicePrincipalType eq 'Application')&`$select=id,appId,displayName" }
                    $idMap["$reqId-appdn"] = $dn; $idMap["$reqId-spdn"] = $dn
                }
                # Submit in chunks
                for ($i = 0; $i -lt $requests.Count; $i += 20) {
                    $chunk = $requests[$i..([Math]::Min($i + 19, $requests.Count - 1))]
                    try {
                        $body = @{ requests = $chunk } | ConvertTo-Json -Depth 6
                        # Fix: literal $batch endpoint required
                        $response = Invoke-MgGraphRequest -Method POST -Uri ("$script:graphBaseUrl/`$batch") -Body $body -ContentType 'application/json'
                        if ($response -and $response.responses) {
                            foreach ($r in $response.responses) {
                                if ($r.status -ge 200 -and $r.status -lt 300 -and $r.body) {
                                    # normalize match
                                    $match = $null
                                    if ($r.body.value) {
                                        $match = $r.body.value | Select-Object -First 1
                                    } else {
                                        $match = $r.body
                                    }
                                    if ($match) {
                                        $servicePrincipalId = $null; $applicationObjectId = $null
                                        if ($r.id -like '*-sp*' -or $r.id -like 'g*') {
                                            $servicePrincipalId = $match.id
                                        }
                                        if ($r.id -like '*-app*' -or $r.id -like '*appdn') {
                                            $applicationObjectId = $match.id
                                        }
                                        $detail = [pscustomobject]@{ appId = $match.appId; servicePrincipalId = $servicePrincipalId; applicationObjectId = $applicationObjectId; displayName = $match.displayName }
                                        Add-TmfCacheEntries -CacheName 'applicationDetailCache' -Objects @($detail) -KeyProperties appId, servicePrincipalId, applicationObjectId, displayName
                                    }
                                }
                            }
                        }
                    } catch {
                        Write-PSFMessage -Level Warning -Message ("Application batch prefetch failed chunk starting index {0}: {1}" -f $i, $_.Exception.Message) -Tag 'batch', 'failed' -ErrorRecord $_
                    }
                }
            }
            $single = {
                param($one)
                if ($script:applicationDetailCache.ContainsKey($one)) {
                    $detail = $script:applicationDetailCache[$one]
                    if ($Expand) {
                        return $detail
                    } elseif ($DisplayName) {
                        return ($detail.displayNam)
                    } elseif ($ReturnObjectId) {
                        return ($detail.applicationObjectId)
                    } else {
                        return $detail.appId
                    }
                }
                return (Resolve-Application -InputReference $one -DontFailIfNotExisting -Expand:$Expand -DisplayName:$DisplayName -ReturnObjectId:$ReturnObjectId -SearchInDesiredConfiguration:$SearchInDesiredConfiguration -Cmdlet $Cmdlet)
            }
            return Invoke-TmfArrayResolution -Inputs $InputReference -Prefetch $prefetch -ResolveSingle $single
        }
        try {
            # Keywords / special tokens
            if ($InputReference -in @('All', 'Office365', 'MicrosoftAdminPortals')) {
                if ($Expand) {
                    return [pscustomobject]@{ appId = $InputReference; id = $InputReference; displayName = $InputReference }
                }
                return $InputReference
            }

            # If cache contains either SP object id or appId, return quickly on -Expand
            if ($script:applicationDetailCache.ContainsKey($InputReference)) {
                if ($Expand) {
                    return $script:applicationDetailCache[$InputReference]
                } else {
                    if ($DisplayName) {
                        return ($script:applicationDetailCache[$InputReference].displayName)
                    }; if ($ReturnObjectId) {
                        return ($script:applicationDetailCache[$InputReference].applicationObjectId)
                    }; return $script:applicationDetailCache[$InputReference].appId
                }
            }

            $appId = $null
            $spObj = $null
            $spId = $null
            $appRegObj = $null
            $appRegId = $null

            if ($InputReference -match $script:guidRegex) {
                # Try application object first (app registration)
                try {
                    $appRegObj = Invoke-MgGraphRequest -Method GET -Uri ("$script:graphBaseUrl/applications/{0}?`$select=id,appId,displayName" -f $InputReference) -ErrorAction Stop
                } catch {
                    $appRegObj = $null
                }
                if ($appRegObj) {
                    $appRegId = $appRegObj.id; $appId = $appRegObj.appId
                }
                # Try service principal object id
                try {
                    $spObj = Invoke-MgGraphRequest -Method GET -Uri ("$script:graphBaseUrl/servicePrincipals/{0}?`$select=id,appId,displayName" -f $InputReference) -ErrorAction Stop
                } catch {
                    $spObj = $null
                }
                if ($spObj -and -not $appId) {
                    $spId = $spObj.id; $appId = $spObj.appId
                }
                # If still missing, maybe provided value is actually appId (client id)
                if (-not $appId) {
                    $spObj = (Invoke-MgGraphRequest -Method GET -Uri ("$script:graphBaseUrl/servicePrincipals/?`$filter=appId eq '{0}'&`$select=id,appId,displayName" -f $InputReference)).value | Select-Object -First 1
                    if ($spObj) {
                        $spId = $spObj.id; $appId = $spObj.appId
                    }
                    if (-not $appRegObj) {
                        $appRegObj = (Invoke-MgGraphRequest -Method GET -Uri ("$script:graphBaseUrl/applications/?`$filter=appId eq '{0}'&`$select=id,appId,displayName" -f $InputReference)).value | Select-Object -First 1
                    }
                    if ($appRegObj) {
                        $appRegId = $appRegObj.id; if (-not $appId) {
                            $appId = $appRegObj.appId
                        }
                    }
                }
            } else {
                # Treat as displayName
                $appRegObj = (Invoke-MgGraphRequest -Method GET -Uri ("$script:graphBaseUrl/applications/?`$filter=displayName eq '{0}'&`$select=id,appId,displayName" -f $InputReference)).value | Select-Object -First 1
                if ($appRegObj) {
                    $appRegId = $appRegObj.id; $appId = $appRegObj.appId
                }
                $spObj = (Invoke-MgGraphRequest -Method GET -Uri ("$script:graphBaseUrl/servicePrincipals/?`$filter=(displayName eq '{0}') and (servicePrincipalType eq 'Application')&`$select=id,appId,displayName" -f $InputReference)).value | Select-Object -First 1
                if ($spObj) {
                    $spId = $spObj.id; if (-not $appId) {
                        $appId = $spObj.appId
                    }
                }
            }

            if (-not $appId -and $SearchInDesiredConfiguration) {
                if ($InputReference -in $script:desiredConfiguration['applications'].displayName) {
                    $appId = $InputReference
                }
            }

            if (-not $appId) {
                if ($DontFailIfNotExisting) {
                    return $InputReference
                } else {
                    throw "Cannot find application $InputReference"
                }
            }

            if (-not $Expand) {
                if ($DisplayName) {
                    return $spObj.displayName
                }
                if ($ReturnObjectId) {
                    # Prefer application registration object id; fallback to service principal id; else appId
                    return $appRegId
                }
                return $appId
            }

            if (-not $spObj) {
                $spObj = (Invoke-MgGraphRequest -Method GET -Uri ("$script:graphBaseUrl/servicePrincipals/?`$filter=appId eq '{0}'&`$select=id,appId,displayName" -f $appId)).value | Select-Object -First 1
                if ($spObj) {
                    $spId = $spObj.id
                }
            }
            if (-not $appRegObj -and $appId) {
                $appRegObj = (Invoke-MgGraphRequest -Method GET -Uri ("$script:graphBaseUrl/applications/?`$filter=appId eq '{0}'&`$select=id,appId,displayName" -f $appId)).value | Select-Object -First 1
                if ($appRegObj) {
                    $appRegId = $appRegObj.id
                }
            }
            $detail = [pscustomobject]@{ appId = $appId; servicePrincipalId = $spId; applicationObjectId = $appRegId; displayName = $spObj.displayName }
            # Cache by identifiers
            foreach ($key in @($detail.appId, $detail.servicePrincipalId, $detail.applicationObjectId, $detail.displayName)) {
                if ($key -and -not $script:applicationDetailCache.ContainsKey($key)) {
                    $script:applicationDetailCache[$key] = $detail
                }
            }
            return $detail
        } catch {
            if ($DontFailIfNotExisting) {
                Write-PSFMessage -Level Warning -Message ("Cannot resolve Application resource for input '{0}'. Searched tenant & desired configuration. Error: {1}" -f $InputReference, $_.Exception.Message) -Tag failed -ErrorRecord $_
                return $InputReference
            } else {
                Write-PSFMessage -Level Warning -Message ("Cannot resolve Application resource for input '{0}'. Searched tenant & desired configuration. Error: {1}" -f $InputReference, $_.Exception.Message) -Tag failed -ErrorRecord $_
                $Cmdlet.ThrowTerminatingError($_)
            }
        }
    }
}