Directory Migration 2.0 Script {currently only into Entra}

Here is my updated Directory Migration 2.0 Script.
This will Migrate just ONE profile, this will NOT migrate all profiles on the machine.

Requirements:
Computer must be in Destination Tenant
Destination Tenant must be connected to Azure via Immy.

How it Works:
We flush out any known enrollments, Rename the Accounts folder in the AAD Broker across all users.
We Should be validating that there is no Autologon registry set, and remove that {{I need to double check this one, it may not have made it in }}
This works off the Immy Primary Person being set, and the Last Logged on User being registered in ImmyBot.
We will pull the Primary Person and then pull the User’s Entra GUID and convert to local SID.
We then Migrate the Last Logged on User’s profile to the Entra SID then join Entra AD.
Allow up to 15 minuets for inTune to do its registration before you do ANYTHING on the machine.
I recommend that you set the inTune Device Primary Person as soon as the device joins.
Once inTune shows the device as compliant, Log the user in, and enroll them into WH4B.

This from my calculations takes about 1/2 the time off the Profile Migrations.

I will look to see if I can in the near future, slip in AD and local user migrations.

This will NOT migrate multiple profiles, if you need that functionality, please use the ImmyBot Directory Migraiton script.

Known issues: Microsoft Consumer Accounts.
You can migrate the profile, but please remove Hello PIN/biometrics first.
You may also have to manually delete that User, we’ve had some oddities with devices that were connected to Consumer Entra.

If your device was MDE joined to the destination tenant, you need to off-board the MDE enrollment prior to profile migration, it seemed to cause it to fail to join Entra.

AutoPilot devices - Must be removed from source and destination tenants. Allow up to 1 hour for MS back-end database to process. You can re-add back to AP post entra join.

<#
.SYNOPSIS
    ImmyBot Metascript — Entra-Only Directory Migration (Primary Person)

.DESCRIPTION
    Migrates the last logged-on local/domain user profile to the Entra identity
    of the computer's assigned Primary Person in Immy. Performs pre-migration
    cleanup of stale MDM/AAD registry artifacts before joining the device to Entra.

    Idempotent behavior:
        Test — Returns $true if device is already Entra-joined and profile is 
               migrated to correct identity. Returns $false if work is needed.
               Use -Force to bypass the already-joined check.
        Set  — Executes only when Test returns $false. After Set completes,
               Test runs again and should return $true.
        Get  — Reports current state. No changes.

    Required Immy context:
        - Computer must have a Primary Person assigned with a valid M365 email.
        - Entra/M365 tenant must be linked to the Immy client.

    Credits:
     Jeremy Barnes - Network Essentials
     James Gerring - Network Essentials
     Darren Kattan - ImmyBot, LLC

     Special Thanks to All the ImmyBot Staff & Immense Networks, LLC
#>

[CmdletBinding()]
param(
    # ── Standard deployment parameters ────────────────────────────────────────
    [Parameter(HelpMessage = "Controls reboot behavior after migration")]
    [ValidateSet('Immediate', 'IfNecessary', 'Suppress')]
    [string]$RebootPreference = 'IfNecessary',

    [Parameter(HelpMessage = "Force migration even if device is already Entra-joined (useful for re-migrating profile or re-joining)")]
    [bool]$Force = $false,

    # ── TROUBLESHOOTING OVERRIDES ──────────────────────────────────────────────
    [Parameter(HelpMessage = "TROUBLESHOOTING ONLY: Skip pre-migration registry cleanup.")]
    [bool]$SkipPreMigrationCleanup = $false,

    [Parameter(HelpMessage = "TROUBLESHOOTING ONLY: Skip Entra user existence check.")]
    [bool]$SkipEntraUserValidation = $false
)

