# Kraken Player - appliance system configuration engine.
#
# This does the SYSTEM setup only (Defender exclusion, ffmpeg pre-place,
# Scheduled Task, fullscreen lockdown, optional legacy kiosk account) - it assumes the player files
# already sit at -InstallDir. It's the single source of truth called by BOTH:
#   - the packaged installer (KrakenPlayerSetup.exe, built by Inno Setup), and
#   - install.ps1 (the raw-zip path), which copies files then delegates here.
# Never run directly by a customer; the installer wraps it.
#
# -Uninstall reverses the system config (the installer/uninstaller removes files).
#
# OS-level automation (registry, local accounts, Defender, autologon, reboot)
# CANNOT run in CI - validate on real Windows Home + Pro hardware.
[CmdletBinding()]
param(
  [string]$InstallDir = (Join-Path $env:ProgramFiles "KrakenPlayer"),
  [string]$KioskUser = "KrakenKiosk",
  [string]$KioskPassword = "",
  [switch]$NoKioskAccount,
  # Enable fullscreen signage lockdown for the already signed-in Windows user.
  # This is the default customer-installer path: one Windows account, no account
  # creation/switching, player starts now and at every login.
  [switch]$EnableKioskShell,
  [switch]$SkipAutologon,
  [switch]$SkipDefenderExclusion,
  [switch]$NoReboot,
  # Opt-in: DPAPI-encrypt (LocalMachine scope) the kiosk password into a
  # SYSTEM/Admin-only file so the guardian's Tier-2 autologon re-assert can fully
  # rewrite the LSA secret after a secret-loss. Default OFF: Tier 1 (flag
  # re-write) covers the reported Windows-Update drift without storing anything.
  [switch]$StoreKioskPassword,
  [switch]$Uninstall
)

$ErrorActionPreference = "Stop"

$principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
  Write-Error "appliance-setup.ps1 must run elevated (the installer runs it for you)."
  exit 1
}

$dataDir    = Join-Path $env:ProgramData "KrakenPlayer"
$binDir     = Join-Path $dataDir "bin"
$configPath = Join-Path $dataDir "config.json"
$shellMarkerPath = Join-Path $dataDir "kiosk-shell.json"
$startMenu  = Join-Path $env:ProgramData "Microsoft\Windows\Start Menu\Programs\Kraken Player"
$startupLnk = Join-Path ([Environment]::GetFolderPath("Startup")) "Kraken Player.lnk"

function Set-RegValue($Path, $Name, $Type, $Value) {
  try {
    if (-not (Test-Path $Path)) { New-Item -Path $Path -Force | Out-Null }
    New-ItemProperty -Path $Path -Name $Name -PropertyType $Type -Value $Value -Force | Out-Null
  } catch { Write-Warning "Could not set $Path\$Name : $_" }
}

# Get-ConsoleUser returns the user OWNING THE INTERACTIVE DESKTOP. Under UAC the
# installer runs elevated as a possibly-DIFFERENT admin account, so $env:USERNAME
# is that admin, NOT the person sitting at the screen. Auto-start must bind to the
# desktop owner or the player silently starts for the wrong account - the
# "installed but nothing happens" a tester lost an hour to (their PC had a
# standard login + a separate admin they elevated with).
function Get-ConsoleUser {
  # Win32_ComputerSystem reports the account attached to the active console. Use
  # it before looking at explorer.exe: after "Switch user", several Explorer
  # processes can exist and the first one returned may belong to the old hidden
  # session (the exact 0.2.96 failure mode).
  try {
    $u = (Get-CimInstance Win32_ComputerSystem).UserName
    if ($u) { return $u }
  } catch {}
  try {
    $ex = Get-CimInstance Win32_Process -Filter "Name='explorer.exe'" -ErrorAction Stop | Select-Object -First 1
    if ($ex) {
      $o = Invoke-CimMethod -InputObject $ex -MethodName GetOwner -ErrorAction Stop
      if ($o.User) { return "$($o.Domain)\$($o.User)" }
    }
  } catch {}
  return "$env:COMPUTERNAME\$env:USERNAME"
}

# Write-InstallReport drops a plain-text summary on the Public Desktop (visible to
# every account) + in ProgramData, so a failed/confusing install is ONE screenshot
# instead of an hour of hunting across accounts. Never contains secrets.
function Write-InstallReport($lines) {
  $body = ($lines -join "`r`n")
  $desktop = [Environment]::GetFolderPath("CommonDesktopDirectory")
  foreach ($t in @((Join-Path $desktop "Kraken Player - setup report.txt"), (Join-Path $dataDir "install-report.txt"))) {
    try { Set-Content -Path $t -Value $body -Encoding UTF8 -ErrorAction SilentlyContinue } catch {}
  }
}

