OfficeScrubC2R-Utilities.ps1

# OfficeScrubC2R-Utilities.psm1
# High-performance utility functions for Office C2R removal
# Uses C# for performance-critical operations

using namespace System
using namespace System.Collections.Generic
using namespace System.IO
using namespace System.Management
using namespace System.Security.Principal
using namespace Microsoft.Win32

#region C# Performance Optimizations

# C# class for high-performance registry operations
Add-Type -TypeDefinition @"
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Microsoft.Win32;
 
public class FastRegistryOperations
{
    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern int RegOpenKeyEx(IntPtr hKey, string subKey, int options, int samDesired, out IntPtr phkResult);
 
    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern int RegCloseKey(IntPtr hKey);
 
    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern int RegQueryValueEx(IntPtr hKey, string lpValueName, IntPtr lpReserved, out uint lpType, IntPtr lpData, ref uint lpcbData);
 
    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern int RegEnumKeyEx(IntPtr hKey, uint dwIndex, System.Text.StringBuilder lpName, ref uint lpcbName, IntPtr lpReserved, IntPtr lpClass, IntPtr lpcbClass, IntPtr lpftLastWriteTime);
 
    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern int RegEnumValue(IntPtr hKey, uint dwIndex, System.Text.StringBuilder lpValueName, ref uint lpcbValueName, IntPtr lpReserved, out uint lpType, IntPtr lpData, IntPtr lpcbData);
 
    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern int RegDeleteKey(IntPtr hKey, string lpSubKey);
 
    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern int RegDeleteValue(IntPtr hKey, string lpValueName);
 
    private const int KEY_READ = 0x20019;
    private const int KEY_WRITE = 0x20006;
    private const int KEY_ALL_ACCESS = 0xF003F;
 
    public static Dictionary<string, string> GetRegistrySubKeys(RegistryHive hive, string subKey)
    {
        var result = new Dictionary<string, string>();
        IntPtr hKey = IntPtr.Zero;
 
        try
        {
            IntPtr hHive = GetHiveHandle(hive);
            if (RegOpenKeyEx(hHive, subKey, 0, KEY_READ, out hKey) == 0)
            {
                uint index = 0;
                var nameBuffer = new System.Text.StringBuilder(256);
                uint nameSize = 256;
 
                while (RegEnumKeyEx(hKey, index, nameBuffer, ref nameSize, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero) == 0)
                {
                    result[nameBuffer.ToString()] = "";
                    nameSize = 256;
                    nameBuffer.Clear();
                    index++;
                }
            }
        }
        finally
        {
            if (hKey != IntPtr.Zero)
                RegCloseKey(hKey);
        }
 
        return result;
    }
 
    public static Dictionary<string, object> GetRegistryValues(RegistryHive hive, string subKey)
    {
        var result = new Dictionary<string, object>();
        IntPtr hKey = IntPtr.Zero;
 
        try
        {
            IntPtr hHive = GetHiveHandle(hive);
            if (RegOpenKeyEx(hHive, subKey, 0, KEY_READ, out hKey) == 0)
            {
                uint index = 0;
                var nameBuffer = new System.Text.StringBuilder(256);
                uint nameSize = 256;
                uint valueType;
 
                while (RegEnumValue(hKey, index, nameBuffer, ref nameSize, IntPtr.Zero, out valueType, IntPtr.Zero, IntPtr.Zero) == 0)
                {
                    result[nameBuffer.ToString()] = valueType;
                    nameSize = 256;
                    nameBuffer.Clear();
                    index++;
                }
            }
        }
        finally
        {
            if (hKey != IntPtr.Zero)
                RegCloseKey(hKey);
        }
 
        return result;
    }
 
    public static bool DeleteRegistryKey(RegistryHive hive, string subKey)
    {
        IntPtr hHive = GetHiveHandle(hive);
        return RegDeleteKey(hHive, subKey) == 0;
    }
 
