# Install-ZBEVPN.ps1
# Consumer installer for ZBEVPN (Service + GUI + prerequisites)
# Run: Right-click -> Run with PowerShell (Admin) OR from elevated PowerShell:
#   powershell -ExecutionPolicy Bypass -File .\Install-ZBEVPN.ps1

# Sanity tag verification 02/15 11:28 am

Set-StrictMode -Version 2
$ErrorActionPreference = "Stop"

# ----------------------------
# Config
# ----------------------------
$CompanyName   = "ZBEVPN"
$SvcName       = "ZBEVPN"
$SvcDisplay    = "ZBEVPN Client"
$InstallRoot   = Join-Path $env:ProgramFiles "ZBEVPN"
$ServiceDir    = $InstallRoot
$GuiDir        = Join-Path $InstallRoot "GUI"
$DataRoot      = Join-Path $env:ProgramData "ZBEVPN"
$LogDir        = Join-Path $DataRoot "logs"
$StateDir      = Join-Path $DataRoot "state"
$InstallStatePath = Join-Path $StateDir "install-state.json"

$PayloadServiceDir = Join-Path $PSScriptRoot "payload\service"
$PayloadGuiDir     = Join-Path $PSScriptRoot "payload\gui"
$PayloadDepsDir    = Join-Path $PSScriptRoot "payload\deps"

$ServiceExeDst = Join-Path $ServiceDir "windows-client.exe"

# Dependency installers (ship these files)
$VcRedistExe   = Join-Path $PayloadDepsDir "vc_redist.x64.exe"
$DotnetDeskExe = Get-ChildItem -LiteralPath $PayloadDepsDir -Filter "windowsdesktop-runtime-8.*-win-x64*.exe" -ErrorAction SilentlyContinue | Select-Object -First 1
$WireGuardExe  = Join-Path $PayloadDepsDir "wireguard-installer.exe"
$WireGuardMsi  = Join-Path $PayloadDepsDir "wireguard-amd64.msi"

# WireGuard detection heuristics
$WireGuardExePath = Join-Path ${env:ProgramFiles} "WireGuard\wireguard.exe"

$IconPath        = Join-Path $PSScriptRoot "payload\icon\ZBEVPN-1.ico"

$UninstallScriptSrc = Join-Path $PSScriptRoot "Uninstall-ZBEVPN.ps1"
$UninstallScriptDst = Join-Path $InstallRoot  "Uninstall-ZBEVPN.ps1"

$PublicDesktop = [Environment]::GetFolderPath('CommonDesktopDirectory')
if ([string]::IsNullOrWhiteSpace($PublicDesktop)) {
  $PublicDesktop = Join-Path $env:PUBLIC "Desktop"
}
$DesktopShortcut = Join-Path $PublicDesktop "ZBEVPN.lnk"

# ----------------------------
# Helpers
# ----------------------------
function Get-WireGuardUninstallEntry {
  $keys = @(
    "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
    "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
  )

  foreach ($k in $keys) {
    try {
      $entry = Get-ItemProperty $k -ErrorAction SilentlyContinue |
        Where-Object { $_.DisplayName -eq "WireGuard" } |
        Select-Object -First 1
      if ($entry) { return $entry }
    } catch {}
  }

  return $null
}

function Get-NetworkSnapshot {
  $routes = @()
  $dns = @()
  $adapters = @()

  try {
    $routes = @(Get-NetRoute -AddressFamily IPv4 -ErrorAction SilentlyContinue |
      Select-Object ifIndex, InterfaceAlias, DestinationPrefix, NextHop, RouteMetric, PolicyStore)
  } catch {}

  try {
    $dns = @(Get-DnsClientServerAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
      Select-Object InterfaceIndex, InterfaceAlias, ServerAddresses)
  } catch {}

  try {
    $adapters = @(Get-NetAdapter -IncludeHidden -ErrorAction SilentlyContinue |
      Select-Object Name, InterfaceDescription, InterfaceGuid, Status, MacAddress, LinkSpeed)
  } catch {}

  return [ordered]@{
    adapters = $adapters
    dns      = $dns
    routes   = $routes
  }
}

