48 views +0 -0

Clone Microsoft 365 User Rights

Maakt een nieuwe Microsoft 365-gebruiker aan met dezelfde indeling en rechten als een bestaande gebruiker.

<#
.SYNOPSIS
    Maakt een nieuwe Microsoft 365-gebruiker aan met DEZELFDE rechten als een
    bestaande "voorbeeld"-gebruiker (template). Bedoeld voor HR-onboarding bij een MSP.

.DESCRIPTION
    Praktijkvoorbeeld: HR vraagt om een nieuw account met dezelfde rechten als een
    bestaande medewerker.

    Het script:
      1. Maakt verbinding met de tenant (Microsoft Graph + Exchange Online).
      2. Leest de rechten van de voorbeeldgebruiker uit:
            - Groepslidmaatschappen (security, Microsoft 365, distributie / mail-enabled security)
            - Licenties (alleen DIRECT toegewezen; groep-gebaseerde worden NIET gedupliceerd)
            - Gedeelde mailboxen: Full Access + Send As + Send on Behalf
            - Directory-/adminrollen (alleen RAPPORTEREN, niet toewijzen)
      3. Leidt de WEERGAVENAAM, UPN en e-mail af van de voorbeeldgebruiker
         (een eventuele affix zoals " | Bedrijfsnaam BV" en het local-part-patroon worden gedetecteerd).
      4. Maakt de nieuwe gebruiker aan met een willekeurig (uitspreekbaar) wachtwoord.
      5. Wijst dezelfde rechten toe.
      6. Eindigt met een overzicht van toegewezen rechten en HELEMAAL ONDERAAN de
         accountgegevens (weergavenaam + e-mail + wachtwoord).

    Cloud-only en hybride tenants: het script DETECTEERT OnPremisesSyncEnabled (op de
    voorbeeldgebruiker en per groep) en slaat over / waarschuwt wat niet in de cloud
    aangemaakt of gewijzigd mag worden.

.NOTES
    Vereiste modules (PowerShell 5.1 of 7):
        Microsoft.Graph.Authentication
        Microsoft.Graph.Users
        Microsoft.Graph.Groups
        Microsoft.Graph.Identity.DirectoryManagement
        ExchangeOnlineManagement
    Alle Microsoft.Graph.*-submodules MOETEN exact dezelfde versie hebben als
    Microsoft.Graph.Authentication. Het script laadt Authentication eerst en pint de
    submodules op die versie. Licenties worden via Invoke-MgGraphRequest toegekend, zodat
    de losse module Microsoft.Graph.Users.Actions (die vaak achterloopt) niet nodig is.
    Importeer NOOIT de meta-module 'Microsoft.Graph' (traag / "Function capacity 4096
    exceeded" op PS 5.1). Getest met Microsoft.Graph 2.x.

    Auteur: SurfHost

.EXAMPLE
    .\Clone-M365UserRights.ps1
    Vraagt interactief om alle gegevens.

.EXAMPLE
    .\Clone-M365UserRights.ps1 -WhatIf
    Toont de volledige preview (afgeleide naam/UPN, groepen, licenties, mailboxen)
    zonder iets te wijzigen.

.EXAMPLE
    .\Clone-M365UserRights.ps1 -TemplateUpn voorbeeld.gebruiker@bedrijf.nl `
        -NewFirstName Voornaam -NewLastName Achternaam
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [string]$TemplateUpn,
    [string]$NewFirstName,
    [string]$NewLastName,
    [string]$Tenant,
    [string]$UsageLocationDefault = 'NL',
    [int]$MailboxWaitMinutes = 15,
    [switch]$NoAutoMapping,
    [switch]$SkipMailboxPermissions,
    [switch]$NoWait,
    [switch]$AdoptExistingUser,
    [switch]$Force
)

$ErrorActionPreference = 'Stop'

# ============================================================================
#  Helpers
# ============================================================================

function Write-Section { param([string]$Title) Write-Host "`n=== $Title ===" -ForegroundColor Cyan }
function Write-Warn    { param([string]$Msg)   Write-Host "  ! $Msg" -ForegroundColor Yellow }
function Write-Ok      { param([string]$Msg)   Write-Host "  + $Msg" -ForegroundColor Green }
function Write-Skip    { param([string]$Msg)   Write-Host "  - $Msg" -ForegroundColor DarkGray }
function Write-Fail    { param([string]$Msg)   Write-Host "  x $Msg" -ForegroundColor Red }

function Get-AP {
    # Veilig een sleutel uit AdditionalProperties van een Graph directoryObject lezen.
    param($Object, [string]$Key)
    if ($Object -and $Object.AdditionalProperties -and $Object.AdditionalProperties.ContainsKey($Key)) {
        return $Object.AdditionalProperties[$Key]
    }
    return $null
}

function ConvertTo-AsciiLower {
    # Diacrieten strippen (e-accent->e, o-umlaut->o), lowercase, spaties weg.
    # Let op: tekens als ss/o/ae/d/l-varianten hebben geen canonieke decompositie; voeg een expliciete
    # mapping toe als Duitse/Noordse tenants relevant worden.
    param([string]$Text)
    if (-not $Text) { return '' }
    $norm = $Text.Normalize([System.Text.NormalizationForm]::FormD)
    $sb = [System.Text.StringBuilder]::new()
    foreach ($ch in $norm.ToCharArray()) {
        $cat = [System.Globalization.CharUnicodeInfo]::GetUnicodeCategory($ch)
        if ($cat -ne [System.Globalization.UnicodeCategory]::NonSpacingMark) {
            [void]$sb.Append($ch)
        }
    }
    $clean = $sb.ToString().Normalize([System.Text.NormalizationForm]::FormC)
    return ($clean.ToLowerInvariant() -replace '\s', '')
}

function New-PronounceablePassword {
    # Patroon: hoofdletter-medeklinker + 3x(klinker+medeklinker) + 1 extra klinker
    #          (= 8 letters, eindigt op klinker) + 5 UNIEKE cijfers + "!".
    # Voorbeeld: Bofelaxi60354!
    $vowels     = 'aeiou'
    $consonants = 'bcdfghjklmnpqrstvwxyz'
    $sb = [System.Text.StringBuilder]::new()
    [void]$sb.Append(([string]$consonants[(Get-Random -Maximum $consonants.Length)]).ToUpper())
    for ($i = 0; $i -lt 3; $i++) {
        [void]$sb.Append($vowels[(Get-Random -Maximum $vowels.Length)])
        [void]$sb.Append($consonants[(Get-Random -Maximum $consonants.Length)])
    }
    [void]$sb.Append($vowels[(Get-Random -Maximum $vowels.Length)])          # extra klinker
    [void]$sb.Append(((Get-Random -InputObject (0..9) -Count 5) -join ''))   # 5 unieke cijfers
    [void]$sb.Append('!')
    return $sb.ToString()
}

function Get-DerivedDisplayName {
    <#
        Leidt de weergavenaam van de nieuwe gebruiker af van die van de voorbeeldgebruiker.
        "Voornaam Achternaam | Bedrijfsnaam BV"  ->  "NieuweVoornaam NieuweAchternaam | Bedrijfsnaam BV"
        Detecteert de naamstructuur (G S / S G / "S, G" / enkel) en behoudt prefix + suffix.
    #>
    param(
        [string]$DisplayName, [string]$Given, [string]$Surname,
        [string]$NewFirst, [string]$NewLast
    )
    $result = [pscustomobject]@{ DisplayName = "$NewFirst $NewLast"; Structure = 'fallback'; Prefix = ''; Suffix = ''; Matched = $false }
    if (-not $DisplayName) { return $result }

    # Kandidaten van langst naar kortst zodat de achternaam de affix niet "afknipt".
    $candidates = @()
    if ($Given -and $Surname) {
        $candidates += @{ Form = "$Given $Surname";  Structure = 'GS';   Build = "$NewFirst $NewLast" }
        $candidates += @{ Form = "$Surname $Given";  Structure = 'SG';   Build = "$NewLast $NewFirst" }
        $candidates += @{ Form = "$Surname, $Given"; Structure = 'S, G'; Build = "$NewLast, $NewFirst" }
        $candidates += @{ Form = "$Surname,$Given";  Structure = 'S,G';  Build = "$NewLast,$NewFirst" }
    }
    if ($Given)   { $candidates += @{ Form = $Given;   Structure = 'G'; Build = $NewFirst } }
    if ($Surname) { $candidates += @{ Form = $Surname; Structure = 'S'; Build = $NewLast } }

    foreach ($c in $candidates) {
        if (-not $c.Form) { continue }
        $idx = $DisplayName.IndexOf($c.Form, [System.StringComparison]::OrdinalIgnoreCase)
        if ($idx -ge 0) {
            $prefix = $DisplayName.Substring(0, $idx)
            $suffix = $DisplayName.Substring($idx + $c.Form.Length)
            $result.DisplayName = "$prefix$($c.Build)$suffix"
            $result.Structure   = $c.Structure
            $result.Prefix      = $prefix
            $result.Suffix      = $suffix
            $result.Matched     = $true
            return $result
        }
    }
    return $result   # geen match -> fallback "First Last"
}

function Get-LocalPartPattern {
    # Detecteert het local-part-patroon van de voorbeeld-UPN t.o.v. voor-/achternaam.
    param([string]$LocalPart, [string]$Given, [string]$Surname)
    $g = ConvertTo-AsciiLower $Given
    $s = ConvertTo-AsciiLower $Surname
    if (-not $g -or -not $s) { return $null }
    $lp = $LocalPart.ToLowerInvariant()
    $g1 = $g.Substring(0,1); $s1 = $s.Substring(0,1)
    $patterns = [ordered]@{
        'given.surname' = "$g.$s"
        'surname.given' = "$s.$g"
        'g.surname'     = "$g1.$s"
        'surname.g'     = "$s.$g1"
        'given_surname' = "${g}_$s"
        'givensurname'  = "$g$s"
        'surnamegiven'  = "$s$g"
        'gsurname'      = "$g1$s"
        'surnameg'      = "$s$g1"
    }
    foreach ($key in $patterns.Keys) {
        if ($patterns[$key] -eq $lp) { return $key }
    }
    return $null
}

function Build-LocalPart {
    param([string]$Pattern, [string]$Given, [string]$Surname)
    $g = ConvertTo-AsciiLower $Given
    $s = ConvertTo-AsciiLower $Surname
    $g1 = if ($g) { $g.Substring(0,1) } else { '' }
    $s1 = if ($s) { $s.Substring(0,1) } else { '' }
    switch ($Pattern) {
        'given.surname' { "$g.$s" }
        'surname.given' { "$s.$g" }
        'g.surname'     { "$g1.$s" }
        'surname.g'     { "$s.$g1" }
        'given_surname' { "${g}_$s" }
        'givensurname'  { "$g$s" }
        'surnamegiven'  { "$s$g" }
        'gsurname'      { "$g1$s" }
        'surnameg'      { "$s$g1" }
        default         { "$g.$s" }   # fallback
    }
}

function Test-IsTemplatePrincipal {
    # Bepaalt of een permissie-principal (User/Trustee/SendOnBehalf-entry) de voorbeeldgebruiker is.
    param($PrincipalString)
    if (-not $PrincipalString) { return $false }
    $p = ("$PrincipalString").ToLowerInvariant()
    if ($script:TplIds.Contains($p)) { return $true }
    # Fallback: definitief oplossen via EXO en object-id vergelijken.
    try {
        $r = Get-Recipient -Identity $PrincipalString -ErrorAction Stop
        return ("$($r.ExternalDirectoryObjectId)" -eq $script:TplObjId)
    } catch { return $false }
}

function Test-IsRoleAssignableGroup {
    # True als de groep aan directory-rollen gekoppeld kan zijn (isAssignableToRole).
    # Zulke groepen kunnen admin-rechten geven; daarom NIET automatisch overnemen.
    param($MemberObject)
    $val = Get-AP $MemberObject 'isAssignableToRole'
    if ($null -ne $val) { return [bool]$val }
    # Niet aanwezig in de memberOf-projectie -> gericht ophalen.
    try {
        $g = Get-MgGroup -GroupId $MemberObject.Id -Property IsAssignableToRole -ErrorAction Stop
        return [bool]$g.IsAssignableToRole
    } catch { return $false }
}

# ============================================================================
#  Preflight: modules
# ============================================================================

# Alle Microsoft.Graph.*-submodules moeten exact dezelfde versie hebben als
# Microsoft.Graph.Authentication. Laad Authentication eerst en pin de submodules op
# die versie (voorkomt "module 'Microsoft.Graph.Authentication' version X is not loaded").
if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication)) {
    Write-Fail "Microsoft.Graph.Authentication ontbreekt. Installeer: Install-Module Microsoft.Graph -Scope CurrentUser"
    return
}
$loadedAuth = Get-Module Microsoft.Graph.Authentication
if ($loadedAuth) {
    $authVer = $loadedAuth.Version          # hergebruik reeds geladen versie (vermijdt conflicten)
} else {
    $authVer = (Get-Module -ListAvailable Microsoft.Graph.Authentication |
                Sort-Object Version -Descending | Select-Object -First 1).Version
    Import-Module Microsoft.Graph.Authentication -RequiredVersion $authVer -ErrorAction Stop
}
Write-Host "  Microsoft.Graph.Authentication $authVer geladen" -ForegroundColor DarkGray

