<#
.SYNOPSIS
Cleans up Horizon installation by removing services, files, and registry entries based on WiX file configuration.

.DESCRIPTION
This script parses a WiX installer file to identify installed services, files, and registry entries, then removes them to perform a complete cleanup of a Horizon installation. It supports both WiX v3.x and v4.x schemas and provides optional transcript logging.

.PARAMETER wixFilePath
Specify the path to the WiX file (.wxs) that contains the installation configuration to be cleaned up.

.PARAMETER generateTranscript
Use this switch to generate a transcript log file with timestamped filename for auditing cleanup operations.

.LINK
https://wixtoolset.org/

.NOTES
Version: 2.1.0
Author: Omnissa
Last Updated: March 5, 2026

- Requires administrative privileges to stop services and modify registry
- Creates timestamped log files when transcript is enabled
- See CHANGELOG.md for version history
          
.EXAMPLE
.\Horizon-Cleanup.ps1 -wixFilePath "C:\temp\horizon.wxs"
Performs cleanup based on the specified WiX file without generating a transcript.

.EXAMPLE
.\Horizon-Cleanup.ps1 -wixFilePath "C:\temp\horizon.wxs" -generateTranscript
Performs cleanup and generates a transcript log file.
    
.ROLE
System Administrator, Software Deployment
#>

[CmdletBinding()]
Param(
    [Parameter(Mandatory = $true, Position = 0, HelpMessage = "Specify the wix file path for clean up.")]
    [string]$wixFilePath,
    [Parameter(Mandatory = $false, Position = 1, HelpMessage = "Use this switch to generate a transcript.")]
    [Switch]$generateTranscript
)

# Script version
$ScriptVersion = "2.1.0"
Write-Host "Horizon Cleanup Tool v$ScriptVersion" -ForegroundColor Cyan
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host ""

# Standard folder IDs and their resolved paths
$standardDirs = @{
    "System64Folder"       = "$env:SystemRoot\System32"
    "SystemFolder"         = "$env:SystemRoot\System32"
    "WindowsFolder"        = "$env:SystemRoot"
    "ProgramFilesFolder"   = "$env:ProgramFiles"
    "ProgramFiles64Folder" = "$env:ProgramW6432"
    "ProgramFiles32Folder" = "$env:ProgramFiles(x86)"
    "DesktopFolder"        = "$env:UserProfile\Desktop"
    "AppDataFolder"        = "$env:APPDATA"
    "CommonAppDataFolder"  = "$env:ProgramData"
    "LocalAppDataFolder"   = "$env:LocalAppData"
    "StartupFolder"        = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"
    "CommonFiles64Folder"  = "${env:ProgramFiles}\Common Files"
}

<#
.SYNOPSIS
Stops a Windows service gracefully, with fallback to process termination if needed.

.DESCRIPTION
Attempts to stop a Windows service using the standard Stop-Service cmdlet first. If the service
cannot be stopped normally, the function identifies the service's executable process and forcibly
terminates it. This two-step approach ensures robust service shutdown even for unresponsive services.

.PARAMETER ServiceName
The name of the Windows service to stop. This should be the service name, not the display name.

.EXAMPLE
Stop-OmnissaService -ServiceName "MyService"
Stops the service named "MyService", falling back to process termination if needed.

.NOTES
- Requires administrative privileges to stop services and terminate processes
- Uses a 1-minute timeout when waiting for normal service shutdown
- Provides colored console output indicating success, warnings, and errors
- If the service executable is not running as a process, no error is thrown
- Does not return a value - success/failure is indicated through console output
#>
function Stop-OmnissaService {
    param(
        [string]$ServiceName
    )

    #Try to stop the service normally
    try {
        $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
        Stop-Service -Name $ServiceName -Force -ErrorAction Stop 
        $svc.WaitForStatus("Stopped", '00:01:00')
        Write-Host "Successfully stopped service $ServiceName" -ForegroundColor Green
    }
    catch {
        Write-Host "Could not stop $ServiceName normally. Checking for associated process..." -ForegroundColor Yellow
    }

    # Get service details (includes binary path)
    $service = Get-CimInstance Win32_Service | Where-Object { $_.Name -eq $ServiceName }

    if ($service -and $service.PathName) {
        $exe = [System.IO.Path]::GetFileNameWithoutExtension(($service.PathName -replace '"', ''))

        # Find and kill process if running
        $proc = Get-Process $exe
        if ($proc) {
            Write-Host "Killing process $($proc.Name) (PID $($proc.Id))" -ForegroundColor Red
            Stop-Process -Id $proc.Id -Force
        }
        else {
            Write-Host "No running process found for $exe" -ForegroundColor Yellow
        }
    }
}


