IntuneEnrollmentRepair.psm1

#Requires -RunAsAdministrator
<#
.SYNOPSIS IntuneEnrollmentRepair - Diagnose and repair Intune enrollment failures.
.NOTES
    Version : 1.0.8
    Requires: PowerShell 5.1+, Run as Administrator
    Supports: Entra-joined, HAADJ, MAM-to-MDM migration, ppkg (WCD) enrollment
    License : MIT
 
    IMPORTANT - before Invoke-IntuneReEnrollment: delete the device from the
    Intune/Entra portal first to avoid duplicate device records.
    IMPORTANT - before ppkg cleanup: you must re-apply a new package manually.
    This module does NOT apply provisioning packages.
#>


# StrictMode -Version Latest throws PropertyNotFoundException on registry reads
# even with -ErrorAction SilentlyContinue. Version 1 (uninitialized vars only) is safe.
Set-StrictMode -Version 1
$ErrorActionPreference = 'Continue'

#region -- Helpers -------------------------------------------------------------

function Write-Status {
    param([string]$Message, [ValidateSet('Info','OK','Warn','Fail','Section')]$Type = 'Info')
    $c = @{ Info='Cyan'; OK='Green'; Warn='Yellow'; Fail='Red'; Section='Magenta' }
    $p = @{ Info='[*]';  OK='[+]';  Warn='[!]';   Fail='[-]'; Section='[=]'     }
    Write-Host "$($p[$Type]) $Message" -ForegroundColor $c[$Type]
}

function Confirm-Action { param([string]$Prompt)
    return (Read-Host "$Prompt [Y/N]") -match '^[Yy]'
}

# Safe registry reader - avoids PropertyNotFoundException under any StrictMode
function Get-RegValue { param([string]$Path, [string]$Name)
    try {
        if (-not (Test-Path $Path)) { return $null }
        return (Get-ItemProperty -Path $Path -Name $Name -EA SilentlyContinue |
                Select-Object -ExpandProperty $Name -EA SilentlyContinue)
    } catch { return $null }
}

