#Requires -RunAsAdministrator
<#
.SYNOPSIS
    Installs the Togen Vision Agent on a Windows host.

.DESCRIPTION
    - Copies vision_agent.exe to $InstallDir (default: %ProgramFiles%\Togen\Vision)
    - Enrolls the machine with the Togen server using the supplied credential
    - Creates a Scheduled Task that starts the agent at system startup (as SYSTEM) and auto-restarts

    When -Server is omitted (automated update_agent context), this script
    auto-detects the server URL from the existing TogenVisionAgent scheduled task,
    reads the stored machine token from HKLM, and downloads + runs upgrade.ps1
    using the machine's own Bearer token. This allows already-enrolled machines
    to self-upgrade without requiring manual credential input.

    The $Enroll parameter accepts either:
      - An admin-issued enrollment token  -> agent starts ACTIVE immediately
      - The open-enroll shared secret     -> agent starts PENDING; an admin must
                                            approve it in the Togen Enroll tab
                                            before footage capture begins.
      - (empty)                           -> enrollment skipped; use when the
                                            machine is already enrolled and you
                                            only want to update the files/tasks.

.PARAMETER Server
    Base URL of the Togen server, e.g. https://togen.example.com
    When omitted, auto-detected from the existing TogenVisionAgent scheduled task.

.PARAMETER Enroll
    Enrollment credential (admin token or open-enroll secret).
    Omit when upgrading an already-enrolled machine.

.PARAMETER InstallDir
    Installation directory.  Defaults to %ProgramFiles%\Togen\Vision.

.PARAMETER TaskName
    Scheduled Task name.  Defaults to TogenVisionAgent.

.PARAMETER LogDir
    Log file directory.  Defaults to %ProgramData%\Togen\Vision\Logs.

.EXAMPLE
    # Admin token - agent activates immediately
    .\install.ps1 -Server https://togen.example.com -Enroll eyJh...

.EXAMPLE
    # Open-enroll secret - agent registers as pending, admin approves in Enroll tab
    .\install.ps1 -Server https://togen.example.com -Enroll tgn-enroll-abc123

.EXAMPLE
    # Automated self-upgrade (no params, server + token auto-detected from registry/task)
    .\install.ps1

.NOTES
    CODE SIGNING: vision_agent.exe is unsigned in v1.
    If SmartScreen blocks the exe, run:
        Unblock-File .\vision_agent.exe; Unblock-File .\install.ps1
    then re-run this installer.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [string]$Server   = '',   # auto-detected when empty
    [string]$Enroll   = '',   # skipped when empty (already-enrolled upgrade path)
    [string]$InstallDir = (Join-Path $env:ProgramFiles 'Togen\Vision'),
    [string]$TaskName   = 'TogenVisionAgent',
    [string]$LogDir     = (Join-Path $env:ProgramData 'Togen\Vision\Logs')
)

