# 06-OrganizeDesktop.ps1 - Single source of truth for the Public Desktop
# layout. Builds three category folders and fills them, so end users see
# a clean desktop instead of 20+ loose icons.
#
# Creates three subfolders under C:\Users\Public\Desktop:
#   Office\          - Excel, Word, PowerPoint, Outlook, OneNote, etc.
#   Shopfloor Tools\ - UDC, eDNC, HostExplorer, MarkZebra, PC-DMIS, Hexagon,
#                      WJ Shopfloor, Defect_Tracker, NTLARS, etc.
#   Web Links\       - .url shortcuts (internal portals)
#
# Two phases:
#   PHASE 1 (Invoke-DesktopSweep): walks everything at the desktop ROOT,
#   classifies each .lnk / .url by filename / extension / .lnk target, and
#   moves it into the right category folder. Unknown items are LEFT AT
#   THE ROOT on purpose. eDNC.lnk and NTLARS.lnk are allowlisted to stay
#   at the root - end users click them too often to bury in a folder.
#
#   PHASE 2 (Add-ShopfloorToolsApps): materializes specific app shortcuts
#   into Shopfloor Tools\ from known .exe paths (UDC, eDNC, NTLARS). For
#   MSI-advertised shortcuts that don't have a resolvable target (WJ
#   Shopfloor, Defect_Tracker), finds the existing .lnk wherever it lives
#   and copies it in. This runs AFTER the sweep so it overwrites any
#   stale copies the sweeper may have moved in.
#
# Also registers an "Organize Public Desktop" scheduled task that re-runs
# the sweep (phase 1) at every logon. Phase 2 doesn't re-run - the
# shortcuts it creates are stable and 06 on first run populates them.
#
# 07-TaskbarLayout.ps1 reads the result of this script and writes
# LayoutModification.xml to pin specific shortcuts to the taskbar.

$ErrorActionPreference = 'Continue'

# ============================================================================
# Admin check - writing to C:\Users\Public\Desktop\* and registering
# system-wide scheduled tasks both require elevation. Fail fast if not
# admin so we don't spam half-successful garbage across the output.
# ============================================================================
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
    Write-Host ""
    Write-Host "ERROR: 06-OrganizeDesktop.ps1 must run as Administrator." -ForegroundColor Red
    Write-Host "       Re-run from an elevated PowerShell:" -ForegroundColor Red
    Write-Host "         Start -> type 'powershell' -> right-click -> Run as administrator" -ForegroundColor Red
    Write-Host ""
    exit 1
}

# Transcript. 06 runs as a SYSTEM scheduled task at every logon via the
# 'Organize Public Desktop' task, so without a transcript its decisions
# (what got added, what got suppressed) are invisible. Append mode keeps
# a history across logons; session header + PID makes individual runs
# easy to find.
$transcriptDir = 'C:\Logs\SFLD'
if (-not (Test-Path $transcriptDir)) {
    try { New-Item -ItemType Directory -Path $transcriptDir -Force | Out-Null } catch {}
}
$transcriptPath = Join-Path $transcriptDir '06-OrganizeDesktop.log'
try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {}
Write-Host ""
Write-Host "================================================================"
Write-Host "=== 06-OrganizeDesktop session start (PID $PID, $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) ==="
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
Write-Host "================================================================"

# ============================================================================
# Cleanup: remove 'Shopfloor Intune Sync' task after sync completes
#
# Monitor-IntuneProgress.ps1 registers that task as BUILTIN\Users + RunLevel
# Limited (needs a user context to show the QR code / Configure-PC prompt),
# and a Limited task cannot Unregister-ScheduledTask itself. So when sync
# completes it just writes C:\Enrollment\sync-complete.txt as a marker and
# the task stays around firing on every logon as a no-op.
#
# 06 runs as SYSTEM via its own sweep task, which DOES have permission to
# delete any task. When we see the marker, drop the stale task so Task
# Scheduler doesn't keep the dead entry forever.
# ============================================================================
$syncCompleteMarker = 'C:\Enrollment\sync-complete.txt'
$syncTaskName = 'Shopfloor Intune Sync'
if (Test-Path -LiteralPath $syncCompleteMarker) {
    $syncTask = Get-ScheduledTask -TaskName $syncTaskName -ErrorAction SilentlyContinue
    if ($syncTask) {
        try {
            Unregister-ScheduledTask -TaskName $syncTaskName -Confirm:$false -ErrorAction Stop
            Write-Host "Removed stale '$syncTaskName' task (sync complete, marker present)."
        } catch {
            Write-Warning "Failed to remove '$syncTaskName' task: $_"
        }
    }
}