$graphSubs = @('Microsoft.Graph.Users','Microsoft.Graph.Groups','Microsoft.Graph.Identity.DirectoryManagement')
$badSubs = @()
foreach ($m in $graphSubs) {
    if (Get-Module -ListAvailable -Name $m | Where-Object { $_.Version -eq $authVer }) {
        Import-Module $m -RequiredVersion $authVer -ErrorAction Stop
    } else {
        $badSubs += $m
    }
}
if ($badSubs.Count) {
    Write-Fail "Versie-mismatch: Microsoft.Graph.Authentication = $authVer, maar deze submodule(s) staan niet op die versie:"
    $badSubs | ForEach-Object { Write-Host "    - $_" -ForegroundColor Yellow }
    Write-Host "  Trek alle Graph-modules gelijk, bijv.:" -ForegroundColor Yellow
    Write-Host "    Update-Module Microsoft.Graph -Force" -ForegroundColor Yellow
    Write-Host "    of: Install-Module Microsoft.Graph -RequiredVersion $authVer -Force -Scope CurrentUser" -ForegroundColor Yellow
    return
}

if (-not (Get-Module -ListAvailable -Name ExchangeOnlineManagement)) {
    Write-Fail "ExchangeOnlineManagement ontbreekt. Installeer: Install-Module ExchangeOnlineManagement -Scope CurrentUser"
    return
}
Import-Module ExchangeOnlineManagement -ErrorAction Stop

# ============================================================================
#  Invoer
# ============================================================================

if (-not $Tenant)       { $Tenant       = Read-Host -Prompt 'Input the tenant admin account' }
if (-not $NewFirstName) { $NewFirstName = Read-Host -Prompt 'Voornaam nieuwe gebruiker' }
if (-not $NewLastName)  { $NewLastName  = Read-Host -Prompt 'Achternaam nieuwe gebruiker' }
if (-not $TemplateUpn)  { $TemplateUpn  = Read-Host -Prompt 'UPN van de voorbeeldgebruiker' }

$NewFirstName = $NewFirstName.Trim()
$NewLastName  = $NewLastName.Trim()
$TemplateUpn  = $TemplateUpn.Trim()

# ============================================================================
#  1. Verbinden (Graph + Exchange Online)
# ============================================================================

Write-Section '1. Verbinden met Microsoft 365'
$scopes = @(
    'User.ReadWrite.All',
    'Group.ReadWrite.All',
    'GroupMember.ReadWrite.All',
    'Directory.Read.All',
    'Organization.Read.All',
    'RoleManagement.Read.Directory',
    'LicenseAssignment.ReadWrite.All'
)
Write-Host "  Verbinden met Microsoft Graph..." -ForegroundColor Cyan
Connect-MgGraph -Scopes $scopes -NoWelcome -ErrorAction Stop | Out-Null

Write-Host "  Verbinden met Exchange Online ($Tenant)..." -ForegroundColor Cyan
Connect-ExchangeOnline -UserPrincipalName $Tenant -ShowBanner:$false -ErrorAction Stop | Out-Null

try { Get-OrganizationConfig -ErrorAction Stop | Out-Null }
catch { Write-Fail "Exchange Online-sessie niet bruikbaar: $($_.Exception.Message)"; return }
Write-Ok 'Verbonden met Graph en Exchange Online.'

$warnings = [System.Collections.Generic.List[string]]::new()

# ============================================================================
#  2. Voorbeeldgebruiker uitlezen
# ============================================================================

Write-Section '2. Voorbeeldgebruiker uitlezen'
$tplProps = 'Id','DisplayName','GivenName','Surname','UserPrincipalName','Mail','MailNickname',
            'UsageLocation','OnPremisesSyncEnabled','AccountEnabled','ProxyAddresses',
            'AssignedLicenses','LicenseAssignmentStates'
try {
    $tpl = Get-MgUser -UserId $TemplateUpn -Property $tplProps -ErrorAction Stop
} catch {
    Write-Fail "Voorbeeldgebruiker '$TemplateUpn' niet gevonden: $($_.Exception.Message)"; return
}

$tplMbx = $null
try { $tplMbx = Get-Mailbox -Identity $tpl.Id -ErrorAction Stop } catch { }
$tplPrimarySmtp = if ($tplMbx) { "$($tplMbx.PrimarySmtpAddress)" } elseif ($tpl.Mail) { $tpl.Mail } else { $tpl.UserPrincipalName }

Write-Host "  Naam      : $($tpl.DisplayName)"
Write-Host "  UPN       : $($tpl.UserPrincipalName)"
Write-Host "  Primair   : $tplPrimarySmtp"
Write-Host "  Voor/Achter: '$($tpl.GivenName)' / '$($tpl.Surname)'"

if ($tpl.OnPremisesSyncEnabled -eq $true) {
    $msg = "Voorbeeldgebruiker is ON-PREM GESYNCED. Een nieuwe gebruiker hoort dan in on-prem AD aangemaakt en gesynchroniseerd te worden. Dit script maakt een CLOUD-ONLY account - ga alleen door als dat bewust is."
    Write-Warn $msg
    $warnings.Add($msg)
}
if (-not $tpl.GivenName -or -not $tpl.Surname) {
    $msg = "Voorbeeldgebruiker mist GivenName en/of Surname; naam-/UPN-afleiding is heuristisch (val terug op weergavenaam splitsen)."
    Write-Warn $msg
    $warnings.Add($msg)
}

# ============================================================================
#  3. Weergavenaam afleiden
# ============================================================================

