<#
.SYNOPSIS
    Performs the actual stop / file-swap / restart for a staged Display Agent
    upgrade. Always run from a SEPARATE one-shot Scheduled Task
    (TogenDisplayAgentSwap), never from within the agent's own process tree.

.DESCRIPTION
    This script is installed alongside display_agent.exe (by install.ps1) so
    it is present for BOTH upgrade paths that exist for this agent:

      1. This bundle's own upgrade.ps1 (multi-file: exe + .py source),
         which stages files as *.new and writes a manifest at
         %ProgramData%\Togen\Display\swap_manifest.json before registering
         the swap task pointing at this installed copy.
      2. The server-rendered GET /enroll/display-upgrade.ps1 (single-file:
         exe only), which stages display_agent.exe.new and registers the
         swap task pointing at this SAME installed copy if present --
         falling back to its own minimal inline swap only when this file is
         missing. Coordinate any change to the "no manifest" fallback shape
         below with the Togen Developer (P1, DVI-1227) if it needs to differ.

    Either way, the separation into its own Scheduled Task (own Job Object)
    is the DVI-1007 fix: a task-hosted process's Job Object does not set
    JOB_OBJECT_LIMIT_BREAKAWAY_OK, so calling Stop-ScheduledTask/Stop-Process
    on the agent's own task from within a process in the SAME job can fail to
    break away and kill the caller instead of the target. Staging new files
    under a *.new name (not the running exe's name) is the DVI-920 fix: it
    sidesteps the AV/Defender file-lock race that broke the Vision agent's
    original stop-then-copy approach.

    Self-unregisters (TogenDisplayAgentSwap only ever runs once) when done.
#>

$ErrorActionPreference = 'Continue'
$TaskName     = 'TogenDisplayAgent'
$SwapTaskName = 'TogenDisplayAgentSwap'
$InstallDir   = Join-Path $env:ProgramFiles 'Togen\Display'
$LogDir       = Join-Path $env:ProgramData  'Togen\Display\Logs'
$LogFile      = Join-Path $LogDir 'agent.log'
$ManifestPath = Join-Path $env:ProgramData  'Togen\Display\swap_manifest.json'

New-Item -ItemType Directory -Path $LogDir -Force -ErrorAction SilentlyContinue | Out-Null
function Write-SwapLog {
    param([string]$Msg)
    try { "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [DisplayAgentSwap] $Msg" | Add-Content -Path $LogFile -ErrorAction SilentlyContinue } catch { }
}

Write-SwapLog 'swap start'

# Build the file list: prefer the manifest (multi-file, written by this
# bundle's upgrade.ps1); fall back to the single-exe convention the
# server-rendered /enroll/display-upgrade.ps1 route stages when no manifest
# is present (Name/NewPath/FinalPath, matching the manifest's own shape).
$Files = $null
if (Test-Path $ManifestPath) {
    try { $Files = Get-Content $ManifestPath -Raw | ConvertFrom-Json } catch {
        Write-SwapLog "manifest read failed, falling back to single-exe swap: $_"
    }
}
if (-not $Files) {
    $singleNew = Join-Path $InstallDir 'display_agent.exe.new'
    if (Test-Path $singleNew) {
        $Files = @([PSCustomObject]@{
            Name = 'display_agent.exe'
            NewPath = $singleNew
            FinalPath = (Join-Path $InstallDir 'display_agent.exe')
        })
    }
}
if (-not $Files) {
    Write-SwapLog 'nothing staged to swap (no manifest, no display_agent.exe.new) -- aborting'
    Unregister-ScheduledTask -TaskName $SwapTaskName -Confirm:$false -ErrorAction SilentlyContinue
    exit 1
}

# 1. Stop the kiosk task and process. Safe here -- this script runs from a
#    Scheduled Task with its own Job Object, distinct from TogenDisplayAgent's.
try { Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue } catch { }
Get-Process -Name 'display_agent' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Write-SwapLog 'agent stopped'

# 2. Wait for file locks to release (AV/Defender may briefly hold the handle
#    after process exit -- the DVI-920 race).
$deadline = (Get-Date).AddSeconds(10)
while ((Get-Date) -lt $deadline) {
    $locked = $false
    foreach ($f in $Files) {
        if (Test-Path $f.FinalPath) {
            try {
                $stream = [System.IO.File]::Open($f.FinalPath, 'Open', 'ReadWrite', 'None')
                $stream.Close()
            } catch { $locked = $true }
        }
    }
    if (-not $locked) { break }
    Start-Sleep -Milliseconds 500
}

# 3. Rename staged .new -> live filenames.
$swapOk = $true
foreach ($f in $Files) {
    try {
        if (Test-Path $f.FinalPath) { Remove-Item $f.FinalPath -Force }
        Move-Item -Path $f.NewPath -Destination $f.FinalPath -Force
        Write-SwapLog "swapped $($f.Name)"
    } catch {
        Write-SwapLog "FAILED to swap $($f.Name): $_"
        $swapOk = $false
    }
}
if ($swapOk) { Write-SwapLog 'swap_complete' } else { Write-SwapLog 'swap_incomplete -- see errors above' }

# 4. Restart the kiosk now if its user is logged on (else it starts at next
#    logon via the existing AtLogOn trigger, untouched by this script).
try {
    Start-ScheduledTask -TaskName $TaskName -ErrorAction Stop
    Write-SwapLog 'agent_restarted ok'
} catch {
    Write-SwapLog "agent_restarted deferred to next logon: $_"
}

Remove-Item $ManifestPath -Force -ErrorAction SilentlyContinue

# 5. Self-unregister -- this task only ever runs once.
Start-Sleep -Seconds 2
Unregister-ScheduledTask -TaskName $SwapTaskName -Confirm:$false -ErrorAction SilentlyContinue
Write-SwapLog 'swap task self-unregistered'