function Save-InstallState {
  New-Item -ItemType Directory -Force -Path $StateDir | Out-Null

  $wgEntry = Get-WireGuardUninstallEntry
  $wgInstalledBefore = Test-WireGuardInstalled

  $state = [ordered]@{
    schemaVersion = 1
    product       = $CompanyName
    capturedAtUtc = ([DateTime]::UtcNow.ToString("o"))
    installRoot   = $InstallRoot
    dataRoot      = $DataRoot
    serviceName   = $SvcName
    uninstallScriptSource = $UninstallScriptSrc
    uninstallScriptDest   = $UninstallScriptDst

    wireGuard = [ordered]@{
      installedBefore = $wgInstalledBefore
      exePath         = $WireGuardExePath
      uninstallString = if ($wgEntry) { [string]$wgEntry.UninstallString } else { "" }
      displayVersion  = if ($wgEntry) { [string]$wgEntry.DisplayVersion } else { "" }
    }

    machine = [ordered]@{
      computerName = $env:COMPUTERNAME
      userName     = $env:USERNAME
      osVersion    = [Environment]::OSVersion.VersionString
      psVersion    = $PSVersionTable.PSVersion.ToString()
    }

    networkBefore = Get-NetworkSnapshot
  }

  $json = $state | ConvertTo-Json -Depth 8
  Set-Content -LiteralPath $InstallStatePath -Value $json -Encoding UTF8

  Write-Log "Install state saved: $InstallStatePath"
  Write-Log "Install state: WireGuard installed before setup = $wgInstalledBefore"
}
function Install-SplitTunnelSeed {
  param(
    [Parameter(Mandatory=$true)][string]$InstallRoot
  )

  Write-Log "SplitTunnel: installing seed"

  $seedSrc = Join-Path $PSScriptRoot "payload\data\runtime.seed"
  $dataDir = Join-Path $InstallRoot "data"
  $seedDst = Join-Path $dataDir "runtime.seed"

  New-Item -ItemType Directory -Force -Path $dataDir | Out-Null

  if (Test-Path $seedSrc) {
    Copy-Item -Force $seedSrc $seedDst
    Write-Log "SplitTunnel: seed copied to $seedDst"
  } else {
    Write-Log "SplitTunnel: seed source missing: $seedSrc"
  }
}

function Stop-And-RemoveExistingInstall {
  Write-Log "Stopping existing ZBEVPN service (if any): $SvcName"

  Write-Log "CLEANUP: entered Stop-And-RemoveExistingInstall"
  Write-Log "CLEANUP: InstallRoot=$InstallRoot"
  Write-Log "CLEANUP: DataRoot=$DataRoot"

  $runtimeDat = Join-Path $DataRoot "runtime.dat"   # => C:\ProgramData\ZBEVPN\runtime.dat

  # Remove runtime state so new install reseeds
  Write-Log "CLEANUP: runtimeDat path=$runtimeDat exists=$(Test-Path $runtimeDat)"
  if (Test-Path $runtimeDat) {
    Remove-Item -Force $runtimeDat
    Write-Log "CLEANUP: runtimeDat removed. exists_now=$(Test-Path $runtimeDat)"
    } else {
    Write-Log "CLEANUP: runtimeDat not present; nothing to remove"
  }

  try { sc.exe stop $SvcName 2>$null | Out-Null } catch {}
  Start-Sleep -Seconds 2

  Write-Log "Deleting existing ZBEVPN service (if any): $SvcName"
  try { sc.exe delete $SvcName 2>$null | Out-Null } catch {}

  # Kill any running service exe just in case (locked file scenario)
  try {
    Get-Process -Name "windows-client" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
  } catch {}

  # Remove install directory so robocopy never overwrites locked binaries
  if (Test-Path -LiteralPath $InstallRoot) {
    Write-Log "Removing existing install folder: $InstallRoot"
    try {
      Remove-Item -LiteralPath $InstallRoot -Recurse -Force -ErrorAction Stop
    } catch {
      Write-Log "WARNING: Could not fully remove $InstallRoot (files locked)."
      throw "Existing install folder is locked; reboot and retry."
    }
  }

  New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null
}