function Invoke-AsSystem { param([string]$Command, [int]$Wait = 60)
    $n = "IntuneRepair_$(Get-Random)"
    $a = New-ScheduledTaskAction -Execute 'powershell.exe' `
             -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$Command`""
    Register-ScheduledTask -TaskName $n -Force `
        -Action $a `
        -Principal (New-ScheduledTaskPrincipal -UserId SYSTEM -RunLevel Highest -LogonType ServiceAccount) `
        -Settings  (New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 5)) | Out-Null
    Start-ScheduledTask -TaskName $n
    Write-Status "Waiting up to $Wait seconds for SYSTEM task..." 'Info'
    $e = 0
    do { Start-Sleep 3; $e += 3
         $s = (Get-ScheduledTask -TaskName $n -EA SilentlyContinue).State
    } while ($s -eq 'Running' -and $e -lt $Wait)
    Unregister-ScheduledTask -TaskName $n -Confirm:$false -EA SilentlyContinue
}

#endregion

#region -- Discovery -----------------------------------------------------------

function Get-EnrollmentGUID {
    # Method 1: OMADM account with ServerVer 4.0
    try {
        foreach ($a in @(Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Provisioning\OMADM\Accounts' -EA SilentlyContinue)) {
            if ((Get-RegValue $a.PSPath 'ServerVer') -eq '4.0') { return $a.PSChildName }
        }
    } catch {}
    # Method 2: Enrollments hive with ProviderID = MS DM Server
    try {
        foreach ($e in @(Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Enrollments' -EA SilentlyContinue)) {
            if ((Get-RegValue $e.PSPath 'ProviderID') -eq 'MS DM Server') { return $e.PSChildName }
        }
    } catch {}
    # Method 3: PushLaunch task path (HAADJ fallback)
    try {
        return @(Get-ScheduledTask -TaskName PushLaunch -EA SilentlyContinue) |
            Where-Object { $_.TaskPath -like '*EnterpriseMgmt*' } |
            Select-Object -ExpandProperty TaskPath |
            ForEach-Object { ($_ -split '\\') | Where-Object { $_ -match '[0-9a-f]{8}-' } } |
            Select-Object -First 1
    } catch {}
    return $null
}

function Get-MdmCert {
    return Get-ChildItem Cert:\LocalMachine\My -EA SilentlyContinue |
        Where-Object { $_.Issuer -like '*Microsoft Intune MDM Device CA*' }
}

function Get-EnrollmentType {
    # Returns: HAADJ | EntraJoined | Unknown
    #
    # dsregcmd alone is unreliable - it can report DomainJoined:NO on a genuine
    # domain-joined device when run offline or in certain cached-credential states.
    # We use four independent signals and require Entra-joined + at least 2 domain
    # signals to classify as HAADJ. This handles AD Connect / HAADJ correctly.
    #
    # Signal 1: dsregcmd DomainJoined:YES
    # Signal 2: Netlogon\Parameters\DomainName registry key (persists offline)
    # Signal 3: Group Policy History key (only written on domain-joined machines)
    # Signal 4: Win32_ComputerSystem.PartOfDomain = True

    $entraJoined = $false
    $domainScore = 0

    try {
        # Join each line individually - avoids whitespace collapse issues with Out-String
        $dsregLines  = @(& dsregcmd /status 2>&1)
        $entraJoined = ($dsregLines | Where-Object { $_ -match 'AzureAdJoined\s*:\s*YES' }).Count -gt 0
        if (($dsregLines | Where-Object { $_ -match 'DomainJoined\s*:\s*YES' }).Count -gt 0) { $domainScore++ }
    } catch {}

    if (Get-RegValue 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' 'DomainName') { $domainScore++ }
    if (Test-Path   'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\History')      { $domainScore++ }

    try {
        $cs = Get-WmiObject Win32_ComputerSystem -EA SilentlyContinue
        if ($cs -and $cs.PartOfDomain) { $domainScore++ }
    } catch {}

    if ($entraJoined -and $domainScore -ge 2) { return 'HAADJ' }
    if ($entraJoined)                         { return 'EntraJoined' }
    return 'Unknown'
}

function Get-SslCertRef { param([string]$Guid)
    if (-not $Guid) { return $null }
    return Get-RegValue "HKLM:\SOFTWARE\Microsoft\Provisioning\OMADM\Accounts\$Guid" 'SslClientCertReference'
}

function Get-EnrollmentProvisioningPackages {
    $pkgs = @()
    try {
        foreach ($pkg in @(Get-ProvisioningPackage -AllInstalledPackages -EA SilentlyContinue)) {
            $isEnroll = $false; $basis = @()

            if ($pkg.Name -match 'enroll|intune|mdm|aad|azure|workplace|bulk' -or
                $pkg.Description -match 'enroll|intune|mdm|aad|workplace') {
                $isEnroll = $true; $basis += 'Name/description keywords'
            }

            $pkgClass = Get-RegValue "HKLM:\SOFTWARE\Microsoft\Provisioning\PackageMetadata\$($pkg.PackageId)" 'PackageClass'
            if ($pkgClass -match 'Provisioning|Enterprise') { $isEnroll = $true; $basis += "PackageClass=$pkgClass" }

            if (-not $isEnroll) {
                foreach ($k in @(Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Enrollments' -EA SilentlyContinue)) {
                    $et = Get-RegValue $k.PSPath 'EnrollmentType'
                    $providerId = Get-RegValue $k.PSPath 'ProviderID'
                    if ($et -eq 12 -or ($et -and -not $providerId)) {
                        $isEnroll = $true; $basis += "EnrollmentType=$et (ppkg)"; break
                    }
                }
            }

            if ($isEnroll) {
                $pkgs += [PSCustomObject]@{
                    PackageId   = $pkg.PackageId; Name = $pkg.Name
                    Version     = $pkg.Version;   InstalledOn = $pkg.Date
                    Basis       = ($basis -join '; '); RawPackage = $pkg
                }
            }
        }
    } catch {}   # Get-ProvisioningPackage not available on all editions
    return $pkgs
}

#endregion

#region -- Checks --------------------------------------------------------------

function Test-RegState { param([string]$Guid)
    $r = @{ ProviderOK=$false; EntDMIDOK=$false; ThumbOK=$false; ExtMgdOK=$true; MAMFound=$false; UrlsOK=$false }
    if (-not $Guid) { return $r }

    $r.ProviderOK = ((Get-RegValue "HKLM:\SOFTWARE\Microsoft\Enrollments\$Guid" 'ProviderID') -eq 'MS DM Server')

    $cert = Get-MdmCert
    if ($cert) {
        $subj = $cert.Subject -replace 'CN=',''
        $r.EntDMIDOK = ((Get-RegValue "HKLM:\SOFTWARE\Microsoft\Enrollments\$Guid\DMClient\MS DM Server" 'EntDMID') -eq $subj)
        $r.ThumbOK   = ((Get-RegValue "HKLM:\SOFTWARE\Microsoft\Enrollments\$Guid" 'DMPCertThumbPrint') -eq $cert.Thumbprint)
    }

    $r.ExtMgdOK = ((Get-RegValue 'HKLM:\SOFTWARE\Microsoft\Enrollments' 'ExternallyManaged') -ne 1)

    foreach ($k in @(Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Enrollments' -EA SilentlyContinue)) {
        if ((Get-RegValue $k.PSPath 'DiscoveryServiceFullURL') -like '*wip.mam.manage.microsoft.com*') {
            $r.MAMFound = $true; break
        }
    }

    $tk = Get-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\TenantInfo\*' -EA SilentlyContinue
    if ($tk) {
        $r.UrlsOK = ((Get-RegValue $tk.PSPath 'MdmEnrollmentUrl') -eq 'https://enrollment.manage.microsoft.com/enrollmentserver/discovery.svc')
    }
    return $r
}

function Test-CertHealth {
    $r = @{ Found=$false; Expired=$false; HasKey=$false; SystemStore=$false; UserStore=$false; TPM=$false; Thumb=$null; Expiry=$null }
    $cert = Get-MdmCert
    if (-not $cert) {
        $uc = Get-ChildItem Cert:\CurrentUser\My -EA SilentlyContinue | Where-Object { $_.Issuer -like '*Microsoft Intune MDM Device CA*' }
        if ($uc) { $r.Found=$true; $r.UserStore=$true; $r.Expiry=$uc.NotAfter; $r.Thumb=$uc.Thumbprint; $r.HasKey=$uc.HasPrivateKey }
        return $r
    }
    $r.Found=$true; $r.SystemStore=$true; $r.Thumb=$cert.Thumbprint; $r.Expiry=$cert.NotAfter
    $r.Expired=($cert.NotAfter -lt (Get-Date)); $r.HasKey=$cert.HasPrivateKey
    if ($cert.HasPrivateKey) {
        try { $r.TPM = ([string](& certutil -store my $cert.Thumbprint 2>&1) -match 'Microsoft Platform Crypto Provider') } catch {}
    }
    return $r
}

function Test-DMWAPService {
    $svc = Get-Service dmwappushservice -EA SilentlyContinue
    if (-not $svc) { return @{ Exists=$false; Running=$false; Auto=$false; StartMode='N/A' } }
    $wmi = Get-WmiObject Win32_Service -Filter "Name='dmwappushservice'" -EA SilentlyContinue
    $mode = if ($wmi) { $wmi.StartMode } else { 'Unknown' }
    return @{ Exists=$true; Running=($svc.Status -eq 'Running'); Auto=($mode -eq 'Auto'); StartMode=$mode }
}

function Test-IMEService {
    $svc = Get-Service IntuneManagementExtension -EA SilentlyContinue
    return @{
        Installed = ($null -ne $svc)
        Running   = ($svc -and $svc.Status -eq 'Running')
        ExeFound  = (Test-Path 'C:\Program Files (x86)\Microsoft Intune Management Extension\Microsoft.Management.Services.IntuneWindowsAgent.exe')
    }
}

function Test-Tasks { param([string]$ActiveGuid)
    $gp   = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'
    $all  = @(Get-ScheduledTask -EA SilentlyContinue | Where-Object { $_.TaskPath -like '*EnterpriseMgmt*' })
    $regs = @(Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Enrollments' -EA SilentlyContinue |
              Where-Object { $_.PSChildName -match $gp } | Select-Object -ExpandProperty PSChildName)

    $retry = @($all | Where-Object { $_.TaskName -like 'Retry Schedule created for incomplete session*' })
    $active = @(); $orphan = @()
    foreach ($t in $all) {
        if ($t.TaskName -like 'Retry Schedule*') { continue }
        $tg = if ($t.TaskPath -match $gp) { $Matches[0] } else { $null }
        if ($tg -and $regs -notcontains $tg) { $orphan += $t } else { $active += $t }
    }
    return @{ All=$all; Active=$active; Orphan=$orphan; Retry=$retry }
}

function Test-SyncLog {
    $p = "$env:TEMP\IntuneRepairDiag"
    Remove-Item $p -Recurse -Force -EA SilentlyContinue
    New-Item $p -ItemType Directory -Force | Out-Null
    try { (New-Object -ComObject Shell.Application).Open('intunemanagementextension://syncapp'); Start-Sleep 5 } catch {}
    Start-Process MdmDiagnosticsTool.exe -ArgumentList "-out `"$p`"" -Wait -NoNewWindow -EA SilentlyContinue
    $r = Join-Path $p 'MDMDiagReport.html'
    if (Test-Path $r) { return @{ Found=$true; Errors=[bool](Select-String $r -Pattern 'The last sync failed' -Quiet) } }
    return @{ Found=$false; Errors=$false }
}