# ============================ UNINSTALL ======================================
if ($Uninstall) {
  Write-Host "==> Removing Scheduled Tasks + stopping the player..."
  Stop-ScheduledTask -TaskName "KrakenPlayer" -ErrorAction SilentlyContinue
  Unregister-ScheduledTask -TaskName "KrakenPlayer" -Confirm:$false -ErrorAction SilentlyContinue
  # The SYSTEM guardian task (autologon self-heal + pending-update apply).
  Stop-ScheduledTask -TaskName "KrakenGuardian" -ErrorAction SilentlyContinue
  Unregister-ScheduledTask -TaskName "KrakenGuardian" -Confirm:$false -ErrorAction SilentlyContinue
  Get-Process kraken-player -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
  # Also kill the kiosk Chrome the player launched (its own KrakenPlayer profile),
  # so uninstall actually clears the on-screen kiosk. Bug: "uninstalled but the
  # players were still running" - the kiosk browser outlived the player process.
  Get-CimInstance Win32_Process | Where-Object { ($_.Name -eq "chrome.exe" -or $_.Name -eq "msedge.exe") -and $_.CommandLine -like "*KrakenPlayer*chromium*" } |
    ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
  # And the Vision AI detector + any camera-holding ffmpeg: Stop-Process on the
  # player is a hard TerminateProcess, so the player's own cleanup never runs and
  # the detector would keep the webcam (light on) forever after an uninstall.
  Get-Process kraken-detector -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
  Get-CimInstance Win32_Process | Where-Object { $_.Name -eq "ffmpeg.exe" -and $_.CommandLine -like "*dshow*" } |
    ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }

  # Restore the kiosk user's Explorer access. Runtime kiosk control writes only
  # these HKCU values, but the uninstaller may be elevated under a different
  # account, so remove them from both the current hive and the dedicated kiosk
  # user's loaded HKEY_USERS hive. Pairing data and the account stay preserved.
  Write-Host "==> Restoring the Windows desktop + taskbar..."
  $policyValues = @(
    @{ Path = "Software\Microsoft\Windows\CurrentVersion\Policies\Explorer"; Names = @("NoWinKeys", "NoRun", "NoControlPanel", "NoTrayContextMenu", "NoViewContextMenu") },
    @{ Path = "Software\Microsoft\Windows\CurrentVersion\Policies\System"; Names = @("DisableTaskMgr", "DisableLockWorkstation") },
    @{ Path = "Software\Policies\Microsoft\Windows\Explorer"; Names = @("DisableNotificationCenter") }
  )
  $sids = @([Security.Principal.WindowsIdentity]::GetCurrent().User.Value)
  try {
    if (Test-Path $shellMarkerPath) {
      $shellInfo = Get-Content $shellMarkerPath -Raw | ConvertFrom-Json
      if ($shellInfo.userSid) { $sids += [string]$shellInfo.userSid }
    }
  } catch {}
  try {
    $kioskSid = (Get-LocalUser -Name $KioskUser -ErrorAction SilentlyContinue).SID.Value
    if ($kioskSid) { $sids += $kioskSid }
  } catch {}
  foreach ($sid in ($sids | Select-Object -Unique)) {
    foreach ($group in $policyValues) {
      $path = "Registry::HKEY_USERS\$sid\$($group.Path)"
      foreach ($name in $group.Names) {
        Remove-ItemProperty -Path $path -Name $name -ErrorAction SilentlyContinue
      }
    }
  }
  try {
    Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public static class KrakenTaskbarRestore {
  [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr FindWindow(string c, string n);
  [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int c);
}
"@
    foreach ($class in @("Shell_TrayWnd", "Shell_SecondaryTrayWnd")) {
      $h = [KrakenTaskbarRestore]::FindWindow($class, $null)
      if ($h -ne [IntPtr]::Zero) { [KrakenTaskbarRestore]::ShowWindow($h, 5) | Out-Null }
    }
  } catch { Write-Warning "Could not restore the taskbar immediately ($_); signing out and back in restores it." }

  Write-Host "==> Removing Defender exclusions..."
  Remove-MpPreference -ExclusionPath $InstallDir -ErrorAction SilentlyContinue
  Remove-MpPreference -ExclusionPath $dataDir -ErrorAction SilentlyContinue
  Remove-MpPreference -ExclusionProcess "kraken-player.exe" -ErrorAction SilentlyContinue
  Remove-MpPreference -ExclusionProcess "ffmpeg.exe" -ErrorAction SilentlyContinue

  Write-Host "==> Clearing auto-login..."
  $winlogon = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
  Set-ItemProperty -Path $winlogon -Name "AutoAdminLogon" -Value "0" -ErrorAction SilentlyContinue
  Remove-ItemProperty -Path $winlogon -Name "DefaultPassword" -ErrorAction SilentlyContinue
  Remove-ItemProperty -Path $winlogon -Name "DefaultUserName" -ErrorAction SilentlyContinue

  Write-Host "==> Removing shortcuts..."
  Remove-Item $startMenu -Recurse -Force -ErrorAction SilentlyContinue
  Remove-Item $startupLnk -Force -ErrorAction SilentlyContinue

  # Remove the guardian's working state (state file, log, any stored DPAPI
  # credential) + the kiosk marker + staged updates. None of this is pairing
  # data, so clearing it is safe; the stored credential especially must not
  # outlive the install.
  Remove-Item (Join-Path $dataDir "guardian-work") -Recurse -Force -ErrorAction SilentlyContinue
  Remove-Item (Join-Path $dataDir "guardian-kiosk.json") -Force -ErrorAction SilentlyContinue
  Remove-Item $shellMarkerPath -Force -ErrorAction SilentlyContinue
  Remove-Item (Join-Path $dataDir "staging") -Recurse -Force -ErrorAction SilentlyContinue

  # The kiosk account + %ProgramData% (device pairing) are intentionally LEFT:
  # removing the account is destructive, and keeping ProgramData means a
  # reinstall stays paired. The uninstaller offers a full purge separately.
  Write-Host "Appliance configuration removed (kiosk account + pairing preserved)."
  exit 0
}