# ---------------------------------------------------------------------------
# Auto-upgrade path: when -Server is not provided, detect it from the existing
# scheduled task and delegate to upgrade.ps1 (using the machine's agent token
# for auth). This path is triggered by the update_agent command on older agents
# that do not natively support running upgrade.ps1.
# ---------------------------------------------------------------------------
if (-not $Server) {
    $ErrorActionPreference = 'Stop'
    function Write-Warn { param([string]$Msg) Write-Host "   WARN: $Msg" -ForegroundColor Yellow }
    Write-Host '>> Auto-upgrade mode: detecting server URL from existing scheduled task' -ForegroundColor Cyan
    # Read server URL from the TogenVisionAgent task arguments
    $existingTask = Get-ScheduledTask -TaskName 'TogenVisionAgent' -ErrorAction SilentlyContinue
    if ($existingTask) {
        $taskArgs = $existingTask.Actions[0].Arguments
        if ($taskArgs -match '--server\s+"?([^\s"]+)"?') { $Server = $Matches[1] }
    }
    if (-not $Server) {
        Write-Host 'ERROR: Cannot detect server URL. Pass -Server <url> explicitly.' -ForegroundColor Red
        exit 1
    }
    Write-Host "   Server: $Server" -ForegroundColor Green
    # Read machine agent token from HKLM (written there by the agent on first enrollment)
    $regKey = Get-ItemProperty -Path 'HKLM:\Software\Togen\Vision' -ErrorAction SilentlyContinue
    $agentToken = if ($regKey -and $regKey.AgentToken) { $regKey.AgentToken } else { '' }
    # Download upgrade.ps1 using the machine Bearer token (server accepts it since v3.2.14)
    $upgradeUrl = $Server.TrimEnd('/') + '/enroll/upgrade.ps1'
    Write-Host ">> Downloading upgrade.ps1 from $upgradeUrl" -ForegroundColor Cyan
    $headers = @{}
    if ($agentToken) { $headers['Authorization'] = "Bearer $agentToken" }
    try {
        $upgradeContent = (Invoke-WebRequest -Uri $upgradeUrl -Headers $headers -UseBasicParsing -ErrorAction Stop).Content
    } catch {
        Write-Host "ERROR: Failed to download upgrade.ps1: $_" -ForegroundColor Red
        exit 1
    }
    # Write and launch upgrade.ps1 (uses full PowerShell path to avoid PATH issues in Session 0)
    $tmpDir = Join-Path $env:TEMP 'togen-auto-upgrade'
    New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null
    $upgradePath = Join-Path $tmpDir 'upgrade.ps1'
    [System.IO.File]::WriteAllText($upgradePath, $upgradeContent, [System.Text.Encoding]::UTF8)
    $psExe = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
    if (-not (Test-Path $psExe)) { $psExe = 'powershell.exe' }
    Write-Host '>> Launching upgrade.ps1 (this process will be replaced)' -ForegroundColor Cyan
    Start-Process -FilePath $psExe `
        -ArgumentList "-ExecutionPolicy Bypass -File `"$upgradePath`"" `
        -Wait
    exit $LASTEXITCODE
}

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

function Write-Step { param([string]$Msg) Write-Host ">> $Msg" -ForegroundColor Cyan }
function Write-OK   { param([string]$Msg) Write-Host "   OK: $Msg" -ForegroundColor Green }
function Write-Warn { param([string]$Msg) Write-Host "   WARN: $Msg" -ForegroundColor Yellow }

# ---------------------------------------------------------------------------
# 1. Locate source exe (same directory as this script)
# ---------------------------------------------------------------------------
Write-Step 'Locating vision_agent.exe'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$SourceExe = Join-Path $ScriptDir 'vision_agent.exe'
if (-not (Test-Path $SourceExe)) {
    throw "vision_agent.exe not found in $ScriptDir - extract the full ZIP next to install.ps1."
}
Write-OK "Found: $SourceExe"
# Python source (DVI-683): the capture worker prefers running under a trusted,
# signed python.exe so it works on hosts with Smart App Control / WDAC enabled,
# where a freshly built unsigned vision_agent.exe is blocked at image load.
$SourcePy = Join-Path $ScriptDir 'vision_agent.py'
if (Test-Path $SourcePy) { Write-OK "Found source: $SourcePy" }

# ---------------------------------------------------------------------------
# 2. Unblock files (suppress SmartScreen on the copied exe)
# ---------------------------------------------------------------------------
Write-Step 'Unblocking files'
Unblock-File -Path $SourceExe -ErrorAction SilentlyContinue
Unblock-File -Path $MyInvocation.MyCommand.Definition -ErrorAction SilentlyContinue
Write-OK 'Files unblocked'

