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

.DESCRIPTION
    - Copies display_agent.exe (+ .py source) to $InstallDir
      (default: %ProgramFiles%\Togen\Display)
    - Bootstraps the WebView2 Evergreen runtime if it is not already present
      (the agent's kiosk window is a WebView2 control -- board decision D1-A)
    - Enrolls the machine with the Togen server using the supplied enrollment
      token (mirrors the Vision agent's --enroll-only pattern)
    - Registers a Scheduled Task that starts the agent AT LOGON, in the
      INTERACTIVE user session -- NOT as SYSTEM. The agent shows a window, so
      it must run in a session with a desktop; SYSTEM (Session 0) cannot host
      one, which is why this differs from the Vision agent's SYSTEM task.
    - Optionally configures Windows auto-logon so a dedicated signage PC
      starts the kiosk without anyone signing in after a reboot.

.PARAMETER Server
    Base URL of the Togen server, e.g. https://togen.example.com

.PARAMETER Enroll
    Enrollment token from Togen Admin -> Agents -> Display -> Generate
    enrollment token. Omit when re-running the installer to update an
    already-enrolled machine's files/task without re-registering.

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

.PARAMETER TaskName
    Scheduled Task name. Defaults to TogenDisplayAgent.

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

.PARAMETER KioskUser
    The user the logon-triggered task runs as. Defaults to the currently
    logged-on interactive user (or the account running this installer under
    RunAs if none is detected). Must be a real local or domain/Azure AD
    account that will log on to this machine.

.PARAMETER AutoLogon
    If set, configures Windows auto-logon for -KioskUser via
    HKLM\...\Winlogon so a dedicated signage PC boots straight into the kiosk
    with nobody present to sign in. Requires -KioskPassword.
    SECURITY NOTE: auto-logon stores the password in the registry in a
    weakly-obfuscated (not encrypted) form -- standard practice for
    single-purpose kiosk/signage hardware with no other local users, but do
    not use this on a shared or general-purpose PC.

.PARAMETER KioskPassword
    Plaintext password for -KioskUser, required only with -AutoLogon.

.EXAMPLE
    # Enroll + install, current interactive user's session
    .\install.ps1 -Server https://togen.example.com -Enroll eyJh...

.EXAMPLE
    # Dedicated signage PC: auto-logon + kiosk on every boot
    .\install.ps1 -Server https://togen.example.com -Enroll eyJh... `
        -KioskUser signage -AutoLogon -KioskPassword 'Str0ngPass!'

.NOTES
    CODE SIGNING: display_agent.exe is unsigned in v1 (see build_agent.ps1
    -Sign). If SmartScreen blocks the exe:
        Unblock-File .\display_agent.exe; Unblock-File .\install.ps1
    then re-run this installer.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory = $true)] [string]$Server,
    [string]$Enroll = '',
    [string]$InstallDir = (Join-Path $env:ProgramFiles 'Togen\Display'),
    [string]$TaskName   = 'TogenDisplayAgent',
    [string]$LogDir     = (Join-Path $env:ProgramData 'Togen\Display\Logs'),
    [string]$KioskUser  = '',
    [switch]$AutoLogon,
    [string]$KioskPassword = ''
)

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 }

if ($AutoLogon -and -not $KioskPassword) {
    throw '-AutoLogon requires -KioskPassword.'
}

# ---------------------------------------------------------------------------
# 1. Locate source files (same directory as this script)
# ---------------------------------------------------------------------------
Write-Step 'Locating display_agent.exe'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$SourceExe = Join-Path $ScriptDir 'display_agent.exe'
if (-not (Test-Path $SourceExe)) {
    throw "display_agent.exe not found in $ScriptDir - extract the full ZIP next to install.ps1."
}
Write-OK "Found: $SourceExe"
$SourcePyFiles = @('display_agent.py', 'winlayer.py', 'server_client.py') |
    ForEach-Object { Join-Path $ScriptDir $_ } | Where-Object { Test-Path $_ }
if ($SourcePyFiles) { Write-OK "Found source: $($SourcePyFiles -join ', ')" }

