<#
.SYNOPSIS
User-scope removal of the ClickOnce app "BPC Q-Pulse 7.1" without admin rights.

.DESCRIPTION
- Detects HKCU Uninstall and the user's Start Menu/Desktop .appref-ms for "BPC Q-Pulse 7.1".
- If UninstallString exists (HKCU ARP), runs it (user-context).
- Force-removes: user's Start Menu + user Desktop shortcut(s), user ClickOnce cache (%LOCALAPPDATA%\Apps\2.0),
  and HKCU Uninstall key(s) — all user-scope.
- Purges ClickOnce Deployment hives in HKCU that reference the app/URL (user-scope).
- Public Desktop shortcut is report-only (no delete) to avoid admin rights.
- Dry-run by default; use -Execute to apply.
- -DetectOnly returns 0 if nothing is found (inverse detect), 1 if found.

.EXITCODES
0 = Success (removed or nothing to do) / DetectOnly: nothing found
1 = DetectOnly: something found
2 = Not found (normal flow)
4 = Partial cleanup
#>

[CmdletBinding(SupportsShouldProcess)]
param(
    [switch]$Execute,
    [switch]$PurgeAllClickOnceCache,  # user-safe, optional
    [switch]$DetectOnly,
    [string]$LogPath   # passed in from SYSTEM script (e.g., C:\Windows\Logs\Software\UserCleanup-*.log)
)

# --- Severity helper: map strings to PSADT numeric codes (1=Info, 2=Warning, 3=Error) ---
function Convert-Severity {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Level
    )
    switch ($Level.ToUpperInvariant()) {
        'INFORMATION' { return 1 }
        'INFO'        { return 1 }
        'WARNING'     { return 2 }
        'ERROR'       { return 3 }
        default       { return 1 }  # Fallback to Information
    }
}

# --- Minimal logger if PSADT isn't loaded in this user process ---
# Note: This shim accepts the numeric Severity (UInt32) to match PSADT v4.
if (-not (Get-Command Write-ADTLogEntry -ErrorAction SilentlyContinue)) {
    function Write-ADTLogEntry {
        param(
            [Parameter(Mandatory)][string]$Message,
            [Parameter(Mandatory)][uint32]$Severity  # 1=Info, 2=Warning, 3=Error
        )
        $sevLabel = switch ($Severity) { 1 { 'Information' } 2 { 'Warning' } 3 { 'Error' } default { 'Information' } }
        $ts   = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
        $line = "[$ts][$sevLabel] $Message"
        if ($LogPath) {
            try { Add-Content -LiteralPath $LogPath -Value $line -Encoding UTF8 } catch { Write-Output $line }
        } else {
            Write-Output $line
        }
    }
}

# ---- Targets (user-scope only) ----
$DisplayNameTarget = 'BPC Q-Pulse 7.1'            # For HKCU ARP & manifests
$ShortcutTarget    = 'BPC Q-Pulse 7.1.appref-ms'  # Exact filename for Start Menu/Desktop

# ---- Helpers (HKCU + user profile only) ----
function Get-ClickOnceUninstallEntries {
    $base = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall'
    if (-not (Test-Path $base)) { return @() }
    Get-ChildItem $base -ErrorAction SilentlyContinue | ForEach-Object {
        $k = $_.PsPath
        $p = Get-ItemProperty -Path $k -ErrorAction SilentlyContinue
        if ($p -and $p.DisplayName -and ($p.DisplayName -like "*$DisplayNameTarget*")) {
            $ver = try { [version]$p.DisplayVersion } catch { [version]'0.0.0.0' }
            [pscustomobject]@{
                KeyPath         = $k
                DisplayName     = $p.DisplayName
                Publisher       = $p.Publisher
                UninstallString = $p.UninstallString
                URLUpdateInfo   = $p.URLUpdateInfo
                DisplayVersion  = $p.DisplayVersion
                VerObj          = $ver
            }
        }
    }
}