# ---------------------------------------------------------------------------
# 2b. Stop any running agent processes so the exe file is not locked during copy.
#     On a reinstall/upgrade the SYSTEM check-in task keeps vision_agent.exe
#     open; Copy-Item fails with "file is being used by another process" unless
#     the process exits first.  Stop-ScheduledTask signals the task but the
#     underlying process may still hold the handle for a moment, so we also
#     kill vision_agent.exe directly and wait for it to exit.
#     Unregister (step 5) follows after the copy.
# ---------------------------------------------------------------------------
Write-Step 'Stopping existing agent processes (if running)'
$CaptureTaskName = "${TaskName}Capture"
# ${CaptureTaskName}Now is the transient interactive-session launcher (step 6c).
foreach ($t in @($TaskName, $CaptureTaskName, "${CaptureTaskName}Now")) {
    $task = Get-ScheduledTask -TaskName $t -ErrorAction SilentlyContinue
    if ($task) {
        Stop-ScheduledTask -TaskName $t -ErrorAction SilentlyContinue
        Write-OK "Stopped task: $t"
    }
}
# Also kill any lingering vision_agent.exe processes directly — Stop-ScheduledTask
# signals the task scheduler but the process may still hold the file handle.
$agentProcs = Get-Process -Name 'vision_agent' -ErrorAction SilentlyContinue
if ($agentProcs) {
    $agentProcs | Stop-Process -Force -ErrorAction SilentlyContinue
    Write-OK "Killed $($agentProcs.Count) vision_agent process(es)"
}
# Kill any stale capture worker (pythonw/python/wscript running --capture-only).
# These run in the interactive session, are not named vision_agent, and survive
# Stop-ScheduledTask if launched by a previous transient task — leaving them would
# produce duplicate capture workers after the restart below.
$capWorkers = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
    Where-Object { $_.CommandLine -and $_.CommandLine -match '--capture-only' }
foreach ($w in $capWorkers) {
    Stop-Process -Id $w.ProcessId -Force -ErrorAction SilentlyContinue
}
if ($capWorkers) { Write-OK "Killed $(@($capWorkers).Count) stale capture worker(s)" }
# Wait up to 10 s for all vision_agent.exe processes to exit and release handles.
$waited = 0
while ((Get-Process -Name 'vision_agent' -ErrorAction SilentlyContinue) -and $waited -lt 10) {
    Start-Sleep -Seconds 1
    $waited++
}
if ($waited -gt 0) { Write-OK "Waited ${waited}s for process exit" }

# ---------------------------------------------------------------------------
# 3. Create install + log directories, copy exe
# ---------------------------------------------------------------------------
Write-Step "Installing to $InstallDir"
if ($PSCmdlet.ShouldProcess($InstallDir, 'Create directory')) {
    New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
    New-Item -ItemType Directory -Path $LogDir     -Force | Out-Null
}
$DestExe = Join-Path $InstallDir 'vision_agent.exe'
if ($PSCmdlet.ShouldProcess($DestExe, 'Copy exe')) {
    Copy-Item -Path $SourceExe -Destination $DestExe -Force
    Unblock-File -Path $DestExe -ErrorAction SilentlyContinue
}
Write-OK "Installed: $DestExe"
$DestPy = Join-Path $InstallDir 'vision_agent.py'
if ((Test-Path $SourcePy) -and $PSCmdlet.ShouldProcess($DestPy, 'Copy source')) {
    Copy-Item -Path $SourcePy -Destination $DestPy -Force
    Unblock-File -Path $DestPy -ErrorAction SilentlyContinue
    Write-OK "Installed source: $DestPy"
}