Write-Section '3. Weergavenaam afleiden'
$dn = Get-DerivedDisplayName -DisplayName $tpl.DisplayName -Given $tpl.GivenName -Surname $tpl.Surname `
        -NewFirst $NewFirstName -NewLast $NewLastName
$NewDisplayName = $dn.DisplayName
if ($dn.Matched) {
    Write-Ok "Structuur '$($dn.Structure)', affix-suffix: '$($dn.Suffix)'"
} else {
    $msg = "Kon naamstructuur niet matchen op weergavenaam; val terug op '$NewFirstName $NewLastName' zonder affix."
    Write-Warn $msg; $warnings.Add($msg)
}
Write-Host "  Nieuwe weergavenaam: $NewDisplayName" -ForegroundColor White

# ============================================================================
#  4. UPN + e-mail afleiden + collision-check
# ============================================================================

Write-Section '4. UPN en e-mail afleiden'
$tplUpnLocal = ($tpl.UserPrincipalName -split '@', 2)[0]
$upnDomain   = ($tpl.UserPrincipalName -split '@', 2)[1]
$smtpDomain  = ($tplPrimarySmtp        -split '@', 2)[1]

$pattern = Get-LocalPartPattern -LocalPart $tplUpnLocal -Given $tpl.GivenName -Surname $tpl.Surname
if (-not $pattern) {
    $pattern = 'given.surname'
    $msg = "Local-part-patroon van de voorbeeld-UPN niet herkend; val terug op 'given.surname'."
    Write-Warn $msg; $warnings.Add($msg)
} else {
    Write-Ok "Local-part-patroon: '$pattern'"
}
$newLocal       = Build-LocalPart -Pattern $pattern -Given $NewFirstName -Surname $NewLastName
$NewUpn         = "$newLocal@$upnDomain"
$NewPrimarySmtp = "$newLocal@$smtpDomain"
$MailNickname   = $newLocal

Write-Host "  Nieuwe UPN        : $NewUpn" -ForegroundColor White
Write-Host "  Verwacht primair  : $NewPrimarySmtp" -ForegroundColor White
if ($upnDomain -ne $smtpDomain) {
    $msg = "UPN-domein ($upnDomain) verschilt van primair SMTP-domein ($smtpDomain). Het werkelijke primaire SMTP-adres wordt door het e-mailadresbeleid bepaald; controleer/zet dit zo nodig handmatig na provisioning."
    Write-Warn $msg; $warnings.Add($msg)
}

# Idempotentie / collision
$existingUser = $null
try { $existingUser = Get-MgUser -UserId $NewUpn -Property Id,DisplayName,Mail,UserPrincipalName,AccountEnabled,CreatedDateTime -ErrorAction Stop } catch { }
if ($existingUser) {
    Write-Warn "Er bestaat AL een gebruiker met UPN ${NewUpn}:"
    Write-Host ("      Weergavenaam : {0}" -f $existingUser.DisplayName)  -ForegroundColor Yellow
    Write-Host ("      Mail         : {0}" -f $existingUser.Mail)         -ForegroundColor Yellow
    Write-Host ("      Ingeschakeld : {0}" -f $existingUser.AccountEnabled) -ForegroundColor Yellow
    Write-Host ("      Aangemaakt   : {0}" -f $existingUser.CreatedDateTime) -ForegroundColor Yellow
    Write-Warn "Er wordt GEEN nieuw account gemaakt; rechten zouden aan DIT bestaande account worden toegevoegd. Controleer of dit echt dezelfde persoon is."
}
try {
    $smtpOwner = Get-Recipient -Identity $NewPrimarySmtp -ErrorAction Stop
    if (-not $existingUser -or "$($smtpOwner.ExternalDirectoryObjectId)" -ne "$($existingUser.Id)") {
        Write-Fail "Het adres $NewPrimarySmtp is al in gebruik door een andere recipient ($($smtpOwner.DisplayName)). Conflict - gestopt."
        return
    }
} catch { }   # niet gevonden = vrij

# ============================================================================
#  5. Groepslidmaatschappen uitlezen + classificeren
# ============================================================================

Write-Section '5. Groepslidmaatschappen uitlezen'
$memberOf = Get-MgUserMemberOf -UserId $tpl.Id -All

$planCloudGroups = [System.Collections.Generic.List[object]]::new()   # via Graph
$planDLGroups    = [System.Collections.Generic.List[object]]::new()   # via EXO
$planSkipGroups  = [System.Collections.Generic.List[object]]::new()
$planRoleGroups  = [System.Collections.Generic.List[string]]::new()   # rol-toewijsbaar: NIET auto-toevoegen
$planRoles       = [System.Collections.Generic.List[string]]::new()

foreach ($mObj in $memberOf) {
    $odata = Get-AP $mObj '@odata.type'
    $name  = Get-AP $mObj 'displayName'
    if (-not $name) { $name = $mObj.Id }

    if ($odata -eq '#microsoft.graph.directoryRole') {
        $planRoles.Add($name); continue
    }
    if ($odata -ne '#microsoft.graph.group') { continue }   # bv. administrativeUnit -> negeren

    $groupTypes = @(Get-AP $mObj 'groupTypes')
    $mailEnabled     = [bool](Get-AP $mObj 'mailEnabled')
    $securityEnabled = [bool](Get-AP $mObj 'securityEnabled')
    $onPremSync      = (Get-AP $mObj 'onPremisesSyncEnabled') -eq $true
    $ruleState       = Get-AP $mObj 'membershipRuleProcessingState'
    $isDynamic = ($groupTypes -contains 'DynamicMembership') -or ($ruleState -eq 'On')
    $isUnified = ($groupTypes -contains 'Unified')

    if ($onPremSync) {
        $planSkipGroups.Add([pscustomobject]@{ Name = $name; Reason = 'on-prem gesynced (lid toevoegen in on-prem AD)' }); continue
    }
    if ($isDynamic) {
        $planSkipGroups.Add([pscustomobject]@{ Name = $name; Reason = 'dynamische groep (regel-gebaseerd lidmaatschap)' }); continue
    }
    if (($securityEnabled -or $isUnified) -and (Test-IsRoleAssignableGroup $mObj)) {
        # Rol-toewijsbare groep kan directory-/adminrollen geven; NIET automatisch toevoegen.
        $planRoleGroups.Add($name); continue
    }
    if ($isUnified) {
        $planCloudGroups.Add([pscustomobject]@{ Id = $mObj.Id; Name = $name; Kind = 'Microsoft 365' }); continue
    }
    if ($mailEnabled) {
        # Distributielijst of mail-enabled security -> via Exchange Online
        $kind = if ($securityEnabled) { 'Mail-enabled security' } else { 'Distributielijst' }
        $planDLGroups.Add([pscustomobject]@{ Id = $mObj.Id; Name = $name; Kind = $kind }); continue
    }
    if ($securityEnabled) {
        $planCloudGroups.Add([pscustomobject]@{ Id = $mObj.Id; Name = $name; Kind = 'Security' }); continue
    }
    $planSkipGroups.Add([pscustomobject]@{ Name = $name; Reason = 'onbekend groepstype' })
}
Write-Ok "$($planCloudGroups.Count) cloud-groep(en), $($planDLGroups.Count) distributie/mail-enabled, $($planSkipGroups.Count) overgeslagen."
if ($planRoleGroups.Count) {
    $warnings.Add("$($planRoleGroups.Count) rol-toewijsbare groep(en) NIET automatisch toegevoegd (mogelijke admin-rechten via groep); beoordeel handmatig.")
}

# ============================================================================
#  6. Licenties uitlezen + classificeren (direct vs groep-gebaseerd)
# ============================================================================

Write-Section '6. Licenties uitlezen'
$subSkus = @(Get-MgSubscribedSku -All)
$skuMap = @{}
foreach ($sku in $subSkus) {
    $skuMap["$($sku.SkuId)"] = [pscustomobject]@{
        PartNumber   = $sku.SkuPartNumber
        Available    = ($sku.PrepaidUnits.Enabled - $sku.ConsumedUnits)
        ServicePlans = $sku.ServicePlans
    }
}
# DisabledPlans per SKU van de voorbeeldgebruiker (om service-plans exact te spiegelen).
$tplDisabled = @{}
foreach ($al in @($tpl.AssignedLicenses)) { $tplDisabled["$($al.SkuId)"] = @($al.DisabledPlans) }

$directSkus = @($tpl.LicenseAssignmentStates | Where-Object { -not $_.AssignedByGroup } | Select-Object -ExpandProperty SkuId -Unique)
$groupSkus  = @($tpl.LicenseAssignmentStates | Where-Object { $_.AssignedByGroup }      | Select-Object -ExpandProperty SkuId -Unique)

$planLicAssign = [System.Collections.Generic.List[object]]::new()
$planLicSkip   = [System.Collections.Generic.List[object]]::new()

foreach ($skuId in $directSkus) {
    $part = if ($skuMap.ContainsKey("$skuId")) { $skuMap["$skuId"].PartNumber } else { "$skuId" }
    $avail = if ($skuMap.ContainsKey("$skuId")) { $skuMap["$skuId"].Available } else { 0 }
    if ($avail -le 0) {
        $planLicSkip.Add([pscustomobject]@{ Part = $part; Reason = "geen vrije seats (beschikbaar: $avail)" }); continue
    }
    $planLicAssign.Add([pscustomobject]@{ SkuId = "$skuId"; Part = $part; DisabledPlans = $tplDisabled["$skuId"] })
}
foreach ($skuId in $groupSkus) {
    if ($directSkus -contains $skuId) { continue }   # ook direct -> wordt al toegewezen
    $part = if ($skuMap.ContainsKey("$skuId")) { $skuMap["$skuId"].PartNumber } else { "$skuId" }
    $planLicSkip.Add([pscustomobject]@{ Part = $part; Reason = 'groep-gebaseerd (volgt automatisch via groepslidmaatschap)' })
}
Write-Ok "$($planLicAssign.Count) licentie(s) toe te wijzen, $($planLicSkip.Count) overgeslagen."

# Waarschuw als de Exchange Online-mailboxplan in een toe te wijzen licentie is UITGEZET
# (overgenomen van de voorbeeldgebruiker): dan krijgt de nieuwe gebruiker GEEN mailbox, falen
# de mailbox/DL/Send As-stappen, en kan Exchange 'recipient not found' tonen in het admin center.
foreach ($lic in $planLicAssign) {
    if (-not $lic.DisabledPlans -or @($lic.DisabledPlans).Count -eq 0) { continue }
    $plans = if ($skuMap.ContainsKey($lic.SkuId)) { $skuMap[$lic.SkuId].ServicePlans } else { @() }
    foreach ($dp in @($lic.DisabledPlans)) {
        $pl = $plans | Where-Object { "$($_.ServicePlanId)" -eq "$dp" }
        if ($pl -and $pl.ServicePlanName -match '^EXCHANGE_S_(ENTERPRISE|STANDARD|DESKLESS|ESSENTIALS)$') {
            $msg = "Exchange Online ($($pl.ServicePlanName)) staat UIT in licentie '$($lic.Part)' (overgenomen van de voorbeeldgebruiker). De nieuwe gebruiker krijgt dan GEEN mailbox; gedeelde mailboxen/DL's/Send As worden niet voltooid en Exchange kan 'recipient not found' tonen."
            Write-Warn $msg; $warnings.Add($msg)
        }
    }
}

# ============================================================================
#  7. Gedeelde mailboxen / Send As / Send on Behalf uitlezen (een pass)
# ============================================================================

Write-Section '7. Mailboxmachtigingen van de voorbeeldgebruiker scannen'
$Username = $tpl.UserPrincipalName    # niet opnieuw vragen - afgeleid van de voorbeeldgebruiker

# Identificatie-set voor snelle, definitieve match (vermijdt onnodige Get-Recipient calls).
$script:TplObjId = "$($tpl.Id)"
$script:TplIds = New-Object 'System.Collections.Generic.HashSet[string]'
foreach ($v in @($tpl.UserPrincipalName, $tplPrimarySmtp, $tpl.Mail, $tpl.Id,
                 $tplMbx.Alias, $tplMbx.Name, $tplMbx.DisplayName, "$($tplMbx.ExchangeGuid)")) {
    if ($v) { [void]$script:TplIds.Add(("$v").ToLowerInvariant()) }
}

$planMbxFull   = [System.Collections.Generic.List[object]]::new()
$planMbxSendAs = [System.Collections.Generic.List[object]]::new()
$planMbxSoB    = [System.Collections.Generic.List[object]]::new()

Write-Host "  Alle mailboxen ophalen..." -ForegroundColor Cyan
$mailboxes = @(Get-Mailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox,SharedMailbox)
$i = 0; $total = $mailboxes.Count
foreach ($mailbox in $mailboxes) {
    $i++
    Write-Progress -Activity "Mailboxmachtigingen controleren" `
        -Status "$i van $total - $($mailbox.PrimarySmtpAddress)" `
        -PercentComplete (($i / [Math]::Max($total,1)) * 100)

    $guid = $mailbox.ExchangeGuid.ToString()
    $row = [pscustomobject]@{ DisplayName = $mailbox.DisplayName; PrimarySmtpAddress = "$($mailbox.PrimarySmtpAddress)"; Guid = $guid }

    # Full Access (exact het patroon uit de aangeleverde snippet, daarna definitief verifieren)
    $permissions = Get-MailboxPermission -Identity $guid -ErrorAction SilentlyContinue |
        Where-Object {
            -not $_.IsInherited -and
            $_.AccessRights -ne $null -and
            ($_.User -like "*$Username*" -or $_.User -eq $Username)
        }
    foreach ($perm in $permissions) {
        if (($perm.AccessRights -contains 'FullAccess') -and (Test-IsTemplatePrincipal $perm.User)) {
            $planMbxFull.Add($row); break
        }
    }

    # Send As
    $sa = Get-RecipientPermission -Identity $guid -ErrorAction SilentlyContinue |
        Where-Object {
            $_.AccessRights -contains 'SendAs' -and
            ($_.Trustee -like "*$Username*" -or $_.Trustee -eq $Username)
        }
    foreach ($p in $sa) {
        if (Test-IsTemplatePrincipal $p.Trustee) { $planMbxSendAs.Add($row); break }
    }

    # Send on Behalf (GrantSendOnBehalfTo bevat canonieke namen)
    foreach ($d in @($mailbox.GrantSendOnBehalfTo)) {
        if (Test-IsTemplatePrincipal $d) { $planMbxSoB.Add($row); break }
    }
}
Write-Progress -Activity "Mailboxmachtigingen controleren" -Completed
Write-Ok "Full Access: $($planMbxFull.Count), Send As: $($planMbxSendAs.Count), Send on Behalf: $($planMbxSoB.Count)"

# ============================================================================
#  8. Preview + bevestiging
# ============================================================================

Write-Section 'PREVIEW - wat gaat er gebeuren'
Write-Host "  Nieuwe gebruiker:" -ForegroundColor White
Write-Host "    Weergavenaam : $NewDisplayName"
Write-Host "    UPN          : $NewUpn"
Write-Host "    Primair SMTP : $NewPrimarySmtp (verwacht)"
Write-Host "    MailNickname : $MailNickname"
Write-Host "    UsageLocation: $(if ($tpl.UsageLocation) { $tpl.UsageLocation } else { $UsageLocationDefault })"
if ($existingUser) { Write-Warn "Account bestaat al - rechten worden aan het bestaande account toegewezen." }

Write-Host "`n  Cloud-groepen (Graph): $($planCloudGroups.Count)" -ForegroundColor White
$planCloudGroups | ForEach-Object { Write-Host "    - $($_.Name)  [$($_.Kind)]" }
Write-Host "  Distributie / mail-enabled (EXO): $($planDLGroups.Count)" -ForegroundColor White
$planDLGroups | ForEach-Object { Write-Host "    - $($_.Name)  [$($_.Kind)]" }
if ($planSkipGroups.Count) {
    Write-Host "  Overgeslagen groepen: $($planSkipGroups.Count)" -ForegroundColor Yellow
    $planSkipGroups | ForEach-Object { Write-Host "    - $($_.Name)  ($($_.Reason))" -ForegroundColor Yellow }
}
if ($planRoleGroups.Count) {
    Write-Host "  ROL-toewijsbare groepen (NIET automatisch toegevoegd - kunnen admin-rechten geven): $($planRoleGroups.Count)" -ForegroundColor Magenta
    $planRoleGroups | ForEach-Object { Write-Host "    - $_" -ForegroundColor Magenta }
}

Write-Host "`n  Licenties toewijzen: $($planLicAssign.Count)" -ForegroundColor White
$planLicAssign | ForEach-Object { Write-Host "    - $($_.Part)" }
if ($planLicSkip.Count) {
    Write-Host "  Licenties NIET toegewezen: $($planLicSkip.Count)" -ForegroundColor Yellow
    $planLicSkip | ForEach-Object { Write-Host "    - $($_.Part)  ($($_.Reason))" -ForegroundColor Yellow }
}

Write-Host "`n  Gedeelde mailboxen:" -ForegroundColor White
Write-Host "    Full Access ($(if ($NoAutoMapping) {'GEEN automapping'} else {'automapping AAN'})): $($planMbxFull.Count)"
$planMbxFull   | ForEach-Object { Write-Host "      - $($_.PrimarySmtpAddress)" }
Write-Host "    Send As: $($planMbxSendAs.Count)"
$planMbxSendAs | ForEach-Object { Write-Host "      - $($_.PrimarySmtpAddress)" }
Write-Host "    Send on Behalf: $($planMbxSoB.Count)"
$planMbxSoB    | ForEach-Object { Write-Host "      - $($_.PrimarySmtpAddress)" }

if ($planRoles.Count) {
    Write-Host "`n  Directory-/adminrollen (NIET gekopieerd - handmatig toewijzen indien nodig):" -ForegroundColor Magenta
    $planRoles | ForEach-Object { Write-Host "    - $_" -ForegroundColor Magenta }
}
if ($warnings.Count) {
    Write-Host "`n  Waarschuwingen:" -ForegroundColor Yellow
    $warnings | ForEach-Object { Write-Host "    ! $_" -ForegroundColor Yellow }
}

# LET OP: dit is de ENIGE -WhatIf-poort. Boven dit punt wordt NIETS geschreven;
# voeg nooit een schrijfactie toe boven deze regel.
if ($WhatIfPreference) {
    Write-Host "`n[WhatIf] Geen wijzigingen uitgevoerd. Bovenstaande preview toont wat er zou gebeuren." -ForegroundColor Yellow
    Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
    return
}
# Veiligheid: een BESTAAND account "adopteren" mag nooit stilzwijgend gebeuren,
# ook niet met -Force. Anders zou een toevallig botsende UPN (naamgenoot, oud-
# medewerker) ongemerkt alle rechten van de voorbeeldgebruiker krijgen.
if ($existingUser -and -not $AdoptExistingUser) {
    if ($Force) {
        Write-Fail "UPN $NewUpn bestaat al ($($existingUser.DisplayName)). -Force past een BESTAAND account niet automatisch aan. Herhaal met -AdoptExistingUser als je bewust rechten aan dit bestaande account wilt toevoegen."
        Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
        Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
        return
    }
    $confirm = Read-Host "`nLET OP: $NewUpn bestaat al ($($existingUser.DisplayName)). Typ exact de UPN om rechten aan dit BESTAANDE account toe te voegen (anders Enter = annuleren)"
    if ($confirm -ne $NewUpn) {
        Write-Host 'Geannuleerd (bestaand account niet bevestigd).' -ForegroundColor Yellow
        Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
        Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
        return
    }
}
if (-not $Force) {
    $ans = Read-Host "`nDoorgaan: account aanmaken + rechten toewijzen? (Y/N)"
    if ($ans -notmatch '^(y|yes|j|ja)$') {
        Write-Host 'Geannuleerd.' -ForegroundColor Yellow
        Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
        Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
        return
    }
}