# ============================ INSTALL / CONFIGURE ============================
$exe = Join-Path $InstallDir "kraken-player.exe"
if (-not (Test-Path $exe)) {
  Write-Error "kraken-player.exe not found at $InstallDir - the installer must place files before configuring."
  exit 1
}

# ---- 1. Defender exclusion FIRST, then VERIFY (Tamper Protection can no-op) --
$defenderOK = $true # true unless we actively tried and couldn't verify
if (-not $SkipDefenderExclusion) {
  $defenderOK = $false
  Write-Host "==> Windows Defender exclusion..."
  try {
    Add-MpPreference -ExclusionPath $InstallDir -ErrorAction Stop
    Add-MpPreference -ExclusionPath $dataDir -ErrorAction Stop
    Add-MpPreference -ExclusionProcess "kraken-player.exe" -ErrorAction Stop
    Add-MpPreference -ExclusionProcess "ffmpeg.exe" -ErrorAction Stop
    $paths = @((Get-MpPreference -ErrorAction Stop).ExclusionPath)
    if ($paths -contains $dataDir -and $paths -contains $InstallDir) {
      $defenderOK = $true
      Write-Host "    OK: exclusion set and verified."
    } else {
      Write-Warning "Defender exclusion did NOT take - Tamper Protection is likely ON. Turn it off (Windows Security -> Virus & threat protection -> Manage settings -> Tamper Protection) and reinstall, or camera capture may be blocked."
    }
  } catch {
    Write-Warning "Could not set Defender exclusion ($_). Camera capture could be blocked until excluded."
  }
}

# ---- 2. data dirs + pre-place ffmpeg + seed config --------------------------
New-Item -ItemType Directory -Force -Path $dataDir | Out-Null
New-Item -ItemType Directory -Force -Path (Join-Path $dataDir "cache") | Out-Null
New-Item -ItemType Directory -Force -Path $binDir | Out-Null

$bundledFfmpeg = Join-Path $InstallDir "bin\ffmpeg.exe"
if (Test-Path $bundledFfmpeg) {
  Copy-Item $bundledFfmpeg (Join-Path $binDir "ffmpeg.exe") -Force
  Write-Host "==> ffmpeg pre-placed (camera capture ready, no download)."
} else {
  Write-Host "==> No bundled ffmpeg; the player will download it on first run (behind the Defender exclusion)."
}

$bundledConfigPath = Join-Path $InstallDir "config.json"
if (Test-Path $configPath) {
  Write-Host "==> Existing device configuration found."

  # The machine identity may survive reinstall, but the environment endpoint
  # must come from THIS installer. 0.2.97 preserved the entire old config, so a
  # PC that had ever run the DEV player showed a valid-looking code created in
  # DEV; APP1 then correctly replied "Pairing code not found" because it had
  # never received that request. Migrate only apiBaseUrl and clear credentials
  # that belong to the old environment, retaining deviceId/name and local prefs.
  if (Test-Path $bundledConfigPath) {
    try {
      $cfg = Get-Content $configPath -Raw | ConvertFrom-Json
      $bundledCfg = Get-Content $bundledConfigPath -Raw | ConvertFrom-Json
      $currentApi = [string]$cfg.apiBaseUrl
      $installerApi = [string]$bundledCfg.apiBaseUrl

      if ($installerApi -and $currentApi.TrimEnd('/') -ne $installerApi.TrimEnd('/')) {
        if ($null -eq $cfg.PSObject.Properties['apiBaseUrl']) {
          $cfg | Add-Member -NotePropertyName apiBaseUrl -NotePropertyValue $installerApi
        } else {
          $cfg.apiBaseUrl = $installerApi
        }

        foreach ($name in @('token', 'tokenExpiresAt', 'pairingCode', 'pairingExpiresAt', 'pairingSecret')) {
          $property = $cfg.PSObject.Properties[$name]
          if ($null -eq $property) {
            $cfg | Add-Member -NotePropertyName $name -NotePropertyValue ""
          } else {
            $property.Value = ""
          }
        }

        $json = $cfg | ConvertTo-Json -Depth 10
        [IO.File]::WriteAllText($configPath, $json, (New-Object System.Text.UTF8Encoding($false)))
        Write-Host "    Environment corrected: '$currentApi' -> '$installerApi'; stale pairing credentials cleared."
      } else {
        Write-Host "    Environment already correct; device pairing state retained."
      }
    } catch {
      Write-Warning "Could not migrate the existing environment setting ($_). Replacing it with this installer's clean APP1 config."
      Copy-Item $bundledConfigPath $configPath -Force
    }
  }
} elseif (Test-Path $bundledConfigPath) {
  Copy-Item $bundledConfigPath $configPath
  Write-Host "==> Config seeded to $configPath"
}