function Test-DuplicateGUIDs {
    $active = @(Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Enrollments' -EA SilentlyContinue |
        Where-Object { (Get-RegValue $_.PSPath 'ProviderID') -eq 'MS DM Server' } |
        Select-Object -ExpandProperty PSChildName)
    return @{ Count=$active.Count; GUIDs=$active; HasDuplicate=($active.Count -gt 1) }
}

#endregion

#region -- Repairs -------------------------------------------------------------

function Repair-MDMUrls {
    $tk = Get-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\CloudDomainJoin\TenantInfo\*' -EA SilentlyContinue
    if (-not $tk) { Write-Status 'TenantInfo key not found. Device may not be Entra-joined.' 'Fail'; return $false }
    @{
        MdmEnrollmentUrl = 'https://enrollment.manage.microsoft.com/enrollmentserver/discovery.svc'
        MdmTermsOfUseUrl = 'https://portal.manage.microsoft.com/TermsofUse.aspx'
        MdmComplianceUrl = 'https://portal.manage.microsoft.com/?portalAction=Compliance'
    }.GetEnumerator() | ForEach-Object {
        New-ItemProperty -LiteralPath $tk.PSPath -Name $_.Key -Value $_.Value -PropertyType String -Force -EA SilentlyContinue | Out-Null
    }
    Write-Status 'MDM URLs configured.' 'OK'; return $true
}

function Repair-ExternallyManagedFlag {
    Set-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Enrollments' -Name ExternallyManaged -Value 0 -Type DWord -Force -EA SilentlyContinue
    Write-Status 'ExternallyManaged reset to 0.' 'OK'
}

function Remove-MAMLeftoverKeys {
    $keys = @(Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Enrollments' -EA SilentlyContinue |
        Where-Object { (Get-RegValue $_.PSPath 'DiscoveryServiceFullURL') -like '*wip.mam.manage.microsoft.com*' })
    if ($keys.Count -eq 0) { Write-Status 'No MAM leftover keys found.' 'OK'; return }
    $keys | ForEach-Object {
        Write-Status " Removing MAM key: $($_.PSChildName)" 'Warn'
        Remove-Item $_.PSPath -Recurse -Force -EA SilentlyContinue
    }
    Write-Status 'MAM leftover keys removed.' 'OK'
}