<#
.SYNOPSIS
Stops and removes Windows services specified in the WiX configuration file.

.DESCRIPTION
Enumerates all ServiceInstall elements in the WiX XML and performs a two-phase cleanup:
1. Stops all services gracefully, falling back to process termination if needed
2. Removes service registrations from Windows and cleans up registry entries
Uses the Stop-OmnissaService helper function for robust service termination.

.EXAMPLE
Stop-Services
Stops and removes all services found in the loaded WiX XML configuration.

.NOTES
- Requires the $xml and $nsMgr variables to be initialized with WiX file content
- Requires administrative privileges to stop services and modify registry
- May require a system reboot after service registry cleanup
- Continues processing even if individual services fail to stop or remove
- Provides colored output for success/failure status of each operation
#>
function Stop-Services {
    Write-Host "Enumerating installed services and stopping them..."
    Write-Host "==================================================="
    
    # Find all <ServiceInstall> nodes
    $services = $xml.SelectNodes("//wx:ServiceInstall", $nsMgr)

    # Output service names
    foreach ($svc in $services) {
        $svcName = $svc.GetAttribute("Name")
        $displayName = $svc.GetAttribute("DisplayName")
        Write-Host "Service Name: $svcName"
        if ($displayName) {
            Write-Host "Display Name: $displayName"
        }
    
        try {
            Write-Host "Stopping service..." -NoNewline
            Stop-OmnissaService $svcName
            Write-Host "[Success]" -ForegroundColor Green
        }
        catch {
            Write-Host " Failed. Error: $_"

        }

        Write-Host ""
    }

    foreach ($svc in $services) {
        $svcName = $svc.GetAttribute("Name")
        $displayName = $svc.GetAttribute("DisplayName")

        Write-Host "Deleting service [$svcName]" -NoNewline
        Get-CimInstance Win32_Service -Filter "Name='$svcName'" | Remove-CimInstance
        
        $regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\$svcName"
        if (Test-Path $regPath) {
            try {
                Remove-Item -Path $regPath -Recurse -Force
                Write-Host "Registry entry for service '$svcName' deleted. A reboot may be required."
            }
            catch {
                Write-Error "Failed to delete registry entry: $_"
            }
        }
        else {
            Write-Host "No registry entry found for service '$svcName'."
        }
        Write-Host ""
    }

    Write-Host ""
}