# Load site config + PC profile
. "$PSScriptRoot\lib\Get-PCProfile.ps1"

$publicDesktop       = 'C:\Users\Public\Desktop'
$shopfloorToolsDir   = Join-Path $publicDesktop 'Shopfloor Tools'
$scriptPath          = $MyInvocation.MyCommand.Path
$scriptDir           = Split-Path -Parent $scriptPath

# Filenames that always stay at the desktop root regardless of classification.
# Empty right now - every shortcut lives inside a category folder for a
# clean end-user desktop. Add entries here if a future shortcut needs to
# stay pinned at root.
$keepAtRoot = @()

# ============================================================================
# Phase 1: Sweep loose shortcuts at desktop root into category folders
# ============================================================================
function Invoke-DesktopSweep {
    param([string]$DesktopPath)

    if (-not (Test-Path -LiteralPath $DesktopPath)) {
        Write-Host "Public desktop not found at $DesktopPath - skipping sweep."
        return
    }

    # Category definitions. Order matters: first match wins. Each category
    # can classify by file Extension, file Name (regex), or .lnk Target
    # (regex, resolved via WScript.Shell).
    $categories = [ordered]@{
        'Office' = @{
            Name = @(
                '^(Excel|Word|PowerPoint|Outlook|OneNote|Access|Publisher|OneDrive)(\s|$)'
            )
            Target = @(
                '\\(EXCEL|WINWORD|POWERPNT|OUTLOOK|ONENOTE|MSACCESS|MSPUB)\.EXE$',
                '\\OneDrive\.exe$'
            )
        }
        'Shopfloor Tools' = @{
            Name = @(
                '^UDC',
                'eDNC', '\bDNC\b', 'DncMain', 'GE DNC', 'NTLARS',
                'Host\s*Explorer', 'ShopFloor', 'TN3270', 'TN5250', 'HE\s*3270', 'HE\s*5250',
                'OpenText',
                'Defect[_\s-]?Tracker',
                'MarkZebra', 'Zebra',
                'PC-?DMIS',
                'Hexagon', 'CLM\s*Tools',
                'Blancco',
                'Keyence'
            )
            Target = @(
                '\\UDC\.exe$',
                'DncMain\.exe$', 'NTLARS\.exe$', 'DNCRemote',
                'HostExplorer\.exe$', 'hostex32\.exe$', 'humicon15\.exe$',
                'PC-?DMIS',
                'Hexagon',
                'Defect_Tracker\.application$'
            )
        }
        'Web Links' = @{
            Extension = @('.url')
        }
    }

    # Category folders are created LAZILY - only when we're about to move
    # something into them - so a Display / Wax-Trace PC without Office
    # doesn't get an empty Office\ folder littering the desktop. See
    # Remove-EmptyCategoryFolders at the end of the script for the
    # post-sweep cleanup pass that finishes the job.

    # WScript.Shell for resolving .lnk targets
    $shell = $null
    try { $shell = New-Object -ComObject WScript.Shell } catch {}

    # Only sweep files at the desktop root, never recurse into the category
    # folders themselves (that would loop things back).
    $items = @(Get-ChildItem -LiteralPath $DesktopPath -File -ErrorAction SilentlyContinue)

    $moved   = 0
    $skipped = 0

    foreach ($item in $items) {
        # Allowlist check: never sweep items the user wants to keep visible
        if ($keepAtRoot -contains $item.Name) {
            Write-Host "  keep:  $($item.Name) (allowlisted)"
            continue
        }

        $category = $null

        # 1. Extension match (cheapest)
        foreach ($cat in $categories.Keys) {
            $exts = $categories[$cat].Extension
            if ($exts -and ($exts -contains $item.Extension)) {
                $category = $cat
                break
            }
        }

        # 2. File name regex match
        if (-not $category) {
            foreach ($cat in $categories.Keys) {
                $pats = $categories[$cat].Name
                if (-not $pats) { continue }
                foreach ($p in $pats) {
                    if ($item.BaseName -match $p) {
                        $category = $cat
                        break
                    }
                }
                if ($category) { break }
            }
        }

        # 3. .lnk target resolution
        if (-not $category -and $item.Extension -ieq '.lnk' -and $shell) {
            try {
                $sc = $shell.CreateShortcut($item.FullName)
                $target = $sc.TargetPath
                if ($target) {
                    foreach ($cat in $categories.Keys) {
                        $pats = $categories[$cat].Target
                        if (-not $pats) { continue }
                        foreach ($p in $pats) {
                            if ($target -match $p) {
                                $category = $cat
                                break
                            }
                        }
                        if ($category) { break }
                    }
                }
            } catch {}
        }

        if ($category) {
            $destDir = Join-Path $DesktopPath $category
            if (-not (Test-Path -LiteralPath $destDir)) {
                try {
                    New-Item -ItemType Directory -Path $destDir -Force -ErrorAction Stop | Out-Null
                } catch {
                    Write-Warning "Failed to create category folder '$category' : $_"
                    continue
                }
            }
            $destPath = Join-Path $destDir $item.Name
            try {
                Move-Item -LiteralPath $item.FullName -Destination $destPath -Force -ErrorAction Stop
                Write-Host "  moved: $($item.Name) -> $category\"
                $moved++
            } catch {
                Write-Warning "Failed to move $($item.Name) : $_"
            }
        } else {
            $skipped++
        }
    }

    if ($shell) {
        try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) | Out-Null } catch {}
    }

    Write-Host "Sweep complete: $moved moved, $skipped unclassified (left at root)."
}