# ============================================================================
#  9. Account aanmaken (of bestaand gebruiken)
# ============================================================================

# Resultaat-trackers voor het eindoverzicht
$resGroups = [System.Collections.Generic.List[string]]::new()
$resLics   = [System.Collections.Generic.List[string]]::new()
$resMbx    = [System.Collections.Generic.List[string]]::new()
$generatedPassword = $null

Write-Section '9. Account aanmaken'
if ($existingUser) {
    $newUser = $existingUser
    Write-Skip "Bestaand account gebruikt: $NewUpn"
} else {
    $generatedPassword = New-PronounceablePassword
    $usageLoc = if ($tpl.UsageLocation) { $tpl.UsageLocation } else { $UsageLocationDefault }
    $body = @{
        AccountEnabled    = $true
        DisplayName       = $NewDisplayName
        GivenName         = $NewFirstName
        Surname           = $NewLastName
        MailNickname      = $MailNickname
        UserPrincipalName = $NewUpn
        UsageLocation     = $usageLoc
        PasswordProfile   = @{
            ForceChangePasswordNextSignIn = $false
            Password                      = $generatedPassword
        }
    }
    $newUser = New-MgUser -BodyParameter $body -ErrorAction Stop
    # Teruglezen en kritieke velden controleren (typefout in body-sleutel wordt anders stil genegeerd).
    $check = Get-MgUser -UserId $newUser.Id -Property DisplayName,UserPrincipalName,MailNickname -ErrorAction Stop
    if ($check.UserPrincipalName -ne $NewUpn -or $check.DisplayName -ne $NewDisplayName) {
        Write-Warn "Aangemaakt account wijkt af van verwacht (UPN '$($check.UserPrincipalName)', naam '$($check.DisplayName)'). Controleer handmatig."
    }
    Write-Ok "Aangemaakt: $NewUpn (Id $($newUser.Id))"
}

# ============================================================================
#  10. Cloud-groepen toewijzen (Graph - direct)
# ============================================================================

Write-Section '10. Cloud-groepen toewijzen'
foreach ($g in $planCloudGroups) {
    try {
        New-MgGroupMember -GroupId $g.Id -DirectoryObjectId $newUser.Id -ErrorAction Stop
        Write-Ok "$($g.Name) [$($g.Kind)]"; $resGroups.Add("Toegevoegd: $($g.Name) [$($g.Kind)]")
    } catch {
        if ("$($_.Exception.Message)" -match 'already exist') {
            Write-Skip "$($g.Name) (was al lid)"; $resGroups.Add("Al lid: $($g.Name)")
        } else {
            Write-Fail "$($g.Name): $($_.Exception.Message)"; $resGroups.Add("MISLUKT: $($g.Name) - $($_.Exception.Message)")
        }
    }
}

# ============================================================================
#  11. Licenties toewijzen (Graph - direct)
# ============================================================================

Write-Section '11. Licenties toewijzen'
foreach ($lic in $planLicAssign) {
    try {
        # Toekennen via REST (Invoke-MgGraphRequest) i.p.v. Set-MgUserLicense, zodat
        # Microsoft.Graph.Users.Actions niet nodig is. JSON-sleutels in camelCase.
        $add = @{ skuId = $lic.SkuId }
        if ($lic.DisabledPlans -and @($lic.DisabledPlans).Count -gt 0) { $add['disabledPlans'] = @($lic.DisabledPlans) }
        $assignBody = @{ addLicenses = @($add); removeLicenses = @() }
        Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/users/$($newUser.Id)/assignLicense" `
            -Body $assignBody -ErrorAction Stop | Out-Null
        Write-Ok $lic.Part; $resLics.Add("Toegewezen: $($lic.Part)")
    } catch {
        Write-Fail "$($lic.Part): $($_.Exception.Message)"; $resLics.Add("MISLUKT: $($lic.Part) - $($_.Exception.Message)")
    }
}
foreach ($s in $planLicSkip) { $resLics.Add("Overgeslagen: $($s.Part) ($($s.Reason))") }

# ============================================================================
#  12. Wachten tot de nieuwe gebruiker zichtbaar is in Exchange Online
# ============================================================================

$needExo = (-not $SkipMailboxPermissions -and ($planMbxFull.Count + $planMbxSendAs.Count + $planMbxSoB.Count) -gt 0) `
           -or ($planDLGroups.Count -gt 0)
$exoReady = $false
# Kant-en-klaar herhaal-commando: account bestaat dan al, EXO is gesynct -> mailbox/DL meteen.
$rerunCmd = ".\Clone-M365UserRights.ps1 -TemplateUpn $TemplateUpn -NewFirstName ""$NewFirstName"" -NewLastName ""$NewLastName"" -AdoptExistingUser"

if ($needExo -and $NoWait) {
    $msg = "-NoWait opgegeven: niet gewacht op de Entra->EXO-sync. Gedeelde mailboxen en distributielijsten zijn UITGESTELD. Draai dit later nogmaals met:`n      $rerunCmd"
    Write-Warn $msg; $warnings.Add($msg)
}
elseif ($needExo) {
    Write-Section '12. Wachten op Entra->Exchange-synchronisatie'
    Write-Host "  Account, groepen en licenties zijn al toegepast. Alleen gedeelde mailboxen en" -ForegroundColor DarkGray
    Write-Host "  distributielijsten wachten tot de nieuwe gebruiker in Exchange Online verschijnt." -ForegroundColor DarkGray
    Write-Host "  Gaat verder zodra dat zo is (controle elke 15s); druk op een toets om over te slaan." -ForegroundColor DarkGray
    $deadline = (Get-Date).AddMinutes($MailboxWaitMinutes)
    $skipped  = $false
    while (-not $exoReady -and (Get-Date) -lt $deadline) {
        try { if (Get-Recipient -Identity $NewUpn -ErrorAction Stop) { $exoReady = $true; break } } catch { }
        $remaining = [int]([Math]::Max(0, ($deadline - (Get-Date)).TotalSeconds))
        Write-Progress -Activity "Wachten tot $NewUpn zichtbaar is in Exchange Online" `
            -Status "resterend: max $remaining s (toets = overslaan)" `
            -PercentComplete (100 - ($remaining / [Math]::Max($MailboxWaitMinutes*60,1) * 100))
        # Onderbreekbaar slapen (~15s): elke 0,5s kijken of er een toets is ingedrukt.
        $slept = 0.0
        while ($slept -lt 15) {
            try { if ([System.Console]::KeyAvailable) { [void][System.Console]::ReadKey($true); $skipped = $true; break } } catch { }
            Start-Sleep -Milliseconds 500; $slept += 0.5
        }
        if ($skipped) { break }
    }
    Write-Progress -Activity "Wachten op Exchange Online" -Completed
    if ($exoReady) {
        Write-Ok "Nieuwe gebruiker zichtbaar in Exchange Online."
    } else {
        $reason = if ($skipped) { "Wachten overgeslagen" } else { "Na $MailboxWaitMinutes min nog niet zichtbaar in EXO" }
        $msg = "$reason. Gedeelde mailboxen en distributielijsten zijn UITGESTELD. Draai dit later nogmaals (EXO is dan gesynct) met:`n      $rerunCmd"
        Write-Warn $msg; $warnings.Add($msg)
    }
}

# ============================================================================
#  13. Distributie / mail-enabled groepen (EXO - na de wait)
# ============================================================================

if ($planDLGroups.Count) {
    Write-Section '13. Distributie/mail-enabled groepen toewijzen'
    foreach ($g in $planDLGroups) {
        if (-not $exoReady) { Write-Skip "$($g.Name) (uitgesteld - EXO sync)"; $resGroups.Add("Uitgesteld (EXO sync): $($g.Name)"); continue }
        try {
            Add-DistributionGroupMember -Identity $g.Id -Member $NewUpn -ErrorAction Stop
            Write-Ok "$($g.Name) [$($g.Kind)]"; $resGroups.Add("Toegevoegd: $($g.Name) [$($g.Kind)]")
        } catch {
            if ("$($_.Exception.Message)" -match 'already a member') {
                Write-Skip "$($g.Name) (was al lid)"; $resGroups.Add("Al lid: $($g.Name)")
            } else {
                Write-Fail "$($g.Name): $($_.Exception.Message)"; $resGroups.Add("MISLUKT: $($g.Name) - $($_.Exception.Message)")
            }
        }
    }
}

# ============================================================================
#  14. Mailboxmachtigingen toewijzen (EXO - na de wait)
# ============================================================================