<#
# Joins a registry path by combining the root and child path.
#>
function Join-RegistryPath {
    param(
        [Parameter(Mandatory)]
        [string]$Root,       # e.g. HKCR, HKLM, HKCU
        [Parameter(Mandatory)]
        [string]$ChildPath
    )

    # Normalize the root
    switch -Regex ($Root.ToUpper()) {
        '^HKCR' { $rootPath = "Registry::HKEY_CLASSES_ROOT" }
        '^HKCU' { $rootPath = "Registry::HKEY_CURRENT_USER" }
        '^HKLM' { $rootPath = "Registry::HKEY_LOCAL_MACHINE" }
        '^HKU' { $rootPath = "Registry::HKEY_USERS" }
        '^HKCC' { $rootPath = "Registry::HKEY_CURRENT_CONFIG" }
        default { throw "Unknown root hive: $Root" }
    }

    # Clean up slashes
    $childPath = $ChildPath.TrimStart('\').TrimStart('/')

    return (Join-Path -Path $rootPath -ChildPath $childPath)
}

<#
.SYNOPSIS
Schedules a file or directory for deletion on next reboot using PendingFileRenameOperations.

.DESCRIPTION
Adds the specified path to the Windows PendingFileRenameOperations registry key, which causes
Windows to delete the file or directory on the next system restart. This is useful when files
are locked by running processes and cannot be deleted immediately.

.PARAMETER Path
The full path to the file or directory to delete on reboot.

.EXAMPLE
Add-PendingFileDelete -Path "C:\Program Files\MyApp"
Schedules the MyApp directory for deletion on next reboot.

.NOTES
- Requires administrative privileges to modify the registry
- The actual deletion occurs during the next system boot
- Existing pending operations are preserved
#>
function Add-PendingFileDelete {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    $regPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager"
    $regName = "PendingFileRenameOperations"

    try {
        # Get existing pending operations
        $existing = Get-ItemProperty -Path $regPath -Name $regName -ErrorAction SilentlyContinue | 
                    Select-Object -ExpandProperty $regName

        # Convert to ArrayList for easier manipulation
        $operations = New-Object System.Collections.ArrayList
        if ($existing) {
            $operations.AddRange($existing)
        }

        # Add new delete operation (path to delete, followed by empty string for delete operation)
        $operations.Add("\??\" + $Path.Replace(":", "")) | Out-Null
        $operations.Add("") | Out-Null

        # Write back to registry
        Set-ItemProperty -Path $regPath -Name $regName -Value $operations.ToArray() -Type MultiString
        Write-Host "Scheduled for deletion on reboot: $Path" -ForegroundColor Cyan
    }
    catch {
        Write-Host "Failed to schedule deletion on reboot: $_" -ForegroundColor Red
    }
}

<#
.SYNOPSIS
Removes registry keys and values specified in the WiX configuration file.

.DESCRIPTION
Enumerates and removes all registry keys and registry values defined in the WiX file. 
Handles both HKLM and HKCU registry hives and provides progress feedback during the cleanup process.
Uses the Join-RegistryPath helper function to construct proper registry paths.
    
.EXAMPLE
Remove-RegistryEntries
Removes all registry entries found in the loaded WiX XML configuration.

.NOTES
- Requires the $xml and $nsMgr variables to be initialized with WiX file content
- Uses Write-Progress to show cleanup progress
- Silently continues on registry access errors but reports them to the user
#>
function Remove-RegistryEntries {
    [long] $regCount = 0;
    Write-Host "Enumerating installed registry keys and removing them..."
    Write-Host "========================================================"
    
    # Define safe paths that are Omnissa/VMware specific and safe to delete
    $safePathPatterns = @(
        "SOFTWARE\\Omnissa",
        "SOFTWARE\\VMware, Inc\.",
        "SOFTWARE\\Teradici"
    )
    
    # Find all File elements
    $regKeys = $xml.SelectNodes("//wx:RegistryKey", $nsMgr)

    # Enumerate and display
    foreach ($key in $regKeys) {
        $root = $key.GetAttribute("Root")
        $subkey = $key.GetAttribute("Key")

        # Safety check: Only delete keys under safe Omnissa/VMware paths
        $isSafePath = $false
        foreach ($pattern in $safePathPatterns) {
            if ($subkey -match $pattern) {
                $isSafePath = $true
                break
            }
        }

        if (-not $isSafePath) {
            Write-Host "[Registry Key] $root\$subkey - SKIPPED (not in safe path)" -ForegroundColor Yellow
            continue
        }

        Write-Host "[Registry Key] $root\$subkey" -NoNewline
        try {
            $fullPath = Join-RegistryPath $root $subkey

            if (Test-Path $fullPath) {
                Remove-Item -Path $fullPath -Recurse -Force -ErrorAction SilentlyContinue
                Write-Host " - Deleted" -ForegroundColor Green
            }
            else {
                Write-Host " - Not found" -ForegroundColor Red
            }
        }
        catch {
            Write-Host " - Error: $_" -ForegroundColor Red
        } 
        $regCount++;
        Write-Progress -Activity "Deleting Registry Entries" `
            -Status "Processed keys: $regCount"
    }

    # WARNING: Deleting individual RegistryValue entries is dangerous and can break the system
    # Many registry values are in shared system locations (HKCR, SYSTEM\Services, etc.)
    # The RegistryKey cleanup above handles deletion more safely by targeting entire key trees
    # under Omnissa/VMware paths only. Do NOT uncomment this section unless you add similar
    # safety filtering to only delete values under product-specific paths.

    $regKeys = $xml.SelectNodes("//wx:RegistryValue", $nsMgr)

    # # Enumerate and display
    foreach ($key in $regKeys) {
        $root = $key.GetAttribute("Root")
        $subkey = $key.GetAttribute("Key")
        $keyName = $key.GetAttribute("Name")

         # Safety check: Only delete keys under safe Omnissa/VMware paths
        $isSafePath = $false
        foreach ($pattern in $safePathPatterns) {
            if ($subkey -match $pattern) {
                $isSafePath = $true
                break
            }
        }

        if (-not $isSafePath) {
            Write-Host "[Registry Key] $root\$subkey - SKIPPED (not in safe path)" -ForegroundColor Yellow
            continue
        }

        $fullPath = Join-RegistryPath $root $subkey 

        Write-Host "[Registry Key] $root\$subkey\$keyName" -NoNewline
        try{
            if (Test-Path $fullPath)
            {
                Remove-ItemProperty -Path $fullPath -Name $keyName -Force -ErrorAction SilentlyContinue
                Write-Host " Deleted" -ForegroundColor Green 
            }
            else
            {
                Write-Host "Not found" -ForegroundColor Red
            }
        }
        catch{
            Write-Host "Error: $_"
        } 
        $regCount++;
        Write-Progress -Activity "Deleting Registry Entries" `
                   -Status "Processed keys: {$regCount}"
    }

    #Remove the entry from the uninstall list
    $uninstallPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$productCode"
    Write-Host "Removing uninstall entry: $uninstallPath" -NoNewline

    try {
        if (Test-Path $uninstallPath)
        {
            Remove-Item -Path $uninstallPath -Recurse -Force -ErrorAction SilentlyContinue
        Write-Host " - Successfully removed" -ForegroundColor Green
        }
        else {
            Write-Host " - Not found" -ForegroundColor Red
        }


        # Remove the entry from the installed applications list
        $wow64Key = Get-ChildItem -Path HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall -Recurse | Where-Object { (Get-ItemProperty $_.PSPath).DisplayName -eq $xml.Wix.Product.Name } 
        if ($wow64Key) {
            Remove-Item -Path $wow64Key.PSPath -Recurse -Force -ErrorAction SilentlyContinue
            Write-Host " - Removed from Wow6432Node" -ForegroundColor Green
        }
        else {
            Write-Host " - Not found in Wow6432Node" -ForegroundColor Red
        }
    }
    catch {
        Write-Host " - Failed to remove: $_" -ForegroundColor Red
    }
}

<#
.SYNOPSIS
Converts a standard GUID to Windows Installer compressed GUID format.

.DESCRIPTION
Windows Installer stores GUIDs in a compressed format where:
- Dashes and braces are removed
- First 8 characters are reversed in pairs
- Next 4 characters are reversed in pairs  
- Next 4 characters are reversed in pairs
- Last 16 characters remain in original order

Example: {12345678-1234-1234-1234-123456789012} → 87654321432143211234123456789012

.PARAMETER Guid
The GUID string to compress (with or without braces and dashes)

.EXAMPLE
Convert-ToCompressedGuid -Guid "{12345678-1234-1234-1234-123456789012}"
Returns: 87654321432143211234123456789012

.NOTES
Used for locating Windows Installer registry entries
#>
function Convert-ToCompressedGuid {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Guid
    )

    $cleanGuid = $Guid -replace '[{}\-]', ''
   
    # Split into parts
    $part1 = $cleanGuid.Substring(0, 8)   # First 8 chars
    $part2 = $cleanGuid.Substring(8, 4)   # Next 4 chars
    $part3 = $cleanGuid.Substring(12, 4)  # Next 4 chars
     $part4and5 = $cleanGuid.Substring(16) # Remaining 16 chars (part 4 + part 5)
    
    # Reverse parts 1-4 completely
    $reversed1 = -join $part1[-1..-$part1.Length]
    $reversed2 = -join $part2[-1..-$part2.Length]
    $reversed3 = -join $part3[-1..-$part3.Length]
    


    # Reverse part 4 and 5 in pairs (swap characters within each pair)
    $reversed4and5 = -join ($part4and5 -split '(..)' | Where-Object { $_ } | ForEach-Object { 
        $_.Substring(1, 1) + $_.Substring(0, 1)
    })
    
    return $reversedGuid = $reversed1 + $reversed2 + $reversed3 + $reversed4and5
}

<#
.SYNOPSIS
Removes Windows Installer cache entries to enable fresh installation.

.DESCRIPTION
Removes Windows Installer product registration, upgrade codes, features, and components
that would otherwise cause the MSI installer to go into repair/modify mode instead of
performing a fresh installation. This is critical for ensuring the next install is clean.

The function removes:
- Product registrations in HKLM\SOFTWARE\Classes\Installer\Products
- Feature registrations in HKLM\SOFTWARE\Classes\Installer\Features
- User data in HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData
- Upgrade code mappings in HKLM\SOFTWARE\Classes\Installer\UpgradeCodes

.EXAMPLE
Remove-WindowsInstallerCache
Removes all Windows Installer cache entries for the current product.

.NOTES
- Requires $xml, $nsMgr, and $productCode variables to be initialized
- Requires administrative privileges
- Must be called AFTER Remove-RegistryEntries
- GUIDs are stored in compressed format in Windows Installer registry
#>
function Remove-WindowsInstallerCache {
    Write-Host ""
    Write-Host "Cleaning Windows Installer cache entries..."
    Write-Host "============================================"
    Write-Host "This ensures the next installation will be treated as NEW, not a repair."
    Write-Host ""

    if (-not $productCode) {
        Write-Host "Product Code not found - skipping Windows Installer cache cleanup" -ForegroundColor Yellow
        return
    }

    Write-Host "Product Code: $productCode"
    
    # Convert Product Code to compressed GUID format
    $compressedProductCode = Convert-ToCompressedGuid -Guid $productCode
    Write-Host "Compressed Product Code: $compressedProductCode"
    Write-Host ""

    # Define all Windows Installer registry paths that need to be removed
    $installerPaths = @(
        "HKLM:\SOFTWARE\Classes\Installer\Products\$compressedProductCode",
        "HKLM:\SOFTWARE\Classes\Installer\Features\$compressedProductCode",
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\$compressedProductCode",
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Components\$compressedProductCode"
    )

    foreach ($path in $installerPaths) {
        if (Test-Path $path) {
            Write-Host "Removing: $path" -NoNewline
            try {
                Remove-Item -Path $path -Recurse -Force -ErrorAction Stop
                Write-Host " - Removed" -ForegroundColor Green
            }
            catch {
                Write-Host " - Failed: $_" -ForegroundColor Red
            }
        }
        else {
            Write-Host "Not found: $path" -ForegroundColor DarkGray
        }
    }

    # Remove Upgrade Code entries (prevents upgrade detection)
    
    if ($upgradeCode) {
        Write-Host "Upgrade Code: $upgradeCode"
        
        $compressedUpgradeCode = Convert-ToCompressedGuid -Guid $upgradeCode
        Write-Host "Compressed Upgrade Code: $compressedUpgradeCode"
        Write-Host ""

        $upgradeCodePaths = @(
            "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes\$compressedUpgradeCode",
            "HKLM:\SOFTWARE\Classes\Installer\UpgradeCodes\$compressedUpgradeCode"
        )

        foreach ($path in $upgradeCodePaths) {
            if (Test-Path $path) {
                Write-Host "Removing: $path" -NoNewline
                try {
                    Remove-Item -Path $path -Recurse -Force -ErrorAction Stop
                    Write-Host " - Removed" -ForegroundColor Green
                }
                catch {
                    Write-Host " - Failed: $_" -ForegroundColor Red
                }
            }
            else {
                Write-Host "Not found: $path" -ForegroundColor DarkGray
            }
        }
    }
    else {
        Write-Host "No Upgrade Code found in WiX file" -ForegroundColor Yellow
    }

    Write-Host ""
    Write-Host "Windows Installer cache cleanup complete." -ForegroundColor Cyan
    Write-Host "The next installation should now be treated as a FRESH install." -ForegroundColor Cyan
    Write-Host ""
}

<#
.SYNOPSIS
Removes files installed by a WiX v3.x installer based on the WiX configuration.

.DESCRIPTION
Enumerates all File elements in the WiX v3.x XML and removes the corresponding files from the file system.
Traverses the Directory hierarchy to construct full file paths by combining standard directory IDs 
with custom folder structures. Handles both standard Windows directories and custom installation paths.
If an InstallLocation is found in the registry, it is used as the base directory for deletion.
After deleting individual files, attempts to remove the main installation directory.

.EXAMPLE
Remove-InstalledFiles_v3
Removes all files found in the loaded WiX v3.x XML configuration.

.NOTES
- Requires $xml and $nsMgr variables to be initialized with WiX file content
- Uses the $standardDirs hashtable to resolve standard Windows directory IDs
- Provides colored output for success/failure status
- Continues processing even if individual files cannot be deleted
- Works specifically with WiX v3.x schema where directories use "Directory" as root
#>
function Remove-InstalledFiles_v3 {
    $regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$productCode"

    if (Test-Path $regPath) {
        $props = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue
        if ($props.PSObject.Properties.Name -contains "InstallLocation") {
            Write-Host "InstallLocation found: $($props.InstallLocation)"
            $installDir = $($props.InstallLocation)
        }
    }

    $files = $xml.SelectNodes("//wx:File", $nsMgr)
    $lockedFiles = @()
    
    foreach ($file in $files) {
        $installPath = ""
        $fileName = $file.Name
        $dirNode = $file.ParentNode
    
        while ($dirNode) {
            if ($dirNode.LocalName -eq "Directory") {
                if ($dirNode.HasAttribute("Name")) {
                    $folderName = $dirNode.GetAttribute("Name")
                    if ($dirNode.ParentNode.Name -eq "SourceDir") {
                        if ($standardDirs.ContainsKey($dirNode.Id)) {
                            $installPath = $standardDirs[$dirNode.Id] + "\" + $installPath
                        }
                        else {
                            $installPath = "Unknown" + "\" + $installPath
                        }
                        break
                    }
       
                    $installPath = $folderName + "\" + $installPath
                }
                else {
                    if ($dirNode.ParentNode.Name -eq "SourceDir") {
                        if ($standardDirs.ContainsKey($dirNode.Id)) {
                            $installPath = $standardDirs[$dirNode.Id] + "\" + $installPath
                        }
                        break
                    }
                }
            }
        
            $dirNode = $dirNode.ParentNode
        }

        $filePath = "$installPath\$fileName"

        if ($fileName -match '\.exe$') {
            $processName = [System.IO.Path]::GetFileNameWithoutExtension($fileName)
            $runningProcess = Get-Process -Name $processName -ErrorAction SilentlyContinue
            if ($runningProcess) {
                Write-Host "Found running process: $processName (PID: $($runningProcess.Id))" -ForegroundColor Yellow
                try {
                    Stop-Process -Id $runningProcess.Id -Force
                    Write-Host "Terminated process: $processName" -ForegroundColor Green
                    Start-Sleep -Milliseconds 500  # Brief pause to allow cleanup
                }
                catch {
                    Write-Host "Failed to terminate process: $processName - $_" -ForegroundColor Red
                }
            }
        }

        try {
       
            if (Test-Path -Path $filePath) {
                Write-Host "Found Path: $filePath" -ForegroundColor Green
                try {
                    Remove-Item -Path $filePath -Force -ErrorAction Stop
                }
                catch {
                    Write-Host "File is locked: $filePath" -ForegroundColor Yellow
                    $lockedFiles += $filePath
                }
            }
            else {
                Write-Host "Not found Path: [$installPath\$fileName]" -ForegroundColor Red
            } 
        }
        catch {
            Write-Host "Error deleting file [$filePath]. Skipping the file."
        }
    }
    
    # Schedule locked files for deletion on reboot
    if ($lockedFiles.Count -gt 0) {
        Write-Host "`nScheduling $($lockedFiles.Count) locked file(s) for deletion on reboot..." -ForegroundColor Yellow
        foreach ($lockedFile in $lockedFiles) {
            Add-PendingFileDelete -Path $lockedFile
        }
    }
    
    if (Test-Path $installDir) {
        try {
            Remove-Item -Path $installDir -Recurse -Force -ErrorAction Stop
            Write-Host "Deleted $installDir and all its contents" -ForegroundColor Green
        }
        catch {
            Write-Host "Failed to delete $installDir - files may be in use" -ForegroundColor Yellow
            Write-Host "Scheduling directory for deletion on next reboot..." -ForegroundColor Yellow
            Add-PendingFileDelete -Path $installDir
        }
    }
}

if ($generateTranscript) {
    Start-Transcript -Path ".\CleanupReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
}

<#
.SYNOPSIS
Removes files installed by a WiX v4.x installer based on the WiX configuration.

.DESCRIPTION
Enumerates all File elements in the WiX v4.x XML and removes the corresponding files from the file system.
Traverses the Directory hierarchy to construct full file paths by combining standard directory IDs 
with custom folder structures. Handles both standard Windows directories and custom installation paths.

.EXAMPLE
Remove-InstalledFiles_v4
Removes all files found in the loaded WiX v4.x XML configuration.

.NOTES
- Requires $xml and $nsMgr variables to be initialized with WiX file content
- Uses the $standardDirs hashtable to resolve standard Windows directory IDs
- Provides colored output for success/failure status
- Continues processing even if individual files cannot be deleted
- Works specifically with WiX v4.x schema where directories use "StandardDirectory" as root
#>

function Remove-InstalledFiles_v4 {
    Write-Host "Enumerating installed files and deleting them..."
    Write-Host "==================================================="

    $regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$productCode"
    Write-Host "Checking for InstallLocation in registry $regPath"

    if (Test-Path $regPath) {
        $props = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue
        if ($props.PSObject.Properties.Name -contains "InstallLocation") {
            Write-Host "InstallLocation found: $($props.InstallLocation)"
            $installDir = $($props.InstallLocation)
        }
    }

    $files = $xml.SelectNodes("//wx:File", $nsMgr)
    $lockedFiles = @()

    #Find all the files.
    foreach ($file in $files) {
        $fileName = $file.Name
        $component = $file.ParentNode
        $dirNode = $component.ParentNode

        $installFolder = ""
        while ($dirNode) {
            if ($dirNode.LocalName -eq "Directory" -or $dirNode.LocalName -eq "StandardDirectory") {
                $basePath = $dirNode.id.Split('.')[0]

                if ($standardDirs.ContainsKey($basePath)) {
                    $installFolder = $standardDirs[$basePath] + "\" + $installFolder
                    break
                }
                
                if ($dirNode.HasAttribute("Name")) {
                    $folderName = $dirNode.GetAttribute("Name")
                    $installFolder = $folderName + "\" + $installFolder
                }
            }
            $dirNode = $dirNode.ParentNode
        }

        $filePath = "$installFolder\$fileName"

        if ($fileName -match '\.exe$') {
            $processName = [System.IO.Path]::GetFileNameWithoutExtension($fileName)
            $runningProcess = Get-Process -Name $processName -ErrorAction SilentlyContinue
            if ($runningProcess) {
                Write-Host "Found running process: $processName (PID: $($runningProcess.Id))" -ForegroundColor Yellow
                try {
                    Stop-Process -Id $runningProcess.Id -Force
                    Write-Host "Terminated process: $processName" -ForegroundColor Green
                    Start-Sleep -Milliseconds 500  # Brief pause to allow cleanup
                }
                catch {
                    Write-Host "Failed to terminate process: $processName - $_" -ForegroundColor Red
                }
            }
        }

        Write-Host "Attempting to delete file: $filePath"

        try {
       
            if (Test-Path -Path $filePath) {
                Write-Host "Found Path: $filePath" -ForegroundColor Green
                try {
                    Remove-Item -Path $filePath -Force -ErrorAction Stop
                }
                catch {
                    Write-Host "File is locked: $filePath" -ForegroundColor Yellow
                    $lockedFiles += $filePath
                }
            }
            else {
                Write-Host "Not found Path: [$filePath]" -ForegroundColor Red
            } 
        }
        catch {
            Write-Host "Error deleting file [$filePath]. Skipping the file."
        }
    }
    
    # Schedule locked files for deletion on reboot
    if ($lockedFiles.Count -gt 0) {
        Write-Host "`nScheduling $($lockedFiles.Count) locked file(s) for deletion on reboot..." -ForegroundColor Yellow
        foreach ($lockedFile in $lockedFiles) {
            Add-PendingFileDelete -Path $lockedFile
        }
    }

    if (Test-Path $installDir) {
        try {
            Remove-Item -Path $installDir -Recurse -Force -ErrorAction Stop
            Write-Host "Deleted $installDir and all its contents" -ForegroundColor Green
        }
        catch {
            Write-Host "Failed to delete $installDir - files may be in use" -ForegroundColor Yellow
            Write-Host "Scheduling directory for deletion on next reboot..." -ForegroundColor Yellow
            Add-PendingFileDelete -Path $installDir
        }
    }

    Write-Host ""
}



$wixVersion = ""
$productCode = ""
$upgradeCode = ""
# Load the WiX file
[xml]$xml = Get-Content $wixFilePath -ErrorAction Stop

# Detect schema
$defaultNamespace = $xml.DocumentElement.NamespaceURI
# Set up namespace manager
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("wx", $defaultNamespace)

if ($defaultNamespace -match "wix/2006") {
    Write-Host "Detected: WiX v3.x schema"
    $wixVersion = "3.0"
}
elseif ($defaultNamespace -match "schemas/v4/wxs") {
    Write-Host "Detected: WiX v4 schema"
    $wixVersion = "4.0"
}
else {
    Write-Warning "Unknown WiX schema: $defaultNamespace"
    exit 1
}

switch ($wixVersion) {
    "3.0" {
        #Display the package details       
        $product = $xml.SelectSingleNode("//wx:Product", $nsMgr)
        Write-Host "Product Name: $($product.Name)"
        Write-Host "Company: $($product.Manufacturer)"
        Write-Host "Version: $($product.Version)"        
        
        $productCode = $xml.Wix.Product.Id
        Write-Host "Product Code: $productCode"
        $upgradeCode = $xml.Wix.Product.UpgradeCode
        Write-Host "Upgrade Code: $upgradeCode"
        Write-Host ""

        Stop-Services
        #Remove-WssmUserInit
        Remove-InstalledFiles_v3
        Remove-RegistryEntries
        Remove-WindowsInstallerCache
    }
    "4.0" { 
        #Display the package details
        $package = $xml.SelectSingleNode("//wx:Package", $nsMgr)
        Write-Host "Package Name: $($package.Name)"
        Write-Host "Company: $($package.Manufacturer)"
        Write-Host "Version: $($package.Version)"

        $productCode = $package.ProductCode
        Write-Host "Product Code: $productCode"
        $upgradeCode = $package.UpgradeCode
        Write-Host "Upgrade Code: $upgradeCode"
        Write-Host ""

        Stop-Services
        Remove-WssmUserInit
        Remove-InstalledFiles_v4
        Remove-RegistryEntries
        Remove-WindowsInstallerCache
    }
    default { 
        Write-Host "Unsupported wix version found." 
    }
}

if ($generateTranscript) {
    Stop-Transcript    
}