dynamicparam {
    New-ParameterCollection -Parameters @(
        Get-DirectoryTypeParameters -DirectoryTypeVariableName 'DestinationDirectoryType'
        $DirectoryType = $DestinationDirectoryType
        switch ($DirectoryType) {
            'AzureAD' {
                New-HelpText -Name "MappingHint" -HelpMessage "If the profile folder name does not match the UPN prefix in the destination directory, Immy will use the email address configured in the default Outlook profile."
                Get-JoinAzureADParameters
                New-CheckboxParameter -Name LimitToPrimaryUser -Position 6 -HelpMessage (@"
This only works in Hybrid Joined environments!

We ask Azure AD for the OnPremisesSecurityIdentifier of the primary user.

Then, we only migrate the profile associated to that user.

This is useful if you don't want to have to exclude all profiles that can't be mapped.
"@ | Out-Markdown)
            }
            'ActiveDirectory' {
                New-JoinableDomainsDropDown
                New-Parameter -Name StaticallyAssignDomainControllerAsDNSServer -Position 6 -Type 'Boolean' -DefaultValue $false -HelpMessage 'Useful for ensuring the machines can hit the new DC'
            }
        }
    )
}

#begin
begin {

    #region --- Helper: Write-StepLog ---
    function Write-StepLog {
        param(
            [string]$Message,
            [ValidateSet('INFO', 'WARN', 'ERROR', 'VERBOSE')]
            [string]$Level = 'INFO'
        )

        $ts = Get-Date -Format 'HH:mm:ss'

        switch ($Level) {
            'INFO'    { Write-Host "[$ts] [INFO]    $Message" -ForegroundColor Cyan }
            'WARN'    { Write-Host "[$ts] [WARN]    $Message" -ForegroundColor Yellow; Write-Warning $Message }
            'ERROR'   { Write-Host "[$ts] [ERROR]   $Message" -ForegroundColor Red; Write-Error $Message }
            'VERBOSE' { Write-Host "[$ts] [VERBOSE] $Message" -ForegroundColor Gray; Write-Verbose $Message }
        }
    }
    #endregion

    #region --- Helper: Get-LastLoggedOnSID (local execution) ---
    function Get-LastLoggedOnSID {
        $result = Invoke-ImmyCommand -ScriptBlock {
            $regPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI'
            $lastUserSID = $null

            try {
                $lastUserSID = (Get-ItemProperty -Path $regPath -ErrorAction Stop).LastLoggedOnUserSID
            } catch {
                $lastUserSID = $null
            }

            if ($lastUserSID -and $lastUserSID -match '^S-1-') {
                return @{
                    Success   = $true
                    SID       = $lastUserSID
                    Source    = 'LogonUI'
                    Fallback  = $false
                    Error     = $null
                }
            }

            # Fallback: ProfileList scan
            $profileListPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
            $profiles = Get-ChildItem $profileListPath -ErrorAction SilentlyContinue |
                Where-Object { $_.PSChildName -match '^S-1-5-21' } |
                ForEach-Object {
                    $props = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue
                    [PSCustomObject]@{
                        SID         = $_.PSChildName
                        ProfilePath = $props.ProfileImagePath
                        LastWrite   = $_.LastWriteTime
                    }
                } | Sort-Object LastWrite -Descending

            if ($profiles) {
                return @{
                    Success   = $true
                    SID       = $profiles[0].SID
                    Source    = 'ProfileList'
                    Fallback  = $true
                    FallbackInfo = @{
                        ProfilePath = $profiles[0].ProfilePath
                        LastWrite   = $profiles[0].LastWrite
                    }
                    Error     = $null
                }
            }

            return @{
                Success   = $false
                SID       = $null
                Source    = $null
                Fallback  = $false
                Error     = 'Could not determine last logged-on user SID from LogonUI or ProfileList.'
            }
        }

        return $result
    }
    #endregion

    #region --- Helper: Get-ProfileBySID (local execution) ---
    function Get-ProfileBySID {
        param([string]$SID)

        $result = Invoke-ImmyCommand -ScriptBlock {
            param([string]$TargetSID)
            $profilePath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$TargetSID"
            if (-not (Test-Path $profilePath)) {
                return @{
                    Success = $false
                    Error   = "No ProfileList entry found for SID '$TargetSID'."
                }
            }
            $props = Get-ItemProperty $profilePath
            return @{
                Success     = $true
                SID         = $TargetSID
                ProfilePath = $props.ProfileImagePath
                State       = $props.State
            }
        } -ArgumentList $SID

        return $result
    }
    #endregion

    #region --- Helper: Get-DSRegStatus (local execution) ---
    function Get-DSRegStatus {
        $result = Invoke-ImmyCommand -ScriptBlock {
            $raw = & dsregcmd /status 2>&1 | Out-String
            $status = [ordered]@{
                AzureAdJoined        = 'NO'
                DomainJoined         = 'NO'
                WorkplaceJoined      = 'NO'
                TenantName           = 'N/A'
                AzureAdTenantId      = 'N/A'
                DeviceName           = 'N/A'
                EnterpriseJoined     = 'NO'
            }
            # Iterate over a copy of keys to avoid enum modification error
            $keys = @($status.Keys)
            foreach ($key in $keys) {
                if ($raw -match "$key\s*:\s*(.+)") {
                    $status[$key] = $Matches[1].Trim()
                }
            }
            return $status
        }

        return $result
    }
    #endregion

    #region --- Pre-Migration Cleanup (local execution) ---
    function Invoke-PreMigrationCleanup {
        param([string]$ProfileSID)

        if ($using:SkipPreMigrationCleanup) {
            Write-StepLog "TROUBLESHOOTING OVERRIDE — Pre-migration cleanup skipped." -Level WARN
            return
        }

        Write-StepLog "========================================"
        Write-StepLog "PRE-MIGRATION CLEANUP — SID: $ProfileSID"
        Write-StepLog "========================================"

        $cleanupResult = Invoke-ImmyCommand -ScriptBlock {
            param([string]$TargetSID)

            $log = @()
            $errors = @()

            # ── 1. Clear MDM Enrollment keys ─────────────────────────────────────
            $log += "Step 1/6: Clearing stale MDM enrollment records..."
            $enrollmentPaths = @(
                "HKLM:\SOFTWARE\Microsoft\Enrollments",
                "HKLM:\SOFTWARE\Microsoft\Enrollments\Status",
                "HKLM:\SOFTWARE\Microsoft\EnterpriseResourceManager\Tracked"
            )
            $enrollmentCount = 0
            foreach ($path in $enrollmentPaths) {
                if (Test-Path $path) {
                    $children = Get-ChildItem $path -ErrorAction SilentlyContinue
                    foreach ($child in $children) {
                        if ($child.PSChildName -match '^[{]?[0-9A-Fa-f\-]{36}[}]?$') {
                            try {
                                Remove-Item $child.PSPath -Recurse -Force -ErrorAction Stop
                                $enrollmentCount++
                            } catch {
                                $errors += "Could not remove enrollment entry $($child.PSChildName): $_"
                            }
                        }
                    }
                }
            }
            $log += "  MDM enrollment entries removed: $enrollmentCount"

            # ── 2. Clear AAD Broker / WAM token caches ────────────────────────────
            $log += "Step 2/6: Clearing AAD Broker / WAM token caches..."
            $aadTokenPaths = @(
                "HKLM:\SOFTWARE\Microsoft\IdentityStore\Cache",
                "HKLM:\SOFTWARE\Microsoft\IdentityStore\LogonCache",
                "HKCU:\SOFTWARE\Microsoft\IdentityStore\Cache",
                "HKCU:\SOFTWARE\Microsoft\IdentityStore\LogonCache"
            )
            foreach ($path in $aadTokenPaths) {
                if (Test-Path $path) {
                    try {
                        Remove-Item $path -Recurse -Force -ErrorAction Stop
                        $log += "  Cleared: $path"
                    } catch {
                        $errors += "Could not clear $path : $_"
                    }
                }
            }

            # ── 3. Remove stale AAD device registration keys ───────────────────────
            $log += "Step 3/6: Removing stale AAD device registration artifacts..."
            $aadDevicePaths = @(
                "HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\JoinInfo",
                "HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\TenantInfo"
            )
            $deviceRegCount = 0
            foreach ($path in $aadDevicePaths) {
                if (Test-Path $path) {
                    $children = Get-ChildItem $path -ErrorAction SilentlyContinue
                    foreach ($child in $children) {
                        try {
                            Remove-Item $child.PSPath -Recurse -Force -ErrorAction Stop
                            $deviceRegCount++
                        } catch {
                            $errors += "Could not remove $($child.PSChildName): $_"
                        }
                    }
                }
            }
            $log += "  AAD device registration entries removed: $deviceRegCount"

            # ── 4. Clear NGC / Windows Hello keys ────────────────────────────────
            $log += "Step 4/6: Clearing NGC / Windows Hello credential provider keys..."
            $ngcPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{D6886603-9D2F-4EB2-B667-1971041FA96B}"
            if (Test-Path $ngcPath) {
                try {
                    Remove-Item $ngcPath -Recurse -Force -ErrorAction Stop
                    $log += "  NGC credential provider key removed."
                } catch {
                    $errors += "Could not remove NGC key: $_"
                }
            } else {
                $log += "  NGC key not present — skipping."
            }

            # ── 5. Remove AAD Storage workplace join token from user hive ────────
            $log += "Step 5/6: Removing AAD Storage workplace join token from user hive..."
            $userHivePath = "Registry::HKEY_USERS\$TargetSID"
            $workplaceJoinPath = "$userHivePath\SOFTWARE\Microsoft\Windows\CurrentVersion\AAD\Storage\https://login.microsoftonline.com"
            if (Test-Path $workplaceJoinPath) {
                try {
                    Remove-Item $workplaceJoinPath -Recurse -Force -ErrorAction Stop
                    $log += "  AAD Storage workplace join token removed."
                } catch {
                    $errors += "Could not remove AAD Storage from user hive: $_"
                }
            } else {
                $log += "  AAD Storage key not present in user hive — skipping."
            }

            # ── 6. Check if profile hive is loaded ───────────────────────────────
            $log += "Step 6/6: Checking profile hive load state..."
            $loadedHive = Get-ChildItem "Registry::HKEY_USERS" -ErrorAction SilentlyContinue |
                Where-Object { $_.PSChildName -eq $TargetSID }
            if ($loadedHive) {
                $log += "  WARNING: Profile hive for SID $TargetSID is currently loaded."
                $hiveLoaded = $true
            } else {
                $log += "  Profile hive is not currently loaded."
                $hiveLoaded = $false
            }

            return @{
                Log        = $log
                Errors     = $errors
                HiveLoaded = $hiveLoaded
            }
        } -ArgumentList $ProfileSID

        foreach ($entry in $cleanupResult.Log) {
            Write-StepLog $entry
        }
        foreach ($err in $cleanupResult.Errors) {
            Write-StepLog $err -Level WARN
        }

        if ($cleanupResult.HiveLoaded) {
            Write-StepLog "Profile hive is loaded. Reboot strongly recommended post-migration." -Level WARN
        }

        Write-StepLog "Pre-migration cleanup complete."
        Write-StepLog "========================================"
    }
    #endregion

    #region --- Shared Prerequisite Resolution ---
    function Resolve-MigrationPrerequisites {
        #Connect to AzureAD 
        #Connect-ImmyAzureAD

        $result = @{
            PrimaryEmail  = $null
            EntraUser     = $null
            EntraUPN      = $null
            EntraGUID     = $null
            EntraSID      = $null
            LastSID       = $null
            Profile       = $null
            DSRegStatus   = $null
            AlreadyJoined = $false
        }

        # ── Resolve Primary Person (Immy server-side) ────────────────────────────
        Write-StepLog "Resolving Immy Primary Person..."
        try {
            $computer            = Get-ImmyComputer -ErrorAction Stop
            $result.PrimaryEmail = $computer.PrimaryPersonEmail

        } catch {
            throw "Failed to retrieve computer record from Immy: $_"
        }

        if ([string]::IsNullOrWhiteSpace($PrimaryPersonEmail)) {
            throw "No Primary Person assigned to this computer in Immy."
        }
        Write-StepLog "Primary Person email: $($PrimaryPersonEmail)"

        # ── Validate Entra User (Immy server-side) ───────────────────────────────
        if (-not $SkipEntraUserValidation) {
            Write-StepLog "Validating Entra user exists..."
            try {
                $entraUser = Get-ImmyAzureADUser -identity $PrimaryPersonEmail -ErrorAction Stop
                if (-not $entraUser) { throw "Get-ImmyAzureADUser returned null." }
                Write-StepLog "Entra user confirmed: $($entraUser.DisplayName) | UPN: $($entraUser.UserPrincipalName)"
                $result.EntraUser = $entraUser.DisplayName
                $result.EntraUPN  = $entraUser.UPN
                $result.EntraGUID = $entraUser.Id
                $result.EntraSID  = $entraUser.SID
            } catch {
                throw "Is the Primary Person Set? Entra user lookup failed for '$($PrimaryPersonEmail)': $_"
            }
        } else {
            Write-StepLog "TROUBLESHOOTING OVERRIDE: Skipping Entra user validation." -Level WARN
            $result.EntraUPN = $result.UPN
        }

        # ── Check Current Join State (target machine) ────────────────────────────
        $result.DSRegStatus   = Get-DSRegStatus
        $result.AlreadyJoined = ($result.DSRegStatus.AzureAdJoined -eq 'YES')

        Write-StepLog "Current join state:"
        Write-StepLog "  AzureAdJoined   : $($result.DSRegStatus.AzureAdJoined)"
        Write-StepLog "  DomainJoined    : $($result.DSRegStatus.DomainJoined)"
        Write-StepLog "  WorkplaceJoined : $($result.DSRegStatus.WorkplaceJoined)"
        Write-StepLog "  TenantName      : $($result.DSRegStatus.TenantName)"
        Write-StepLog "  TenantId        : $($result.DSRegStatus.AzureAdTenantId)"

        # ── Resolve Last Logged-On Profile (target machine) ──────────────────────
        # Skip source profile resolution when already Entra-joined (post-migration),
        # unless Force is set to allow re-migration.
        if ((-not $result.AlreadyJoined) -or $Force) {
            Write-StepLog "Resolving last logged-on user profile..."
            $sidResult = Get-LastLoggedOnSID
            if (-not $sidResult.Success) {
                throw "Could not resolve last logged-on SID: $($sidResult.Error)"
            }
            $result.LastSID = $sidResult.SID
            if ($sidResult.Fallback) {
                Write-StepLog "Used ProfileList fallback for SID." -Level WARN
            }
            Write-StepLog "Last logged-on SID: $($result.LastSID) (Source: $($sidResult.Source))"

            # ── Resolve Profile Details (target machine) ─────────────────────────
            $profileResult = Get-ProfileBySID -SID $result.LastSID
            if (-not $profileResult.Success) {
                throw "Could not resolve profile for SID '$($result.LastSID)': $($profileResult.Error)"
            }
            $result.Profile = $profileResult
            Write-StepLog "Target profile: $($result.Profile.ProfilePath) | State: $($result.Profile.State)"
        } else {
            Write-StepLog "Device already Entra-joined — source profile resolution skipped."
        }

        return $result
    }
    #endregion

    #region --- METHOD SWITCH ---
    switch ($method) {
    

        #──────────────────────────────────────────────────────────────────────────
        # GET — Report current state. No changes.
        #──────────────────────────────────────────────────────────────────────────
        "get" {
            #Connect Azure AD   
            #Connect-ImmyAzureAD

            Write-StepLog "========================================"
            Write-StepLog "METHOD: GET — Reporting current state"
            Write-StepLog "========================================"

            try {
                $prereqs = Resolve-MigrationPrerequisites
            } catch {
                Write-StepLog "Prerequisite resolution error: $_" -Level WARN
            }

            Write-StepLog "========================================"
            Write-StepLog "CURRENT STATE SUMMARY"
            Write-StepLog "========================================"
            Write-StepLog "  Primary Person Email : $($prereqs?.PrimaryEmail ?? 'N/A')"
            Write-StepLog "  Entra UPN            : $($prereqs?.EntraUPN ?? 'N/A')"
            Write-StepLog "  Entra GUID           : $($prereqs?.EntraGUID ?? 'N/A')"
            Write-StepLog "  Entra SID            : $($prereqs?.EntraSID ?? 'N/A')"
            Write-StepLog "  Source Profile Path  : $($prereqs?.Profile?.ProfilePath ?? 'N/A')"
            Write-StepLog "  Source Profile SID   : $($prereqs?.LastSID ?? 'N/A')"
            Write-StepLog "  AzureAD Joined       : $($prereqs?.DSRegStatus?.AzureAdJoined ?? 'N/A')"
            Write-StepLog "  Domain Joined        : $($prereqs?.DSRegStatus?.DomainJoined ?? 'N/A')"
            Write-StepLog "  Workplace Joined     : $($prereqs?.DSRegStatus?.WorkplaceJoined ?? 'N/A')"
            Write-StepLog "  Tenant Name          : $($prereqs?.DSRegStatus?.TenantName ?? 'N/A')"
            Write-StepLog "  Tenant ID            : $($prereqs?.DSRegStatus?.AzureAdTenantId ?? 'N/A')"
            Write-StepLog "========================================"

            return $prereqs
        }

        #──────────────────────────────────────────────────────────────────────────
        # TEST — Idempotent state check.
        # Returns $true if device is already in desired state (Entra-joined).
        # Returns $false if work is needed.
        # Use -Force to bypass the already-joined check.
        #──────────────────────────────────────────────────────────────────────────
        "test" {
            Write-StepLog "Connecting to Azure AD in Test Mode"
            #Connect Azure AD   
            #Connect-ImmyAzureAD

            Write-StepLog "========================================"
            Write-StepLog "METHOD: TEST — Checking desired state"
            Write-StepLog "========================================"

            try {
                $prereqs = Resolve-MigrationPrerequisites
            } catch {
                Write-StepLog "Prerequisite resolution failed: $_" -Level ERROR
                return $false
            }

            # ── Force parameter bypasses already-joined check ────────────────────
            if ($Force) {
                Write-StepLog "FORCE parameter set — bypassing already-joined check." -Level WARN
                Write-StepLog "Migration will execute regardless of current join state."
                Write-StepLog "========================================"
                Write-StepLog "TEST RESULT: FALSE — Force migration requested"
                Write-StepLog "========================================"
                return $false
            }

            # ── Desired State Check: Is device Entra-joined? ─────────────────────
            if ($prereqs.AlreadyJoined) {
                if (-not $prereqs.EntraSID) {
                    Write-StepLog "Device is Entra-joined but destination user SID was not resolved." -Level WARN
                    Write-StepLog "========================================"
                    Write-StepLog "TEST RESULT: FALSE — Migration required"
                    Write-StepLog "========================================"
                    return $false
                }
                Write-StepLog "Device is already Entra-joined."
                Write-StepLog "  TenantName : $($prereqs.DSRegStatus.TenantName)"
                Write-StepLog "  TenantId   : $($prereqs.DSRegStatus.AzureAdTenantId)"
                Write-StepLog "  EntraSID   : $($prereqs.EntraSID)"
                Write-StepLog "========================================"
                Write-StepLog "TEST RESULT: TRUE — Already in desired state"
                Write-StepLog "========================================"
                return $true
            }

            # Not joined — work is needed
            Write-StepLog "Device is NOT Entra-joined."
            Write-StepLog "========================================"
            Write-StepLog "TEST RESULT: FALSE — Migration required"
            Write-StepLog "========================================"
            return $false
        }

        #──────────────────────────────────────────────────────────────────────────
        # SET — Execute full migration.
        # Only runs when Test returns $false.
        #──────────────────────────────────────────────────────────────────────────
        "set" {
            #Connect Azure AD   
            #Connect-ImmyAzureAD

            Write-StepLog "========================================"
            Write-StepLog "METHOD: SET — Beginning Entra migration"
            Write-StepLog "========================================"

            if ($Force) {
                Write-StepLog "FORCE mode enabled — proceeding with migration despite existing join state." -Level WARN
            }

            # ── Step 1: Resolve prerequisites ─────────────────────────────────────
            Write-StepLog "STEP 1/6: Resolving prerequisites..."
            try {
                $prereqs = Resolve-MigrationPrerequisites
            } catch {
                throw "Prerequisite resolution failed — aborting: $_"
            }
            Write-StepLog "STEP 1/6: Prerequisites resolved."

            # ── Step 2: Pre-migration cleanup ─────────────────────────────────────
            Write-StepLog "STEP 2/6: Running pre-migration cleanup..."
            Invoke-PreMigrationCleanup -ProfileSID $prereqs.LastSID
            Write-StepLog "STEP 2/6: Pre-migration cleanup complete."

            # ── Step 3: Migrate user profile ──────────────────────────────────────
            Write-StepLog "STEP 3/6: Migrating user profile..."
            Write-StepLog "  Source profile : $($prereqs.Profile.ProfilePath)"
            Write-StepLog "  Source SID     : $($prereqs.LastSID)"
            Write-StepLog "  Destination UPN: $($prereqs.EntraUPN) (AzureAD)"
            Write-StepLog "  Destination SID: $($prereqs.EntraSID)"

            try {
                $migrateParams = @{
                    ProfilePath         = $prereqs.Profile.ProfilePath
                    SID                 = $prereqs.EntraSID
                }
                #                DestinationSIDType  = 'AzureAD'  ##pulled out
                #if ($OAuthInfo) { $migrateParams.OAuthInfo = $OAuthInfo }

                Set-UserProfileOwner @migrateParams -ErrorAction Stop
                Write-StepLog "STEP 3/6: Profile migration completed."
            } catch {
                throw "Profile migration failed: $_"
            }

            # ── Step 4: Entra device join ────────────────────────────────────────
            Write-StepLog "STEP 4/6: Initiating Entra device join..."
            Write-StepLog "  RequireIntuneEnrollment : $RequireIntuneEnrollment"

            try {
                $joinParams = @{ RequireIntuneEnrollment = $RequireIntuneEnrollment }
                if ($OAuthInfo)   { $joinParams.OAuthInfo   = $OAuthInfo   }
                if ($DEMUsername) { $joinParams.DEMUsername  = $DEMUsername }
                if ($DEMPassword) { $joinParams.DEMPassword  = $DEMPassword }

                Join-AzureAD @joinParams -ErrorAction Stop
                Write-StepLog "STEP 4/6: Entra device join completed."
            } catch {
                throw "Entra device join failed: $_"
            }

            # ── Step 5: Post-join validation ─────────────────────────────────────
            Write-StepLog "STEP 5/6: Validating post-join state..."
            $postJoinStatus = Get-DSRegStatus

            Write-StepLog "  AzureAdJoined   : $($postJoinStatus.AzureAdJoined)"
            Write-StepLog "  TenantName      : $($postJoinStatus.TenantName)"
            Write-StepLog "  TenantId        : $($postJoinStatus.AzureAdTenantId)"

            if ($postJoinStatus.AzureAdJoined -eq 'YES') {
                Write-StepLog "STEP 5/6: dsregcmd confirms Entra join. Tenant: $($postJoinStatus.TenantName)"
            } else {
                Write-StepLog "STEP 5/6: dsregcmd not yet showing AzureAdJoined = YES. Expected — will reflect after reboot." -Level WARN
            }

            # ── Step 6: Reboot handling ───────────────────────────────────────────
            Write-StepLog "STEP 6/6: Reboot preference: $RebootPreference"
            switch ($RebootPreference) {
                'Immediate'   { Write-StepLog "  Immediate reboot scheduled." }
                'IfNecessary' { Write-StepLog "  Reboot if required." }
                'Suppress'    { Write-StepLog "  Reboot suppressed — manual reboot required." -Level WARN }
            }

            Write-StepLog "========================================"
            Write-StepLog "SET COMPLETE — Migration finished for $($prereqs.PrimaryEmail)."
            Write-StepLog "========================================"
        }
    }
    #endregion

}
#endbegin