if (-not $SkipMailboxPermissions -and ($planMbxFull.Count + $planMbxSendAs.Count + $planMbxSoB.Count) -gt 0) {
    Write-Section '14. Mailboxmachtigingen toewijzen'
    $autoMap = -not $NoAutoMapping

    foreach ($mb in $planMbxFull) {
        if (-not $exoReady) { Write-Skip "FullAccess $($mb.PrimarySmtpAddress) (uitgesteld)"; $resMbx.Add("Uitgesteld (EXO sync): FullAccess $($mb.PrimarySmtpAddress)"); continue }
        try {
            Add-MailboxPermission -Identity $mb.Guid -User $newUser.Id -AccessRights FullAccess `
                -InheritanceType All -AutoMapping:$autoMap -Confirm:$false -ErrorAction Stop | Out-Null
            Write-Ok "FullAccess: $($mb.PrimarySmtpAddress)"; $resMbx.Add("FullAccess: $($mb.PrimarySmtpAddress)")
        } catch {
            if ("$($_.Exception.Message)" -match 'already has|ManagementObjectAlreadyExists') {
                Write-Skip "FullAccess $($mb.PrimarySmtpAddress) (had al)"; $resMbx.Add("Had al: FullAccess $($mb.PrimarySmtpAddress)")
            } else { Write-Fail "FullAccess $($mb.PrimarySmtpAddress): $($_.Exception.Message)"; $resMbx.Add("MISLUKT: FullAccess $($mb.PrimarySmtpAddress)") }
        }
    }
    foreach ($mb in $planMbxSendAs) {
        if (-not $exoReady) { Write-Skip "SendAs $($mb.PrimarySmtpAddress) (uitgesteld)"; $resMbx.Add("Uitgesteld (EXO sync): SendAs $($mb.PrimarySmtpAddress)"); continue }
        try {
            Add-RecipientPermission -Identity $mb.Guid -Trustee $NewUpn -AccessRights SendAs -Confirm:$false -ErrorAction Stop | Out-Null
            Write-Ok "SendAs: $($mb.PrimarySmtpAddress)"; $resMbx.Add("SendAs: $($mb.PrimarySmtpAddress)")
        } catch {
            if ("$($_.Exception.Message)" -match 'already') {
                Write-Skip "SendAs $($mb.PrimarySmtpAddress) (had al)"; $resMbx.Add("Had al: SendAs $($mb.PrimarySmtpAddress)")
            } else { Write-Fail "SendAs $($mb.PrimarySmtpAddress): $($_.Exception.Message)"; $resMbx.Add("MISLUKT: SendAs $($mb.PrimarySmtpAddress)") }
        }
    }
    foreach ($mb in $planMbxSoB) {
        if (-not $exoReady) { Write-Skip "SendOnBehalf $($mb.PrimarySmtpAddress) (uitgesteld)"; $resMbx.Add("Uitgesteld (EXO sync): SendOnBehalf $($mb.PrimarySmtpAddress)"); continue }
        try {
            Set-Mailbox -Identity $mb.Guid -GrantSendOnBehalfTo @{ Add = $NewUpn } -Confirm:$false -ErrorAction Stop
            Write-Ok "SendOnBehalf: $($mb.PrimarySmtpAddress)"; $resMbx.Add("SendOnBehalf: $($mb.PrimarySmtpAddress)")
        } catch {
            Write-Fail "SendOnBehalf $($mb.PrimarySmtpAddress): $($_.Exception.Message)"; $resMbx.Add("MISLUKT: SendOnBehalf $($mb.PrimarySmtpAddress)")
        }
    }
} elseif ($SkipMailboxPermissions) {
    Write-Skip "Mailboxmachtigingen overgeslagen (-SkipMailboxPermissions)."
}

# ============================================================================
#  15. Eindoverzicht  (rechten eerst, accountgegevens HELEMAAL ONDERAAN)
# ============================================================================

Write-Host "`n"
Write-Host "############################################################" -ForegroundColor Cyan
Write-Host "#                  OVERZICHT TOEGEWEZEN RECHTEN            #" -ForegroundColor Cyan
Write-Host "############################################################" -ForegroundColor Cyan

Write-Host "`n[ Groepen ]" -ForegroundColor White
if ($resGroups.Count) { $resGroups | ForEach-Object { Write-Host "  - $_" } } else { Write-Host "  (geen)" }

Write-Host "`n[ Licenties ]" -ForegroundColor White
if ($resLics.Count) { $resLics | ForEach-Object { Write-Host "  - $_" } } else { Write-Host "  (geen)" }

Write-Host "`n[ Gedeelde mailboxen ]" -ForegroundColor White
if ($resMbx.Count) { $resMbx | ForEach-Object { Write-Host "  - $_" } } else { Write-Host "  (geen)" }

if ($planRoleGroups.Count) {
    Write-Host "`n[ Rol-toewijsbare groepen - NIET toegevoegd, beoordeel handmatig (admin-rechten via groep) ]" -ForegroundColor Magenta
    $planRoleGroups | ForEach-Object { Write-Host "  - $_" -ForegroundColor Magenta }
}
if ($planRoles.Count) {
    Write-Host "`n[ Directory-/adminrollen - NIET gekopieerd, handmatig indien nodig ]" -ForegroundColor Magenta
    $planRoles | ForEach-Object { Write-Host "  - $_" -ForegroundColor Magenta }
}
if ($warnings.Count) {
    Write-Host "`n[ Waarschuwingen ]" -ForegroundColor Yellow
    $warnings | ForEach-Object { Write-Host "  ! $_" -ForegroundColor Yellow }
}

# --- Accountgegevens: HELEMAAL ONDERAAN ---
Write-Host "`n"
Write-Host "============================================================" -ForegroundColor Green
Write-Host "         ACCOUNTGEGEVENS  (GEVOELIG - niet loggen)          " -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
Write-Host ("  Weergavenaam : {0}" -f $NewDisplayName) -ForegroundColor White
Write-Host ("  E-mail / UPN : {0}" -f $NewUpn) -ForegroundColor White
if ($upnDomain -ne $smtpDomain) {
    Write-Host ("                 (verwacht primair SMTP: {0} - controleer/zet handmatig)" -f $NewPrimarySmtp) -ForegroundColor DarkYellow
}
if ($generatedPassword) {
    Write-Host ("  Wachtwoord   : {0}" -f $generatedPassword) -ForegroundColor White
    Write-Host "  (wijzigen NIET verplicht bij eerste aanmelding)" -ForegroundColor DarkGray
} else {
    Write-Host "  Wachtwoord   : (bestaand account - geen nieuw wachtwoord gegenereerd)" -ForegroundColor DarkGray
}
Write-Host "============================================================" -ForegroundColor Green
Write-Host "  Bewaar deze gegevens in je wachtwoordmanager en wis daarna de console." -ForegroundColor Yellow

# Opruimen
Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null

Versie voor rechtstreeks plakken in PowerShell. Vul het CONFIG-blok in (4 verplichte regels).

# =====================================================================================
#  Clone-M365UserRights - PLAK-VERSIE (voor rechtstreeks plakken in PowerShell)
#  Maakt een nieuwe M365-gebruiker met dezelfde rechten als een voorbeeldgebruiker.
#
#  GEBRUIK:
#    1. Vul hieronder het CONFIG-blok in (4 verplichte regels).
#    2. Zet $WhatIf op $false -> je ziet een PREVIEW zonder dat er iets wijzigt.
#
#  Vereiste modules: Microsoft.Graph.Authentication/Users/Groups/Identity.DirectoryManagement
#  + ExchangeOnlineManagement (alle Graph-submodules op dezelfde versie).
#
#  Tip: plak in Windows Terminal, VS Code of PowerShell ISE voor de meest betrouwbare
#  plak-ervaring. De klassieke (blauwe) console werkt meestal ook, omdat dit blok geen
#  Read-Host gebruikt.
# =====================================================================================

# ============================ CONFIG - VUL DIT IN ====================================
$Tenant       = 'admin@klant.nl'        # tenant-admin account (UPN) voor de verbinding
$TemplateUpn  = 'voorbeeld@klant.nl'    # UPN van de voorbeeldgebruiker (rechten afkijken)
$NewFirstName = 'Voornaam'              # voornaam nieuwe gebruiker
$NewLastName  = 'Achternaam'            # achternaam nieuwe gebruiker

# --- opties (mag je laten staan) ---
$WhatIf                 = $false        # $true = alleen tonen; zet op $false om door te voeren
$NoAutoMapping          = $false        # $true = gedeelde mailboxen NIET automatisch in Outlook
$SkipMailboxPermissions = $false        # $true = mailboxrechten overslaan
$NoWait                 = $false        # $true = niet wachten op EXO-sync (mailbox/DL uitstellen)
$AdoptExistingUser      = $false        # $true = rechten toevoegen aan een AL bestaand account
$UsageLocationDefault   = 'NL'
$MailboxWaitMinutes     = 15
# =====================================================================================

& {
$ErrorActionPreference = 'Stop'

# ============================================================================
#  Helpers
# ============================================================================

function Write-Section { param([string]$Title) Write-Host "`n=== $Title ===" -ForegroundColor Cyan }
function Write-Warn    { param([string]$Msg)   Write-Host "  ! $Msg" -ForegroundColor Yellow }
function Write-Ok      { param([string]$Msg)   Write-Host "  + $Msg" -ForegroundColor Green }
function Write-Skip    { param([string]$Msg)   Write-Host "  - $Msg" -ForegroundColor DarkGray }
function Write-Fail    { param([string]$Msg)   Write-Host "  x $Msg" -ForegroundColor Red }

function Get-AP {
    # Veilig een sleutel uit AdditionalProperties van een Graph directoryObject lezen.
    param($Object, [string]$Key)
    if ($Object -and $Object.AdditionalProperties -and $Object.AdditionalProperties.ContainsKey($Key)) {
        return $Object.AdditionalProperties[$Key]
    }
    return $null
}

function ConvertTo-AsciiLower {
    # Diacrieten strippen (e-accent->e, o-umlaut->o), lowercase, spaties weg.
    param([string]$Text)
    if (-not $Text) { return '' }
    $norm = $Text.Normalize([System.Text.NormalizationForm]::FormD)
    $sb = [System.Text.StringBuilder]::new()
    foreach ($ch in $norm.ToCharArray()) {
        $cat = [System.Globalization.CharUnicodeInfo]::GetUnicodeCategory($ch)
        if ($cat -ne [System.Globalization.UnicodeCategory]::NonSpacingMark) {
            [void]$sb.Append($ch)
        }
    }
    $clean = $sb.ToString().Normalize([System.Text.NormalizationForm]::FormC)
    return ($clean.ToLowerInvariant() -replace '\s', '')
}

function New-PronounceablePassword {
    # Patroon: hoofdletter-medeklinker + 3x(klinker+medeklinker) + 1 extra klinker
    #          (= 8 letters, eindigt op klinker) + 5 UNIEKE cijfers + "!".  Bijv. Bofelaxi60354!
    $vowels     = 'aeiou'
    $consonants = 'bcdfghjklmnpqrstvwxyz'
    $sb = [System.Text.StringBuilder]::new()
    [void]$sb.Append(([string]$consonants[(Get-Random -Maximum $consonants.Length)]).ToUpper())
    for ($i = 0; $i -lt 3; $i++) {
        [void]$sb.Append($vowels[(Get-Random -Maximum $vowels.Length)])
        [void]$sb.Append($consonants[(Get-Random -Maximum $consonants.Length)])
    }
    [void]$sb.Append($vowels[(Get-Random -Maximum $vowels.Length)])
    [void]$sb.Append(((Get-Random -InputObject (0..9) -Count 5) -join ''))
    [void]$sb.Append('!')
    return $sb.ToString()
}

function Get-DerivedDisplayName {
    # Leidt de weergavenaam van de nieuwe gebruiker af van die van de voorbeeldgebruiker
    # en behoudt een eventuele affix ("| Bedrijfsnaam BV") + naamstructuur.
    param(
        [string]$DisplayName, [string]$Given, [string]$Surname,
        [string]$NewFirst, [string]$NewLast
    )
    $result = [pscustomobject]@{ DisplayName = "$NewFirst $NewLast"; Structure = 'fallback'; Prefix = ''; Suffix = ''; Matched = $false }
    if (-not $DisplayName) { return $result }

    $candidates = @()
    if ($Given -and $Surname) {
        $candidates += @{ Form = "$Given $Surname";  Structure = 'GS';   Build = "$NewFirst $NewLast" }
        $candidates += @{ Form = "$Surname $Given";  Structure = 'SG';   Build = "$NewLast $NewFirst" }
        $candidates += @{ Form = "$Surname, $Given"; Structure = 'S, G'; Build = "$NewLast, $NewFirst" }
        $candidates += @{ Form = "$Surname,$Given";  Structure = 'S,G';  Build = "$NewLast,$NewFirst" }
    }
    if ($Given)   { $candidates += @{ Form = $Given;   Structure = 'G'; Build = $NewFirst } }
    if ($Surname) { $candidates += @{ Form = $Surname; Structure = 'S'; Build = $NewLast } }

    foreach ($c in $candidates) {
        if (-not $c.Form) { continue }
        $idx = $DisplayName.IndexOf($c.Form, [System.StringComparison]::OrdinalIgnoreCase)
        if ($idx -ge 0) {
            $prefix = $DisplayName.Substring(0, $idx)
            $suffix = $DisplayName.Substring($idx + $c.Form.Length)
            $result.DisplayName = "$prefix$($c.Build)$suffix"
            $result.Structure   = $c.Structure
            $result.Prefix      = $prefix
            $result.Suffix      = $suffix
            $result.Matched     = $true
            return $result
        }
    }
    return $result
}

function Get-LocalPartPattern {
    param([string]$LocalPart, [string]$Given, [string]$Surname)
    $g = ConvertTo-AsciiLower $Given
    $s = ConvertTo-AsciiLower $Surname
    if (-not $g -or -not $s) { return $null }
    $lp = $LocalPart.ToLowerInvariant()
    $g1 = $g.Substring(0,1); $s1 = $s.Substring(0,1)
    $patterns = [ordered]@{
        'given.surname' = "$g.$s"
        'surname.given' = "$s.$g"
        'g.surname'     = "$g1.$s"
        'surname.g'     = "$s.$g1"
        'given_surname' = "${g}_$s"
        'givensurname'  = "$g$s"
        'surnamegiven'  = "$s$g"
        'gsurname'      = "$g1$s"
        'surnameg'      = "$s$g1"
    }
    foreach ($key in $patterns.Keys) {
        if ($patterns[$key] -eq $lp) { return $key }
    }
    return $null
}

function Build-LocalPart {
    param([string]$Pattern, [string]$Given, [string]$Surname)
    $g = ConvertTo-AsciiLower $Given
    $s = ConvertTo-AsciiLower $Surname
    $g1 = if ($g) { $g.Substring(0,1) } else { '' }
    $s1 = if ($s) { $s.Substring(0,1) } else { '' }
    switch ($Pattern) {
        'given.surname' { "$g.$s" }
        'surname.given' { "$s.$g" }
        'g.surname'     { "$g1.$s" }
        'surname.g'     { "$s.$g1" }
        'given_surname' { "${g}_$s" }
        'givensurname'  { "$g$s" }
        'surnamegiven'  { "$s$g" }
        'gsurname'      { "$g1$s" }
        'surnameg'      { "$s$g1" }
        default         { "$g.$s" }
    }
}

function Test-IsTemplatePrincipal {
    param($PrincipalString)
    if (-not $PrincipalString) { return $false }
    $p = ("$PrincipalString").ToLowerInvariant()
    if ($script:TplIds.Contains($p)) { return $true }
    try {
        $r = Get-Recipient -Identity $PrincipalString -ErrorAction Stop
        return ("$($r.ExternalDirectoryObjectId)" -eq $script:TplObjId)
    } catch { return $false }
}

function Test-IsRoleAssignableGroup {
    # True als de groep aan directory-rollen gekoppeld kan zijn (isAssignableToRole).
    param($MemberObject)
    $val = Get-AP $MemberObject 'isAssignableToRole'
    if ($null -ne $val) { return [bool]$val }
    try {
        $g = Get-MgGroup -GroupId $MemberObject.Id -Property IsAssignableToRole -ErrorAction Stop
        return [bool]$g.IsAssignableToRole
    } catch { return $false }
}

# ============================================================================
#  CONFIG controleren
# ============================================================================

$NewFirstName = "$NewFirstName".Trim()
$NewLastName  = "$NewLastName".Trim()
$TemplateUpn  = "$TemplateUpn".Trim()
$Tenant       = "$Tenant".Trim()

if (-not $Tenant -or -not $TemplateUpn -or -not $NewFirstName -or -not $NewLastName -or
    $Tenant -eq 'admin@klant.nl' -or $TemplateUpn -eq 'voorbeeld@klant.nl' -or
    $NewFirstName -eq 'Voornaam' -or $NewLastName -eq 'Achternaam') {
    Write-Fail "Vul eerst het CONFIG-blok bovenaan in: `$Tenant, `$TemplateUpn, `$NewFirstName, `$NewLastName."
    return
}

# ============================================================================
#  Preflight: modules (Authentication eerst, submodules op dezelfde versie)
# ============================================================================

if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication)) {
    Write-Fail "Microsoft.Graph.Authentication ontbreekt. Installeer: Install-Module Microsoft.Graph -Scope CurrentUser"
    return
}
$loadedAuth = Get-Module Microsoft.Graph.Authentication
if ($loadedAuth) {
    $authVer = $loadedAuth.Version
} else {
    $authVer = (Get-Module -ListAvailable Microsoft.Graph.Authentication |
                Sort-Object Version -Descending | Select-Object -First 1).Version
    Import-Module Microsoft.Graph.Authentication -RequiredVersion $authVer -ErrorAction Stop
}
Write-Host "  Microsoft.Graph.Authentication $authVer geladen" -ForegroundColor DarkGray

$graphSubs = @('Microsoft.Graph.Users','Microsoft.Graph.Groups','Microsoft.Graph.Identity.DirectoryManagement')
$badSubs = @()
foreach ($m in $graphSubs) {
    if (Get-Module -ListAvailable -Name $m | Where-Object { $_.Version -eq $authVer }) {
        Import-Module $m -RequiredVersion $authVer -ErrorAction Stop
    } else {
        $badSubs += $m
    }
}
if ($badSubs.Count) {
    Write-Fail "Versie-mismatch: Microsoft.Graph.Authentication = $authVer, maar deze submodule(s) staan niet op die versie:"
    $badSubs | ForEach-Object { Write-Host "    - $_" -ForegroundColor Yellow }
    Write-Host "  Trek alle Graph-modules gelijk, bijv.: Update-Module Microsoft.Graph -Force" -ForegroundColor Yellow
    Write-Host "  of: Install-Module Microsoft.Graph -RequiredVersion $authVer -Force -Scope CurrentUser" -ForegroundColor Yellow
    return
}

if (-not (Get-Module -ListAvailable -Name ExchangeOnlineManagement)) {
    Write-Fail "ExchangeOnlineManagement ontbreekt. Installeer: Install-Module ExchangeOnlineManagement -Scope CurrentUser"
    return
}
Import-Module ExchangeOnlineManagement -ErrorAction Stop

# ============================================================================
#  1. Verbinden (Graph + Exchange Online)
# ============================================================================

Write-Section '1. Verbinden met Microsoft 365'
$scopes = @(
    'User.ReadWrite.All',
    'Group.ReadWrite.All',
    'GroupMember.ReadWrite.All',
    'Directory.Read.All',
    'Organization.Read.All',
    'RoleManagement.Read.Directory',
    'LicenseAssignment.ReadWrite.All'
)
Write-Host "  Verbinden met Microsoft Graph..." -ForegroundColor Cyan
Connect-MgGraph -Scopes $scopes -NoWelcome -ErrorAction Stop | Out-Null

Write-Host "  Verbinden met Exchange Online ($Tenant)..." -ForegroundColor Cyan
Connect-ExchangeOnline -UserPrincipalName $Tenant -ShowBanner:$false -ErrorAction Stop | Out-Null

try { Get-OrganizationConfig -ErrorAction Stop | Out-Null }
catch { Write-Fail "Exchange Online-sessie niet bruikbaar: $($_.Exception.Message)"; return }
Write-Ok 'Verbonden met Graph en Exchange Online.'

$warnings = [System.Collections.Generic.List[string]]::new()

# ============================================================================
#  2. Voorbeeldgebruiker uitlezen
# ============================================================================

Write-Section '2. Voorbeeldgebruiker uitlezen'
$tplProps = 'Id','DisplayName','GivenName','Surname','UserPrincipalName','Mail','MailNickname',
            'UsageLocation','OnPremisesSyncEnabled','AccountEnabled','ProxyAddresses',
            'AssignedLicenses','LicenseAssignmentStates'
try {
    $tpl = Get-MgUser -UserId $TemplateUpn -Property $tplProps -ErrorAction Stop
} catch {
    Write-Fail "Voorbeeldgebruiker '$TemplateUpn' niet gevonden: $($_.Exception.Message)"; return
}

$tplMbx = $null
try { $tplMbx = Get-Mailbox -Identity $tpl.Id -ErrorAction Stop } catch { }
$tplPrimarySmtp = if ($tplMbx) { "$($tplMbx.PrimarySmtpAddress)" } elseif ($tpl.Mail) { $tpl.Mail } else { $tpl.UserPrincipalName }

Write-Host "  Naam      : $($tpl.DisplayName)"
Write-Host "  UPN       : $($tpl.UserPrincipalName)"
Write-Host "  Primair   : $tplPrimarySmtp"
Write-Host "  Voor/Achter: '$($tpl.GivenName)' / '$($tpl.Surname)'"

if ($tpl.OnPremisesSyncEnabled -eq $true) {
    $msg = "Voorbeeldgebruiker is ON-PREM GESYNCED. Een nieuwe gebruiker hoort dan in on-prem AD aangemaakt en gesynchroniseerd te worden. Dit script maakt een CLOUD-ONLY account - ga alleen door als dat bewust is."
    Write-Warn $msg
    $warnings.Add($msg)
}
if (-not $tpl.GivenName -or -not $tpl.Surname) {
    $msg = "Voorbeeldgebruiker mist GivenName en/of Surname; naam-/UPN-afleiding is heuristisch (val terug op weergavenaam splitsen)."
    Write-Warn $msg
    $warnings.Add($msg)
}

# ============================================================================
#  3. Weergavenaam afleiden
# ============================================================================

Write-Section '3. Weergavenaam afleiden'
$dn = Get-DerivedDisplayName -DisplayName $tpl.DisplayName -Given $tpl.GivenName -Surname $tpl.Surname `
        -NewFirst $NewFirstName -NewLast $NewLastName
$NewDisplayName = $dn.DisplayName
if ($dn.Matched) {
    Write-Ok "Structuur '$($dn.Structure)', affix-suffix: '$($dn.Suffix)'"
} else {
    $msg = "Kon naamstructuur niet matchen op weergavenaam; val terug op '$NewFirstName $NewLastName' zonder affix."
    Write-Warn $msg; $warnings.Add($msg)
}
Write-Host "  Nieuwe weergavenaam: $NewDisplayName" -ForegroundColor White

# ============================================================================
#  4. UPN + e-mail afleiden + collision-check
# ============================================================================

Write-Section '4. UPN en e-mail afleiden'
$tplUpnLocal = ($tpl.UserPrincipalName -split '@', 2)[0]
$upnDomain   = ($tpl.UserPrincipalName -split '@', 2)[1]
$smtpDomain  = ($tplPrimarySmtp        -split '@', 2)[1]

$pattern = Get-LocalPartPattern -LocalPart $tplUpnLocal -Given $tpl.GivenName -Surname $tpl.Surname
if (-not $pattern) {
    $pattern = 'given.surname'
    $msg = "Local-part-patroon van de voorbeeld-UPN niet herkend; val terug op 'given.surname'."
    Write-Warn $msg; $warnings.Add($msg)
} else {
    Write-Ok "Local-part-patroon: '$pattern'"
}
$newLocal       = Build-LocalPart -Pattern $pattern -Given $NewFirstName -Surname $NewLastName
$NewUpn         = "$newLocal@$upnDomain"
$NewPrimarySmtp = "$newLocal@$smtpDomain"
$MailNickname   = $newLocal

Write-Host "  Nieuwe UPN        : $NewUpn" -ForegroundColor White
Write-Host "  Verwacht primair  : $NewPrimarySmtp" -ForegroundColor White
if ($upnDomain -ne $smtpDomain) {
    $msg = "UPN-domein ($upnDomain) verschilt van primair SMTP-domein ($smtpDomain). Het werkelijke primaire SMTP-adres wordt door het e-mailadresbeleid bepaald; controleer/zet dit zo nodig handmatig na provisioning."
    Write-Warn $msg; $warnings.Add($msg)
}

# Idempotentie / collision
$existingUser = $null
try { $existingUser = Get-MgUser -UserId $NewUpn -Property Id,DisplayName,Mail,UserPrincipalName,AccountEnabled,CreatedDateTime -ErrorAction Stop } catch { }
if ($existingUser) {
    Write-Warn "Er bestaat AL een gebruiker met UPN ${NewUpn}:"
    Write-Host ("      Weergavenaam : {0}" -f $existingUser.DisplayName)    -ForegroundColor Yellow
    Write-Host ("      Mail         : {0}" -f $existingUser.Mail)           -ForegroundColor Yellow
    Write-Host ("      Ingeschakeld : {0}" -f $existingUser.AccountEnabled) -ForegroundColor Yellow
    Write-Host ("      Aangemaakt   : {0}" -f $existingUser.CreatedDateTime) -ForegroundColor Yellow
    Write-Warn "Er wordt GEEN nieuw account gemaakt; rechten zouden aan DIT bestaande account worden toegevoegd. Controleer of dit echt dezelfde persoon is."
}
try {
    $smtpOwner = Get-Recipient -Identity $NewPrimarySmtp -ErrorAction Stop
    if (-not $existingUser -or "$($smtpOwner.ExternalDirectoryObjectId)" -ne "$($existingUser.Id)") {
        Write-Fail "Het adres $NewPrimarySmtp is al in gebruik door een andere recipient ($($smtpOwner.DisplayName)). Conflict - gestopt."
        return
    }
} catch { }

# ============================================================================
#  5. Groepslidmaatschappen uitlezen + classificeren
# ============================================================================

Write-Section '5. Groepslidmaatschappen uitlezen'
$memberOf = Get-MgUserMemberOf -UserId $tpl.Id -All

$planCloudGroups = [System.Collections.Generic.List[object]]::new()
$planDLGroups    = [System.Collections.Generic.List[object]]::new()
$planSkipGroups  = [System.Collections.Generic.List[object]]::new()
$planRoleGroups  = [System.Collections.Generic.List[string]]::new()
$planRoles       = [System.Collections.Generic.List[string]]::new()

foreach ($mObj in $memberOf) {
    $odata = Get-AP $mObj '@odata.type'
    $name  = Get-AP $mObj 'displayName'
    if (-not $name) { $name = $mObj.Id }

    if ($odata -eq '#microsoft.graph.directoryRole') {
        $planRoles.Add($name); continue
    }
    if ($odata -ne '#microsoft.graph.group') { continue }

    $groupTypes = @(Get-AP $mObj 'groupTypes')
    $mailEnabled     = [bool](Get-AP $mObj 'mailEnabled')
    $securityEnabled = [bool](Get-AP $mObj 'securityEnabled')
    $onPremSync      = (Get-AP $mObj 'onPremisesSyncEnabled') -eq $true
    $ruleState       = Get-AP $mObj 'membershipRuleProcessingState'
    $isDynamic = ($groupTypes -contains 'DynamicMembership') -or ($ruleState -eq 'On')
    $isUnified = ($groupTypes -contains 'Unified')

    if ($onPremSync) {
        $planSkipGroups.Add([pscustomobject]@{ Name = $name; Reason = 'on-prem gesynced (lid toevoegen in on-prem AD)' }); continue
    }
    if ($isDynamic) {
        $planSkipGroups.Add([pscustomobject]@{ Name = $name; Reason = 'dynamische groep (regel-gebaseerd lidmaatschap)' }); continue
    }
    if (($securityEnabled -or $isUnified) -and (Test-IsRoleAssignableGroup $mObj)) {
        $planRoleGroups.Add($name); continue
    }
    if ($isUnified) {
        $planCloudGroups.Add([pscustomobject]@{ Id = $mObj.Id; Name = $name; Kind = 'Microsoft 365' }); continue
    }
    if ($mailEnabled) {
        $kind = if ($securityEnabled) { 'Mail-enabled security' } else { 'Distributielijst' }
        $planDLGroups.Add([pscustomobject]@{ Id = $mObj.Id; Name = $name; Kind = $kind }); continue
    }
    if ($securityEnabled) {
        $planCloudGroups.Add([pscustomobject]@{ Id = $mObj.Id; Name = $name; Kind = 'Security' }); continue
    }
    $planSkipGroups.Add([pscustomobject]@{ Name = $name; Reason = 'onbekend groepstype' })
}
Write-Ok "$($planCloudGroups.Count) cloud-groep(en), $($planDLGroups.Count) distributie/mail-enabled, $($planSkipGroups.Count) overgeslagen."
if ($planRoleGroups.Count) {
    $warnings.Add("$($planRoleGroups.Count) rol-toewijsbare groep(en) NIET automatisch toegevoegd (mogelijke admin-rechten via groep); beoordeel handmatig.")
}

# ============================================================================
#  6. Licenties uitlezen + classificeren (direct vs groep-gebaseerd)
# ============================================================================

Write-Section '6. Licenties uitlezen'
$subSkus = @(Get-MgSubscribedSku -All)
$skuMap = @{}
foreach ($sku in $subSkus) {
    $skuMap["$($sku.SkuId)"] = [pscustomobject]@{
        PartNumber   = $sku.SkuPartNumber
        Available    = ($sku.PrepaidUnits.Enabled - $sku.ConsumedUnits)
        ServicePlans = $sku.ServicePlans
    }
}
$tplDisabled = @{}
foreach ($al in @($tpl.AssignedLicenses)) { $tplDisabled["$($al.SkuId)"] = @($al.DisabledPlans) }

$directSkus = @($tpl.LicenseAssignmentStates | Where-Object { -not $_.AssignedByGroup } | Select-Object -ExpandProperty SkuId -Unique)
$groupSkus  = @($tpl.LicenseAssignmentStates | Where-Object { $_.AssignedByGroup }      | Select-Object -ExpandProperty SkuId -Unique)

$planLicAssign = [System.Collections.Generic.List[object]]::new()
$planLicSkip   = [System.Collections.Generic.List[object]]::new()

foreach ($skuId in $directSkus) {
    $part = if ($skuMap.ContainsKey("$skuId")) { $skuMap["$skuId"].PartNumber } else { "$skuId" }
    $avail = if ($skuMap.ContainsKey("$skuId")) { $skuMap["$skuId"].Available } else { 0 }
    if ($avail -le 0) {
        $planLicSkip.Add([pscustomobject]@{ Part = $part; Reason = "geen vrije seats (beschikbaar: $avail)" }); continue
    }
    $planLicAssign.Add([pscustomobject]@{ SkuId = "$skuId"; Part = $part; DisabledPlans = $tplDisabled["$skuId"] })
}
foreach ($skuId in $groupSkus) {
    if ($directSkus -contains $skuId) { continue }
    $part = if ($skuMap.ContainsKey("$skuId")) { $skuMap["$skuId"].PartNumber } else { "$skuId" }
    $planLicSkip.Add([pscustomobject]@{ Part = $part; Reason = 'groep-gebaseerd (volgt automatisch via groepslidmaatschap)' })
}
Write-Ok "$($planLicAssign.Count) licentie(s) toe te wijzen, $($planLicSkip.Count) overgeslagen."

# Waarschuw als de Exchange Online-mailboxplan in een toe te wijzen licentie is UITGEZET
# (overgenomen van de voorbeeldgebruiker): dan krijgt de nieuwe gebruiker GEEN mailbox, falen
# de mailbox/DL/Send As-stappen, en kan Exchange 'recipient not found' tonen in het admin center.
foreach ($lic in $planLicAssign) {
    if (-not $lic.DisabledPlans -or @($lic.DisabledPlans).Count -eq 0) { continue }
    $plans = if ($skuMap.ContainsKey($lic.SkuId)) { $skuMap[$lic.SkuId].ServicePlans } else { @() }
    foreach ($dp in @($lic.DisabledPlans)) {
        $pl = $plans | Where-Object { "$($_.ServicePlanId)" -eq "$dp" }
        if ($pl -and $pl.ServicePlanName -match '^EXCHANGE_S_(ENTERPRISE|STANDARD|DESKLESS|ESSENTIALS)$') {
            $msg = "Exchange Online ($($pl.ServicePlanName)) staat UIT in licentie '$($lic.Part)' (overgenomen van de voorbeeldgebruiker). De nieuwe gebruiker krijgt dan GEEN mailbox; gedeelde mailboxen/DL's/Send As worden niet voltooid en Exchange kan 'recipient not found' tonen."
            Write-Warn $msg; $warnings.Add($msg)
        }
    }
}

# ============================================================================
#  7. Gedeelde mailboxen / Send As / Send on Behalf uitlezen (een pass)
# ============================================================================

Write-Section '7. Mailboxmachtigingen van de voorbeeldgebruiker scannen'
$Username = $tpl.UserPrincipalName

$script:TplObjId = "$($tpl.Id)"
$script:TplIds = New-Object 'System.Collections.Generic.HashSet[string]'
foreach ($v in @($tpl.UserPrincipalName, $tplPrimarySmtp, $tpl.Mail, $tpl.Id,
                 $tplMbx.Alias, $tplMbx.Name, $tplMbx.DisplayName, "$($tplMbx.ExchangeGuid)")) {
    if ($v) { [void]$script:TplIds.Add(("$v").ToLowerInvariant()) }
}

$planMbxFull   = [System.Collections.Generic.List[object]]::new()
$planMbxSendAs = [System.Collections.Generic.List[object]]::new()
$planMbxSoB    = [System.Collections.Generic.List[object]]::new()

Write-Host "  Alle mailboxen ophalen..." -ForegroundColor Cyan
$mailboxes = @(Get-Mailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox,SharedMailbox)
$i = 0; $total = $mailboxes.Count
foreach ($mailbox in $mailboxes) {
    $i++
    Write-Progress -Activity "Mailboxmachtigingen controleren" `
        -Status "$i van $total - $($mailbox.PrimarySmtpAddress)" `
        -PercentComplete (($i / [Math]::Max($total,1)) * 100)

    $guid = $mailbox.ExchangeGuid.ToString()
    $row = [pscustomobject]@{ DisplayName = $mailbox.DisplayName; PrimarySmtpAddress = "$($mailbox.PrimarySmtpAddress)"; Guid = $guid }

    $permissions = Get-MailboxPermission -Identity $guid -ErrorAction SilentlyContinue |
        Where-Object {
            -not $_.IsInherited -and
            $_.AccessRights -ne $null -and
            ($_.User -like "*$Username*" -or $_.User -eq $Username)
        }
    foreach ($perm in $permissions) {
        if (($perm.AccessRights -contains 'FullAccess') -and (Test-IsTemplatePrincipal $perm.User)) {
            $planMbxFull.Add($row); break
        }
    }

    $sa = Get-RecipientPermission -Identity $guid -ErrorAction SilentlyContinue |
        Where-Object {
            $_.AccessRights -contains 'SendAs' -and
            ($_.Trustee -like "*$Username*" -or $_.Trustee -eq $Username)
        }
    foreach ($p in $sa) {
        if (Test-IsTemplatePrincipal $p.Trustee) { $planMbxSendAs.Add($row); break }
    }

    foreach ($d in @($mailbox.GrantSendOnBehalfTo)) {
        if (Test-IsTemplatePrincipal $d) { $planMbxSoB.Add($row); break }
    }
}
Write-Progress -Activity "Mailboxmachtigingen controleren" -Completed
Write-Ok "Full Access: $($planMbxFull.Count), Send As: $($planMbxSendAs.Count), Send on Behalf: $($planMbxSoB.Count)"

# ============================================================================
#  8. Preview
# ============================================================================

Write-Section 'PREVIEW - wat gaat er gebeuren'
Write-Host "  Nieuwe gebruiker:" -ForegroundColor White
Write-Host "    Weergavenaam : $NewDisplayName"
Write-Host "    UPN          : $NewUpn"
Write-Host "    Primair SMTP : $NewPrimarySmtp (verwacht)"
Write-Host "    MailNickname : $MailNickname"
Write-Host "    UsageLocation: $(if ($tpl.UsageLocation) { $tpl.UsageLocation } else { $UsageLocationDefault })"
if ($existingUser) { Write-Warn "Account bestaat al - rechten worden aan het bestaande account toegewezen." }

Write-Host "`n  Cloud-groepen (Graph): $($planCloudGroups.Count)" -ForegroundColor White
$planCloudGroups | ForEach-Object { Write-Host "    - $($_.Name)  [$($_.Kind)]" }
Write-Host "  Distributie / mail-enabled (EXO): $($planDLGroups.Count)" -ForegroundColor White
$planDLGroups | ForEach-Object { Write-Host "    - $($_.Name)  [$($_.Kind)]" }
if ($planSkipGroups.Count) {
    Write-Host "  Overgeslagen groepen: $($planSkipGroups.Count)" -ForegroundColor Yellow
    $planSkipGroups | ForEach-Object { Write-Host "    - $($_.Name)  ($($_.Reason))" -ForegroundColor Yellow }
}
if ($planRoleGroups.Count) {
    Write-Host "  ROL-toewijsbare groepen (NIET automatisch toegevoegd - kunnen admin-rechten geven): $($planRoleGroups.Count)" -ForegroundColor Magenta
    $planRoleGroups | ForEach-Object { Write-Host "    - $_" -ForegroundColor Magenta }
}

Write-Host "`n  Licenties toewijzen: $($planLicAssign.Count)" -ForegroundColor White
$planLicAssign | ForEach-Object { Write-Host "    - $($_.Part)" }
if ($planLicSkip.Count) {
    Write-Host "  Licenties NIET toegewezen: $($planLicSkip.Count)" -ForegroundColor Yellow
    $planLicSkip | ForEach-Object { Write-Host "    - $($_.Part)  ($($_.Reason))" -ForegroundColor Yellow }
}

Write-Host "`n  Gedeelde mailboxen:" -ForegroundColor White
Write-Host "    Full Access ($(if ($NoAutoMapping) {'GEEN automapping'} else {'automapping AAN'})): $($planMbxFull.Count)"
$planMbxFull   | ForEach-Object { Write-Host "      - $($_.PrimarySmtpAddress)" }
Write-Host "    Send As: $($planMbxSendAs.Count)"
$planMbxSendAs | ForEach-Object { Write-Host "      - $($_.PrimarySmtpAddress)" }
Write-Host "    Send on Behalf: $($planMbxSoB.Count)"
$planMbxSoB    | ForEach-Object { Write-Host "      - $($_.PrimarySmtpAddress)" }

if ($planRoles.Count) {
    Write-Host "`n  Directory-/adminrollen (NIET gekopieerd - handmatig toewijzen indien nodig):" -ForegroundColor Magenta
    $planRoles | ForEach-Object { Write-Host "    - $_" -ForegroundColor Magenta }
}
if ($warnings.Count) {
    Write-Host "`n  Waarschuwingen:" -ForegroundColor Yellow
    $warnings | ForEach-Object { Write-Host "    ! $_" -ForegroundColor Yellow }
}

# --- WhatIf-poort: hierboven wordt NIETS geschreven ---
if ($WhatIf) {
    Write-Host "`n[PREVIEW] `$WhatIf = `$true -> er is NIETS gewijzigd. Controleer bovenstaande preview." -ForegroundColor Yellow
    Write-Host "Zet in het CONFIG-blok `$WhatIf = `$false en plak het blok opnieuw om door te voeren." -ForegroundColor Yellow
    Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
    return
}

# Veiligheid: een BESTAAND account niet stilzwijgend aanpassen.
if ($existingUser -and -not $AdoptExistingUser) {
    Write-Fail "UPN $NewUpn bestaat al ($($existingUser.DisplayName)). Er is NIETS gewijzigd. Wil je bewust rechten aan dit bestaande account toevoegen? Zet `$AdoptExistingUser = `$true in het CONFIG-blok en plak opnieuw."
    Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
    Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
    return
}

# ============================================================================
#  9. Account aanmaken (of bestaand gebruiken)
# ============================================================================

$resGroups = [System.Collections.Generic.List[string]]::new()
$resLics   = [System.Collections.Generic.List[string]]::new()
$resMbx    = [System.Collections.Generic.List[string]]::new()
$generatedPassword = $null

Write-Section '9. Account aanmaken'
if ($existingUser) {
    $newUser = $existingUser
    Write-Skip "Bestaand account gebruikt: $NewUpn"
} else {
    $generatedPassword = New-PronounceablePassword
    $usageLoc = if ($tpl.UsageLocation) { $tpl.UsageLocation } else { $UsageLocationDefault }
    $body = @{
        AccountEnabled    = $true
        DisplayName       = $NewDisplayName
        GivenName         = $NewFirstName
        Surname           = $NewLastName
        MailNickname      = $MailNickname
        UserPrincipalName = $NewUpn
        UsageLocation     = $usageLoc
        PasswordProfile   = @{
            ForceChangePasswordNextSignIn = $false
            Password                      = $generatedPassword
        }
    }
    $newUser = New-MgUser -BodyParameter $body -ErrorAction Stop
    $check = Get-MgUser -UserId $newUser.Id -Property DisplayName,UserPrincipalName,MailNickname -ErrorAction Stop
    if ($check.UserPrincipalName -ne $NewUpn -or $check.DisplayName -ne $NewDisplayName) {
        Write-Warn "Aangemaakt account wijkt af van verwacht (UPN '$($check.UserPrincipalName)', naam '$($check.DisplayName)'). Controleer handmatig."
    }
    Write-Ok "Aangemaakt: $NewUpn (Id $($newUser.Id))"
}

# ============================================================================
#  10. Cloud-groepen toewijzen (Graph - direct)
# ============================================================================

Write-Section '10. Cloud-groepen toewijzen'
foreach ($g in $planCloudGroups) {
    try {
        New-MgGroupMember -GroupId $g.Id -DirectoryObjectId $newUser.Id -ErrorAction Stop
        Write-Ok "$($g.Name) [$($g.Kind)]"; $resGroups.Add("Toegevoegd: $($g.Name) [$($g.Kind)]")
    } catch {
        if ("$($_.Exception.Message)" -match 'already exist') {
            Write-Skip "$($g.Name) (was al lid)"; $resGroups.Add("Al lid: $($g.Name)")
        } else {
            Write-Fail "$($g.Name): $($_.Exception.Message)"; $resGroups.Add("MISLUKT: $($g.Name) - $($_.Exception.Message)")
        }
    }
}

# ============================================================================
#  11. Licenties toewijzen (via Invoke-MgGraphRequest)
# ============================================================================

Write-Section '11. Licenties toewijzen'
foreach ($lic in $planLicAssign) {
    try {
        $add = @{ skuId = $lic.SkuId }
        if ($lic.DisabledPlans -and @($lic.DisabledPlans).Count -gt 0) { $add['disabledPlans'] = @($lic.DisabledPlans) }
        $assignBody = @{ addLicenses = @($add); removeLicenses = @() }
        Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/users/$($newUser.Id)/assignLicense" `
            -Body $assignBody -ErrorAction Stop | Out-Null
        Write-Ok $lic.Part; $resLics.Add("Toegewezen: $($lic.Part)")
    } catch {
        Write-Fail "$($lic.Part): $($_.Exception.Message)"; $resLics.Add("MISLUKT: $($lic.Part) - $($_.Exception.Message)")
    }
}
foreach ($s in $planLicSkip) { $resLics.Add("Overgeslagen: $($s.Part) ($($s.Reason))") }

# ============================================================================
#  12. Wachten tot de nieuwe gebruiker zichtbaar is in Exchange Online
# ============================================================================

$needExo = (-not $SkipMailboxPermissions -and ($planMbxFull.Count + $planMbxSendAs.Count + $planMbxSoB.Count) -gt 0) `
           -or ($planDLGroups.Count -gt 0)
$exoReady = $false
$rerunHint = "Zet `$NoWait = `$false en `$AdoptExistingUser = `$true in het CONFIG-blok en plak dit blok later nogmaals."

if ($needExo -and $NoWait) {
    $msg = "-NoWait: niet gewacht op de Entra->EXO-sync. Gedeelde mailboxen en distributielijsten zijn UITGESTELD. $rerunHint"
    Write-Warn $msg; $warnings.Add($msg)
}
elseif ($needExo) {
    Write-Section '12. Wachten op Entra->Exchange-synchronisatie'
    Write-Host "  Account, groepen en licenties zijn al toegepast. Alleen gedeelde mailboxen en" -ForegroundColor DarkGray
    Write-Host "  distributielijsten wachten tot de nieuwe gebruiker in Exchange Online verschijnt." -ForegroundColor DarkGray
    Write-Host "  Gaat verder zodra dat zo is (controle elke 15s); druk op een toets om over te slaan." -ForegroundColor DarkGray
    $deadline = (Get-Date).AddMinutes($MailboxWaitMinutes)
    $skipped  = $false
    while (-not $exoReady -and (Get-Date) -lt $deadline) {
        try { if (Get-Recipient -Identity $NewUpn -ErrorAction Stop) { $exoReady = $true; break } } catch { }
        $remaining = [int]([Math]::Max(0, ($deadline - (Get-Date)).TotalSeconds))
        Write-Progress -Activity "Wachten tot $NewUpn zichtbaar is in Exchange Online" `
            -Status "resterend: max $remaining s (toets = overslaan)" `
            -PercentComplete (100 - ($remaining / [Math]::Max($MailboxWaitMinutes*60,1) * 100))
        $slept = 0.0
        while ($slept -lt 15) {
            try { if ([System.Console]::KeyAvailable) { [void][System.Console]::ReadKey($true); $skipped = $true; break } } catch { }
            Start-Sleep -Milliseconds 500; $slept += 0.5
        }
        if ($skipped) { break }
    }
    Write-Progress -Activity "Wachten op Exchange Online" -Completed
    if ($exoReady) {
        Write-Ok "Nieuwe gebruiker zichtbaar in Exchange Online."
    } else {
        $reason = if ($skipped) { "Wachten overgeslagen" } else { "Na $MailboxWaitMinutes min nog niet zichtbaar in EXO" }
        $msg = "$reason. Gedeelde mailboxen en distributielijsten zijn UITGESTELD. $rerunHint"
        Write-Warn $msg; $warnings.Add($msg)
    }
}

# ============================================================================
#  13. Distributie / mail-enabled groepen (EXO - na de wait)
# ============================================================================

if ($planDLGroups.Count) {
    Write-Section '13. Distributie/mail-enabled groepen toewijzen'
    foreach ($g in $planDLGroups) {
        if (-not $exoReady) { Write-Skip "$($g.Name) (uitgesteld - EXO sync)"; $resGroups.Add("Uitgesteld (EXO sync): $($g.Name)"); continue }
        try {
            Add-DistributionGroupMember -Identity $g.Id -Member $NewUpn -ErrorAction Stop
            Write-Ok "$($g.Name) [$($g.Kind)]"; $resGroups.Add("Toegevoegd: $($g.Name) [$($g.Kind)]")
        } catch {
            if ("$($_.Exception.Message)" -match 'already a member') {
                Write-Skip "$($g.Name) (was al lid)"; $resGroups.Add("Al lid: $($g.Name)")
            } else {
                Write-Fail "$($g.Name): $($_.Exception.Message)"; $resGroups.Add("MISLUKT: $($g.Name) - $($_.Exception.Message)")
            }
        }
    }
}

# ============================================================================
#  14. Mailboxmachtigingen toewijzen (EXO - na de wait)
# ============================================================================

if (-not $SkipMailboxPermissions -and ($planMbxFull.Count + $planMbxSendAs.Count + $planMbxSoB.Count) -gt 0) {
    Write-Section '14. Mailboxmachtigingen toewijzen'
    $autoMap = -not $NoAutoMapping

    foreach ($mb in $planMbxFull) {
        if (-not $exoReady) { Write-Skip "FullAccess $($mb.PrimarySmtpAddress) (uitgesteld)"; $resMbx.Add("Uitgesteld (EXO sync): FullAccess $($mb.PrimarySmtpAddress)"); continue }
        try {
            Add-MailboxPermission -Identity $mb.Guid -User $newUser.Id -AccessRights FullAccess `
                -InheritanceType All -AutoMapping:$autoMap -Confirm:$false -ErrorAction Stop | Out-Null
            Write-Ok "FullAccess: $($mb.PrimarySmtpAddress)"; $resMbx.Add("FullAccess: $($mb.PrimarySmtpAddress)")
        } catch {
            if ("$($_.Exception.Message)" -match 'already has|ManagementObjectAlreadyExists') {
                Write-Skip "FullAccess $($mb.PrimarySmtpAddress) (had al)"; $resMbx.Add("Had al: FullAccess $($mb.PrimarySmtpAddress)")
            } else { Write-Fail "FullAccess $($mb.PrimarySmtpAddress): $($_.Exception.Message)"; $resMbx.Add("MISLUKT: FullAccess $($mb.PrimarySmtpAddress)") }
        }
    }
    foreach ($mb in $planMbxSendAs) {
        if (-not $exoReady) { Write-Skip "SendAs $($mb.PrimarySmtpAddress) (uitgesteld)"; $resMbx.Add("Uitgesteld (EXO sync): SendAs $($mb.PrimarySmtpAddress)"); continue }
        try {
            Add-RecipientPermission -Identity $mb.Guid -Trustee $NewUpn -AccessRights SendAs -Confirm:$false -ErrorAction Stop | Out-Null
            Write-Ok "SendAs: $($mb.PrimarySmtpAddress)"; $resMbx.Add("SendAs: $($mb.PrimarySmtpAddress)")
        } catch {
            if ("$($_.Exception.Message)" -match 'already') {
                Write-Skip "SendAs $($mb.PrimarySmtpAddress) (had al)"; $resMbx.Add("Had al: SendAs $($mb.PrimarySmtpAddress)")
            } else { Write-Fail "SendAs $($mb.PrimarySmtpAddress): $($_.Exception.Message)"; $resMbx.Add("MISLUKT: SendAs $($mb.PrimarySmtpAddress)") }
        }
    }
    foreach ($mb in $planMbxSoB) {
        if (-not $exoReady) { Write-Skip "SendOnBehalf $($mb.PrimarySmtpAddress) (uitgesteld)"; $resMbx.Add("Uitgesteld (EXO sync): SendOnBehalf $($mb.PrimarySmtpAddress)"); continue }
        try {
            Set-Mailbox -Identity $mb.Guid -GrantSendOnBehalfTo @{ Add = $NewUpn } -Confirm:$false -ErrorAction Stop
            Write-Ok "SendOnBehalf: $($mb.PrimarySmtpAddress)"; $resMbx.Add("SendOnBehalf: $($mb.PrimarySmtpAddress)")
        } catch {
            Write-Fail "SendOnBehalf $($mb.PrimarySmtpAddress): $($_.Exception.Message)"; $resMbx.Add("MISLUKT: SendOnBehalf $($mb.PrimarySmtpAddress)")
        }
    }
} elseif ($SkipMailboxPermissions) {
    Write-Skip "Mailboxmachtigingen overgeslagen (`$SkipMailboxPermissions = `$true)."
}