# ============================================================================
# Phase 2: Materialize specific app shortcuts into Shopfloor Tools\
#
# Runs AFTER the sweep. Creates fresh .lnk files for apps we can target by
# .exe path (UDC, eDNC, NTLARS) and copies existing .lnk files for apps
# that are MSI-advertised and have no resolvable target (WJ Shopfloor,
# Defect_Tracker). Overwrites whatever the sweeper may have moved in -
# this is intentional because fresh shortcuts are more reliable than
# whatever install crud might be floating around.
#
# Each app is CONDITIONAL - if its source isn't present on this PC, the
# entry is silently skipped. Safe to run on all shopfloor PC types.
# ============================================================================

function New-ShopfloorLnk {
    param(
        [string]$Path,
        [string]$Target,
        [string]$Arguments = '',
        [string]$WorkingDirectory = ''
    )
    $wsh = New-Object -ComObject WScript.Shell
    try {
        $sc = $wsh.CreateShortcut($Path)
        $sc.TargetPath = $Target
        if ($Arguments)        { $sc.Arguments = $Arguments }
        if ($WorkingDirectory) { $sc.WorkingDirectory = $WorkingDirectory }
        elseif ($Target) {
            $parent = Split-Path -Parent $Target
            if ($parent) { $sc.WorkingDirectory = $parent }
        }
        $sc.IconLocation = "$Target,0"
        $sc.Save()
        return $true
    } catch {
        Write-Warning "Failed to create $Path : $_"
        return $false
    } finally {
        try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($wsh) | Out-Null } catch {}
    }
}

# Look up an existing .lnk file across the paths where installers / ppkgs
# typically drop them. Used for apps where we don't build the .lnk from
# an .exe (because the target is an MSI-advertised Darwin descriptor).
function Find-ExistingLnk {
    param([string]$Filename)

    $candidates = @(
        (Join-Path $publicDesktop $Filename),
        (Join-Path $shopfloorToolsDir $Filename),
        "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\$Filename",
        "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Shopfloor Tools\$Filename"
    )
    foreach ($c in $candidates) {
        if (Test-Path -LiteralPath $c) { return $c }
    }
    return $null
}