    public static bool DeleteRegistryValue(RegistryHive hive, string subKey, string valueName)
    {
        IntPtr hKey = IntPtr.Zero;
 
        try
        {
            IntPtr hHive = GetHiveHandle(hive);
            if (RegOpenKeyEx(hHive, subKey, 0, KEY_WRITE, out hKey) == 0)
            {
                return RegDeleteValue(hKey, valueName) == 0;
            }
        }
        finally
        {
            if (hKey != IntPtr.Zero)
                RegCloseKey(hKey);
        }
 
        return false;
    }
 
    private static IntPtr GetHiveHandle(RegistryHive hive)
    {
        switch (hive)
        {
            case RegistryHive.ClassesRoot: return new IntPtr(0x80000000);
            case RegistryHive.CurrentUser: return new IntPtr(0x80000001);
            case RegistryHive.LocalMachine: return new IntPtr(0x80000002);
            case RegistryHive.Users: return new IntPtr(0x80000003);
            default: return IntPtr.Zero;
        }
    }
}
 
public class FastFileOperations
{
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern bool DeleteFile(string lpFileName);
 
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern bool RemoveDirectory(string lpPathName);
 
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, int dwFlags);
 
    private const int MOVEFILE_DELAY_UNTIL_REBOOT = 0x4;
 
    public static bool DeleteFileFast(string filePath)
    {
        return DeleteFile(filePath);
    }
 
    public static bool RemoveDirectoryFast(string directoryPath)
    {
        return RemoveDirectory(directoryPath);
    }
 
    public static bool ScheduleDeleteOnReboot(string filePath)
    {
        return MoveFileEx(filePath, null, MOVEFILE_DELAY_UNTIL_REBOOT);
    }
}
 
public class ProcessManager
{
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, int dwProcessId);
 
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
 
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool CloseHandle(IntPtr hObject);
 
    private const uint PROCESS_TERMINATE = 0x0001;
    private const uint PROCESS_QUERY_INFORMATION = 0x0400;
 
    public static bool TerminateProcessById(int processId)
    {
        IntPtr hProcess = OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_INFORMATION, false, processId);
        if (hProcess == IntPtr.Zero)
            return false;
 
        bool result = TerminateProcess(hProcess, 0);
        CloseHandle(hProcess);
        return result;
    }
}
 
public class GuidDecoder
{
    // Base85 decoding table (ASCII character to value mapping)
    private static readonly byte[] DecodeTable = new byte[256];
 
    static GuidDecoder()
    {
        // Initialize decode table with 0xff (invalid) for all characters
        for (int i = 0; i < 256; i++)
        {
            DecodeTable[i] = 0xff;
        }
 
        // Set valid Base85 character mappings
        byte[] validChars = { 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124 };
        byte[] values = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0xff, 0xff, 0xff, 0x16, 0xff, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0xff, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0xff, 0x53, 0x54, 0xff };
 
        for (int i = 0; i < validChars.Length && i < values.Length; i++)
        {
            DecodeTable[validChars[i]] = values[i];
        }
    }
 
    public static string DecodeGuid(string encodedGuid)
    {
        if (string.IsNullOrEmpty(encodedGuid))
            return null;
 
        try
        {
            bool failed = false;
            uint total = 0;
            uint pow85 = 1;
            var decode = new System.Text.StringBuilder();
 
            int maxLength = Math.Min(encodedGuid.Length, 20);
 
            for (int i = 0; i < maxLength; i++)
            {
                failed = true;
 
                if (i % 5 == 0)
                {
                    total = 0;
                    pow85 = 1;
                }
 
                int charCode = (int)encodedGuid[i];
 
                if (charCode >= 128 || DecodeTable[charCode] == 0xff)
                {
                    break;
                }
 
                uint hexValue = DecodeTable[charCode];
                total += hexValue * pow85;
 
                if (i % 5 == 4)
                {
                    decode.Append(total.ToString("X8"));
                }
 
                pow85 *= 85;
                failed = false;
            }
 
            if (!failed && decode.Length >= 32)
            {
                string decodedString = decode.ToString();
 
                // Reconstruct GUID in the specific order from the VBScript
                return "{" +
                       decodedString.Substring(0, 8) + "-" +
                       decodedString.Substring(12, 4) + "-" +
                       decodedString.Substring(8, 4) + "-" +
                       decodedString.Substring(22, 2) + decodedString.Substring(20, 2) + "-" +
                       decodedString.Substring(18, 2) + decodedString.Substring(16, 2) +
                       decodedString.Substring(30, 2) + decodedString.Substring(28, 2) +
                       decodedString.Substring(26, 2) + decodedString.Substring(24, 2) +
                       "}";
            }
 
            return null;
        }
        catch
        {
            return null;
        }
    }
}
"@