function Create-StartMenuShortcut {
  $guiExe = Get-AnyExeInDir $GuiDir
  if ($null -eq $guiExe) { return }

  $programs = [Environment]::GetFolderPath('CommonPrograms')
  $folder = Join-Path $programs "ZBEVPN"
  New-Item -ItemType Directory -Force -Path $folder | Out-Null

  $lnkPath = Join-Path $folder "ZBEVPN.lnk"
  Write-Log "Creating Start Menu shortcut: $lnkPath"

  $wsh = New-Object -ComObject WScript.Shell
  $lnk = $wsh.CreateShortcut($lnkPath)
  $lnk.TargetPath = $guiExe
  $lnk.WorkingDirectory = $GuiDir
  $lnk.IconLocation = "$guiExe,0"
  $lnk.Save()
}

function Get-PropString($obj, [string]$propName) {
  if ($null -eq $obj) { return "" }
  $p = $obj.PSObject.Properties[$propName]
  if ($null -eq $p) { return "" }
  return [string]$p.Value
}

function Write-Log([string]$msg) {
  $ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
  $line = "[$ts] $msg"
  Write-Host $line
  try {
    New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
    Add-Content -LiteralPath (Join-Path $LogDir "installer.log") -Value $line
  } catch { }
}

function Assert-Admin {
  $id = [Security.Principal.WindowsIdentity]::GetCurrent()
  $p  = New-Object Security.Principal.WindowsPrincipal($id)
  if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    throw "Installer must be run as Administrator."
  }
}

function Assert-Exists([string]$path, [string]$what) {
  if (-not (Test-Path -LiteralPath $path)) {
    throw ("Missing {0}: {1}" -f $what, $path)
  }
}

function Robocopy-Mirror([string]$src, [string]$dst) {
  New-Item -ItemType Directory -Force -Path $dst | Out-Null
  & robocopy $src $dst "/MIR" "/R:2" "/W:1" "/Z" "/FFT" "/NP" | Out-Null
  $rc = $LASTEXITCODE
  Write-Log "Robocopy exit code: $rc"
  if (($rc -ge 8) -and ($rc -ne 10) -and ($rc -ne 11)) {
    throw "Robocopy failed ($rc) copying $src -> $dst"
  }
}
function Get-AnyExeInDir([string]$dir) {
  $exe = Get-ChildItem -LiteralPath $dir -Filter "*.exe" -File -ErrorAction SilentlyContinue | Select-Object -First 1
  if ($null -eq $exe) { return $null }
  return $exe.FullName
}

function Test-DotNetDesktop8Installed {
  # Primary check (most reliable): dotnet shared framework folder
  $fxDir = "C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App"
  try {
    if (Test-Path $fxDir) {
      $v = Get-ChildItem $fxDir -Directory -ErrorAction SilentlyContinue |
           Where-Object { $_.Name -like "8.*" } |
           Select-Object -First 1
      if ($v) {
        Write-Log "DOTNET detected via filesystem: $($v.FullName)"
        return $true
      }
    }
  } catch { }

  # Fallback: registry 64-bit view (some systems won’t populate what we expect)
  try {
    $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
      [Microsoft.Win32.RegistryHive]::LocalMachine,
      [Microsoft.Win32.RegistryView]::Registry64
    )
    $subPath = "SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App"
    $k = $baseKey.OpenSubKey($subPath)
    if (-not $k) { return $false }

    foreach ($name in $k.GetSubKeyNames()) {
      if ($name -like "8.*") { return $true }
    }
    return $false
  } catch {
    return $false
  }
}

