Src/Private/Get-AbrExoHybrid.ps1

function Get-AbrExoHybrid {
    <#
    .SYNOPSIS
    Documents Exchange hybrid configuration, identity synchronisation source,
    and on-premises integration status for tenant $TenantId.
    If no hybrid is configured, explicitly documents that this is a cloud-only deployment.
    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory)]
        [string]$TenantId
    )

    begin {
        Write-PScriboMessage -Message "Collecting Exchange Online Hybrid/Identity Integration data for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'Hybrid'
    }

    process {
        Section -Style Heading2 'Identity Integration' {
            Paragraph "The following section documents the hybrid Exchange configuration and identity synchronisation model for tenant $TenantId."
            BlankLine

            #region Hybrid Configuration Detection
            $IsHybrid          = $false
            $IsDirSynced       = $false
            $HybridConfig      = $null

            try {
                Write-Host " - Checking for hybrid Exchange configuration..."
                $HybridConfig = Get-HybridConfiguration -ErrorAction SilentlyContinue

                if ($HybridConfig -and $HybridConfig.OnPremisesSmartHost) {
                    $IsHybrid = $true
                    Write-Host " - Hybrid configuration detected." -ForegroundColor Yellow
                } else {
                    Write-Host " - No hybrid Exchange configuration detected (cloud-only deployment)." -ForegroundColor Cyan
                }
            } catch {
                Write-AbrDebugLog "Get-HybridConfiguration returned an error (expected in cloud-only tenants): $($_.Exception.Message)" 'DEBUG' 'Hybrid'
            }

            # Check directory sync from OrgConfig
            try {
                $OrgConfig = Get-OrganizationConfig -ErrorAction SilentlyContinue
                $IsDirSynced = ($OrgConfig.IsDirSynced -eq $true)
            } catch {}

            # Try Graph for Entra Connect details
            $DirSyncEnabled     = $false
            $DirSyncServer      = '--'
            $LastDirSyncTime    = '--'
            try {
                $OrgResp = Invoke-MgGraphRequest -Method GET `
                    -Uri 'https://graph.microsoft.com/v1.0/organization?$select=id,onPremisesSyncEnabled,onPremisesLastSyncDateTime,onPremisesDomainName' `
                    -ErrorAction SilentlyContinue
                $MgOrg = if ($OrgResp.value) { $OrgResp.value[0] } else { $null }
                if ($MgOrg) {
                    $DirSyncEnabled  = ($MgOrg.onPremisesSyncEnabled -eq $true)
                    $LastDirSyncTime = if ($MgOrg.onPremisesLastSyncDateTime) {
                        [datetime]$MgOrg.onPremisesLastSyncDateTime | Get-Date -Format 'yyyy-MM-dd HH:mm UTC'
                    } else { 'Never / Not Applicable' }
                }
            } catch {}

            #region Deployment Model Summary
            Section -Style Heading3 'Deployment Model' {
                $ModelObj = [System.Collections.ArrayList]::new()
                $DeploymentType = if ($IsHybrid) { 'Exchange Hybrid (on-premises + cloud)' } elseif ($DirSyncEnabled -or $IsDirSynced) { 'Cloud-only Exchange with Entra ID Connect sync' } else { 'Cloud-only (Entra ID native / no on-premises sync)' }
                $SourceOfAuthority = if ($IsHybrid -or $DirSyncEnabled -or $IsDirSynced) { 'On-premises Active Directory (synced to Entra ID)' } else { 'Entra ID (cloud-native)' }

                $modelInObj = [ordered] @{
                    'Deployment Type'                   = $DeploymentType
                    'Source of Authority'               = $SourceOfAuthority
                    'Directory Synchronisation Active'  = ($DirSyncEnabled -or $IsDirSynced)
                    'Last Sync Time (Entra ID)'         = $LastDirSyncTime
                    'Exchange Hybrid Configured'        = $IsHybrid
                }
                $ModelObj.Add([pscustomobject](ConvertTo-HashToYN $modelInObj)) | Out-Null

                $ModelTableParams = @{ Name = "Deployment Model - $TenantId"; List = $true; ColumnWidths = 45, 55 }
                if ($Report.ShowTableCaptions) { $ModelTableParams['Caption'] = "- $($ModelTableParams.Name)" }
                $ModelObj | Table @ModelTableParams

                if (-not $IsHybrid -and -not $DirSyncEnabled -and -not $IsDirSynced) {
                    BlankLine
                    Paragraph "CONFIRMED: This tenant is a cloud-only Microsoft 365 deployment. No Exchange on-premises hybrid configuration is present. All mailboxes, identities, and policies are managed natively in Exchange Online and Entra ID."
                }
            }
            #endregion

            #region Hybrid Configuration Details (only if hybrid)
            if ($IsHybrid -and $HybridConfig) {
                Section -Style Heading3 'Exchange Hybrid Configuration' {
                    Paragraph "Exchange Hybrid mode allows seamless coexistence between Exchange on-premises and Exchange Online."
                    BlankLine

                    $HybObj = [System.Collections.ArrayList]::new()
                    $hybInObj = [ordered] @{
                        'On-Premises Smart Host'            = if ($HybridConfig.OnPremisesSmartHost) { $HybridConfig.OnPremisesSmartHost -join ', ' } else { 'Not Set' }
                        'Edge Transport'                    = if ($HybridConfig.EdgeTransportServers) { $HybridConfig.EdgeTransportServers -join ', ' } else { 'None (using Centralized Mail Transport)' }
                        'Features Enabled'                  = if ($HybridConfig.Features) { $HybridConfig.Features -join ', ' } else { 'None' }
                        'Inbound Connectors'                = if ($HybridConfig.InboundConnectors) { $HybridConfig.InboundConnectors -join ', ' } else { 'Not Set' }
                        'Outbound Connectors'               = if ($HybridConfig.OutboundConnectors) { $HybridConfig.OutboundConnectors -join ', ' } else { 'Not Set' }
                        'TLS Certificate Subject'           = if ($HybridConfig.TlsCertificateName) { $HybridConfig.TlsCertificateName } else { 'Not Set' }
                        'On-Premises Organisation'          = if ($HybridConfig.OnPremisesOrganization) { $HybridConfig.OnPremisesOrganization -join ', ' } else { 'Not Set' }
                        'Receive Connectors (on-prem)'      = if ($HybridConfig.ReceivingTransportServers) { $HybridConfig.ReceivingTransportServers -join ', ' } else { 'Not Set' }
                    }
                    $HybObj.Add([pscustomobject]$hybInObj) | Out-Null
                    $HybTableParams = @{ Name = "Hybrid Configuration Details - $TenantId"; List = $true; ColumnWidths = 40, 60 }
                    if ($Report.ShowTableCaptions) { $HybTableParams['Caption'] = "- $($HybTableParams.Name)" }
                    $HybObj | Table @HybTableParams
                    $script:ExcelSheets['Hybrid Config'] = $HybObj
                }

                #region On-premises Organisation Details
                try {
                    $OnPremOrgs = Get-OnPremisesOrganization -ErrorAction SilentlyContinue
                    if ($OnPremOrgs) {
                        Section -Style Heading3 'On-Premises Organisation' {
                            Paragraph "The following on-premises Exchange organisation(s) are registered in the hybrid relationship."
                            BlankLine
                            $OpOrgsObj = [System.Collections.ArrayList]::new()
                            foreach ($Org in $OnPremOrgs) {
                                $opInObj = [ordered] @{
                                    'Name'                  = $Org.Name
                                    'Organisation GUID'     = $Org.OrganizationGuid
                                    'HybridDomains'         = ($Org.HybridDomains -join ', ')
                                    'Inbound Connector'     = if ($Org.InboundConnector) { $Org.InboundConnector } else { 'Not Set' }
                                    'Outbound Connector'    = if ($Org.OutboundConnector) { $Org.OutboundConnector } else { 'Not Set' }
                                }
                                $OpOrgsObj.Add([pscustomobject]$opInObj) | Out-Null
                            }
                            $OpTableParams = @{ Name = "On-Premises Organisations - $TenantId"; List = $false; ColumnWidths = 18, 26, 22, 17, 17 }
                            if ($Report.ShowTableCaptions) { $OpTableParams['Caption'] = "- $($OpTableParams.Name)" }
                            if ($OpOrgsObj.Count -gt 0) { $OpOrgsObj | Table @OpTableParams }
                        }
                    }
                } catch {}
                #endregion
            }
            #endregion

            #region Mailbox source analysis
            try {
                Write-Host " - Analysing mailbox source of authority..."
                $AllMailboxes = Get-Mailbox -ResultSize Unlimited -ErrorAction Stop |
                    Where-Object { $_.RecipientTypeDetails -in @('UserMailbox','SharedMailbox') }

                $CloudMailboxes  = @($AllMailboxes | Where-Object { -not $_.IsDirSynced })
                $SyncedMailboxes = @($AllMailboxes | Where-Object { $_.IsDirSynced -eq $true })

                Section -Style Heading3 'Mailbox Source of Authority' {
                    Paragraph "The following summarises whether mailboxes are cloud-native or synced from on-premises Active Directory."
                    BlankLine

                    $SrcObj = [System.Collections.ArrayList]::new()
                    $srcInObj = [ordered] @{
                        'Total Mailboxes'                           = @($AllMailboxes).Count
                        'Cloud-Native (Entra ID managed)'           = $CloudMailboxes.Count
                        'Synced from On-Premises AD'                = $SyncedMailboxes.Count
                    }
                    $SrcObj.Add([pscustomobject]$srcInObj) | Out-Null

                    $SrcTableParams = @{ Name = "Mailbox Source of Authority - $TenantId"; List = $true; ColumnWidths = 55, 45 }
                    if ($Report.ShowTableCaptions) { $SrcTableParams['Caption'] = "- $($SrcTableParams.Name)" }
                    $SrcObj | Table @SrcTableParams
                }
            } catch {
                Write-ExoError 'Hybrid' "Unable to retrieve mailbox source data: $($_.Exception.Message)"
            }
            #endregion
        }
    }

    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'Hybrid'
    }
}