function Add-ShopfloorToolsApps {
    # App registry - each entry describes how to materialize one shortcut
    # into Shopfloor Tools\. Folder creation is deferred until we know we
    # have at least one app to put in it (lazy creation, so PC types with
    # nothing installed don't get an empty Shopfloor Tools folder).
    #
    #   Kind = 'exe'       -> build a fresh .lnk from ExePath
    #   Kind = 'existing'  -> copy an existing .lnk via Find-ExistingLnk
    $cfgApps = Get-ProfileValue 'desktopApps'

    if ($null -ne $cfgApps -and $cfgApps.Count -gt 0) {
        $apps = @($cfgApps | ForEach-Object {
            $entry = @{ Name = $_.name; Kind = $_.kind }
            if ($_.kind -eq 'exe')      { $entry.ExePath    = $_.exePath }
            if ($_.kind -eq 'existing') { $entry.SourceName = $_.sourceName }
            $entry
        })
    } else {
        $apps = @(
            @{ Name = 'UDC';            Kind = 'exe';      ExePath = 'C:\Program Files\UDC\UDC.exe' }
            @{ Name = 'eDNC';           Kind = 'exe';      ExePath = 'C:\Program Files (x86)\Dnc\bin\DncMain.exe' }
            @{ Name = 'NTLARS';         Kind = 'exe';      ExePath = 'C:\Program Files (x86)\Dnc\Common\NTLARS.exe' }
            @{ Name = 'WJ Shopfloor';   Kind = 'existing'; SourceName = 'WJ Shopfloor.lnk' }
            @{ Name = 'Defect_Tracker'; Kind = 'exe';      ExePath = 'C:\Program Files (x86)\WJF_Defect_Tracker\Defect_Tracker.exe' }
        )
    }

    # Lazy folder creation - only create Shopfloor Tools\ the first time
    # we have an app that's actually going to land in it. PC types with
    # nothing installed get no empty folder.
    $ensureToolsDir = {
        if (-not (Test-Path -LiteralPath $shopfloorToolsDir)) {
            try {
                New-Item -ItemType Directory -Path $shopfloorToolsDir -Force -ErrorAction Stop | Out-Null
                return $true
            } catch {
                Write-Warning "Failed to create $shopfloorToolsDir : $_"
                return $false
            }
        }
        return $true
    }

    foreach ($app in $apps) {
        $dest = Join-Path $shopfloorToolsDir "$($app.Name).lnk"

        switch ($app.Kind) {
            'exe' {
                if (Test-Path -LiteralPath $app.ExePath) {
                    if (-not (& $ensureToolsDir)) { break }
                    if (New-ShopfloorLnk -Path $dest -Target $app.ExePath) {
                        Write-Host "  created: $($app.Name) -> $dest"
                    }
                } else {
                    Write-Host "  skip:    $($app.Name) - target not installed ($($app.ExePath))" -ForegroundColor DarkGray
                }
            }

            'existing' {
                $src = Find-ExistingLnk $app.SourceName
                if ($src) {
                    # Skip if the sweep already moved the source into the
                    # destination folder (src == dest -> "cannot overwrite
                    # the item with itself").
                    if ((Resolve-Path -LiteralPath $src -ErrorAction SilentlyContinue).Path -eq
                        (Join-Path $shopfloorToolsDir $app.SourceName)) {
                        Write-Host "  exists:  $($app.Name) (already in Shopfloor Tools)"
                    } else {
                        if (-not (& $ensureToolsDir)) { break }
                        try {
                            Copy-Item -LiteralPath $src -Destination $dest -Force -ErrorAction Stop
                            Write-Host "  copied:  $($app.Name) from $src"
                        } catch {
                            Write-Warning "Failed to copy $src -> $dest : $_"
                        }
                    }
                } else {
                    Write-Host "  skip:    $($app.Name) - no source .lnk found" -ForegroundColor DarkGray
                }
            }
        }
    }
}

# ============================================================================
# Phase 3: Remove empty category folders
#
# If the sweep classified nothing into a given category (e.g. no Office
# on a Display PC, no Shopfloor Tools apps on a kiosk), we don't want an
# empty folder cluttering the desktop. The scheduled-task sweep also runs
# this so categories that go empty between logons self-heal.
# ============================================================================
function Remove-EmptyCategoryFolders {
    foreach ($cat in @('Office', 'Shopfloor Tools', 'Web Links')) {
        $dir = Join-Path $publicDesktop $cat
        if (-not (Test-Path -LiteralPath $dir)) { continue }
        $contents = @(Get-ChildItem -LiteralPath $dir -Force -ErrorAction SilentlyContinue)
        if ($contents.Count -eq 0) {
            try {
                Remove-Item -LiteralPath $dir -Force -ErrorAction Stop
                Write-Host "  removed empty: $cat\"
            } catch {
                Write-Warning "Failed to remove empty $dir : $_"
            }
        }
    }
}