#endregion

#region Constants
$script:SCRIPT_VERSION = "2.19"
$script:SCRIPT_NAME = "OfficeScrubC2R"
$script:RET_VAL_FILE = "ScrubRetValFile.txt"
$script:OFFICE_NAME = "Office C2R / O365"

# Registry Hive Constants
$script:HKCR = 0x80000000
$script:HKCU = 0x80000001
$script:HKLM = 0x80000002
$script:HKU = 0x80000003

# Error Constants
$script:ERROR_SUCCESS = 0
$script:ERROR_FAIL = 1
$script:ERROR_REBOOT_REQUIRED = 2
$script:ERROR_USERCANCEL = 4
$script:ERROR_STAGE1 = 8
$script:ERROR_STAGE2 = 16
$script:ERROR_INCOMPLETE = 32
$script:ERROR_DCAF_FAILURE = 64
$script:ERROR_ELEVATION_USERDECLINED = 128
$script:ERROR_ELEVATION = 256
$script:ERROR_SCRIPTINIT = 512
$script:ERROR_RELAUNCH = 1024
$script:ERROR_UNKNOWN = 2048
$script:ERROR_ALL = 4095
$script:ERROR_USER_ABORT = 0xC000013A
$script:ERROR_SUCCESS_CONFIG_COMPLETE = 1728
$script:ERROR_SUCCESS_REBOOT_REQUIRED = 3010

# Registry Paths
$script:REG_ARP = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\"
$script:REG_O15RPROPERTYBAG = "SOFTWARE\Microsoft\Office\15.0\ClickToRun\propertyBag\"
$script:REG_O15C2RCONFIGURATION = "SOFTWARE\Microsoft\Office\15.0\ClickToRun\Configuration\"
$script:REG_O15C2RPRODUCTIDS = "SOFTWARE\Microsoft\Office\15.0\ClickToRun\ProductReleaseIDs\Active\"
$script:REG_O16C2RCONFIGURATION = "SOFTWARE\Microsoft\Office\16.0\ClickToRun\Configuration\"
$script:REG_O16C2RPRODUCTIDS = "SOFTWARE\Microsoft\Office\16.0\ClickToRun\ProductReleaseIDs\Active\"
$script:REG_C2RCONFIGURATION = "SOFTWARE\Microsoft\Office\ClickToRun\Configuration\"
$script:REG_C2RPRODUCTIDS = "SOFTWARE\Microsoft\Office\ClickToRun\ProductReleaseIDs\"
#endregion

#region Global Variables
$script:ErrorCode = $script:ERROR_SUCCESS
$script:LogInitialized = $false
$script:IsElevated = $false
$script:Is64Bit = $false
$script:RebootRequired = $false
$script:Quiet = $true
$script:DetectOnly = $false
$script:Force = $false
$script:RemoveAll = $false
$script:KeepLicense = $false
$script:Offline = $false
$script:ForceArpUninstall = $false
$script:ClearTaskBand = $false
$script:UnpinMode = $false
$script:SkipSD = $false
$script:NoElevate = $false
$script:UserConsent = $false
$script:ReturnErrorOrSuccess = $false
$script:TestRerun = $false
$script:SetRunOnce = $false
$script:Rerun = $false
$script:RemoveOse = $false
$script:NoCancel = $false
$script:C2R = $true