# ============================================================================
#  15. Eindoverzicht  (rechten eerst, accountgegevens HELEMAAL ONDERAAN)
# ============================================================================

Write-Host "`n"
Write-Host "############################################################" -ForegroundColor Cyan
Write-Host "#                  OVERZICHT TOEGEWEZEN RECHTEN            #" -ForegroundColor Cyan
Write-Host "############################################################" -ForegroundColor Cyan

Write-Host "`n[ Groepen ]" -ForegroundColor White
if ($resGroups.Count) { $resGroups | ForEach-Object { Write-Host "  - $_" } } else { Write-Host "  (geen)" }

Write-Host "`n[ Licenties ]" -ForegroundColor White
if ($resLics.Count) { $resLics | ForEach-Object { Write-Host "  - $_" } } else { Write-Host "  (geen)" }

Write-Host "`n[ Gedeelde mailboxen ]" -ForegroundColor White
if ($resMbx.Count) { $resMbx | ForEach-Object { Write-Host "  - $_" } } else { Write-Host "  (geen)" }

if ($planRoleGroups.Count) {
    Write-Host "`n[ Rol-toewijsbare groepen - NIET toegevoegd, beoordeel handmatig (admin-rechten via groep) ]" -ForegroundColor Magenta
    $planRoleGroups | ForEach-Object { Write-Host "  - $_" -ForegroundColor Magenta }
}
if ($planRoles.Count) {
    Write-Host "`n[ Directory-/adminrollen - NIET gekopieerd, handmatig indien nodig ]" -ForegroundColor Magenta
    $planRoles | ForEach-Object { Write-Host "  - $_" -ForegroundColor Magenta }
}
if ($warnings.Count) {
    Write-Host "`n[ Waarschuwingen ]" -ForegroundColor Yellow
    $warnings | ForEach-Object { Write-Host "  ! $_" -ForegroundColor Yellow }
}