# A KIOSK appliance is remotely managed, so it must accept the OS-level commands the
# dashboard sends. Without this, Screen Settings > Volume / Timezone are queued, the
# player refuses them with "system commands disabled", and the setting silently never
# applies (the dashboard used to report success anyway). A Standard install - someone's
# laptop - keeps the guard on deliberately.
# Patched IN PLACE so a reinstall over an already-paired config flips this one flag
# without re-pairing. UTF8 with no BOM: Go's json.Unmarshal rejects a BOM.
if ((-not $NoKioskAccount -or $EnableKioskShell) -and (Test-Path $configPath)) {
  try {
    $cfg = Get-Content $configPath -Raw | ConvertFrom-Json
    if ($null -eq $cfg.PSObject.Properties['allowSystemCommands']) {
      $cfg | Add-Member -NotePropertyName allowSystemCommands -NotePropertyValue $true
    } else {
      $cfg.allowSystemCommands = $true
    }
    $json = $cfg | ConvertTo-Json -Depth 10
    [IO.File]::WriteAllText($configPath, $json, (New-Object System.Text.UTF8Encoding($false)))
    Write-Host "==> Kiosk install: allowSystemCommands enabled (volume + timezone from the dashboard)."
  } catch {
    Write-Warning "Could not enable allowSystemCommands in ${configPath}: $_"
  }
}

# ---- 3. detector deps best-effort (a box without Python just idles vision) ---
$reqPath = Join-Path $InstallDir "vision\requirements.txt"
if ((Test-Path $reqPath) -and (Get-Command python -ErrorAction SilentlyContinue)) {
  try {
    Write-Host "==> Installing Vision AI detector dependencies (first run only)..."
    python -m pip install --quiet --upgrade pip | Out-Null
    python -m pip install --quiet -r $reqPath | Out-Null
  } catch { Write-Host "    Detector deps failed - live counting stays off until the standalone detector is installed (capture/preview unaffected)." }
}

# ---- 4. kiosk lockdown -------------------------------------------------------
$caption = (Get-CimInstance Win32_OperatingSystem).Caption
if ($caption -match "Home") {
  Write-Warning "Windows Home ('$caption'): policy lockdown keys are ignored; power + autologon still apply."
}
Write-Host "==> Power + screensaver + lock-screen lockdown..."
powercfg /change monitor-timeout-ac 0  2>$null
powercfg /change monitor-timeout-dc 0  2>$null
powercfg /change standby-timeout-ac 0  2>$null
powercfg /change standby-timeout-dc 0  2>$null
powercfg /change disk-timeout-ac 0     2>$null
powercfg /change hibernate-timeout-ac 0 2>$null
powercfg /h off 2>$null
$desk = "HKCU:\Control Panel\Desktop"
Set-RegValue $desk "ScreenSaveActive" "String" "0"
Set-RegValue $desk "ScreenSaverIsSecure" "String" "0"
Set-RegValue $desk "ScreenSaveTimeOut" "String" "0"
Set-RegValue "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization" "NoLockScreen" "DWord" 1
Set-RegValue "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" "NoAutoRebootWithLoggedOnUsers" "DWord" 1