# Path Variables
$script:AppData = $null
$script:LocalAppData = $null
$script:Temp = $null
$script:AllUsersProfile = $null
$script:ProgramFiles = $null
$script:ProgramFilesX86 = $null
$script:CommonProgramFiles = $null
$script:CommonProgramFilesX86 = $null
$script:ProgramData = $null
$script:WinDir = $null
$script:WICacheDir = $null
$script:ScrubDir = $null
$script:ScriptDir = $null
$script:LogDir = $null
$script:ProfilesDirectory = $null
$script:PackageFolder = $null
$script:PackageGuid = $null
$script:OSVersion = $null
$script:OSInfo = $null
$script:VersionNT = 0

# Collections
$script:InstalledSku = @{}
$script:RemoveSku = @{}
$script:KeepSku = @{}
$script:KeepLis = @{}
$script:KeepFolder = @{}
$script:Apps = @{}
$script:DelRegKey = @{}
$script:KeepReg = @{}
$script:C2RSuite = @{}
$script:DelInUse = @{}
$script:DelFolder = @{}
$script:SC = @{}

# Office Process Names
$script:OfficeProcesses = @(
    "appvshnotify.exe",
    "integratedoffice.exe",
    "integrator.exe",
    "firstrun.exe",
    "communicator.exe",
    "msosync.exe",
    "OneNoteM.exe",
    "iexplore.exe",
    "mavinject32.exe",
    "werfault.exe",
    "perfboost.exe",
    "roamingoffice.exe",
    "officeclicktorun.exe",
    "officeondemand.exe",
    "OfficeC2RClient.exe"
)
#endregion

#region Logging Functions
function Write-LogHeader {
    param([string]$Message)
    if ($script:LogInitialized) {
        $timestamp = Get-Date -Format "HH:mm:ss"
        Write-Output "================================================"
        Write-Output "$timestamp - $Message"
        Write-Output "================================================"
    }
}

function Write-LogSubHeader {
    param([string]$Message)
    if ($script:LogInitialized) {
        $timestamp = Get-Date -Format "HH:mm:ss"
        Write-Output "----------------------------------------"
        Write-Output "$timestamp - $Message"
        Write-Output "----------------------------------------"
    }
}

function Write-Log {
    param([string]$Message)
    if ($script:LogInitialized) {
        $timestamp = Get-Date -Format "HH:mm:ss"
        Write-Output "$timestamp - $Message"
    }
}

function Write-LogOnly {
    param([string]$Message)
    if ($script:LogInitialized -and -not $script:Quiet) {
        $timestamp = Get-Date -Format "HH:mm:ss"
        Write-Output "$timestamp - $Message"
    }
}

function Initialize-Log {
    param([string]$LogPath)

    if (-not $script:LogInitialized) {
        $script:LogDir = $LogPath
        if (-not (Test-Path $script:LogDir)) {
            [void](New-Item -ItemType Directory -Path $script:LogDir -Force)
        }
        $script:LogInitialized = $true
        Write-LogHeader "Office C2R Scrubber v$script:SCRIPT_VERSION - Log Initialized"
    }
}
#endregion

#region Registry Functions
function Test-RegistryKeyExists {
    param(
        [Microsoft.Win32.RegistryHive]$Hive,
        [string]$SubKey
    )

    try {
        $regKey = [Microsoft.Win32.Registry]::$($Hive.ToString()).OpenSubKey($SubKey, $false)
        if ($regKey) {
            $regKey.Close()
            return $true
        }
        return $false
    }
    catch {
        return $false
    }
}

function Get-RegistryValue {
    param(
        [Microsoft.Win32.RegistryHive]$Hive,
        [string]$SubKey,
        [string]$ValueName,
        [string]$ValueType = "REG_SZ"
    )

    try {
        $regKey = [Microsoft.Win32.Registry]::$($Hive.ToString()).OpenSubKey($SubKey, $false)
        if ($regKey) {
            $value = $regKey.GetValue($ValueName)
            $regKey.Close()
            return $value
        }
        return $null
    }
    catch {
        return $null
    }
}