# ============================================================================
# Phase 4: Edge default browser + startup tabs (delegated to 08)
#
# 08-EdgeDefaultBrowser.ps1 resolves startup-tab URLs from .url files on
# the Public Desktop (or in Web Links\ after the sweep). The .url files
# are delivered by DSC AFTER this initial shopfloor setup, so the first
# run at imaging time won't find them (falls back to hardcoded URLs,
# Plant Apps gets skipped). By calling 08 from inside 06, every SYSTEM
# scheduled-task logon re-run of 06 also re-runs 08 -- so after DSC drops
# the .url files and the next sweep files them into Web Links\, 08
# picks them up and updates the Edge policy. Self-healing.
# ============================================================================
function Invoke-EdgeDefaultBrowser {
    $edgeScript = Join-Path $scriptDir '08-EdgeDefaultBrowser.ps1'
    if (-not (Test-Path -LiteralPath $edgeScript)) {
        Write-Host "  08-EdgeDefaultBrowser.ps1 not found - skipping"
        return
    }
    try {
        & $edgeScript
    } catch {
        Write-Warning "08-EdgeDefaultBrowser.ps1 failed: $_"
    }
}

# ============================================================================
# Scheduled task registration (re-runs 06 at every logon as SYSTEM)
# ============================================================================
function Register-SweepScheduledTask {
    param([string]$ScriptPath)

    $taskName = 'Organize Public Desktop'

    if (-not (Test-Path -LiteralPath $ScriptPath)) {
        Write-Warning "Cannot register scheduled task - script not found at $ScriptPath"
        return
    }

    try {
        $action = New-ScheduledTaskAction `
            -Execute 'powershell.exe' `
            -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$ScriptPath`""

        $trigger = New-ScheduledTaskTrigger -AtLogOn

        # Run as SYSTEM so the script can (a) pass its admin check at the
        # top, (b) write to Public Desktop / ProgramData Start Menu / the
        # Default User profile without Access Denied, and (c) register the
        # next iteration of itself idempotently. Earlier versions used
        # -GroupId 'BUILTIN\Users' -RunLevel Limited which fired at logon
        # but the script exited immediately on the non-admin check.
        $principal = New-ScheduledTaskPrincipal `
            -UserId    'NT AUTHORITY\SYSTEM' `
            -LogonType ServiceAccount `
            -RunLevel  Highest

        $settings = New-ScheduledTaskSettingsSet `
            -AllowStartIfOnBatteries `
            -DontStopIfGoingOnBatteries `
            -StartWhenAvailable `
            -ExecutionTimeLimit (New-TimeSpan -Minutes 5)

        # -ErrorAction Stop is required because Register-ScheduledTask emits
        # non-terminating errors on failure (Access Denied etc), which try/catch
        # would otherwise miss and we'd falsely claim success.
        Register-ScheduledTask `
            -TaskName   $taskName `
            -Action     $action `
            -Trigger    $trigger `
            -Principal  $principal `
            -Settings   $settings `
            -Force `
            -ErrorAction Stop | Out-Null

        Write-Host "Scheduled task '$taskName' registered (runs at every logon)."
    } catch {
        Write-Warning "Failed to register scheduled task : $_"
    }
}

# ============================================================================
# Main
# ============================================================================
Write-Host ""
Write-Host "=== Phase 0: create Office shortcuts (if Office is installed) ==="
# Delegated to 05-OfficeShortcuts.ps1. Office is installed via ppkg but
# doesn't finish streaming until AFTER the first reboot, so the first
# imaging-time run of 06 finds no Office and this no-ops. On the second
# logon (after reboot), Office is installed and 05 creates the shortcuts
# so Phase 1 below can sweep them into the Office\ folder.
$officeScript = Join-Path $scriptDir '05-OfficeShortcuts.ps1'
if (Test-Path -LiteralPath $officeScript) {
    try { & $officeScript } catch { Write-Warning "05-OfficeShortcuts.ps1 failed: $_" }
} else {
    Write-Host "  05-OfficeShortcuts.ps1 not found - skipping"
}

Write-Host ""
Write-Host "=== Phase 1: sweep loose shortcuts into category folders ==="
Invoke-DesktopSweep -DesktopPath $publicDesktop

Write-Host ""
Write-Host "=== Phase 2: populate Shopfloor Tools with app shortcuts ==="
Add-ShopfloorToolsApps

Write-Host ""
Write-Host "=== Phase 3: auto-apply startup items from PC profile ==="
# Creates .lnk files in AllUsers Startup folder based on the profile's
# startupItems. This runs as SYSTEM at every logon via the sweep scheduled
# task, so any item the tech removes via Configure-PC would bounce right
# back unless we remember the removal. startup-overrides.json tracks labels
# the tech has explicitly suppressed; items in that list are skipped here
# and re-enabling them via Configure-PC removes them from the list.
$startupDir = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup'
$overridesPath = 'C:\ProgramData\GE\Shopfloor\startup-overrides.json'

$suppressed = @()
if (Test-Path -LiteralPath $overridesPath) {
    try {
        $overrides = Get-Content -LiteralPath $overridesPath -Raw | ConvertFrom-Json
        if ($overrides.suppressed) { $suppressed = @($overrides.suppressed) }
    } catch {
        Write-Warning "  Failed to parse $overridesPath : $_"
    }
}

$cfgStartup = Get-ProfileValue 'startupItems'

if ($null -ne $cfgStartup -and $cfgStartup.Count -gt 0) {
    if (-not (Test-Path $startupDir)) {
        New-Item -ItemType Directory -Path $startupDir -Force | Out-Null
    }
    $edgePath = @(
        'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
        'C:\Program Files\Microsoft\Edge\Application\msedge.exe'
    ) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1

    foreach ($si in $cfgStartup) {
        if ($suppressed -contains $si.label) {
            Write-Host "  suppressed: $($si.label) (tech disabled via Configure-PC)"
            continue
        }
        $lnkPath = Join-Path $startupDir "$($si.label).lnk"
        # Skip if already exists (idempotent - don't overwrite user changes)
        if (Test-Path -LiteralPath $lnkPath) {
            Write-Host "  exists: $($si.label)"
            continue
        }
        switch ($si.type) {
            'exe' {
                if (Test-Path -LiteralPath $si.target) {
                    if (New-ShopfloorLnk -Path $lnkPath -Target $si.target) {
                        Write-Host "  added:  $($si.label)"
                    }
                } else {
                    Write-Host "  skip:   $($si.label) - target not installed" -ForegroundColor DarkGray
                }
            }
            'existing' {
                $src = Find-ExistingLnk $si.sourceLnk
                if ($src) {
                    try {
                        Copy-Item -LiteralPath $src -Destination $lnkPath -Force
                        Write-Host "  added:  $($si.label)"
                    } catch { Write-Warning "  failed: $($si.label) - $_" }
                } else {
                    Write-Host "  skip:   $($si.label) - source not found" -ForegroundColor DarkGray
                }
            }
            'url' {
                $url = $null
                if ($si.urlKey -and $siteConfig -and $siteConfig.urls) {
                    $url = $siteConfig.urls.$($si.urlKey)
                }
                if ($url -and $edgePath) {
                    if (New-ShopfloorLnk -Path $lnkPath -Target $edgePath -Arguments "--new-window `"$url`"") {
                        Write-Host "  added:  $($si.label)"
                    }
                } else {
                    Write-Host "  skip:   $($si.label) - URL or Edge not available" -ForegroundColor DarkGray
                }
            }
        }
    }
} else {
    Write-Host "  (no startup items in profile)"
}

Write-Host ""
Write-Host "=== Phase 4: remove empty category folders ==="
Remove-EmptyCategoryFolders

Write-Host ""
Write-Host "=== Phase 5: Edge default browser + startup tabs ==="
Invoke-EdgeDefaultBrowser

Write-Host ""
Write-Host "=== Phase 6: register logon sweeper scheduled task ==="
Register-SweepScheduledTask -ScriptPath $scriptPath

Write-Host ""
Write-Host "=== 06-OrganizeDesktop session end ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) ==="
try { Stop-Transcript | Out-Null } catch {}
exit 0