function Test-VcRedistInstalled {
  # Most reliable check for VC++ 2015–2022 x64 runtime
  $key = "HKLM:\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64"
  try {
    $p = Get-ItemProperty -Path $key -ErrorAction Stop
    if ($p.Installed -eq 1) {
      Write-Log "VC++ detected: Installed=1, Version=$($p.Version), Major=$($p.Major), Minor=$($p.Minor), Bld=$($p.Bld), RBld=$($p.RBld)"
      return $true
    }
    Write-Log "VC++ key present but Installed != 1"
    return $false
  } catch {
    Write-Log "VC++ registry key not found: $key"
    return $false
  }
}


function Test-WireGuardInstalled {
  # WireGuard should have files on disk; registry alone is not enough.
  $paths = @(
    "C:\Program Files\WireGuard\wireguard.exe",
    "C:\Program Files\WireGuard\wg.exe"
  )

  $hasFiles = $false
  foreach ($p in $paths) {
    if (Test-Path -LiteralPath $p) { $hasFiles = $true; break }
  }

  # If you already had a registry-based check, keep it as a signal,
  # but require files to exist to treat as installed.
  $regInstalled = $false
  try {
    # Best-effort: consider it "registered" if any uninstall entry contains WireGuard
    $k1 = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
    $k2 = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
    $regInstalled = @(
      Get-ItemProperty $k1 -ErrorAction SilentlyContinue
      Get-ItemProperty $k2 -ErrorAction SilentlyContinue
    ) | Where-Object { $_.DisplayName -match "^WireGuard$" } | Select-Object -First 1 | ForEach-Object { $true }

    if (-not $regInstalled) { $regInstalled = $false }
  } catch { $regInstalled = $false }

  if ($regInstalled -and -not $hasFiles) {
    Write-Log "WireGuard registry indicates installed, but binaries missing. Will reinstall."
    return $false
  }

  if ($hasFiles) {
    Write-Log "WireGuard detected on disk."
    return $true
  }

  Write-Log "WireGuard not detected."
  return $false
}