function Repair-DMWAPService {
    $svc = Get-Service dmwappushservice -EA SilentlyContinue
    if (-not $svc) { Write-Status 'dmwappushservice not found.' 'Warn'; return }
    $wmi = Get-WmiObject Win32_Service -Filter "Name='dmwappushservice'" -EA SilentlyContinue
    if ($wmi -and $wmi.StartMode -ne 'Auto') {
        Set-Service dmwappushservice -StartupType Automatic -EA SilentlyContinue
        Write-Status 'dmwappushservice set to Automatic.' 'OK'
    }
    if ($svc.Status -ne 'Running') {
        Start-Service dmwappushservice -EA SilentlyContinue; Start-Sleep 3; $svc.Refresh()
        if ($svc.Status -eq 'Running') { Write-Status 'dmwappushservice started.' 'OK' }
        else { Write-Status 'Failed to start dmwappushservice.' 'Fail' }
    } else { Write-Status 'dmwappushservice is running.' 'OK' }
}

function Repair-PrivateKey { param([string]$Thumbprint, [bool]$TPMBacked)
    if ($TPMBacked) { Write-Status 'TPM-backed cert - certutil repairstore cannot help. Re-enrollment required.' 'Fail'; return $false }
    $out = & certutil -repairstore my $Thumbprint 2>&1
    if ([string]($out -join ' ') -match 'completed successfully') { Write-Status 'Private key repaired.' 'OK'; return $true }
    Write-Status "certutil repairstore failed: $($out -join ' ')" 'Fail'; return $false
}

function Remove-EnrollmentArtifacts { param([string]$Guid)
    if (-not $Guid) { Write-Status 'No GUID provided.' 'Warn'; return }
    Write-Status "Removing artifacts for GUID: $Guid" 'Info'
    @(
        "HKLM:\SOFTWARE\Microsoft\Enrollments\$Guid",
        "HKLM:\SOFTWARE\Microsoft\Enrollments\Status\$Guid",
        "HKLM:\SOFTWARE\Microsoft\EnterpriseResourceManager\Tracked\$Guid",
        "HKLM:\SOFTWARE\Microsoft\PolicyManager\AdmxInstalled\$Guid",
        "HKLM:\SOFTWARE\Microsoft\PolicyManager\Providers\$Guid",
        "HKLM:\SOFTWARE\Microsoft\Provisioning\OMADM\Accounts\$Guid",
        "HKLM:\SOFTWARE\Microsoft\Provisioning\OMADM\Logger\$Guid",
        "HKLM:\SOFTWARE\Microsoft\Provisioning\OMADM\Sessions\$Guid"
    ) | Where-Object { Test-Path $_ } | ForEach-Object { Remove-Item $_ -Recurse -Force -EA SilentlyContinue; Write-Status " Removed: $_" 'Info' }

    # Clear CurrentEnrollmentId pointer so OMADM Logger no longer references a stale GUID
    Remove-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Provisioning\OMADM\Logger' `
        -Name CurrentEnrollmentId -Force -EA SilentlyContinue

    Get-MdmCert | Remove-Item -Force -EA SilentlyContinue
    Get-ChildItem Cert:\LocalMachine\My -EA SilentlyContinue |
        Where-Object { $_.Issuer -match 'SC_Online_Issuing' } | Remove-Item -Force -EA SilentlyContinue

    @(Get-ScheduledTask -EA SilentlyContinue |
        Where-Object { $_.TaskPath -like "*$Guid*" -or $_.TaskPath -like '*EnterpriseMgmt*' }) |
        ForEach-Object { Unregister-ScheduledTask -TaskName $_.TaskName -Confirm:$false -EA SilentlyContinue; Write-Status " Removed task: $($_.TaskName)" 'Info' }

    # Remove WManSvc cached MDM policy state - can cause re-enrollment to pick up
    # stale session data and fail. Safe to delete; rebuilt on next MDM sync.
    $wman = 'C:\Windows\ServiceState\wmansvc'
    if (Test-Path $wman) {
        Get-ChildItem $wman -File -EA SilentlyContinue | Remove-Item -Force -EA SilentlyContinue
        Write-Status " Cleared WManSvc cache: $wman" 'Info'
    }

    Write-Status 'Enrollment artifacts removed.' 'OK'
}

function Remove-StaleRetryTasks {
    $tasks = @(Get-ScheduledTask -EA SilentlyContinue | Where-Object { $_.TaskName -like 'Retry Schedule created for incomplete session*' })
    if ($tasks.Count -eq 0) { Write-Status 'No stale retry tasks.' 'OK'; return }
    $tasks | ForEach-Object { Unregister-ScheduledTask -TaskName $_.TaskName -Confirm:$false -EA SilentlyContinue }
    Write-Status "$($tasks.Count) stale retry task(s) removed." 'OK'
}

function Remove-OrphanedEnrollmentTasks { param([string[]]$Names)
    if ($Names.Count -eq 0) { Write-Status 'No orphaned tasks to remove.' 'OK'; return }
    $Names | ForEach-Object {
        Unregister-ScheduledTask -TaskName $_ -Confirm:$false -EA SilentlyContinue
        Write-Status " Removed: $_" 'Info'
    }
    Write-Status "$($Names.Count) orphaned task(s) removed." 'OK'
}

function Remove-OrphanedEnrollmentGUID { param([string]$OrphanGuid)
    if (-not $OrphanGuid) { return }
    Write-Status "Removing orphaned GUID: $OrphanGuid" 'Warn'
    Remove-EnrollmentArtifacts -Guid $OrphanGuid
}