# ---------------------------------------------------------------------------
# 3b. Generate machine-level SSH key (DVI-693)
#     Stored in C:\ProgramData\Togen\ so all users — local accounts, Azure AD
#     profiles, and the SYSTEM service — share one key with no per-user dependency.
# ---------------------------------------------------------------------------
Write-Step 'Generating machine-level SSH key for Vision footage transfer'
$TogenDataDir = Join-Path $env:ProgramData 'Togen'
$SshKeyPath   = Join-Path $TogenDataDir 'id_togen_vision'
if ($PSCmdlet.ShouldProcess($SshKeyPath, 'Generate SSH key')) {
    New-Item -ItemType Directory -Path $TogenDataDir -Force | Out-Null
    # Guard: ssh-keygen may be absent on machines without OpenSSH Client.
    # Without this guard, a CommandNotFoundException aborts the script under
    # $ErrorActionPreference = 'Stop', skipping enrollment + Scheduled Task creation.
    $SshKeygen = Get-Command ssh-keygen -ErrorAction SilentlyContinue
    if (-not $SshKeygen) {
        Write-Warn 'ssh-keygen not found - SSH key generation skipped.'
        Write-Warn 'Install OpenSSH Client (Settings -> Optional Features -> OpenSSH Client),'
        Write-Warn 'then re-run install.ps1 to generate the machine key.'
    } elseif (-not (Test-Path $SshKeyPath)) {
        & $SshKeygen -t ed25519 -f $SshKeyPath -N '""'
        Write-OK "Machine SSH key generated: $SshKeyPath"
        Write-Host ''
        Write-Host '  *** ACTION REQUIRED: authorize this machine on the footage server ***' -ForegroundColor Yellow
        Write-Host '  Add the following public key to aiv@165.188.108.55:~/.ssh/authorized_keys' -ForegroundColor Yellow
        Write-Host ''
        Get-Content "$SshKeyPath.pub"
        Write-Host ''
    } else {
        Write-OK "Machine SSH key already exists (skipped): $SshKeyPath"
    }
    # DVI-798: grant interactive-session users read on the private key. ssh-keygen under SYSTEM
    # sets a SYSTEM-only ACL; the capture worker runs as the logged-in user (not SYSTEM), so
    # without this grant paramiko fails with [Errno 13] Permission denied and every push path
    # (paramiko/scp/rsync) is skipped. S-1-5-11 = Authenticated Users (locale-independent).
    if (Test-Path $SshKeyPath) {
        # Pure icacls approach (no PS ACL API): /reset removes explicit DENY ACEs, /inheritance:r
        # disables inheritance, /grant:r sets final permissions. Avoids Set-Acl edge cases.
        & icacls $SshKeyPath /reset 2>$null | Out-Null
        & icacls $SshKeyPath /inheritance:r 2>$null | Out-Null
        & icacls $SshKeyPath /grant:r 'NT AUTHORITY\SYSTEM:(F)' 2>$null | Out-Null
        & icacls $SshKeyPath /grant:r '*S-1-5-11:(R)' 2>$null | Out-Null
        Write-OK 'SSH key ACL: SYSTEM full + Authenticated Users read (required for paramiko SFTP push)'
    }
}