function Install-VcRedist {
  if (Test-VcRedistInstalled) {
    Write-Log "VC++ Redistributable (x64 2015-2022) already installed."
    return
  }

  Assert-Exists $VcRedistExe "VC++ redist installer"

  $vcLogDir = Join-Path $DataRoot "logs"
  New-Item -ItemType Directory -Force -Path $vcLogDir | Out-Null
  $vcLog = Join-Path $vcLogDir ("vcredist-" + (Get-Date -Format "yyyyMMdd-HHmmss") + ".log")

  Write-Log "Installing VC++ Redistributable..."
  Write-Log "  VC++ exe: $VcRedistExe"
  Write-Log "  VC++ log: $vcLog"

  # Use Start-Process so we can read a real exit code
  $p = Start-Process -FilePath $VcRedistExe -ArgumentList "/quiet /norestart /log `"$vcLog`"" -Wait -PassThru
  Write-Log "VC++ exit code: $($p.ExitCode)"

  # Treat these as success:
  # 0 = success
  # 1638 = already installed (another version)
  # 3010 = success, reboot required
  if ($p.ExitCode -notin 0, 1638, 3010) {
    Write-Log "VC++ installer failed with exit code $($p.ExitCode)."
    if (Test-Path $vcLog) {
      Write-Log "VC++ INSTALL LOG TAIL:"
      try { Get-Content $vcLog -Tail 50 | ForEach-Object { Write-Log $_ } } catch { }
    }
    throw "VC++ installer failed (exit code $($p.ExitCode))."
  }

  Start-Sleep -Seconds 5
  if (-not (Test-VcRedistInstalled)) { throw "VC++ Redistributable install did not verify." }

  if ($p.ExitCode -eq 3010) {
    Write-Log "VC++ installed; reboot required (3010). Continuing."
  } else {
    Write-Log "VC++ Redistributable installed."
  }
}

function Install-DotNetDesktop {
  if (Test-DotNetDesktop8Installed) {
    Write-Log ".NET 8 Windows Desktop Runtime already installed."
    return
  }
  if ($null -eq $DotnetDeskExe) {
    throw "Missing .NET Desktop Runtime installer in payload\deps (expected windowsdesktop-runtime-8.*-win-x64*.exe)."
  }

  $logPath = Join-Path $env:TEMP "dotnet_desktop_install.log"
  Write-Log "Installing .NET 8 Windows Desktop Runtime..."
  Write-Log "  installer: $($DotnetDeskExe.FullName)"
  Write-Log "  log: $logPath"

  & $DotnetDeskExe.FullName /install /quiet /norestart /log $logPath | Out-Null
  $ec = $LASTEXITCODE
  Write-Log ".NET installer exit code: $ec"

  # 0 = success, 3010 = success (reboot required)
  if ($ec -ne 0 -and $ec -ne 3010) {
    Write-Log "DOTNET INSTALL LOG TAIL:"
    try { Get-Content $logPath -Tail 50 | ForEach-Object { Write-Log $_ } } catch { }
    throw ".NET Desktop Runtime installer failed (exit code $ec)."
  }

  # Give Windows Installer/registry time to settle
  Start-Sleep -Seconds 5
  for ($i=0; $i -lt 6; $i++) {
    if (Test-DotNetDesktop8Installed) {
      if ($ec -eq 3010) {
        Write-Log ".NET Desktop Runtime installed; reboot required (3010). Continuing."
      } else {
        Write-Log ".NET Desktop Runtime installed."
      }
      return
    }
    Start-Sleep -Seconds 3
  }

  # Still not visible: dump evidence
 Write-Log "DOTNET VERIFY FAILED. Evidence snapshot:"

    # Filesystem snapshot (primary signal)
    try {
      $fxDir = "C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App"
      if (Test-Path $fxDir) {
        Get-ChildItem $fxDir -Directory -ErrorAction SilentlyContinue | ForEach-Object { Write-Log ("  fs: " + $_.Name) }
      } else {
        Write-Log "  fs: missing: $fxDir"
      }
    } catch { }

    # Registry snapshot (secondary)
    try {
      $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
        [Microsoft.Win32.RegistryHive]::LocalMachine,
        [Microsoft.Win32.RegistryView]::Registry64
      )
      $subPath = "SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App"
      $k = $baseKey.OpenSubKey($subPath)
      if ($k) {
        Write-Log ("  reg: subkeys: " + (($k.GetSubKeyNames()) -join ", "))
      } else {
        Write-Log "  reg: missing: HKLM:\$subPath"
      }
    } catch { }
  Write-Log "DOTNET INSTALL LOG TAIL:"
  try { Get-Content $logPath -Tail 50 | ForEach-Object { Write-Log $_ } } catch { }

  throw ".NET Desktop Runtime install did not verify."
}

function Install-WireGuard {
  if (Test-WireGuardInstalled) {
    Write-Log "WireGuard already installed."
    return
  }

  # Prefer MSI for guaranteed silent install + prevent auto-launch
  if (Test-Path -LiteralPath $WireGuardMsi) {
    $wgLog = Join-Path $LogDir ("wireguard-msi-" + (Get-Date -Format "yyyyMMdd-HHmmss") + ".log")
    Write-Log "Installing WireGuard (MSI, silent, DO_NOT_LAUNCH=1)..."
    Write-Log "  MSI: $WireGuardMsi"
    Write-Log "  MSI log: $wgLog"

    $args = "/i `"$WireGuardMsi`" DO_NOT_LAUNCH=1 /qn /norestart /L*v `"$wgLog`""
    $p = Start-Process -FilePath "$env:SystemRoot\System32\msiexec.exe" -ArgumentList $args -WindowStyle Hidden -Wait -PassThru
    $ec = $p.ExitCode
    Write-Log "WireGuard MSI exit code: $ec"

    if ($ec -notin 0,3010,1638) { throw "WireGuard MSI install failed (exit code $ec). See $wgLog" }

    # Safety net: if anything launched WireGuard GUI, kill it
    Start-Sleep -Milliseconds 500
    Get-Process wireguard -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
  }
  elseif (Test-Path -LiteralPath $WireGuardExe) {
    Write-Log "Installing WireGuard (EXE, best-effort silent)..."
    & $WireGuardExe /install /quiet /norestart | Out-Null
    $ec = $LASTEXITCODE
    Write-Log "WireGuard EXE exit code: $ec"
    if ($ec -notin 0,3010,1638) { throw "WireGuard EXE install failed (exit code $ec)." }

    Start-Sleep -Milliseconds 500
    Get-Process wireguard -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
  }
  else {
    throw "Missing WireGuard installer in payload\deps (wireguard-amd64.msi or wireguard-installer.exe)."
  }

  Start-Sleep -Seconds 3
  if (-not (Test-WireGuardInstalled)) { throw "WireGuard install did not verify." }
  Write-Log "WireGuard installed."
}