function Remove-ProvisioningPackageAndArtifacts { param([object]$Package)
    Write-Status "Removing ppkg: $($Package.Name) [$($Package.PackageId)]" 'Warn'
    try { Remove-ProvisioningPackage -PackageId $Package.PackageId -EA Stop; Write-Status ' Package removed.' 'OK' }
    catch { Write-Status " Remove-ProvisioningPackage failed: $_ Continuing cleanup." 'Warn' }

    $pkgId = $Package.PackageId
    if ($pkgId -and (Test-Path "HKLM:\SOFTWARE\Microsoft\Provisioning\PackageMetadata\$pkgId")) {
        Remove-Item "HKLM:\SOFTWARE\Microsoft\Provisioning\PackageMetadata\$pkgId" -Recurse -Force -EA SilentlyContinue
        Write-Status " Removed PackageMetadata key." 'Info'
    }

    $gp = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'
    foreach ($k in @(Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Enrollments' -EA SilentlyContinue)) {
        if ($k.PSChildName -notmatch $gp) { continue }
        $et   = Get-RegValue $k.PSPath 'EnrollmentType'
        $pid2 = Get-RegValue $k.PSPath 'ProviderID'
        $disc = Get-RegValue $k.PSPath 'DiscoveryServiceFullURL'
        if ($et -eq 12 -or ($et -and -not $pid2 -and $disc -notlike '*wip.mam*')) {
            Write-Status " Removing ppkg GUID: $($k.PSChildName)" 'Info'
            Remove-EnrollmentArtifacts -Guid $k.PSChildName
        }
    }

    foreach ($sub in 'Logger','Sessions') {
        foreach ($k in @(Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Provisioning\OMADM\$sub" -EA SilentlyContinue)) {
            if ($k.PSChildName -match $gp -and -not (Test-Path "HKLM:\SOFTWARE\Microsoft\Enrollments\$($k.PSChildName)")) {
                Remove-Item $k.PSPath -Recurse -Force -EA SilentlyContinue
                Write-Status " Removed orphaned OMADM $sub key: $($k.PSChildName)" 'Info'
            }
        }
    }

    Remove-StaleRetryTasks
    Write-Host ''
    Write-Status '+==================================================+' 'Warn'
    Write-Status '| PROVISIONING PACKAGE REMOVED |' 'Warn'
    Write-Status '| Re-apply a new package manually to re-enroll. |' 'Warn'
    Write-Status '+==================================================+' 'Warn'
    Write-Host ''
}

function Start-MDMReEnrollment {
    Write-Status 'Triggering DeviceEnroller.exe as SYSTEM...' 'Info'
    Invoke-AsSystem 'C:\Windows\System32\DeviceEnroller.exe /C /AutoenrollMDM' -Wait 90
    Write-Status 'Waiting 30s for enrollment to settle...' 'Info'; Start-Sleep 30
    $cert = Get-MdmCert
    if ($cert) { Write-Status "Re-enrollment succeeded. Thumbprint: $($cert.Thumbprint)" 'OK'; return $true }
    Write-Status 'MDM cert not yet present - may still be in progress. Recheck in 5 min.' 'Warn'; return $false
}

function Start-IMESync {
    try { (New-Object -ComObject Shell.Application).Open('intunemanagementextension://syncapp') } catch {}
    $t = Get-ScheduledTask -EA SilentlyContinue | Where-Object { $_.TaskName -eq 'Schedule #1 created by enrollment client' }
    if ($t) { Start-ScheduledTask -TaskName $t.TaskName -EA SilentlyContinue; Write-Status 'IME sync triggered.' 'OK' }
}

#endregion

#region -- Public Functions ----------------------------------------------------

function Invoke-IntuneEnrollmentDiagnostics {
    <#
    .SYNOPSIS
        Full Intune enrollment diagnostic. Detects ppkg vs standard enrollment and
        branches behaviour accordingly. Use -Fix to auto-remediate found issues.
 
    .DESCRIPTION
        ppkg enrolled + failures -> with -Fix: removes package + artifacts. Admin re-applies manually.
        ppkg enrolled + healthy -> informational only, no action.
        Standard + failures -> -Fix applies safe repairs. Invoke-IntuneReEnrollment for full wipe.
        Standard + healthy -> all green.
 
    .EXAMPLE
        Invoke-IntuneEnrollmentDiagnostics
        Invoke-IntuneEnrollmentDiagnostics -Fix
    #>

    [CmdletBinding()] param([switch]$Fix)

    Write-Host ''
    Write-Status '===================================================' 'Section'
    Write-Status " Intune Enrollment Diagnostics v1.0.8$(if($Fix){' [-Fix]'})" 'Section'
    Write-Status '===================================================' 'Section'
    Write-Host ''

    # Failure flag accumulator - used for ppkg branch decision at the end
    $fail = @{ cert=$false; reg=$false; svc=$false; task=$false; sync=$false }

    #-- Step 0: Provisioning Packages -----------------------------------------
    Write-Status 'STEP 0: Provisioning Package Detection' 'Section'
    $ppkgs   = @(Get-EnrollmentProvisioningPackages)
    $hasPpkg = ($ppkgs.Count -gt 0)
    if ($hasPpkg) {
        Write-Status "$($ppkgs.Count) enrollment ppkg(s) found - device enrolled via WCD/ppkg." 'Warn'
        $ppkgs | ForEach-Object { Write-Status " $($_.Name) v$($_.Version) installed $($_.InstalledOn)" 'Info' }
    } else { Write-Status 'No enrollment provisioning packages detected. Standard MDM enrollment.' 'OK' }
    Write-Host ''

    #-- Step 1: GUID + Join Type -----------------------------------------------
    Write-Status 'STEP 1: Enrollment GUID and join type' 'Section'
    $guid    = Get-EnrollmentGUID
    $jtype   = Get-EnrollmentType
    Write-Status "Join type: $jtype" 'Info'
    if ($guid) { Write-Status "Active GUID: $guid" 'OK' }
    else       { Write-Status 'No enrollment GUID found. Device does not appear to be enrolled.' 'Fail'; $fail.reg=$true }

    $dupe = Test-DuplicateGUIDs
    if ($dupe.HasDuplicate) {
        Write-Status "DUPLICATE GUIDs: $($dupe.Count) active MS DM Server entries found." 'Fail'; $fail.reg=$true
        $dupe.GUIDs | ForEach-Object {
            $m = if ($_ -eq $guid) { '<-- active' } else { '<-- ORPHAN' }
            Write-Status " $_ $m" 'Warn'
        }
        if ($Fix -and -not $hasPpkg) {
            $dupe.GUIDs | Where-Object { $_ -ne $guid } | ForEach-Object {
                if (Confirm-Action "Remove orphaned GUID $_?") { Remove-OrphanedEnrollmentGUID $_ }
            }
        }
    } else { Write-Status 'No duplicate GUIDs.' 'OK' }
    Write-Host ''

    #-- Step 2: Certificate ----------------------------------------------------
    Write-Status 'STEP 2: MDM Certificate Health' 'Section'
    $ch = Test-CertHealth
    if ($ch.Found) {
        if ($ch.SystemStore) { Write-Status 'Certificate in LocalMachine\My store.' 'OK' }
        if ($ch.UserStore)   { Write-Status 'Certificate in WRONG store (CurrentUser\My).' 'Fail'; $fail.cert=$true }
        Write-Status "Thumbprint : $($ch.Thumb)" 'Info'
        Write-Status "Expires : $($ch.Expiry)" 'Info'
        if ($ch.Expired)  { Write-Status 'Certificate EXPIRED. Re-enrollment required.' 'Fail'; $fail.cert=$true }
        else              { Write-Status 'Certificate within validity period.' 'OK' }
        if ($ch.HasKey)   { Write-Status 'Private key present.' 'OK' }
        else              { Write-Status 'Private key MISSING.' 'Fail'; $fail.cert=$true
                            if ($Fix -and -not $hasPpkg) { Repair-PrivateKey $ch.Thumb $ch.TPM } }
        if ($ch.TPM)      { Write-Status 'TPM-backed (Microsoft Platform Crypto Provider).' 'OK' }
        else              { Write-Status 'Software-backed (not TPM).' 'Warn' }
    } else {
        Write-Status 'MDM certificate MISSING from all stores. Re-enrollment required.' 'Fail'; $fail.cert=$true
    }

    $ssl = Get-SslCertRef $guid
    if ($ssl)                        { Write-Status "SslClientCertReference: $ssl" 'OK' }
    elseif ($jtype -eq 'HAADJ')      { Write-Status 'SslClientCertReference absent. Normal on modern HAADJ enrollment via Entra registration path.' 'Warn' }
    else                             { Write-Status 'SslClientCertReference absent - normal for Entra-joined enrollment.' 'Info' }
    Write-Host ''

    #-- Step 3: Registry -------------------------------------------------------
    Write-Status 'STEP 3: Registry Enrollment State' 'Section'
    $rs = Test-RegState $guid
    if ($rs.ProviderOK)   { Write-Status 'ProviderID = MS DM Server.' 'OK' }
    elseif ($jtype -eq 'HAADJ') { Write-Status 'ProviderID absent. Normal on modern HAADJ enrolled via Entra registration path.' 'Warn' }
    else                  { Write-Status 'ProviderID absent - expected on Entra-joined devices.' 'Info' }

    if ($rs.EntDMIDOK)    { Write-Status 'EntDMID matches cert subject.' 'OK' }
    else                  { Write-Status 'EntDMID mismatch - may resolve after reboot.' 'Warn' }

    if ($rs.ThumbOK)      { Write-Status 'DMPCertThumbPrint matches cert.' 'OK' }
    else                  { Write-Status 'DMPCertThumbPrint mismatch/missing. Usually resolves after reboot + sync.' 'Warn' }

    if ($rs.ExtMgdOK)     { Write-Status 'ExternallyManaged not blocking enrollment.' 'OK' }
    else                  { Write-Status 'ExternallyManaged=1 detected! Causes 0x80180026.' 'Fail'; $fail.reg=$true
                            if ($Fix -and -not $hasPpkg) { Repair-ExternallyManagedFlag } }

    if ($rs.MAMFound)     { Write-Status 'MAM leftover keys detected - can block re-enrollment.' 'Fail'; $fail.reg=$true
                            if ($Fix -and -not $hasPpkg) { Remove-MAMLeftoverKeys } }
    else                  { Write-Status 'No MAM leftover keys.' 'OK' }

    if ($rs.UrlsOK)       { Write-Status 'MDM URLs correctly configured.' 'OK' }
    else                  { Write-Status 'MDM URLs missing or incorrect.' 'Fail'; $fail.reg=$true
                            if ($Fix -and -not $hasPpkg) { Repair-MDMUrls } }
    Write-Host ''

    #-- Step 4: Services -------------------------------------------------------
    Write-Status 'STEP 4: Services' 'Section'
    $dw = Test-DMWAPService
    if (-not $dw.Exists) { Write-Status 'dmwappushservice not found.' 'Warn' }
    elseif ($dw.Running -and $dw.Auto) { Write-Status 'dmwappushservice running, startup=Automatic.' 'OK' }
    else {
        $msg = if (-not $dw.Running) { "NOT running (startup=$($dw.StartMode))" } else { "running but startup=$($dw.StartMode) - won't survive reboot" }
        Write-Status "dmwappushservice $msg." 'Fail'; $fail.svc=$true
        if ($Fix) { Repair-DMWAPService }
    }

    $ime = Test-IMEService
    if ($ime.Running)        { Write-Status 'IME service running.' 'OK' }
    elseif ($ime.Installed)  { Write-Status 'IME installed but NOT running.' 'Warn'; $fail.svc=$true
                               if ($Fix) { Start-Service IntuneManagementExtension -EA SilentlyContinue; Write-Status 'IME start attempted.' 'Info' } }
    elseif ($ime.ExeFound)   { Write-Status 'IME exe present but service not installed.' 'Fail'; $fail.svc=$true }
    else                     { Write-Status 'IME not installed.' 'Warn' }
    Write-Host ''

    #-- Step 5: Scheduled Tasks ------------------------------------------------
    Write-Status 'STEP 5: Enrollment Scheduled Tasks' 'Section'
    $ts = Test-Tasks $guid
    Write-Status "Total: $($ts.All.Count) Active: $($ts.Active.Count) Orphaned: $($ts.Orphan.Count) Retry: $($ts.Retry.Count)" 'Info'

    if ($ts.Active.Count -gt 0) {
        Write-Status 'Active tasks:' 'OK'
        $ts.Active | ForEach-Object { Write-Status " $($_.TaskName) [$($_.State)]" 'Info' }
    } else { Write-Status 'No active enrollment tasks found.' 'Fail'; $fail.task=$true }

    if ($ts.Orphan.Count -gt 0) {
        Write-Status "$($ts.Orphan.Count) orphaned task(s) (GUID not in registry):" 'Warn'
        $ts.Orphan | ForEach-Object { Write-Status " $($_.TaskName)" 'Warn' }
        if ($Fix -and -not $hasPpkg) { Remove-OrphanedEnrollmentTasks ($ts.Orphan | Select-Object -ExpandProperty TaskName) }
        else { Write-Status ' Run -Fix to remove.' 'Info' }
    } else { Write-Status 'No orphaned tasks.' 'OK' }

    if ($ts.Retry.Count -gt 0) {
        Write-Status "$($ts.Retry.Count) stale retry task(s):" 'Warn'
        $ts.Retry | ForEach-Object { Write-Status " $($_.TaskName)" 'Warn' }
        if ($Fix) { Remove-StaleRetryTasks } else { Write-Status ' Run -Fix to remove.' 'Info' }
    } else { Write-Status 'No stale retry tasks.' 'OK' }
    Write-Host ''

    #-- Step 6: Sync Log -------------------------------------------------------
    Write-Status 'STEP 6: MDM Sync Log' 'Section'
    $log = Test-SyncLog
    if (-not $log.Found)  { Write-Status 'MDMDiagReport not generated - MdmDiagnosticsTool may have failed.' 'Warn' }
    elseif ($log.Errors)  { Write-Status 'Sync errors in MDM diagnostic report.' 'Fail'; $fail.sync=$true }
    else                  { Write-Status 'No sync errors detected.' 'OK' }
    Write-Host ''

    #-- Branch: ppkg vs standard -----------------------------------------------
    $anyFail = $fail.Values -contains $true
    if ($hasPpkg -and $anyFail) {
        Write-Status '===================================================' 'Section'
        Write-Status ' PROVISIONING PACKAGE ENROLLMENT IS BROKEN ' 'Fail'
        Write-Status ' Remove package + re-apply a new one manually. ' 'Warn'
        Write-Status ' Do NOT use Invoke-IntuneReEnrollment here. ' 'Info'
        Write-Status '===================================================' 'Section'
        if ($Fix) {
            $ppkgs | ForEach-Object {
                if (Confirm-Action "Remove package '$($_.Name)' and all artifacts?") {
                    Remove-ProvisioningPackageAndArtifacts $_
                }
            }
        } else { Write-Status 'Run -Fix to remove the broken package and artifacts.' 'Info' }
    } elseif ($hasPpkg -and -not $anyFail) {
        Write-Status 'Provisioning package enrollment is healthy. No action needed.' 'OK'
    } else {
        Write-Status '===================================================' 'Section'
        Write-Status ' Diagnostics Complete' 'Section'
        if (-not $Fix -and $anyFail) {
            Write-Status ' Run -Fix to auto-remediate safe issues.' 'Info'
            Write-Status ' Run Invoke-IntuneReEnrollment for full re-enrollment.' 'Info'
        } elseif (-not $anyFail) {
            Write-Status ' No enrollment failures detected.' 'OK'
        }
        Write-Status '===================================================' 'Section'
    }
    Write-Host ''
}

function Invoke-IntuneReEnrollment {
    <#
    .SYNOPSIS
        Full destructive re-enrollment for standard (non-ppkg) devices.
        For ppkg devices use: Invoke-IntuneEnrollmentDiagnostics -Fix
        Delete the device from the Intune/Entra portal BEFORE running this.
    .EXAMPLE
        Invoke-IntuneReEnrollment
        Invoke-IntuneReEnrollment -Force
    #>

    [CmdletBinding()] param([switch]$Force)

    $ppkgs = @(Get-EnrollmentProvisioningPackages)
    if ($ppkgs.Count -gt 0) {
        Write-Status 'WARNING: ppkg enrollment detected.' 'Fail'
        $ppkgs | ForEach-Object { Write-Status " $($_.Name)" 'Info' }
        Write-Status 'Use Invoke-IntuneEnrollmentDiagnostics -Fix for ppkg devices.' 'Warn'
        if (-not (Confirm-Action 'Continue anyway? (Not recommended for ppkg devices)')) { return }
    }

    Write-Host ''
    Write-Status '===================================================' 'Section'
    Write-Status ' Intune FULL Re-Enrollment !! DESTRUCTIVE !!' 'Fail'
    Write-Status ' Delete device from Intune portal first!' 'Warn'
    Write-Status '===================================================' 'Section'
    Write-Host ''
    if (-not $Force -and -not (Confirm-Action 'Proceed with full re-enrollment?')) { Write-Status 'Cancelled.' 'Info'; return }

    $guid = Get-EnrollmentGUID

    Write-Host ''; Write-Status 'Phase 1: MDM URLs'                   'Section'; Repair-MDMUrls
    Write-Host ''; Write-Status 'Phase 2: ExternallyManaged flag'     'Section'; Repair-ExternallyManagedFlag
    Write-Host ''; Write-Status 'Phase 3: MAM leftover keys'          'Section'; Remove-MAMLeftoverKeys
    Write-Host ''; Write-Status 'Phase 4: Stale retry tasks'          'Section'; Remove-StaleRetryTasks
    Write-Host ''; Write-Status 'Phase 5: Enrollment artifacts'       'Section'
    if ($guid) {
        Remove-EnrollmentArtifacts $guid
    } else {
        Write-Status 'No GUID - removing all GUID-shaped subkeys...' 'Warn'
        @(Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Enrollments' -EA SilentlyContinue) |
            Where-Object { $_.PSChildName -match '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' } |
            ForEach-Object { Remove-Item $_.PSPath -Recurse -Force -EA SilentlyContinue }
        Get-MdmCert | Remove-Item -Force -EA SilentlyContinue
    }
    Write-Host ''; Write-Status 'Phase 6: dmwappushservice'           'Section'; Repair-DMWAPService
    Write-Host ''; Write-Status 'Phase 7: Re-enrollment (SYSTEM)'     'Section'; $ok = Start-MDMReEnrollment
    Write-Host ''; Write-Status 'Phase 8: IME sync'                   'Section'; Start-IMESync
    Write-Host ''; Write-Status 'Phase 9: Post-enrollment validation' 'Section'; Start-Sleep 10; Invoke-IntuneEnrollmentDiagnostics

    Write-Host ''
    if ($ok) { Write-Status 'Re-enrollment complete. Monitor in Intune portal.' 'OK' }
    else      { Write-Status 'Re-enrollment may still be in progress. Recheck in 5 min.' 'Warn' }
}

function Get-IntuneEnrollmentSummary {
    <#
    .SYNOPSIS Structured enrollment summary. Suitable for RMM pre-checks or monitoring.
    .EXAMPLE Get-IntuneEnrollmentSummary
    #>

    $guid  = Get-EnrollmentGUID; $cert = Get-MdmCert
    $ime   = Test-IMEService;    $dw   = Test-DMWAPService
    $ts    = Test-Tasks $guid;   $dup  = Test-DuplicateGUIDs
    $ppkgs = @(Get-EnrollmentProvisioningPackages)

    [PSCustomObject]@{
        EnrolledGUID      = $guid
        JoinType          = Get-EnrollmentType
        PpkgEnrolled      = ($ppkgs.Count -gt 0)
        PpkgNames         = ($ppkgs.Name -join ', ')
        DuplicateGUIDs    = $dup.HasDuplicate
        CertPresent       = ([bool]$cert)
        CertExpired       = if ($cert) { $cert.NotAfter -lt (Get-Date) } else { 'N/A' }
        CertExpiry        = if ($cert) { $cert.NotAfter.ToString('yyyy-MM-dd') } else { 'N/A' }
        CertThumbprint    = if ($cert) { $cert.Thumbprint } else { 'N/A' }
        IMERunning        = $ime.Running
        DMWAPRunning      = $dw.Running
        DMWAPAutomatic    = $dw.Auto
        ActiveTasks       = $ts.Active.Count
        OrphanedTasks     = $ts.Orphan.Count
        RetryTasks        = $ts.Retry.Count
    }
}

#endregion

Export-ModuleMember -Function @(
    'Invoke-IntuneEnrollmentDiagnostics',
    'Invoke-IntuneReEnrollment',
    'Get-IntuneEnrollmentSummary',
    'Get-EnrollmentProvisioningPackages',
    'Repair-MDMUrls',
    'Repair-ExternallyManagedFlag',
    'Remove-MAMLeftoverKeys',
    'Repair-DMWAPService',
    'Remove-EnrollmentArtifacts',
    'Remove-ProvisioningPackageAndArtifacts',
    'Remove-StaleRetryTasks',
    'Remove-OrphanedEnrollmentTasks',
    'Remove-OrphanedEnrollmentGUID',
    'Start-MDMReEnrollment'
)