function Set-RegistryValue {
    param(
        [Microsoft.Win32.RegistryHive]$Hive,
        [string]$SubKey,
        [string]$ValueName,
        [object]$Value,
        [Microsoft.Win32.RegistryValueKind]$ValueType = [Microsoft.Win32.RegistryValueKind]::String
    )

    try {
        $regKey = [Microsoft.Win32.Registry]::$($Hive.ToString()).OpenSubKey($SubKey, $true)
        if ($regKey) {
            $regKey.SetValue($ValueName, $Value, $ValueType)
            $regKey.Close()
            return $true
        }
        return $false
    }
    catch {
        return $false
    }
}

function Remove-RegistryKey {
    param(
        [Microsoft.Win32.RegistryHive]$Hive,
        [string]$SubKey
    )

    try {
        $parentKey = Split-Path $SubKey -Parent
        $keyName = Split-Path $SubKey -Leaf

        if ($parentKey -eq "") {
            $parentKey = $SubKey
            $keyName = ""
        }

        $regKey = [Microsoft.Win32.Registry]::$($Hive.ToString()).OpenSubKey($parentKey, $true)
        if ($regKey) {
            if ($keyName -eq "") {
                $regKey.Close()
                [Microsoft.Win32.Registry]::$($Hive.ToString()).DeleteSubKey($parentKey, $true)
            }
            else {
                $regKey.DeleteSubKey($keyName, $true)
                $regKey.Close()
            }
            return $true
        }
        return $false
    }
    catch {
        return $false
    }
}

function Remove-RegistryValue {
    param(
        [Microsoft.Win32.RegistryHive]$Hive,
        [string]$SubKey,
        [string]$ValueName
    )

    try {
        $regKey = [Microsoft.Win32.Registry]::$($Hive.ToString()).OpenSubKey($SubKey, $true)
        if ($regKey) {
            $regKey.DeleteValue($ValueName, $true)
            $regKey.Close()
            return $true
        }
        return $false
    }
    catch {
        return $false
    }
}

function Get-RegistrySubKeys {
    param(
        [Microsoft.Win32.RegistryHive]$Hive,
        [string]$SubKey
    )

    try {
        $regKey = [Microsoft.Win32.Registry]::$($Hive.ToString()).OpenSubKey($SubKey, $false)
        if ($regKey) {
            $subKeys = $regKey.GetSubKeyNames()
            $regKey.Close()
            return $subKeys
        }
        return @()
    }
    catch {
        return @()
    }
}

function Get-RegistryValues {
    param(
        [Microsoft.Win32.RegistryHive]$Hive,
        [string]$SubKey
    )

    try {
        $regKey = [Microsoft.Win32.Registry]::$($Hive.ToString()).OpenSubKey($SubKey, $false)
        if ($regKey) {
            $valueNames = $regKey.GetValueNames()
            $regKey.Close()
            return $valueNames
        }
        return @()
    }
    catch {
        return @()
    }
}
#endregion

#region File Operations
function Remove-FileFast {
    param([string]$FilePath)

    try {
        if (Test-Path $FilePath) {
            return [FastFileOperations]::DeleteFileFast($FilePath)
        }
        return $true
    }
    catch {
        return $false
    }
}

function Remove-FolderFast {
    param([string]$FolderPath)

    try {
        if (Test-Path $FolderPath) {
            return [FastFileOperations]::RemoveDirectoryFast($FolderPath)
        }
        return $true
    }
    catch {
        return $false
    }
}

function Schedule-DeleteOnReboot {
    param([string]$Path)

    try {
        return [FastFileOperations]::ScheduleDeleteOnReboot($Path)
    }
    catch {
        return $false
    }
}

function Remove-FolderRecursive {
    param(
        [string]$Path,
        [switch]$Force
    )

    try {
        if (Test-Path $Path) {
            if ($Force) {
                Get-ChildItem -Path $Path -Recurse -Force | Remove-Item -Force -Recurse
            }
            Remove-Item -Path $Path -Force -Recurse
            return $true
        }
        return $true
    }
    catch {
        return $false
    }
}
#endregion