# ---------------------------------------------------------------------------
# 4. Connectivity pre-check + Enroll
#    Quick TCP test before launching the agent so a firewall/DNS block
#    produces an immediate, readable error instead of a silent 10-30 s stall.
# ---------------------------------------------------------------------------
Write-Step "Enrolling with Togen server: $Server"
$ServerHost = ([System.Uri]$Server).Host
$ServerPort = if (([System.Uri]$Server).Port -gt 0) { ([System.Uri]$Server).Port } else { 443 }
Write-Host "   Checking connectivity to ${ServerHost}:${ServerPort}..."
$tcpTest = Test-NetConnection -ComputerName $ServerHost -Port $ServerPort -InformationLevel Quiet -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
if (-not $tcpTest) {
    Write-Warn "Cannot reach ${ServerHost}:${ServerPort} from this machine."
    Write-Warn "Enrollment skipped. Check:"
    Write-Warn "  1. Network/firewall allows outbound HTTPS (port 443) to $ServerHost"
    Write-Warn "  2. DNS resolves $ServerHost  (nslookup $ServerHost)"
    Write-Warn "  3. VPN/proxy is not blocking the connection"
    Write-Warn "Re-run install.ps1 once connectivity is confirmed."
} else {
    Write-OK "Connectivity OK"
    $LogFile     = Join-Path $LogDir 'install-enroll.log'
    $LogFileErr  = Join-Path $LogDir 'install-enroll-err.log'
    $proc = Start-Process `
        -FilePath $DestExe `
        -ArgumentList '--server', $Server, '--enroll', $Enroll, '--enroll-only' `
        -Wait -PassThru -NoNewWindow `
        -RedirectStandardOutput $LogFile `
        -RedirectStandardError  $LogFileErr

    # Exit codes: 0 = active, 2 = pending (awaiting admin approval), anything else = error.
    switch ($proc.ExitCode) {
        0 {
            Write-OK 'Enrolled and ACTIVE - capture will begin once the Scheduled Task starts.'
        }
        2 {
            Write-Warn 'Enrolled as PENDING.'
            Write-Warn 'An admin must approve this machine in the Togen Enroll tab.'
            Write-Warn 'The agent will activate automatically after approval; no reinstall needed.'
        }
        default {
            Write-Warn "Enrollment exited with code $($proc.ExitCode)."
            # Show the actual error from the server so the user knows why it failed
            # (e.g. "enrollment_token expired" → re-generate from the Togen Enroll tab).
            $enrollOut = if (Test-Path $LogFile)    { (Get-Content $LogFile    -Raw).Trim() } else { '' }
            $enrollErr = if (Test-Path $LogFileErr) { (Get-Content $LogFileErr -Raw).Trim() } else { '' }
            if ($enrollOut) { Write-Warn "Enrollment output: $enrollOut" }
            if ($enrollErr) { Write-Warn "Enrollment error:  $enrollErr" }
            if (-not $enrollOut -and -not $enrollErr) {
                Write-Warn "Stdout log : $LogFile"
                Write-Warn "Stderr log : $LogFileErr"
            }
            Write-Warn 'The Scheduled Task will still be created; re-run with a valid credential if needed.'
            Write-Warn 'If the error says "expired", re-generate the bootstrap command from the Togen Vision Enroll tab.'
        }
    }
}

# ---------------------------------------------------------------------------
# 4b. Upload SSH public key to Togen server (DVI-769)
#     The agent token is now stored in HKLM by the enrollment step above.
#     Upload the public key so admin Approve can auto-authorize it on the
#     rsync server without requiring a separate upgrade.ps1 run.
# ---------------------------------------------------------------------------
Write-Step 'Uploading SSH public key to Togen server'
$PubKeyFile4b = "$SshKeyPath.pub"
if (Test-Path $PubKeyFile4b) {
    $AgentToken4b = $null
    try {
        $AgentToken4b = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Togen\Vision' -Name 'AgentToken' -ErrorAction SilentlyContinue).AgentToken
    } catch { }
    if ($AgentToken4b) {
        $PubKeyContent4b = (Get-Content $PubKeyFile4b -Raw).Trim()
        $Body4b = @{ ssh_public_key = $PubKeyContent4b } | ConvertTo-Json -Compress
        try {
            $null = Invoke-RestMethod -Uri "$Server/vision/agents/ssh-key" `
                -Method POST `
                -Headers @{ Authorization = "Bearer $AgentToken4b"; 'Content-Type' = 'application/json' } `
                -Body $Body4b -UseBasicParsing
            Write-OK 'SSH public key uploaded to Togen server'
        } catch {
            Write-Warn "Could not upload SSH public key: $_"
            Write-Warn "Run upgrade.ps1 after install to retry the upload."
        }
    } else {
        Write-Warn 'No agent token in HKLM - SSH key not uploaded (enrollment may have failed).'
    }
} else {
    Write-Warn 'No SSH public key found (ssh-keygen may be unavailable).'
    Write-Warn 'Install OpenSSH Client and re-run install.ps1, or run upgrade.ps1, to upload the key.'
}

# ---------------------------------------------------------------------------
# 5. Remove any existing task with the same name
# ---------------------------------------------------------------------------
Write-Step "Registering Scheduled Task: $TaskName"
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue

# ---------------------------------------------------------------------------
# 6. Create Scheduled Task (at system startup, runs as SYSTEM, auto-restart)
# ---------------------------------------------------------------------------
$LogPath  = Join-Path $LogDir 'agent.log'
$ErrPath  = Join-Path $LogDir 'agent-err.log'