function Ensure-DataDirsAndPerms {
  Write-Log "Provisioning ProgramData folder: $DataRoot"
  New-Item -ItemType Directory -Force -Path $DataRoot | Out-Null
  New-Item -ItemType Directory -Force -Path $LogDir | Out-Null

  # Allow standard users to read/write app data (GUI runs as user; service as SYSTEM).
  # Keep it simple: Users Modify on ProgramData\ZBEVPN.
  Write-Log "Setting ACLs on $DataRoot (Users:Modify)"
  & icacls $DataRoot /grant "Users:(OI)(CI)M" /T /C | Out-Null
}

function Install-AppFiles {

  Assert-Exists $PayloadServiceDir "service payload folder"
  Assert-Exists $PayloadGuiDir     "GUI payload folder"

  Write-Log "Preparing install root: $InstallRoot"


  New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null

  #
  # SERVICE
  #
  Write-Log "Copying service payload -> $ServiceDir"
  New-Item -ItemType Directory -Force -Path $ServiceDir | Out-Null
  Robocopy-Mirror $PayloadServiceDir $ServiceDir
  #
  # Uninstaller (segregated script)
  #
  Assert-Exists $UninstallScriptSrc "Uninstall-ZBEVPN.ps1 (bundle root)"
  Copy-Item -LiteralPath $UninstallScriptSrc -Destination $UninstallScriptDst -Force
  Write-Log "Uninstaller installed: $UninstallScriptDst"

  if (-not (Test-Path -LiteralPath $ServiceExeDst)) {
      throw "Service EXE missing after copy: $ServiceExeDst"
  }

  #
  # GUI
  #
  Write-Log "Copying GUI payload -> $GuiDir"
  New-Item -ItemType Directory -Force -Path $GuiDir | Out-Null
  Robocopy-Mirror $PayloadGuiDir $GuiDir

  $guiExe = Get-ChildItem -LiteralPath $GuiDir -Recurse -Filter "*.exe" -File -ErrorAction SilentlyContinue | Select-Object -First 1
  if (-not $guiExe) {
      throw "GUI EXE missing after copy."
  }

  Write-Log "GUI EXE installed: $($guiExe.FullName)"
}

function Install-Service {
  Write-Log "Installing Windows service: $SvcName"

  # Stop+delete if exists
  sc.exe stop  $SvcName 2>$null | Out-Null
  sc.exe delete $SvcName 2>$null | Out-Null

  # Create service
  sc.exe create $SvcName binPath= "`"$ServiceExeDst`"" DisplayName= "`"$SvcDisplay`"" start= auto | Out-Null
  sc.exe description $SvcName "ZBEVPN Windows client service" | Out-Null

  # Recovery: restart 3 times
  sc.exe failure $SvcName reset= 0 actions= restart/5000/restart/5000/restart/5000 | Out-Null
  sc.exe failureflag $SvcName 1 | Out-Null

  sc.exe start $SvcName | Out-Null
  Start-Sleep -Seconds 1

  $svc = Get-Service -Name $SvcName -ErrorAction Stop
  Write-Log "Service status: $($svc.Status)"
}