# ---------------------------------------------------------------------------
# 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 + respawn processes so files are not locked
#     during copy (reinstall/upgrade case).
# ---------------------------------------------------------------------------
Write-Step 'Stopping existing agent processes (if running)'
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($existingTask) {
    Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
    Write-OK "Stopped task: $TaskName"
}
foreach ($procName in @('display_agent', 'pythonw')) {
    $procs = Get-Process -Name $procName -ErrorAction SilentlyContinue |
        Where-Object { $_.Path -and $_.Path.StartsWith($InstallDir, [StringComparison]::OrdinalIgnoreCase) }
    if ($procs) {
        $procs | Stop-Process -Force -ErrorAction SilentlyContinue
        Write-OK "Killed $($procs.Count) $procName process(es)"
    }
}
$waited = 0
while ((Get-Process -Name 'display_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. Install directories + copy files
# ---------------------------------------------------------------------------
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
    # The kiosk runs as the (non-admin) logon user but this installer runs
    # elevated, so admin-owned log files (e.g. agent.log written during
    # --enroll-only) would be unwritable from the kiosk session and the
    # agent would silently fall back to stderr-only logging. Grant Users
    # modify on the log dir + existing files (found live in DVI-1227 P3 e2e).
    icacls $LogDir /grant '*S-1-5-32-545:(OI)(CI)M' /T | Out-Null
}
$DestExe = Join-Path $InstallDir 'display_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"
foreach ($src in $SourcePyFiles) {
    $dest = Join-Path $InstallDir (Split-Path -Leaf $src)
    Copy-Item -Path $src -Destination $dest -Force
    Unblock-File -Path $dest -ErrorAction SilentlyContinue
}
# swap_agent.ps1 is installed alongside the agent (not just dropped in by an
# upgrade) so it is present from the very first install -- both this bundle's
# own upgrade.ps1 AND the server-rendered /enroll/display-upgrade.ps1 route
# look for it at $InstallDir\swap_agent.ps1 and prefer it over a minimal
# inline fallback (see swap_agent.ps1's header comment).
$SwapScriptSrc = Join-Path $ScriptDir 'swap_agent.ps1'
if (Test-Path $SwapScriptSrc) {
    Copy-Item -Path $SwapScriptSrc -Destination (Join-Path $InstallDir 'swap_agent.ps1') -Force
    Write-OK 'Installed swap_agent.ps1'
}

# ---------------------------------------------------------------------------
# 4. WebView2 Evergreen runtime bootstrap
#    Most managed/recent Win10/11 images already have it (it ships with Edge
#    and is serviced via Windows Update), but older unmanaged boxes may not.
#    Detection: the Evergreen runtime registers its version under this
#    per-machine (or per-user, checked as a fallback) key.
# ---------------------------------------------------------------------------
Write-Step 'Checking WebView2 Evergreen runtime'
function Get-WebView2Version {
    $keys = @(
        'HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}',
        'HKLM:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}',
        'HKCU:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}'
    )
    foreach ($k in $keys) {
        $item = Get-ItemProperty -Path $k -Name 'pv' -ErrorAction SilentlyContinue
        if ($item -and $item.pv -and $item.pv -ne '0.0.0.0') { return $item.pv }
    }
    return $null
}
$wv2 = Get-WebView2Version
if ($wv2) {
    Write-OK "WebView2 Evergreen runtime present: $wv2"
} else {
    Write-Warn 'WebView2 Evergreen runtime not detected -- downloading bootstrapper'
    $wv2Setup = Join-Path $env:TEMP 'MicrosoftEdgeWebview2Setup.exe'
    try {
        Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/p/?LinkId=2124703' `
            -OutFile $wv2Setup -UseBasicParsing -TimeoutSec 120
        if ($PSCmdlet.ShouldProcess($wv2Setup, 'Install WebView2 runtime')) {
            $p = Start-Process -FilePath $wv2Setup -ArgumentList '/silent', '/install' -Wait -PassThru
            if ($p.ExitCode -eq 0) {
                Write-OK 'WebView2 Evergreen runtime installed'
            } else {
                Write-Warn "WebView2 installer exited with code $($p.ExitCode) -- the kiosk window may fail to start."
            }
        }
    } catch {
        Write-Warn "Could not download/install the WebView2 runtime: $_"
        Write-Warn 'Install it manually from https://developer.microsoft.com/microsoft-edge/webview2/ and re-run.'
    }
}

# ---------------------------------------------------------------------------
# 5. Connectivity pre-check + enroll
# ---------------------------------------------------------------------------
if ($Enroll) {
    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 network/firewall/DNS, then re-run install.ps1.'
    } 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
        if ($proc.ExitCode -eq 0) {
            Write-OK 'Enrolled - the kiosk will start once the Scheduled Task runs.'
        } else {
            Write-Warn "Enrollment exited with code $($proc.ExitCode)."
            $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" }
            Write-Warn 'Enrollment tokens are single-use and expire after 1h -- mint a fresh'
            Write-Warn 'one in Admin -> Agents -> Display if this one was already used/expired.'
            Write-Warn 'The Scheduled Task will still be created; re-run with a valid token if needed.'
        }
    }
} else {
    Write-Warn 'No -Enroll token supplied -- skipping registration (assumes this machine is already enrolled).'
}

# ---------------------------------------------------------------------------
# 6. Resolve the kiosk user (must be an interactive session -- SYSTEM cannot
#    host a window, unlike the Vision agent's checkin task).
# ---------------------------------------------------------------------------
Write-Step 'Resolving kiosk session user'
if (-not $KioskUser) {
    try { $KioskUser = (Get-CimInstance Win32_ComputerSystem -ErrorAction Stop).UserName } catch { }
    if (-not $KioskUser) { $KioskUser = "$env:USERDOMAIN\$env:USERNAME" }
}
Write-OK "Kiosk task will run at logon as: $KioskUser"