# --- Accountgegevens: HELEMAAL ONDERAAN ---
Write-Host "`n"
Write-Host "============================================================" -ForegroundColor Green
Write-Host "         ACCOUNTGEGEVENS  (GEVOELIG - niet loggen)          " -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green
Write-Host ("  Weergavenaam : {0}" -f $NewDisplayName) -ForegroundColor White
Write-Host ("  E-mail / UPN : {0}" -f $NewUpn) -ForegroundColor White
if ($upnDomain -ne $smtpDomain) {
    Write-Host ("                 (verwacht primair SMTP: {0} - controleer/zet handmatig)" -f $NewPrimarySmtp) -ForegroundColor DarkYellow
}
if ($generatedPassword) {
    Write-Host ("  Wachtwoord   : {0}" -f $generatedPassword) -ForegroundColor White
    Write-Host "  (wijzigen NIET verplicht bij eerste aanmelding)" -ForegroundColor DarkGray
} else {
    Write-Host "  Wachtwoord   : (bestaand account - geen nieuw wachtwoord gegenereerd)" -ForegroundColor DarkGray
}
Write-Host "============================================================" -ForegroundColor Green
Write-Host "  Bewaar deze gegevens in je wachtwoordmanager en wis daarna de console." -ForegroundColor Yellow

Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null

}