# Write a hidden-window launcher script for the check-in task.
# Using Start-Process -NoNewWindow avoids a visible console in the taskbar
# when the task fires. (SYSTEM / Session-0 tasks are normally invisible to
# interactive users at boot, but this keeps things clean when tasks are
# started manually from an admin context too.)
$PsExe     = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
if (-not (Test-Path $PsExe)) { $PsExe = 'powershell.exe' }
$RunScript = Join-Path $InstallDir 'run_agent.ps1'
"Start-Process -FilePath '$DestExe' -ArgumentList @('--server', '$Server') -RedirectStandardOutput '$LogPath' -RedirectStandardError '$ErrPath' -NoNewWindow -Wait" |
    Set-Content $RunScript -Encoding UTF8

$Action = New-ScheduledTaskAction -Execute $PsExe `
            -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$RunScript`"" `
            -WorkingDirectory $InstallDir
$Trigger  = New-ScheduledTaskTrigger -AtStartup
$Settings = New-ScheduledTaskSettingsSet `
    -ExecutionTimeLimit ([TimeSpan]::Zero) `
    -RestartCount 5 `
    -RestartInterval (New-TimeSpan -Minutes 1) `
    -StartWhenAvailable `
    -MultipleInstances IgnoreNew

# Run as SYSTEM so the agent starts at boot without an interactive session.
# SYSTEM can read the HKLM-stored enrollment token and reach the Togen server over HTTPS.
$Principal = New-ScheduledTaskPrincipal -UserID "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest

if ($PSCmdlet.ShouldProcess($TaskName, 'Register Scheduled Task')) {
    Register-ScheduledTask `
        -TaskName  $TaskName `
        -Trigger   $Trigger `
        -Action    $Action `
        -Settings  $Settings `
        -Principal $Principal `
        -Force | Out-Null
}
Write-OK "Scheduled Task '$TaskName' created (triggers at startup, runs as SYSTEM)"
Write-OK "Agent logs: $LogPath"

# ---------------------------------------------------------------------------
# 6b. Capture task - runs in the interactive user session (DVI-683)
#     The SYSTEM check-in task above runs in Session 0 and cannot see the user's
#     desktop, so screen capture cannot work there. This second task runs as the
#     logged-in user (AtLogOn) so capture, idle detection, and foreground-window
#     queries operate on the real desktop. It reads the effective capture policy
#     published by the SYSTEM task and writes frames under the user profile.
# ---------------------------------------------------------------------------
$CaptureTask = "${TaskName}Capture"
Write-Step "Registering capture Scheduled Task: $CaptureTask"
Unregister-ScheduledTask -TaskName $CaptureTask -Confirm:$false -ErrorAction SilentlyContinue

# Let the interactive user write capture logs into the shared (SYSTEM-created) log dir.
# BUILTIN\Users = S-1-5-32-545; (OI)(CI)M = inherit + Modify.
& icacls $LogDir /grant '*S-1-5-32-545:(OI)(CI)M' /T 2>$null | Out-Null

$CapLog  = Join-Path $LogDir 'capture.log'
$CapErr  = Join-Path $LogDir 'capture-err.log'

# Prefer the Microsoft-signed python.exe over the unsigned vision_agent.exe for
# capture (DVI-683). On hosts with Smart App Control / WDAC enabled the freshly
# built exe is blocked at image load (CodeIntegrity Event 3077), whereas signed
# python.exe is trusted and script enforcement is disabled, so running the .py
# source works without any code-signing or WDAC allow-listing. Falls back to the
# exe when Python or the source is unavailable.
$PyExe = $null
if (Test-Path $DestPy) {
    # Use Get-Command instead of where.exe: on machines without Python, where.exe
    # exits non-zero and writes to stderr, which PowerShell promotes to a
    # NativeCommandError under $ErrorActionPreference = 'Stop', aborting the
    # script before the capture task is created. Get-Command returns $null
    # cleanly when the command is not found.
    $cand = Get-Command python -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source
    if ($cand -and (Test-Path $cand)) {
        # Windows Store app stubs (AppData\Local\Microsoft\WindowsApps\python.exe)
        # are reparse points; Get-AuthenticodeSignature throws a native CLR
        # IOException that -ErrorAction SilentlyContinue cannot suppress.
        # Use try/catch so the script always continues past this check.
        try {
            $sig = Get-AuthenticodeSignature $cand -ErrorAction Stop
            if ($sig -and $sig.Status -eq 'Valid') { $PyExe = $cand }
        } catch {
            Write-Warn "Skipped python candidate (could not verify signature): $cand"
        }
    }
}
if ($PyExe) {
    Write-Step "Ensuring capture dependencies for $PyExe"
    try {
        & $PyExe -m pip install --quiet --disable-pip-version-check Pillow psutil requests keyring av 2>&1 |
            Out-File (Join-Path $LogDir 'capture-deps.log')
        Write-OK 'Capture dependencies installed (Pillow, psutil, requests, keyring, av).'
    } catch {
        Write-Warn "pip install for capture deps reported an issue: $_ (see capture-deps.log)"
    }
}