function Select-BestEntry {
    param($Entries)
    if (-not $Entries -or $Entries.Count -eq 0) { return $null }
    $Entries |
        Sort-Object `
            @{ Expression = { $_.DisplayName.Length }; Descending = $true },
            @{ Expression = { $_.VerObj };             Descending = $true } |
        Select-Object -First 1
}

function Find-UserStartMenuApprefShortcuts {
    $root = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs'
    if (-not (Test-Path $root)) { return @() }

    $found = @()
    $hits = Get-ChildItem -Path $root -Recurse -Filter '*.appref-ms' -ErrorAction SilentlyContinue |
            Where-Object { $_.Name -ieq $ShortcutTarget }
    if ($hits) { $found += $hits }

    # Fallback: content match for renamed shortcuts
    $all = Get-ChildItem -Path $root -Recurse -Filter '*.appref-ms' -ErrorAction SilentlyContinue
    foreach ($f in $all) {
        try {
            $raw = Get-Content -LiteralPath $f.FullName -Raw -ErrorAction Stop
            if ($raw -like "*$DisplayNameTarget*") {
                if (-not ($found.FullName -contains $f.FullName)) { $found += $f }
            }
        } catch {}
    }
    return $found
}

function Find-DesktopApprefShortcuts {
    # User desktops only + report-only Public Desktop (no delete)
    $desktops = @(
        (Join-Path $env:USERPROFILE 'Desktop')
    )
    if ($env:OneDrive) { $desktops += (Join-Path $env:OneDrive 'Desktop') }
    try {
        $odDirs = Get-ChildItem -Path $env:USERPROFILE -Directory -Filter 'OneDrive*' -ErrorAction SilentlyContinue
        foreach ($d in $odDirs) { $desktops += (Join-Path $d.FullName 'Desktop') }
    } catch {}
    # Add Public Desktop only for reporting
    $publicDesktop = 'C:\Users\Public\Desktop'
    $desktopsForDelete = $desktops | Sort-Object -Unique
    $desktopsForReport = $desktopsForDelete + $publicDesktop

    $report = @()
    foreach ($desk in ($desktopsForReport | Sort-Object -Unique)) {
        if (-not (Test-Path $desk)) { continue }
        $hits = Get-ChildItem -LiteralPath $desk -Filter '*.appref-ms' -File -ErrorAction SilentlyContinue |
                Where-Object { $_.Name -ieq $ShortcutTarget -or $_.Name -like "*$DisplayNameTarget*.appref-ms" }
        if ($hits) { $report += $hits }
    }

    # Return both sets (user-delete set separately calculated at call site)
    return [pscustomobject]@{
        Report = $report
        DeleteCandidatesRootPaths = $desktopsForDelete
    }
}

function Read-ApprefDetails {
    param([string]$ApprefPath)
    $raw = Get-Content -LiteralPath $ApprefPath -Raw -ErrorAction SilentlyContinue
    if (-not $raw) { return $null }
    $m = [regex]::Match($raw, 'deploymentProvider\s+url\s*=\s*["'']?([^"''\r\n]+)["'']?', 'IgnoreCase')
    $url = if ($m.Success) { $m.Groups[1].Value } else { $null }
    [pscustomobject]@{ ApprefPath = $ApprefPath; DeploymentUrl = $url }
}

# Manifest-guided cache removal (user-scope)
function Remove-ClickOnceCacheForApp {
    param(
        [string]$NameHint,
        [string]$UrlHint
    )

    $cacheRoot = Join-Path $env:LOCALAPPDATA 'Apps\2.0'
    if (-not (Test-Path $cacheRoot)) {
        Write-ADTLogEntry -Message "ClickOnce cache root not found: $cacheRoot" -Severity (Convert-Severity 'Information')
        return $true
    }

    $hostToken = $null
    if ($UrlHint) {
        try {
            $u = [uri]$UrlHint
            if ($u.Host) { $hostToken = $u.Host.ToLowerInvariant() }
        } catch {}
    }

    $manifestPatterns = @('*.application','*.manifest')
    $topFoldersToDelete = New-Object System.Collections.Generic.HashSet[string]

    Write-ADTLogEntry -Message "Scanning manifests under $cacheRoot for app references…" -Severity (Convert-Severity 'Information')

    Get-ChildItem -Path $cacheRoot -Recurse -Include $manifestPatterns -File -Force -ErrorAction SilentlyContinue |
        ForEach-Object {
            $f = $_.FullName
            $raw = $null
            try { $raw = Get-Content -LiteralPath $f -Raw -ErrorAction Stop } catch { return }
            if (-not $raw) { return }

            $hit = $false
            if ($hostToken -and ($raw.ToLowerInvariant() -like "*$hostToken*")) { $hit = $true }
            elseif ($NameHint -and ($raw -like "*$NameHint*")) { $hit = $true }

            if ($hit) {
                # climb to first-level child of Apps\2.0 and remove that
                $parent = Split-Path $f -Parent
                while ($parent -and (Split-Path $parent -Parent) -and ((Split-Path $parent -Parent) -ne $cacheRoot)) {
                    $parent = Split-Path $parent -Parent
                }
                if ($parent -and (Split-Path $parent -Parent) -eq $cacheRoot) {
                    [void]$topFoldersToDelete.Add($parent)
                } else {
                    [void]$topFoldersToDelete.Add((Split-Path $f -Parent))
                }
            }
        }

    if ($topFoldersToDelete.Count -eq 0) {
        # Conservative fallback tokens, still user-scope only
        $fallbackTokens = @('bpc','qpulse','q','pulse','71')
        Write-ADTLogEntry -Message "No manifest-linked folders found; falling back to token match on folder names." -Severity (Convert-Severity 'Warning')
        Get-ChildItem -Path $cacheRoot -Recurse -Directory -Force -ErrorAction SilentlyContinue | ForEach-Object {
            $n = ($_.FullName -replace '[^a-zA-Z0-9]','').ToLowerInvariant()
            foreach ($t in $fallbackTokens) {
                if ($t -and $n -like "*$t*") { [void]$topFoldersToDelete.Add($_.FullName); break }
            }
        }
    }

    if ($topFoldersToDelete.Count -eq 0) {
        Write-ADTLogEntry -Message "No candidate ClickOnce cache folders matched." -Severity (Convert-Severity 'Warning')
        return $true
    }

    $ok = $true
    foreach ($t in $topFoldersToDelete) {
        if ($PSCmdlet.ShouldProcess($t, "Remove ClickOnce cache (top hashed folder)")) {
            try {
                Remove-Item -LiteralPath $t -Recurse -Force -ErrorAction Stop
                Write-ADTLogEntry -Message "Removed cache folder: $t" -Severity (Convert-Severity 'Information')
            } catch {
                Write-ADTLogEntry -Message "Failed to remove cache folder: $t : $($_.Exception.Message)" -Severity (Convert-Severity 'Warning')
                $ok = $false
            }
        } else {
            Write-ADTLogEntry -Message "Would remove cache folder: $t (WhatIf)" -Severity (Convert-Severity 'Information')
        }
    }
    return $ok
}

# Purge ClickOnce Deployment hives (HKCU only)
function Remove-ClickOnceDeploymentRegistryRefs {
    param(
        [string]$NameHint,
        [string]$UrlHint
    )

    $roots = @(
        'HKCU:\Software\Classes\Software\Microsoft\Windows\CurrentVersion\Deployment',
        'HKCU:\Software\Microsoft\Windows\CurrentVersion\Deployment'
    )

    $tokens = @()
    if ($NameHint) { $tokens += $NameHint }
    if ($UrlHint)  { $tokens += $UrlHint }
    if ($tokens.Count -eq 0) { return $true }

    $removedAny = $false
    foreach ($root in $roots) {
        if (-not (Test-Path $root)) { continue }
        Write-ADTLogEntry -Message "Scanning ClickOnce deployment hive: $root" -Severity (Convert-Severity 'Information')

        $keys = Get-ChildItem -Path $root -Recurse -ErrorAction SilentlyContinue
        foreach ($k in $keys) {
            $hit = $false
            try {
                $props = Get-ItemProperty -Path $k.PSPath -ErrorAction SilentlyContinue
                if ($props) {
                    foreach ($p in $props.PSObject.Properties) {
                        $val = [string]$p.Value
                        foreach ($t in $tokens) {
                            if ($t -and $val -and ($val -like "*$t*")) { $hit = $true; break }
                        }
                        if ($hit) { break }
                    }
                }
            } catch {}

            if ($hit) {
                if ($PSCmdlet.ShouldProcess($k.PSPath, "Remove ClickOnce deployment key (HKCU)")) {
                    try {
                        Remove-Item -LiteralPath $k.PSPath -Recurse -Force -ErrorAction Stop
                        Write-ADTLogEntry -Message "Removed deployment key: $($k.PSPath)" -Severity (Convert-Severity 'Information')
                        $removedAny = $true
                    } catch {
                        Write-ADTLogEntry -Message "Failed to remove deployment key: $($k.PSPath) : $($_.Exception.Message)" -Severity (Convert-Severity 'Warning')
                    }
                } else {
                    Write-ADTLogEntry -Message "Would remove deployment key: $($k.PSPath) (WhatIf)" -Severity (Convert-Severity 'Information')
                    $removedAny = $true
                }
            }
        }
    }
    return $removedAny
}

function Invoke-RegisteredUninstall {
    param($Entry)
    if (-not $Entry.UninstallString) { return $true }

    Write-ADTLogEntry -Message "Attempting registered uninstall (user-context): $($Entry.UninstallString)" -Severity (Convert-Severity 'Information')
    try {
        $cmd = $Entry.UninstallString.Trim()
        $file = $null; $args = ""
        if ($cmd.StartsWith('"')) {
            $end = $cmd.IndexOf('"',1)
            if ($end -lt 1) { throw "Malformed quoted path in UninstallString." }
            $file = $cmd.Substring(1, $end-1)
            $args = $cmd.Substring($end+1).Trim()
        } else {
            $m = [regex]::Match($cmd, '^\S+')
            $file = $m.Value
            $args = $cmd.Substring($file.Length).Trim()
        }

        if ($PSCmdlet.ShouldProcess($file, "Start-Process $args")) {
            $p = Start-Process -FilePath $file -ArgumentList $args -PassThru -WindowStyle Hidden
            $p.WaitForExit()
            Write-ADTLogEntry -Message "Registered uninstall exited with code $($p.ExitCode)" -Severity (Convert-Severity 'Information')
            return ($p.ExitCode -eq 0)
        } else {
            Write-ADTLogEntry -Message "Would run registered uninstall (WhatIf)" -Severity (Convert-Severity 'Information')
            return $true
        }
    } catch {
        Write-ADTLogEntry -Message "Registered uninstall failed: $($_.Exception.Message)" -Severity (Convert-Severity 'Warning')
        return $false
    }
}

# ---- Main (user-scope only) ----
$partial  = $false
$notFound = $false
if (-not $Execute) { $WhatIfPreference = $true; Write-ADTLogEntry -Message "Dry run. Pass -Execute to perform removal." -Severity (Convert-Severity 'Warning') }

Write-ADTLogEntry -Message "Targeting ClickOnce app '$DisplayNameTarget' (shortcut '$ShortcutTarget') for user '$env:USERNAME'..." -Severity (Convert-Severity 'Information')

$entries   = @(Get-ClickOnceUninstallEntries)
$bestEntry = Select-BestEntry -Entries $entries
if (-not $bestEntry) { Write-ADTLogEntry -Message "No HKCU ARP entry matched '$DisplayNameTarget'." -Severity (Convert-Severity 'Information') }

$userStartMenu = @(Find-UserStartMenuApprefShortcuts)
if ($userStartMenu.Count -gt 0) {
    Write-ADTLogEntry -Message ("User Start Menu shortcut(s): " + ($userStartMenu.FullName -join '; ')) -Severity (Convert-Severity 'Information')
} else {
    Write-ADTLogEntry -Message "No user Start Menu .appref-ms found for '$ShortcutTarget'." -Severity (Convert-Severity 'Information')
}

$deskObj = Find-DesktopApprefShortcuts
$deskShortcuts = @($deskObj.Report)
if ($deskShortcuts.Count -gt 0) {
    Write-ADTLogEntry -Message ("Desktop shortcuts (report-only; includes Public): " + ($deskShortcuts.FullName -join '; ')) -Severity (Convert-Severity 'Information')
}

# Deployment URL hint from any user Start Menu shortcut
$urlHint = $null
if ($userStartMenu.Count -gt 0) {
    $appref = Read-ApprefDetails -ApprefPath $userStartMenu[0].FullName
    if ($appref -and $appref.DeploymentUrl) {
        $urlHint = $appref.DeploymentUrl
        Write-ADTLogEntry -Message "Deployment URL hint: $urlHint" -Severity (Convert-Severity 'Information')
    }
}

# Inverse detect
if (-not $bestEntry -and $userStartMenu.Count -eq 0 -and $deskShortcuts.Count -eq 0) {
    Write-ADTLogEntry -Message "No matching ClickOnce install or shortcuts detected (user-scope)." -Severity (Convert-Severity 'Information')
    if ($DetectOnly) { Write-ADTLogEntry -Message "DetectOnly: nothing found -> 0" -Severity (Convert-Severity 'Information'); exit 0 }
    $notFound = $true
} elseif ($DetectOnly) {
    Write-ADTLogEntry -Message "DetectOnly: found something -> 1" -Severity (Convert-Severity 'Information')
    exit 1
}

# Graceful uninstall (HKCU ARP)
if ($bestEntry) {
    Write-ADTLogEntry -Message "HKCU Uninstall match: $($bestEntry.DisplayName)  Version: $($bestEntry.DisplayVersion)" -Severity (Convert-Severity 'Information')
    if (-not (Invoke-RegisteredUninstall -Entry $bestEntry)) { $partial = $true }
}

# Remove user Start Menu shortcuts (user-scope)
foreach ($sc in $userStartMenu) {
    if ($PSCmdlet.ShouldProcess($sc.FullName, "Remove user .appref-ms shortcut")) {
        try { Remove-Item -LiteralPath $sc.FullName -Force -ErrorAction Stop; Write-ADTLogEntry -Message "Removed: $($sc.FullName)" -Severity (Convert-Severity 'Information') }
        catch { Write-ADTLogEntry -Message "Failed to remove $($sc.FullName): $($_.Exception.Message)" -Severity (Convert-Severity 'Warning'); $partial = $true }
    } else {
        Write-ADTLogEntry -Message "Would remove: $($sc.FullName) (WhatIf)" -Severity (Convert-Severity 'Information')
    }
}

# Remove user Desktop shortcuts only under user-owned desktops (no Public Desktop)
foreach ($desk in ($deskObj.DeleteCandidatesRootPaths | Sort-Object -Unique)) {
    if (-not (Test-Path $desk)) { continue }
    $hits = Get-ChildItem -LiteralPath $desk -Filter '*.appref-ms' -File -ErrorAction SilentlyContinue |
            Where-Object { $_.Name -ieq $ShortcutTarget -or $_.Name -like "*$DisplayNameTarget*.appref-ms" }
    foreach ($h in $hits) {
        if ($PSCmdlet.ShouldProcess($h.FullName, "Remove user Desktop .appref-ms shortcut")) {
            try { Remove-Item -LiteralPath $h.FullName -Force -ErrorAction Stop; Write-ADTLogEntry -Message "Removed: $($h.FullName)" -Severity (Convert-Severity 'Information') }
            catch { Write-ADTLogEntry -Message "Failed to remove $($h.FullName): $($_.Exception.Message)" -Severity (Convert-Severity 'Warning'); $partial = $true }
        } else {
            Write-ADTLogEntry -Message "Would remove: $($h.FullName) (WhatIf)" -Severity (Convert-Severity 'Information')
        }
    }
}

# Remove ALL matching HKCU ARP entries
$allArp = @(Get-ClickOnceUninstallEntries)
foreach ($arp in $allArp) {
    if ($PSCmdlet.ShouldProcess($arp.KeyPath, "Remove HKCU Uninstall entry")) {
        try { Remove-Item -LiteralPath $arp.KeyPath -Recurse -Force -ErrorAction Stop; Write-ADTLogEntry -Message "Removed HKCU ARP: $($arp.DisplayName)" -Severity (Convert-Severity 'Information') }
        catch { Write-ADTLogEntry -Message "Failed to remove HKCU ARP at $($arp.KeyPath): $($_.Exception.Message)" -Severity (Convert-Severity 'Warning'); $partial = $true }
    } else {
        Write-ADTLogEntry -Message "Would remove HKCU ARP at $($arp.KeyPath) (WhatIf)" -Severity (Convert-Severity 'Information')
    }
}

# ClickOnce deployment hives cleanup (HKCU)
$depCleaned = Remove-ClickOnceDeploymentRegistryRefs -NameHint $DisplayNameTarget -UrlHint $urlHint
if (-not $depCleaned) { Write-ADTLogEntry -Message "No deployment keys matched (may already be clean)." -Severity (Convert-Severity 'Information') }

# Cache cleanup (manifest-guided; %LOCALAPPDATA%\Apps\2.0)
$cacheOk = Remove-ClickOnceCacheForApp -NameHint $DisplayNameTarget -UrlHint $urlHint
if (-not $cacheOk) { $partial = $true }

# Optional: dfshim cache purge (user-context safe)
if ($PurgeAllClickOnceCache) {
    Write-ADTLogEntry -Message "Purging user ClickOnce cache via dfshim (user-context)..." -Severity (Convert-Severity 'Warning')
    if ($PSCmdlet.ShouldProcess("rundll32.exe", "dfshim.dll,ShArpMaintain CleanOnlineAppCache")) {
        try {
            Start-Process -FilePath "rundll32.exe" -ArgumentList "dfshim.dll,ShArpMaintain CleanOnlineAppCache" -Wait -WindowStyle Hidden
            Write-ADTLogEntry -Message "dfshim cache purge attempted." -Severity (Convert-Severity 'Information')
        } catch {
            Write-ADTLogEntry -Message "dfshim cache purge failed: $($_.Exception.Message)" -Severity (Convert-Severity 'Warning')
            $partial = $true
        }
    } else {
        Write-ADTLogEntry -Message "Would purge dfshim cache (WhatIf)" -Severity (Convert-Severity 'Information')
    }
}

Write-ADTLogEntry -Message ("Summary (user-scope): ARP entries removed=" + $allArp.Count +
                            "; User StartMenu removed=" + $userStartMenu.Count +
                            "; Desktop shortcuts reported (incl. Public)=" + $deskShortcuts.Count +
                            "; Cache removal OK=" + $cacheOk) -Severity (Convert-Severity 'Information')

if ($partial) { exit 4 }
elseif ($notFound) { exit 2 }
else { exit 0 }