# ---------------------------------------------------------------------------
# 6b. Optional Windows auto-logon (dedicated signage PC).
# ---------------------------------------------------------------------------
if ($AutoLogon) {
    Write-Step "Configuring Windows auto-logon for $KioskUser"
    $WinlogonKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
    $userParts = $KioskUser -split '\\', 2
    $autoLogonDomain = if ($userParts.Count -eq 2) { $userParts[0] } else { $env:COMPUTERNAME }
    $autoLogonUser   = if ($userParts.Count -eq 2) { $userParts[1] } else { $userParts[0] }
    if ($PSCmdlet.ShouldProcess($WinlogonKey, 'Configure AutoAdminLogon')) {
        Set-ItemProperty -Path $WinlogonKey -Name 'AutoAdminLogon'   -Value '1'
        Set-ItemProperty -Path $WinlogonKey -Name 'DefaultUserName'  -Value $autoLogonUser
        Set-ItemProperty -Path $WinlogonKey -Name 'DefaultDomainName' -Value $autoLogonDomain
        Set-ItemProperty -Path $WinlogonKey -Name 'DefaultPassword'  -Value $KioskPassword
    }
    Write-OK 'Auto-logon configured. See README.md security note re: plaintext password storage.'
}

# ---------------------------------------------------------------------------
# 7. Register the logon-triggered Scheduled Task (interactive session, not
#    SYSTEM -- the agent shows a window and needs a real desktop session).
# ---------------------------------------------------------------------------
Write-Step "Registering Scheduled Task: $TaskName"
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue

$LogPath = Join-Path $LogDir 'agent-stdout.log'
$ErrPath = Join-Path $LogDir 'agent-stderr.log'
# Hidden-window launcher so the console-less exe's rare stdout/stderr still
# lands in a file instead of a flashing console (mirrors the Vision agent's
# run_agent.ps1 wrapper convention).
$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 -AtLogOn -User $KioskUser
$Settings = New-ScheduledTaskSettingsSet `
    -ExecutionTimeLimit ([TimeSpan]::Zero) `
    -RestartCount 999 `
    -RestartInterval (New-TimeSpan -Minutes 1) `
    -StartWhenAvailable `
    -MultipleInstances IgnoreNew `
    -AllowStartIfOnBatteries `
    -DontStopIfGoingOnBatteries

# Interactive logon type + Limited run level: the agent only needs to show a
# window and reach the network, not elevated rights (unlike the SYSTEM
# Vision checkin task, which needs HKLM read access this agent doesn't).
$Principal = New-ScheduledTaskPrincipal -UserId $KioskUser -LogonType Interactive -RunLevel Limited

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 logon for $KioskUser, restarts on failure)"

# ---------------------------------------------------------------------------
# 7b. Start the kiosk now if that user is already logged on (mirrors the
#     Vision agent's DVI-798 "start now" fix -- AtLogOn only fires on a
#     *fresh* logon, not for a user already signed in when this script runs).
# ---------------------------------------------------------------------------
Write-Step 'Starting the kiosk now (if the kiosk user is already logged on)'
$ActiveUser = $null
try { $ActiveUser = (Get-CimInstance Win32_ComputerSystem -ErrorAction Stop).UserName } catch { }
$activeShortName = if ($ActiveUser -and $ActiveUser.Contains('\')) { $ActiveUser.Split('\')[-1] } else { $ActiveUser }
$kioskShortName  = if ($KioskUser -and $KioskUser.Contains('\'))  { $KioskUser.Split('\')[-1] }  else { $KioskUser }
if ($ActiveUser -and $activeShortName -eq $kioskShortName) {
    try {
        Start-ScheduledTask -TaskName $TaskName -ErrorAction Stop
        Write-OK "Kiosk started now in the session of $ActiveUser"
    } catch {
        Write-Warn "Could not start the task immediately: $_"
        Write-Warn 'It will start automatically at the next logon.'
    }
} else {
    Write-Warn "Kiosk user ($KioskUser) is not currently logged on -- it will start at next logon."
    if ($AutoLogon) { Write-Warn 'Reboot now to exercise the configured auto-logon.' }
}

Write-Host ''
Write-Host 'Installation complete.' -ForegroundColor Green
Write-Host "  Server:    $Server"
Write-Host "  Task:      $TaskName  (runs at logon for $KioskUser, restarts on failure)"
Write-Host "  Logs:      $LogDir  (agent.log has the real agent activity; agent-std*.log rarely has anything)"
Write-Host ''
Write-Host 'Manage this display in Togen Admin -> Agents -> Display: assign a' -ForegroundColor Yellow
Write-Host 'dashboard, set the window layer (top/bottom/windowed/wallpaper) and' -ForegroundColor Yellow
Write-Host 'monitor, name it, and configure Present/interface/splash options.' -ForegroundColor Yellow