# Look for pythonw.exe alongside python.exe — the windowless Python launcher
# runs scripts without a console window, eliminating the blank cmd visible in
# the user's taskbar during the AtLogOn capture task.
$PywExe = $null
if ($PyExe) {
    $pywCand = $PyExe -replace '\\python\.exe$', '\pythonw.exe'
    if (Test-Path $pywCand) { $PywExe = $pywCand }
}

if ($PywExe) {
    # pythonw.exe: no console window by design — use it directly, no cmd wrapper.
    # Python logging writes to the rotating file handler in ProgramData; stdout
    # and stderr are silently discarded by pythonw, which is fine.
    $CapAction = New-ScheduledTaskAction -Execute $PywExe `
                    -Argument "`"$DestPy`" --capture-only --server `"$Server`"" `
                    -WorkingDirectory $InstallDir
    Write-OK "Capture will run via pythonw.exe (no console window): $PywExe"
} elseif ($PyExe) {
    # python.exe found but no pythonw.exe — use wscript.exe VBScript launcher.
    # wscript.exe creates the child process with SW_HIDE from the very first frame,
    # eliminating the brief console flash that PowerShell -WindowStyle Hidden still
    # shows on Windows 22H2 Task Scheduler.
    $VbsScript = Join-Path $InstallDir 'run_capture.vbs'
@"
Set oShell = CreateObject("WScript.Shell")
oShell.Run Chr(34) & "$PyExe" & Chr(34) & " " & Chr(34) & "$DestPy" & Chr(34) & " --capture-only --server " & Chr(34) & "$Server" & Chr(34), 0, True
"@ | Set-Content $VbsScript -Encoding ASCII
    $CapAction = New-ScheduledTaskAction -Execute 'wscript.exe' `
                    -Argument "`"$VbsScript`"" `
                    -WorkingDirectory $InstallDir
    Write-OK "Capture will run via wscript.exe VBScript (python.exe, no window): $PyExe"
} else {
    # No Python — use wscript.exe VBScript launcher for the exe.
    $VbsScript = Join-Path $InstallDir 'run_capture.vbs'
@"
Set oShell = CreateObject("WScript.Shell")
oShell.Run Chr(34) & "$DestExe" & Chr(34) & " --capture-only --server " & Chr(34) & "$Server" & Chr(34), 0, True
"@ | Set-Content $VbsScript -Encoding ASCII
    $CapAction = New-ScheduledTaskAction -Execute 'wscript.exe' `
                    -Argument "`"$VbsScript`"" `
                    -WorkingDirectory $InstallDir
    Write-OK "Capture will run via wscript.exe VBScript (exe, no window)"
}
$CapTrigger  = New-ScheduledTaskTrigger -AtLogOn
$CapSettings = New-ScheduledTaskSettingsSet `
    -ExecutionTimeLimit ([TimeSpan]::Zero) `
    -RestartCount 5 `
    -RestartInterval (New-TimeSpan -Minutes 1) `
    -StartWhenAvailable `
    -MultipleInstances IgnoreNew

# Run as whichever user is logged on, inside their interactive session (not SYSTEM).
$CapPrincipal = New-ScheduledTaskPrincipal -GroupId 'BUILTIN\Users' -RunLevel Limited

if ($PSCmdlet.ShouldProcess($CaptureTask, 'Register capture Scheduled Task')) {
    Register-ScheduledTask `
        -TaskName  $CaptureTask `
        -Trigger   $CapTrigger `
        -Action    $CapAction `
        -Settings  $CapSettings `
        -Principal $CapPrincipal `
        -Force | Out-Null
}
Write-OK "Capture Task '$CaptureTask' created (triggers at user logon, runs in the interactive session)"

# ---------------------------------------------------------------------------
# 6c. Start capture NOW in the current interactive session (DVI-798)
#     The AtLogOn trigger above only fires on a *fresh* logon — it does NOT fire
#     for a user who is already logged in when this script runs, which is the
#     normal case (an admin runs install/upgrade from their own desktop). The
#     result was that the capture worker never started: SYSTEM check-in kept
#     reporting the agent healthy, but no frames were produced and no
#     footage/<machine_id> directory was ever created on the rsync server.
#
#     Start-ScheduledTask on the BUILTIN\Users group task is unreliable — the
#     interactive token only resolves on a real logon event. Instead, detect the
#     active console user and run the worker via a transient task bound to that
#     user with LogonType Interactive, which resolves their existing token and
#     launches the process in their session (verified: runs as the logged-in
#     user in session 1, LastTaskResult 0). The AtLogOn group task above still
#     covers future logons / reboots.
# ---------------------------------------------------------------------------
Write-Step 'Starting capture worker in the current interactive session'
$ActiveUser = $null
try { $ActiveUser = (Get-CimInstance Win32_ComputerSystem -ErrorAction Stop).UserName } catch { }
if ($ActiveUser) {
    $NowTask = "${CaptureTask}Now"
    $NowPrincipal = New-ScheduledTaskPrincipal -UserId $ActiveUser -LogonType Interactive -RunLevel Limited
    Unregister-ScheduledTask -TaskName $NowTask -Confirm:$false -ErrorAction SilentlyContinue
    if ($PSCmdlet.ShouldProcess($NowTask, 'Register + start capture launcher')) {
        # No trigger: this task only runs when started here, so it never
        # double-launches a worker alongside the AtLogOn task on reboot.
        Register-ScheduledTask -TaskName $NowTask -Action $CapAction -Settings $CapSettings -Principal $NowPrincipal -Force | Out-Null
        Start-ScheduledTask -TaskName $NowTask -ErrorAction SilentlyContinue
        Write-OK "Capture worker launched now in the session of $ActiveUser"
    }
} else {
    Write-Warn 'No interactive user is logged on — capture cannot run (screen capture requires a desktop session).'
    Write-Warn 'Capture will start automatically at the next user logon (AtLogOn task).'
}
Write-OK "Capture logs: $CapLog"

# ---------------------------------------------------------------------------
# 7. Start the agent now (don't wait for next reboot)
# ---------------------------------------------------------------------------
Write-Step 'Starting agent now'
try {
    Start-ScheduledTask -TaskName $TaskName
    Write-OK 'Check-in agent started via Scheduled Task.'
} catch {
    Write-Warn "Could not start the check-in task immediately: $_"
    Write-Warn "The agent will start automatically at next system startup."
}
try {
    Start-ScheduledTask -TaskName $CaptureTask
    Write-OK 'Capture worker started in the interactive session.'
} catch {
    Write-Warn "Could not start the capture task immediately: $_"
    Write-Warn "It will start automatically at the next user logon."
}

Write-Host ''
Write-Host 'Installation complete.' -ForegroundColor Green
Write-Host "  Server:   $Server"
Write-Host "  Check-in: $TaskName  (runs at startup as SYSTEM, restarts on failure)"
Write-Host "  Capture:  $CaptureTask  (runs at user logon, in the interactive session)"
Write-Host "  Logs:     $LogDir"
Write-Host ''
Write-Host 'If this machine registered as PENDING, visit the Togen admin panel' -ForegroundColor Yellow
Write-Host '  -> Settings -> Vision -> Enroll tab -> Approve this machine.' -ForegroundColor Yellow