# ---- 5. dedicated kiosk account (unless -NoKioskAccount) ----------------------
# Default: bind auto-start to whoever owns the desktop, NOT $env:USERNAME (under
# UAC that's the elevating admin, which is how the player got bound to an account
# the tester never logged into). Kiosk mode overrides this to KrakenKiosk below.
$consoleUser = Get-ConsoleUser
$taskUser = $consoleUser
$didKioskAccount = $false
if (-not $NoKioskAccount) {
  Write-Host "==> Dedicated kiosk account '$KioskUser'..."
  if (-not $KioskPassword) {
    $chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789".ToCharArray()
    $syms  = "!@#%^*-_=+".ToCharArray()
    $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
    $buf = New-Object 'System.Byte[]' 32
    $rng.GetBytes($buf)
    $pw = -join (0..21 | ForEach-Object { $chars[$buf[$_] % $chars.Length] })
    $pw += $syms[$buf[22] % $syms.Length]
    $pw += [char](48 + ($buf[23] % 10))
    $KioskPassword = $pw
  }
  $sec = ConvertTo-SecureString $KioskPassword -AsPlainText -Force
  try {
    if (Get-LocalUser -Name $KioskUser -ErrorAction SilentlyContinue) {
      Set-LocalUser -Name $KioskUser -Password $sec
    } else {
      New-LocalUser -Name $KioskUser -Password $sec -FullName "Kraken Kiosk" `
        -Description "Kraken Player kiosk account" -PasswordNeverExpires -AccountNeverExpires | Out-Null
      Add-LocalGroupMember -Group "Users" -Member $KioskUser -ErrorAction SilentlyContinue
    }
    $taskUser = "$env:COMPUTERNAME\$KioskUser"
    $didKioskAccount = $true
    Write-Host "    OK: kiosk account ready."
  } catch {
    Write-Warning "Could not configure the kiosk account ($_). Using the current user for autostart."
  }
}

# ---- 5a. current-user kiosk marker -----------------------------------------
# Runtime taskbar suppression used to key off guardian-kiosk.json, but that file
# also instructs the SYSTEM guardian to force auto-login into the dedicated
# Kraken account. Keep those responsibilities separate. kiosk-shell.json enables
# reversible fullscreen lockdown for the current interactive user without
# creating, promoting, or switching Windows accounts.
if ($EnableKioskShell) {
  try {
    $consoleAccount = New-Object -TypeName Security.Principal.NTAccount -ArgumentList $consoleUser
    $consoleSid = $consoleAccount.Translate([Security.Principal.SecurityIdentifier]).Value
    $shellMarker = @{ user = $consoleUser; userSid = $consoleSid } | ConvertTo-Json -Compress
    Set-Content -Path $shellMarkerPath -Value $shellMarker -Encoding UTF8
    Write-Host "==> Current-user signage mode enabled for '$consoleUser'."
  } catch {
    Write-Warning "Could not write the current-user kiosk marker ($_). The player will still start, but taskbar suppression may be unavailable."
  }
} else {
  Remove-Item $shellMarkerPath -Force -ErrorAction SilentlyContinue
}

# Repair an upgrade from 0.2.96: that build may have left Windows configured to
# auto-login as the separate Kraken account. The single-account installer must
# positively turn that off before it starts under the signed-in user.
if ($NoKioskAccount) {
  Write-Host "==> Removing any legacy Kraken-account auto-login..."
  $legacyWinlogon = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
  Set-ItemProperty -Path $legacyWinlogon -Name "AutoAdminLogon" -Value "0" -ErrorAction SilentlyContinue
  Remove-ItemProperty -Path $legacyWinlogon -Name "DefaultPassword" -ErrorAction SilentlyContinue
  Remove-ItemProperty -Path $legacyWinlogon -Name "DefaultUserName" -ErrorAction SilentlyContinue
  Remove-ItemProperty -Path $legacyWinlogon -Name "DefaultDomainName" -ErrorAction SilentlyContinue
  Remove-Item (Join-Path $dataDir "guardian-kiosk.json") -Force -ErrorAction SilentlyContinue
  Remove-Item (Join-Path (Join-Path $dataDir "guardian-work") "kiosk-cred.dpapi") -Force -ErrorAction SilentlyContinue

  # 0.2.96 created this account itself; remove only that exact installer-owned
  # account, never an arbitrary similarly named customer account and never the
  # account currently running the desktop. Pairing data lives in ProgramData and
  # is retained.
  try {
    $legacyUser = Get-LocalUser -Name $KioskUser -ErrorAction SilentlyContinue
    $consoleLeaf = ($consoleUser -split '\\')[-1]
    if ($legacyUser -and $legacyUser.Description -eq "Kraken Player kiosk account" -and $consoleLeaf -ne $KioskUser) {
      Remove-LocalUser -Name $KioskUser -ErrorAction Stop
      Write-Host "    Removed the legacy installer-owned '$KioskUser' Windows account."
    }
  } catch {
    Write-Warning "Could not remove the legacy installer-owned '$KioskUser' account ($_). It is no longer used for login or player startup."
  }
}

# ---- 6. auto-restart logon Scheduled Task -----------------------------------
$action  = New-ScheduledTaskAction -Execute $exe -Argument "--config ""$configPath""" -WorkingDirectory $dataDir
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $taskUser
$settings = New-ScheduledTaskSettingsSet `
  -RestartCount 9999 -RestartInterval (New-TimeSpan -Minutes 1) `
  -ExecutionTimeLimit (New-TimeSpan -Days 3650) `
  -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
$taskRunLevel = $(if ($didKioskAccount) { "Highest" } else { "Limited" })
$taskPrincipal = New-ScheduledTaskPrincipal -UserId $taskUser -LogonType Interactive -RunLevel $taskRunLevel
Unregister-ScheduledTask -TaskName "KrakenPlayer" -Confirm:$false -ErrorAction SilentlyContinue
Register-ScheduledTask -TaskName "KrakenPlayer" -Action $action -Trigger $trigger -Settings $settings -Principal $taskPrincipal | Out-Null
Write-Host "==> Auto-restart logon task registered (runs as $taskUser)."

# ---- 6b. SYSTEM guardian task + working dirs ---------------------------------
# The KrakenPlayer task above runs as the (non-admin) kiosk USER and its AtLogOn
# trigger is bound to that ONE user SID. That leaves three overnight failure
# modes a user-context task cannot fix: Windows Update reboots the box into the
# wrong session (the SID trigger never fires), kiosk autologon drifts and nothing
# re-asserts it, and self-update can't write the admin-only install dir. The
# GUARDIAN closes all three: a SECOND task that runs as NT AUTHORITY\SYSTEM,
# AtStartup + every 5 minutes, whether or not anyone is logged in.
#
# PRIVILEGE MODEL: SYSTEM can write HKLM\Winlogon (autologon) and Program Files
# (the swap). It NEVER runs staged bytes directly - it verifies them against the
# Ed25519 public key COMPILED INTO kraken-player.exe first (the private key lives
# offline / in CI), so a compromised kiosk user can stage garbage but cannot make
# the guardian execute it as SYSTEM. The guardian's Execute target is this
# trusted admin-installed exe, reusing its pinned key + verification code.
$stagingDir = Join-Path $dataDir "staging"
$workDir    = Join-Path $dataDir "guardian-work"
$cacheDir   = Join-Path $dataDir "cache"
New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null
New-Item -ItemType Directory -Force -Path $workDir | Out-Null
# Lock the work dir to SYSTEM + Administrators ONLY (well-known SIDs, locale
# independent), and set its OWNER to Administrators (the owner implicitly holds
# WRITE_DAC): the guardian writes the verified update binary here before swapping
# it into Program Files, so the kiosk user must never be able to touch it. The
# guardian re-asserts owner + ACL and allow-list-verifies them every run too.
try {
  & icacls $workDir /setowner "*S-1-5-32-544" | Out-Null
  & icacls $workDir /inheritance:r /grant:r "*S-1-5-18:(OI)(CI)F" "*S-1-5-32-544:(OI)(CI)F" | Out-Null
} catch { Write-Warning "Could not lock down $workDir ($_); the guardian will re-assert it at runtime." }

# Lock the ProgramData\KrakenPlayer ROOT (S3 defense in depth). Drop the broad
# inherited ProgramData ACEs and grant SYSTEM + Administrators full; the kiosk
# Users group keeps READ/traverse on the root but is granted WRITE only on the
# specific subpaths the non-admin player must persist to (config.json, cache, bin,
# staging) - NOT the root itself. That keeps a kiosk-planted DLL out of any dir a
# SYSTEM process would search (the guardian additionally runs with
# WorkingDirectory=System32, so ProgramData is never on its search path).
# guardian-work stays SYSTEM/Admins-only (locked above).
# S-1-5-32-545 = Users, S-1-5-11 = Authenticated Users (covers a domain console
# user in the non-kiosk fallback who may not be in the local Users group). Both
# get READ/traverse on the root and WRITE on the player's own subpaths only.
try {
  # Reset the ROOT owner to Administrators too (D2). If the root was ever created
  # by the non-admin kiosk user (an earlier standard-user MkdirAll), that user
  # stays OWNER and implicitly holds WRITE_DAC - it could re-grant itself write on
  # the root regardless of the DACL below. /setowner closes that, mirroring the
  # guardian-work lockdown.
  & icacls $dataDir /setowner "*S-1-5-32-544" | Out-Null
  & icacls $dataDir /inheritance:r `
    /grant:r "*S-1-5-18:(OI)(CI)F" `
    /grant:r "*S-1-5-32-544:(OI)(CI)F" `
    /grant:r "*S-1-5-32-545:(RX)" `
    /grant:r "*S-1-5-11:(RX)" | Out-Null
  foreach ($d in @($cacheDir, $binDir, $stagingDir)) {
    if (Test-Path $d) { & icacls $d /grant "*S-1-5-32-545:(OI)(CI)M" "*S-1-5-11:(OI)(CI)M" | Out-Null }
  }
  if (Test-Path $configPath) { & icacls $configPath /grant "*S-1-5-32-545:(M)" "*S-1-5-11:(M)" | Out-Null }
} catch { Write-Warning "Could not lock down $dataDir ($_); the player keeps its inherited access." }

Write-Host "==> Registering SYSTEM guardian task (autologon self-heal + locked-down self-update)..."
try {
  # WorkingDirectory MUST be a non-writable dir: this task runs as SYSTEM, and a
  # writable CWD sits on the DLL search path, so a DLL planted by the kiosk user
  # in a kiosk-writable CWD would load AS SYSTEM (a full escalation, independent
  # of the update path). System32 is admin-only. The guardian additionally calls
  # every OS tool by absolute path.
  $sys32 = Join-Path $env:SystemRoot "System32"
  $gAction  = New-ScheduledTaskAction -Execute $exe -Argument "--guardian" -WorkingDirectory $sys32
  # AtStartup catches a box rebooting into the wrong state; the 5-min repetition
  # re-asserts a box that drifts while running. Use a finite (10-year) duration,
  # NOT [TimeSpan]::MaxValue (E1): MaxValue is silently REJECTED on some Windows
  # builds (notably Home), which drops the repetition and degrades the guardian to
  # AtStartup-only - no periodic self-heal / update-apply / stuck recovery. A long
  # finite duration is honored on Home + Pro and is effectively indefinite for an
  # appliance.
  $gStartup = New-ScheduledTaskTrigger -AtStartup
  $gRepeat  = New-ScheduledTaskTrigger -Once -At (Get-Date) `
    -RepetitionInterval (New-TimeSpan -Minutes 5) -RepetitionDuration (New-TimeSpan -Days 3650)
  # SYSTEM, no stored password, runs whether or not anyone is logged in. 1h time
  # limit so a wedged run can't linger; IgnoreNew so a slow run isn't overlapped.
  $gPrincipal = New-ScheduledTaskPrincipal -UserId "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest
  $gSettings  = New-ScheduledTaskSettingsSet `
    -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
    -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 1)
  Unregister-ScheduledTask -TaskName "KrakenGuardian" -Confirm:$false -ErrorAction SilentlyContinue
  Register-ScheduledTask -TaskName "KrakenGuardian" -Action $gAction -Trigger @($gStartup, $gRepeat) -Settings $gSettings -Principal $gPrincipal | Out-Null
  Start-ScheduledTask -TaskName "KrakenGuardian" -ErrorAction SilentlyContinue
  Write-Host "    OK: KrakenGuardian registered (SYSTEM, at-startup + every 5 min)."
} catch {
  Write-Warning "Could not register the guardian task ($_). Autologon self-heal + locked-down self-update will be unavailable."
}

# ---- 7. LSA-encrypted auto-login (bundled Autologon64.exe) --------------------
# Kiosk mode is only safe if the box ACTUALLY auto-logs into the kiosk account.
# Autologon can silently no-op (Tamper Protection / policy), which used to leave
# the box rebooting into whatever account it had before - where the kiosk task
# never fires and nothing appears (a tester lost an hour to exactly this). So we
# VERIFY it took, and if it didn't we DON'T strand the box: fall back to running
# under the current desktop user right now (no reboot, no password to hunt for).
$didAutologon = $false
if ($didKioskAccount -and -not $SkipAutologon) {
  $autologonExe = Join-Path $InstallDir "tools\Autologon64.exe"
  if (Test-Path $autologonExe) {
    Write-Host "==> Enabling auto-login for '$KioskUser' (LSA-encrypted)..."
    try { & $autologonExe /accepteula $KioskUser $env:COMPUTERNAME $KioskPassword | Out-Null } catch {}
    # Read it back - "did not throw" does NOT mean the setting stuck.
    $wl = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
    $aal = (Get-ItemProperty -Path $wl -Name AutoAdminLogon -ErrorAction SilentlyContinue).AutoAdminLogon
    $dun = (Get-ItemProperty -Path $wl -Name DefaultUserName -ErrorAction SilentlyContinue).DefaultUserName
    if ($aal -eq "1" -and $dun -and ($dun -match [regex]::Escape($KioskUser))) {
      $didAutologon = $true
      Write-Host "    OK: auto-login enabled + verified."
    } else {
      Write-Warning "Auto-login did NOT verify (Tamper Protection or policy)."
    }
  } else {
    Write-Warning "Autologon64.exe not present at $InstallDir\tools."
  }
  if (-not $didAutologon) {
    # Fallback: rebind auto-start to the person at the screen and start it now, so
    # the box works immediately instead of rebooting into a dead account.
    Write-Host "==> Kiosk auto-login unavailable; running under '$consoleUser' now instead."
    $taskUser = $consoleUser
    $fbTrigger = New-ScheduledTaskTrigger -AtLogOn -User $taskUser
    $fbPrincipal = New-ScheduledTaskPrincipal -UserId $taskUser -LogonType Interactive -RunLevel Limited
    Unregister-ScheduledTask -TaskName "KrakenPlayer" -Confirm:$false -ErrorAction SilentlyContinue
    Register-ScheduledTask -TaskName "KrakenPlayer" -Action $action -Trigger $fbTrigger -Settings $settings -Principal $fbPrincipal | Out-Null
    Start-ScheduledTask -TaskName "KrakenPlayer" -ErrorAction SilentlyContinue
  }
}

# ---- 7b. guardian kiosk marker (tells the guardian this is a kiosk box) -------
# The guardian only re-asserts autologon when THIS marker is present, so a normal
# PC (no kiosk autologon) is never forced into autologon. It also carries which
# account autologon must land in + the account the guardian re-binds the
# KrakenPlayer task to if Windows drops it. Non-secret (user + machine name only).
if ($didKioskAccount -and $didAutologon) {
  $kioskMarker = @{ kioskUser = $KioskUser; domain = $env:COMPUTERNAME } | ConvertTo-Json -Compress
  try {
    Set-Content -Path (Join-Path $dataDir "guardian-kiosk.json") -Value $kioskMarker -Encoding UTF8
    Write-Host "==> Guardian kiosk marker written (autologon self-heal armed for '$KioskUser')."
  } catch { Write-Warning "Could not write the guardian kiosk marker ($_); autologon self-heal will be inactive." }

  # OPT-IN Tier 2: DPAPI-encrypt (LocalMachine) the kiosk password into the
  # SYSTEM/Admin-only guardian-work dir, so the guardian can fully re-run
  # Autologon64 (LSA-secret rewrite) after a secret loss, not just restore flags.
  # Default OFF - this stores a credential at rest (mitigated by DPAPI + the ACL a
  # non-admin cannot read). Tier 1 covers the common Windows-Update flag drift.
  if ($StoreKioskPassword -and $KioskPassword) {
    try {
      $credFile = Join-Path (Join-Path $dataDir "guardian-work") "kiosk-cred.dpapi"
      Add-Type -AssemblyName System.Security
      $plain = [System.Text.Encoding]::UTF8.GetBytes($KioskPassword)
      $enc   = [System.Security.Cryptography.ProtectedData]::Protect($plain, $null, 'LocalMachine')
      [System.IO.File]::WriteAllBytes($credFile, $enc)
      [Array]::Clear($plain, 0, $plain.Length)
      Write-Host "    OK: kiosk credential stored (DPAPI, SYSTEM/Admin-only) for Tier-2 autologon self-heal."
    } catch { Write-Warning "Could not store the kiosk credential for Tier 2 ($_); Tier 1 self-heal still applies." }
  }
}

# ---- 8. Start-menu / Startup shortcuts --------------------------------------
try {
  New-Item -ItemType Directory -Force -Path $startMenu | Out-Null
  $wsh = New-Object -ComObject WScript.Shell
  $relaunch = $wsh.CreateShortcut((Join-Path $startMenu "Relaunch Kraken Player.lnk"))
  $relaunch.TargetPath = "powershell.exe"
  $relaunch.Arguments = "-NoProfile -WindowStyle Hidden -Command ""Start-ScheduledTask -TaskName KrakenPlayer"""
  $relaunch.Save()
  $stop = $wsh.CreateShortcut((Join-Path $startMenu "Stop Kraken Player.lnk"))
  $stop.TargetPath = "powershell.exe"
  # The stop shortcut must also clear the detector + any camera-holding ffmpeg:
  # Stop-Process on the player is a hard kill (its own child cleanup never runs),
  # and a leftover detector keeps the webcam open (light on) until the next start.
  # NOTE the backtick escapes: in a double-quoted PS string `$_ yields a literal
  # $_ in the generated shortcut. The previous \$_ form does NOT escape in
  # PowerShell (backslash isn't an escape char), so $_ expanded to empty at
  # setup time and the shortcut's kill clauses were silently broken.
  $stop.Arguments = "-NoProfile -WindowStyle Hidden -Command ""Stop-ScheduledTask -TaskName KrakenPlayer; Get-Process kraken-player -ErrorAction SilentlyContinue | Stop-Process -Force; Get-CimInstance Win32_Process | Where-Object { (`$_.Name -eq 'chrome.exe' -or `$_.Name -eq 'msedge.exe') -and `$_.CommandLine -like '*KrakenPlayer*chromium*' } | ForEach-Object { Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue }; Get-Process kraken-detector -ErrorAction SilentlyContinue | Stop-Process -Force; Get-CimInstance Win32_Process | Where-Object { `$_.Name -eq 'ffmpeg.exe' -and `$_.CommandLine -like '*dshow*' } | ForEach-Object { Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue }"""
  $stop.Save()
  # NO Startup-folder autostart shortcut here: the Scheduled Task (at-logon +
  # restart-on-exit) is the SOLE autostart supervisor. A Startup shortcut on top
  # would launch a second player at logon that races the single-instance lock and
  # can flap (a window flashing = "players popping up"). Remove a stale one.
  Remove-Item $startupLnk -Force -ErrorAction SilentlyContinue
  Write-Host "==> Start-menu shortcuts created (task is the sole autostart)."
} catch { Write-Host "Could not create shortcuts (non-fatal): $_" }

# Start only after the marker, permissions, guardian, and shortcuts are ready.
# Starting earlier lets the first player process race setup and miss the kiosk
# marker, which presents as "installed but nothing automatically opens".
if (-not $didKioskAccount) {
  Write-Host "==> Starting Kraken Player now..."
  Start-ScheduledTask -TaskName "KrakenPlayer" -ErrorAction SilentlyContinue
  Start-Sleep -Seconds 2
  $playerProcess = Get-Process kraken-player -ErrorAction SilentlyContinue | Select-Object -First 1
  if ($playerProcess) {
    Write-Host "    OK: Kraken Player is running."
  } else {
    Write-Warning "The player task was installed but did not remain running. Use the setup report on the Desktop for support."
  }
}

# ---- 9. reboot (only if kiosk auto-login VERIFIED, and not suppressed) --------
# Only reboot when auto-login actually took (section 7 verified it). A failed
# auto-login already fell back to running under the current user, so there is
# nothing to reboot into - rebooting then would just drop into a dead account
# (the silent "installed but nothing happens" a tester hit).
if ($didAutologon -and -not $NoReboot) {
  Write-Host "Restarting in 30 seconds into kiosk mode (cancel: shutdown /a)..."
  shutdown /r /t 30 /c "Kraken Player setup complete - restarting into kiosk mode." 2>$null
}

# ---- final summary + on-desktop report (so a failure is one screenshot) -------
$report = @("Kraken Player - setup report", "============================", "")
if ($didKioskAccount -and $didAutologon) {
  $report += "Result: SIGNAGE (kiosk) mode. This PC auto-logs-in as '$KioskUser' and boots straight into the player."
  $report += "The Windows taskbar and normal desktop shortcuts stay locked while content is playing."
  $report += "Maintenance: press Ctrl+Shift+Q to open the desktop for 10 minutes; press Ctrl+Shift+K to lock it again."
  $report += "Next: reboot to enter kiosk mode."
} elseif ($didKioskAccount) {
  $report += "Result: kiosk auto-login could NOT be set on this PC (Tamper Protection or policy)."
  $report += "The player is instead running under your current account NOW - it is working, no reboot needed."
} elseif ($EnableKioskShell) {
  $report += "Result: installed for your current Windows account. No separate Kraken user was created."
  $report += "The player is open now, starts on every login, and returns automatically if it closes."
  $report += "The taskbar is hidden while the player is active. Maintenance: Ctrl+Shift+Q; return to player: Ctrl+Shift+K."
} else {
  $report += "Result: running under your current account. The player is open now and starts on every login."
}
$report += "Runs as: $taskUser"
$report += ""
$report += "Camera support (Defender exclusion): " + $(if ($defenderOK) { "OK." } else { "NOT verified. If the camera will not capture, turn OFF Tamper Protection (Windows Security > Virus & threat protection > Manage settings) and reinstall." })
$report += ""
$report += "If the player is not on screen: open the Start menu and run 'Relaunch Kraken Player'."
$report += "To stop it: Start menu > 'Stop Kraken Player'."
Write-InstallReport $report

Write-Host ""
Write-Host "Kraken Player configured. A 'Kraken Player - setup report.txt' is on the Desktop."
$report | ForEach-Object { Write-Host "  $_" }