function Create-DesktopShortcut {
  $guiExe = Get-AnyExeInDir $GuiDir
  if ($null -eq $guiExe) {
    Write-Log "WARNING: No GUI EXE found in $GuiDir. Desktop shortcut not created."
    return
  }

  $desktopDir = Split-Path -Parent $DesktopShortcut
  if (-not (Test-Path -LiteralPath $desktopDir)) {
    Write-Log "Desktop folder missing; creating: $desktopDir"
    New-Item -ItemType Directory -Force -Path $desktopDir | Out-Null
  }

  Write-Log "Creating desktop shortcut: $DesktopShortcut"
  $wsh = New-Object -ComObject WScript.Shell
  $lnk = $wsh.CreateShortcut($DesktopShortcut)
  $lnk.TargetPath = $guiExe
  $lnk.WorkingDirectory = $GuiDir

  # Prefer embedded icon from GUI exe if present; else use shipped ico
  if (Test-Path -LiteralPath $guiExe) {
    $lnk.IconLocation = "$guiExe,0"
  } elseif (Test-Path -LiteralPath $IconPath) {
    $lnk.IconLocation = $IconPath
  }

  $lnk.Save()
}

function Write-UninstallHint {
  Write-Log "Installed. To uninstall: run .\Uninstall-ZBEVPN.ps1 as Administrator."
}

# ----------------------------
# Main
# ----------------------------
try {
  Assert-Admin

  Write-Log "=== ZBEVPN Installer starting ==="
  Write-Log "InstallRoot: $InstallRoot"
  Write-Log "ProgramData: $DataRoot"

  # Validate payload
  Assert-Exists $PayloadServiceDir "payload\service"
  Assert-Exists $PayloadGuiDir "payload\gui"
  Assert-Exists $PayloadDepsDir "payload\deps"

  Save-InstallState

  # Dependencies first
  Install-VcRedist
  Install-DotNetDesktop
  Install-WireGuard

  Stop-And-RemoveExistingInstall

  # App files + provisioning
  Ensure-DataDirsAndPerms
  Install-AppFiles
  Install-SplitTunnelSeed -InstallRoot $InstallRoot
  Install-Service
  Create-DesktopShortcut
  Create-StartMenuShortcut
  Write-UninstallHint
  Write-Log "=== ZBEVPN Installer finished OK ==="
  Write-Log "Registering uninstall entry in Windows..."

  $uninstallKey = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\ZBEVPN"

  New-Item -Path $uninstallKey -Force | Out-Null

  Set-ItemProperty -Path $uninstallKey -Name "DisplayName" -Value "ZBEVPN"
  Set-ItemProperty -Path $uninstallKey -Name "Publisher" -Value "CloudXpertSystems"
  Set-ItemProperty -Path $uninstallKey -Name "DisplayVersion" -Value "1.0.0"

  # IMPORTANT: call PowerShell explicitly
  $uninstallCmd = "powershell.exe -ExecutionPolicy Bypass -File `"$InstallRoot\Uninstall-ZBEVPN.ps1`" -RemoveProgramData"

  Set-ItemProperty -Path $uninstallKey -Name "UninstallString" -Value $uninstallCmd

  # Optional but recommended
  Set-ItemProperty -Path $uninstallKey -Name "InstallLocation" -Value $InstallRoot
  Set-ItemProperty -Path $uninstallKey -Name "DisplayIcon" -Value "$InstallRoot\GUI\zbevpn-gui.exe"

  Write-Log "Uninstall entry registered."
  exit 0
}
catch {
  Write-Log "ERROR: $($_.Exception.Message)"
  Write-Log "Installer failed."
  exit 1
}