#region Process Management
function Stop-OfficeProcesses {
    param([switch]$Force)

    Write-LogSubHeader "Stopping Office processes"

    foreach ($processName in $script:OfficeProcesses) {
        $processes = Get-Process -Name $processName -ErrorAction SilentlyContinue
        foreach ($process in $processes) {
            try {
                Write-Log "Stopping process: $($process.ProcessName) (PID: $($process.Id))"
                if ($Force) {
                    $process.Kill()
                }
                else {
                    $process.CloseMainWindow()
                    if (-not $process.WaitForExit(5000)) {
                        $process.Kill()
                    }
                }
            }
            catch {
                Write-Log "Failed to stop process: $($process.ProcessName) - $($_.Exception.Message)"
            }
        }
    }
}

function Test-ProcessRunning {
    param([string]$ProcessName)

    $processes = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue
    return $processes.Count -gt 0
}
#endregion

#region Office Detection
function Test-IsC2R {
    param([string]$Value)

    $c2rPatterns = @(
        "officeclicktorun",
        "o365proplus",
        "o365business",
        "o365homepremium",
        "o365home",
        "o365smallbusiness",
        "o365midsizebusiness",
        "o365enterprise"
    )

    $valueLower = $Value.ToLower()
    foreach ($pattern in $c2rPatterns) {
        if ($valueLower -like "*$pattern*") {
            return $true
        }
    }
    return $false
}

function Get-InstalledOfficeProducts {
    Write-LogSubHeader "Detecting installed Office products"

    $products = @{}

    # Check O15 C2R Configuration
    Write-LogOnly "Check for O15 C2R products"
    $o15ProductReleaseIds = Get-RegistryValue -Hive LocalMachine -SubKey $script:REG_O15C2RCONFIGURATION -ValueName "ProductReleaseIds"
    if ($o15ProductReleaseIds) {
        $o15Products = $o15ProductReleaseIds -split ","
        $o15Version = Get-RegistryValue -Hive LocalMachine -SubKey "$script:REG_O15C2RPRODUCTIDS\culture" -ValueName "x-none"
        foreach ($prod in $o15Products) {
            Write-LogOnly "Found O15 C2R product in Configuration: $prod"
            if (-not $products.ContainsKey($prod.ToLower())) {
                Write-LogOnly "Add new product to dictionary: $($prod.ToLower())"
                $products[$prod.ToLower()] = $o15Version
            }
        }
    }

    # Check O15 PropertyBag
    $o15PropertyBag = Get-RegistryValue -Hive LocalMachine -SubKey $script:REG_O15RPROPERTYBAG -ValueName "productreleaseid"
    if ($o15PropertyBag) {
        $o15Products = $o15PropertyBag -split ","
        $o15Version = Get-RegistryValue -Hive LocalMachine -SubKey "$script:REG_O15C2RPRODUCTIDS\culture" -ValueName "x-none"
        foreach ($prod in $o15Products) {
            Write-LogOnly "Found O15 C2R product in PropertyBag: $prod"
            if (-not $products.ContainsKey($prod.ToLower())) {
                Write-LogOnly "Add new product to dictionary: $($prod.ToLower())"
                $products[$prod.ToLower()] = $o15Version
            }
        }
    }

    # Check Office C2R products (>=QR8)
    Write-LogOnly "Check for Office C2R products (>=QR8)"
    $activeConfiguration = Get-RegistryValue -Hive LocalMachine -SubKey $script:REG_C2RPRODUCTIDS -ValueName "ActiveConfiguration"
    if ($activeConfiguration) {
        $versionFallback = Get-RegistryValue -Hive LocalMachine -SubKey "$script:REG_C2RPRODUCTIDS\$activeConfiguration\culture" -ValueName "x-none"

        $cultures = Get-RegistrySubKeys -Hive LocalMachine -SubKey "$script:REG_C2RPRODUCTIDS\$activeConfiguration\culture"
        if ($cultures) {
            foreach ($cult in $cultures) {
                if ($cult.ToLower() -like "*x-none*") {
                    $versionFallback = Get-RegistryValue -Hive LocalMachine -SubKey "$script:REG_C2RPRODUCTIDS\$activeConfiguration\culture\$cult" -ValueName "Version"
                }
            }
        }

        $officeProducts = Get-RegistrySubKeys -Hive LocalMachine -SubKey "$script:REG_C2RPRODUCTIDS\$activeConfiguration"
        if ($officeProducts) {
            foreach ($prod in $officeProducts) {
                $sProd = $prod.ToLower()
                if ($sProd -like "*.*") {
                    $sProd = $sProd.Substring(0, $sProd.IndexOf("."))
                }

                if ($sProd -notin @("culture", "stream")) {
                    Write-LogOnly "Found Office C2R product in Configuration: $prod"
                    if (-not $products.ContainsKey($sProd)) {
                        Write-LogOnly "Add new product to dictionary: $sProd"
                        $displayVersion = Get-RegistryValue -Hive LocalMachine -SubKey "$script:REG_C2RPRODUCTIDS\$activeConfiguration\$prod\x-none" -ValueName "Version"
                        if ($displayVersion) {
                            $products[$sProd] = $displayVersion
                        }
                        else {
                            $products[$sProd] = $versionFallback
                        }
                    }
                }
            }
        }
    }

    # Check ARP entries
    Write-LogOnly "Check ARP entries for C2R products"
    $arpKeys = Get-RegistrySubKeys -Hive LocalMachine -SubKey $script:REG_ARP
    foreach ($key in $arpKeys) {
        $displayName = Get-RegistryValue -Hive LocalMachine -SubKey "$script:REG_ARP$key" -ValueName "DisplayName"
        if ($displayName -and (Test-IsC2R $displayName)) {
            $uninstallString = Get-RegistryValue -Hive LocalMachine -SubKey "$script:REG_ARP$key" -ValueName "UninstallString"
            if ($uninstallString) {
                Write-LogOnly "Found C2R product in ARP: $displayName"
                $script:C2RSuite[$key] = $displayName
            }
        }
    }

    return $products
}
#endregion

#region System Information
function Test-IsElevated {
    $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
    return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Get-SystemInfo {
    try {
        $computerSystem = Get-WmiObject -Class Win32_ComputerSystem -ErrorAction Stop
        $script:Is64Bit = $computerSystem.SystemType -like "*64*"

        $os = Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop
        $script:OSVersion = $os.Version
        $script:OSInfo = "$($os.Caption) $($os.OtherTypeDescription), SP $($os.ServicePackMajorVersion), Version: $($os.Version), Codepage: $($os.CodeSet), Country Code: $($os.CountryCode), Language: $($os.OSLanguage)"

        if ($script:OSVersion) {
            $versionParts = $script:OSVersion -split "\."
            if ($versionParts.Count -ge 2) {
                $script:VersionNT = [int]$versionParts[0] * 100 + [int]$versionParts[1]
            }
        }
    }
    catch {
        Write-Warning "Failed to get system information: $($_.Exception.Message)"
        $script:Is64Bit = [Environment]::Is64BitOperatingSystem
        $script:OSVersion = [Environment]::OSVersion.Version.ToString()
        $script:OSInfo = "Unknown OS"
        $script:VersionNT = 0
    }
}

function Initialize-Environment {
    # Set environment variables
    $script:AppData = $env:APPDATA
    $script:LocalAppData = $env:LOCALAPPDATA
    $script:Temp = $env:TEMP
    $script:AllUsersProfile = $env:ALLUSERSPROFILE
    $script:ProgramFiles = $env:ProgramFiles
    $script:CommonProgramFiles = $env:CommonProgramFiles
    $script:ProgramData = $env:ProgramData
    $script:WinDir = $env:WINDIR
    $script:WICacheDir = "$script:WinDir\Installer"
    $script:ScrubDir = "$script:Temp\$script:SCRIPT_NAME"
    $script:ScriptDir = Split-Path -Parent $MyInvocation.PSCommandPath
    $script:Notepad = "$script:WinDir\notepad.exe"

    # Set 64-bit specific paths
    if ($script:Is64Bit) {
        $script:ProgramFilesX86 = ${env:ProgramFiles(x86)}
        $script:CommonProgramFilesX86 = ${env:CommonProgramFiles(x86)}
    }

    # Get profiles directory
    $script:ProfilesDirectory = Get-RegistryValue -Hive LocalMachine -SubKey "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" -ValueName "ProfilesDirectory"
    if (-not $script:ProfilesDirectory -or -not (Test-Path $script:ProfilesDirectory)) {
        $script:ProfilesDirectory = Split-Path -Parent $env:USERPROFILE
    }

    # Create temp directory
    if (-not (Test-Path $script:ScrubDir)) {
        [void](New-Item -ItemType Directory -Path $script:ScrubDir -Force)
    }

    # Set default log directory
    $script:LogDir = $script:ScrubDir
}
#endregion

#region Error Handling
function Set-ErrorCode {
    param([int]$ErrorBit)
    $script:ErrorCode = $script:ErrorCode -bor $ErrorBit
}

function Clear-ErrorCode {
    param([int]$ErrorBit)
    $script:ErrorCode = $script:ErrorCode -band (-bnot $ErrorBit)
}

function Set-ReturnValue {
    param([int]$Err)
    $script:ErrorCode = $Err
    if ($script:ScrubDir) {
        $retValPath = Join-Path $script:ScrubDir $script:RET_VAL_FILE
        Set-Content -Path $retValPath -Value $Err -Force
    }
}

function Get-ReturnValueFromFile {
    if ($script:ScrubDir) {
        $retValPath = Join-Path $script:ScrubDir $script:RET_VAL_FILE
        if (Test-Path $retValPath) {
            $reader = [System.IO.StreamReader]::new($retValPath)
            try {
                $content = $reader.ReadToEnd()
            }
            finally {
                $reader.Close()
            }
            return [int]$content
        }
    }
    return $script:ERROR_UNKNOWN
}
#endregion

#region GUID Operations
function Get-ExpandedGuid {
    param([string]$Guid)

    if ($Guid.Length -eq 32) {
        # Convert compressed GUID to expanded format
        $expanded = $Guid.Substring(0, 8) + "-" + $Guid.Substring(8, 4) + "-" + $Guid.Substring(12, 4) + "-" + $Guid.Substring(16, 4) + "-" + $Guid.Substring(20, 12)
        return $expanded.ToUpper()
    }
    return $Guid.ToUpper()
}

function Get-CompressedGuid {
    param([string]$Guid)

    if ($Guid.Length -eq 36 -and $Guid.Contains("-")) {
        # Convert expanded GUID to compressed format
        return $Guid.Replace("-", "").ToUpper()
    }
    return $Guid.ToUpper()
}

function Get-DecodedGuid {
    param(
        [string]$EncGuid,
        [string]$Guid
    )

    try {
        # Use C# for GUID decoding
        $decodedGuid = [GuidDecoder]::DecodeGuid($EncGuid)

        if ($decodedGuid) {
            return $decodedGuid
        }

        # Fallback to expanded GUID format if decoding fails
        return Get-ExpandedGuid $Guid
    }
    catch {
        Write-Log "Failed to decode GUID: $($_.Exception.Message)"
        return Get-ExpandedGuid $Guid
    }
}
#endregion

#region Service Management
function Remove-Service {
    param([string]$ServiceName)

    try {
        $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
        if ($service) {
            Write-Log "Removing service: $ServiceName"
            if ($service.Status -eq "Running") {
                Stop-Service -Name $ServiceName -Force
            }
            [void](sc.exe delete $ServiceName)
            return $true
        }
        return $true
    }
    catch {
        Write-Log "Failed to remove service: $ServiceName - $($_.Exception.Message)"
        return $false
    }
}
#endregion

Export-ModuleMember -Function *