[CmdletBinding()]
param(
    [string]$InstallDir = (Join-Path $env:LOCALAPPDATA "VeloraMesh\bin"),
    [string]$ConfigPath = (Join-Path $env:APPDATA "VeloraMesh\client.json"),
    [string]$DeviceName = $env:COMPUTERNAME,
    [string]$ServerUrl = "https://api.veloramesh.com:2053",
    [string]$SourceClient = "",
    [string]$NetworkName = "",
    [string]$JoinSecretFile = "",
    [string]$Listen = "0.0.0.0:42001",
    [string]$Advertise = "",
    [string]$Discovery = "edge.veloramesh.com:3478",
    [string]$ForwardBroadcastUDP = "",
    [switch]$Background,
    [switch]$Tunnel,
    [switch]$SkipConfig,
    [switch]$Service,
    [switch]$StartService,
    [switch]$ReplaceService,
    [switch]$Fresh,
    [switch]$Uninstall,
    [switch]$Purge,
    [switch]$SkipFirewall,
    [switch]$Status,
    [switch]$Interactive,
    [switch]$NonInteractive
)

$ErrorActionPreference = "Stop"
$script:MenuAction = ""
$script:DefaultUserInstallDir = Join-Path $env:LOCALAPPDATA "VeloraMesh\bin"
$script:DefaultUserConfigPath = Join-Path $env:APPDATA "VeloraMesh\client.json"
$script:DefaultMachineInstallDir = Join-Path $env:ProgramFiles "VeloraMesh"
$script:DefaultMachineConfigPath = Join-Path $env:ProgramData "VeloraMesh\client.json"
$script:DefaultMachineDataDir = Split-Path -Parent $script:DefaultMachineConfigPath
$script:JoinSecret = ""
$script:LogMaxBytes = 5MB
$script:LogBackups = 5
$script:ServiceWasRunning = $false
$script:ClientUpdatePending = $false
$script:HadPreviousClient = $false
$script:DestinationReplaced = $false
$script:StagedClient = ""
$script:PreviousClient = ""
$script:PackageFileUpdates = @()

function Write-Step {
    param([string]$Message)
    Write-Output "[veloramesh] $Message"
}

function Resolve-ClientBinary {
    param(
        [string]$ExplicitPath,
        [string]$ScriptDir
    )

    if ($ExplicitPath -ne "") {
        $resolved = Resolve-Path -LiteralPath $ExplicitPath -ErrorAction Stop
        return $resolved.Path
    }

    $packageClient = Join-Path $ScriptDir "veloramesh-client.exe"
    if (Test-Path -LiteralPath $packageClient) {
        return (Resolve-Path -LiteralPath $packageClient).Path
    }

    throw "veloramesh-client.exe was not found. Run this script from the Windows package or pass -SourceClient."
}

function Test-WintunAvailable {
    param([string]$ClientPath)

    $clientDir = Split-Path -Parent $ClientPath
    return Test-Path -LiteralPath (Join-Path $clientDir "wintun.dll")
}

function Test-IsAdministrator {
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = [Security.Principal.WindowsPrincipal]::new($identity)
    return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Get-PrivateMachinePrincipals {
    $values = @(
        ([Security.Principal.SecurityIdentifier]::new(
            [Security.Principal.WellKnownSidType]::LocalSystemSid,
            $null)).Value,
        ([Security.Principal.SecurityIdentifier]::new(
            [Security.Principal.WellKnownSidType]::BuiltinAdministratorsSid,
            $null)).Value,
        ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value)
    )

    foreach ($value in ($values | Select-Object -Unique)) {
        [Security.Principal.SecurityIdentifier]::new($value)
    }
}

function Set-PrivateDirectoryAcl {
    param([string]$Path)

    $acl = [Security.AccessControl.DirectorySecurity]::new()
    $acl.SetAccessRuleProtection($true, $false)
    $inheritance = [Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
        [Security.AccessControl.InheritanceFlags]::ObjectInherit
    foreach ($principal in (Get-PrivateMachinePrincipals)) {
        $rule = [Security.AccessControl.FileSystemAccessRule]::new(
            $principal,
            [Security.AccessControl.FileSystemRights]::FullControl,
            $inheritance,
            [Security.AccessControl.PropagationFlags]::None,
            [Security.AccessControl.AccessControlType]::Allow)
        [void]$acl.AddAccessRule($rule)
    }
    Set-Acl -LiteralPath $Path -AclObject $acl
}

function Set-PrivateFileAcl {
    param([string]$Path)

    $acl = [Security.AccessControl.FileSecurity]::new()
    $acl.SetAccessRuleProtection($true, $false)
    foreach ($principal in (Get-PrivateMachinePrincipals)) {
        $rule = [Security.AccessControl.FileSystemAccessRule]::new(
            $principal,
            [Security.AccessControl.FileSystemRights]::FullControl,
            [Security.AccessControl.AccessControlType]::Allow)
        [void]$acl.AddAccessRule($rule)
    }
    Set-Acl -LiteralPath $Path -AclObject $acl
}

function Protect-DefaultMachineData {
    $path = [System.IO.Path]::GetFullPath($script:DefaultMachineDataDir)
    if (-not (Test-Path -LiteralPath $path -PathType Container)) {
        throw "machine data directory is missing: $path"
    }

    $root = Get-Item -LiteralPath $path -Force
    if (($root.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
        throw "refusing to secure a reparse-point machine data directory: $path"
    }

    Set-PrivateDirectoryAcl -Path $path
    $children = @(Get-ChildItem -LiteralPath $path -Force -Recurse)
    foreach ($child in $children) {
        if (($child.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
            throw "refusing to secure a machine data tree containing a reparse point: $($child.FullName)"
        }
    }
    foreach ($directory in @($children | Where-Object { $_.PSIsContainer })) {
        Set-PrivateDirectoryAcl -Path $directory.FullName
    }
    foreach ($file in @($children | Where-Object { -not $_.PSIsContainer })) {
        Set-PrivateFileAcl -Path $file.FullName
    }
}

function Get-ListenPort {
    param([string]$ListenAddress)

    $value = $ListenAddress.Trim()
    $lastColon = $value.LastIndexOf(":")
    if ($lastColon -lt 0) {
        return "Any"
    }
    $portText = $value.Substring($lastColon + 1).Trim()
    $port = 0
    if ([int]::TryParse($portText, [ref]$port) -and $port -gt 0 -and $port -le 65535) {
        return $port
    }
    return "Any"
}

function Get-ConfiguredNetworkId {
    param(
        [string]$ConfigFile,
        [string]$Name
    )

    if ([string]::IsNullOrWhiteSpace($Name) -or -not (Test-Path -LiteralPath $ConfigFile)) {
        return ""
    }
    try {
        $cfg = Get-Content -LiteralPath $ConfigFile -Raw | ConvertFrom-Json
    } catch {
        return ""
    }

    $networks = @($cfg.networks)
    foreach ($network in $networks) {
        if (($network.name -ieq $Name) -and -not [string]::IsNullOrWhiteSpace([string]$network.id)) {
            return [string]$network.id
        }
    }

    $autoConnect = @($networks | Where-Object { $_.autoConnect -eq $true -and -not [string]::IsNullOrWhiteSpace([string]$_.id) })
    if ($autoConnect.Count -eq 1) {
        return [string]$autoConnect[0].id
    }
    return ""
}

function Ensure-FirewallRule {
    param(
        [string]$ClientPath,
        [string]$ListenAddress
    )

    $displayName = "VeloraMesh Client UDP In"
    $listenPort = Get-ListenPort -ListenAddress $ListenAddress
    $existing = @(Get-NetFirewallRule -DisplayName $displayName -ErrorAction SilentlyContinue)
    if ($existing.Count -eq 0) {
        $ruleArgs = @{
            DisplayName = $displayName
            Direction = "Inbound"
            Action = "Allow"
            Protocol = "UDP"
            Program = $ClientPath
            Profile = "Any"
        }
        if ($listenPort -ne "Any") {
            $ruleArgs.LocalPort = $listenPort
        }
        New-NetFirewallRule @ruleArgs | Out-Null
        Write-Step "created Windows Firewall rule $displayName"
        return
    }

    $existing | Set-NetFirewallRule -Enabled True -Direction Inbound -Action Allow -Profile Any
    $existing | Get-NetFirewallApplicationFilter | Set-NetFirewallApplicationFilter -Program $ClientPath
    $existing | Get-NetFirewallPortFilter | Set-NetFirewallPortFilter -Protocol UDP -LocalPort $listenPort
    Write-Step "updated Windows Firewall rule $displayName"
}

function Remove-FirewallRuleIfPresent {
    $displayName = "VeloraMesh Client UDP In"
    $existing = @(Get-NetFirewallRule -DisplayName $displayName -ErrorAction SilentlyContinue)
    if ($existing.Count -eq 0) {
        return
    }
    $existing | Remove-NetFirewallRule
    Write-Step "removed Windows Firewall rule $displayName"
}

function Rotate-LogFile {
    param(
        [string]$Path,
        [long]$MaxBytes = $script:LogMaxBytes,
        [int]$Backups = $script:LogBackups
    )

    if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        return
    }
    $item = Get-Item -LiteralPath $Path
    if ($item.Length -lt $MaxBytes) {
        return
    }
    try {
        for ($i = $Backups - 1; $i -ge 1; $i--) {
            $src = "$Path.$i"
            $dst = "$Path.$($i + 1)"
            if (Test-Path -LiteralPath $src -PathType Leaf) {
                Remove-Item -LiteralPath $dst -Force -ErrorAction SilentlyContinue
                Move-Item -LiteralPath $src -Destination $dst -Force
            }
        }
        Move-Item -LiteralPath $Path -Destination "$Path.1" -Force
        Write-Step "rotated log $Path"
    } catch {
        Write-Step "log rotation skipped for ${Path}: $($_.Exception.Message)"
    }
}

function Invoke-LogRotation {
    $programData = $env:ProgramData
    if ([string]::IsNullOrWhiteSpace($programData)) {
        $programData = "C:\ProgramData"
    }
    Rotate-LogFile -Path (Join-Path $programData "VeloraMesh\logs\service.log")
}

function Invoke-ServiceRemoval {
    param([string]$ClientPath)

    if ($null -ne (Get-ManagedClientService) -and (Test-Path -LiteralPath $ClientPath -PathType Leaf)) {
        Invoke-OptionalClientServiceCommand -ClientPath $ClientPath -Action "stop"
        Invoke-OptionalClientServiceCommand -ClientPath $ClientPath -Action "uninstall"
    }

    if ($null -ne (Get-ManagedClientService)) {
        Invoke-OptionalSC -Action "stop"
        Invoke-OptionalSC -Action "delete"
    }

    $deadline = [DateTime]::UtcNow.AddSeconds(20)
    while ($null -ne (Get-ManagedClientService)) {
        if ([DateTime]::UtcNow -ge $deadline) {
            throw "VeloraMeshClient is still installed; installed files were kept"
        }
        Start-Sleep -Milliseconds 250
    }
}

function Invoke-OptionalClientServiceCommand {
    param(
        [string]$ClientPath,
        [string]$Action
    )

    try {
        Invoke-InstallerClientProcess -ClientPath $ClientPath -Arguments @("service", $Action) -Quiet | Out-Null
    }
    catch {
    }
}

function Invoke-InstallerClientProcess {
    param(
        [string]$ClientPath,
        [string[]]$Arguments,
        [switch]$Quiet
    )

    $previousErrorAction = $ErrorActionPreference
    $previousExitCode = $global:LASTEXITCODE
    $global:LASTEXITCODE = $null
    try {
        # Windows PowerShell must drain stderr and wait for exit before rollback can touch the binary.
        $ErrorActionPreference = "Continue"
        $PSNativeCommandUseErrorActionPreference = $false
        $output = @(& $ClientPath @Arguments 2>&1 | ForEach-Object {
            $line = $_.ToString()
            if (-not $Quiet) { Write-Host $line }
            $line
        })
        $exitCode = $global:LASTEXITCODE
    }
    finally {
        $ErrorActionPreference = $previousErrorAction
        $global:LASTEXITCODE = $previousExitCode
    }
    if ($null -eq $exitCode) {
        throw "could not execute client command: $($output -join [Environment]::NewLine)"
    }
    return [pscustomobject]@{ ExitCode = $exitCode; Output = $output }
}

function Invoke-OptionalSC {
    param([string]$Action)

    $previousErrorAction = $ErrorActionPreference
    try {
        $ErrorActionPreference = "Continue"
        sc.exe $Action VeloraMeshClient *> $null
    }
    catch {
    }
    finally {
        $ErrorActionPreference = $previousErrorAction
    }
}

function Get-ManagedClientService {
    return Get-Service -Name "VeloraMeshClient" -ErrorAction SilentlyContinue
}

function Stop-ManagedClientServiceForUpdate {
    $service = Get-ManagedClientService
    if ($null -eq $service -or $service.Status -eq [System.ServiceProcess.ServiceControllerStatus]::Stopped) {
        return
    }

    $script:ServiceWasRunning = $true
    Write-Step "stopping active Windows service for binary update"
    Stop-Service -Name "VeloraMeshClient" -Force -ErrorAction Stop
    $service.WaitForStatus([System.ServiceProcess.ServiceControllerStatus]::Stopped, [TimeSpan]::FromSeconds(20))
}

function Install-ClientBinaryAtomically {
    param(
        [string]$Source,
        [string]$Destination
    )

    $directory = Split-Path -Parent $Destination
    $token = [Guid]::NewGuid().ToString("N")
    $script:StagedClient = Join-Path $directory ".veloramesh-client.$token.new.exe"
    $script:PreviousClient = Join-Path $directory ".veloramesh-client.$token.previous.exe"
    $script:ClientUpdatePending = $true

    Copy-Item -LiteralPath $Source -Destination $script:StagedClient -Force
    $result = Invoke-InstallerClientProcess -ClientPath $script:StagedClient -Arguments @("version") -Quiet
    if ($result.ExitCode -ne 0) {
        throw "staged veloramesh-client version check failed"
    }

    Stop-ManagedClientServiceForUpdate
    if (Test-Path -LiteralPath $Destination -PathType Leaf) {
        Move-Item -LiteralPath $Destination -Destination $script:PreviousClient -Force
        $script:HadPreviousClient = $true
    }
    Move-Item -LiteralPath $script:StagedClient -Destination $Destination -Force
    $script:DestinationReplaced = $true
}

function Install-PackageFileAtomically {
    param(
        [string]$Source,
        [string]$Destination
    )

    $directory = Split-Path -Parent $Destination
    $leaf = Split-Path -Leaf $Destination
    $token = [Guid]::NewGuid().ToString("N")
    $update = [pscustomobject]@{
        Destination = $Destination
        Staged = Join-Path $directory ".$leaf.$token.new"
        Previous = Join-Path $directory ".$leaf.$token.previous"
        HadPrevious = $false
        DestinationReplaced = $false
    }
    $script:PackageFileUpdates += $update

    Copy-Item -LiteralPath $Source -Destination $update.Staged -Force
    if (Test-Path -LiteralPath $Destination -PathType Leaf) {
        Move-Item -LiteralPath $Destination -Destination $update.Previous -Force
        $update.HadPrevious = $true
    }
    Move-Item -LiteralPath $update.Staged -Destination $Destination -Force
    $update.DestinationReplaced = $true
}

function Undo-PackageFileUpdates {
    for ($i = $script:PackageFileUpdates.Count - 1; $i -ge 0; $i--) {
        $update = $script:PackageFileUpdates[$i]
        Remove-TransactionFile -Path $update.Staged
        if ($update.HadPrevious -and (Test-Path -LiteralPath $update.Previous -PathType Leaf)) {
            Remove-TransactionFile -Path $update.Destination
            Move-Item -LiteralPath $update.Previous -Destination $update.Destination -Force
        } elseif ($update.DestinationReplaced) {
            Remove-TransactionFile -Path $update.Destination
        }
        $update.HadPrevious = $false
        $update.DestinationReplaced = $false
    }
    $script:PackageFileUpdates = @()
}

function Remove-TransactionFile {
    param([string]$Path)

    $wait = [System.Diagnostics.Stopwatch]::StartNew()
    while ($true) {
        try {
            [System.IO.File]::Delete($Path)
            return
        } catch {
            $cause = $_.Exception.GetBaseException()
            $win32Code = $cause.HResult -band 0xffff
            # A closing process or scanner may briefly retain a sharing lock.
            if ($win32Code -notin @(32, 33) -or $wait.ElapsedMilliseconds -ge 5000) {
                throw
            }
            Start-Sleep -Milliseconds 100
        }
    }
}

function Complete-PackageFileUpdates {
    foreach ($update in $script:PackageFileUpdates) {
        Remove-Item -LiteralPath $update.Staged -Force -ErrorAction SilentlyContinue
        Remove-Item -LiteralPath $update.Previous -Force -ErrorAction SilentlyContinue
    }
    $script:PackageFileUpdates = @()
}

function Undo-ClientBinaryUpdate {
    param([string]$Destination)

    if (-not $script:ClientUpdatePending) {
        return
    }
    $filesChanged = $script:HadPreviousClient -or $script:DestinationReplaced -or
        @($script:PackageFileUpdates | Where-Object { $_.HadPrevious -or $_.DestinationReplaced }).Count -gt 0
    if ($filesChanged) {
        $service = Get-ManagedClientService
        if ($null -ne $service -and $service.Status -ne [System.ServiceProcess.ServiceControllerStatus]::Stopped) {
            Stop-Service -Name "VeloraMeshClient" -Force -ErrorAction Stop
            $service.WaitForStatus([System.ServiceProcess.ServiceControllerStatus]::Stopped, [TimeSpan]::FromSeconds(20))
        }
    }
    Undo-PackageFileUpdates
    Remove-TransactionFile -Path $script:StagedClient
    if ($script:HadPreviousClient -and (Test-Path -LiteralPath $script:PreviousClient -PathType Leaf)) {
        Remove-TransactionFile -Path $Destination
        Move-Item -LiteralPath $script:PreviousClient -Destination $Destination -Force
    } elseif ($script:DestinationReplaced) {
        Remove-TransactionFile -Path $Destination
    }
    # File restoration is complete even if restarting the original service fails.
    $script:HadPreviousClient = $false
    $script:DestinationReplaced = $false
    $service = Get-ManagedClientService
    if ($script:ServiceWasRunning -and $null -ne $service) {
        Start-Service -Name "VeloraMeshClient" -ErrorAction Stop
        $service.WaitForStatus([System.ServiceProcess.ServiceControllerStatus]::Running, [TimeSpan]::FromSeconds(20))
        $script:ServiceWasRunning = $false
    }
    $script:ClientUpdatePending = $false
}

function Complete-ClientBinaryUpdate {
    Complete-PackageFileUpdates
    Remove-Item -LiteralPath $script:StagedClient -Force -ErrorAction SilentlyContinue
    Remove-Item -LiteralPath $script:PreviousClient -Force -ErrorAction SilentlyContinue
    $script:ClientUpdatePending = $false
    $script:DestinationReplaced = $false
}

function Assert-SafeInstallPath {
    param([string]$Path)

    $full = [System.IO.Path]::GetFullPath($Path)
    $root = [System.IO.Path]::GetPathRoot($full)
    if ($full -eq $root) {
        throw "refusing to purge filesystem root: $full"
    }
    $leaf = Split-Path -Leaf $full
    if ($leaf -eq "VeloraMesh") {
        return
    }
    $parent = Split-Path -Parent $full
    if ($leaf -eq "bin" -and (Split-Path -Leaf $parent) -eq "VeloraMesh") {
        return
    }
    throw "refusing to purge unexpected install path: $full"
}

function Assert-SafePurgePath {
    param(
        [string]$Path,
        [string]$ExpectedLeaf
    )

    $full = [System.IO.Path]::GetFullPath($Path)
    $root = [System.IO.Path]::GetPathRoot($full)
    if ($full -eq $root) {
        throw "refusing to purge filesystem root: $full"
    }
    $leaf = Split-Path -Leaf $full
    if ($leaf -ne $ExpectedLeaf) {
        throw "refusing to purge unexpected path: $full"
    }
}

function Remove-InstalledFiles {
    param(
        [string]$InstallPath,
        [string]$ConfigFile
    )

    if (Test-Path -LiteralPath $InstallPath) {
        Assert-SafeInstallPath -Path $InstallPath
        Remove-Item -LiteralPath $InstallPath -Recurse -Force
        Write-Step "removed install dir $InstallPath"
    }
    if (Test-Path -LiteralPath $ConfigFile -PathType Leaf) {
        Assert-SafePurgePath -Path $ConfigFile -ExpectedLeaf "client.json"
        Remove-Item -LiteralPath $ConfigFile -Force
        Write-Step "removed config $ConfigFile"
    }
}

function Remove-DefaultMachineData {
    $path = [System.IO.Path]::GetFullPath($script:DefaultMachineDataDir)
    if (-not (Test-Path -LiteralPath $path)) {
        return
    }
    if ((Split-Path -Leaf $path) -ne "VeloraMesh") {
        throw "refusing to purge unexpected machine data path: $path"
    }
    Remove-Item -LiteralPath $path -Recurse -Force
    Write-Step "removed machine data dir $path"
}

function Remove-DefaultUserFilesIfDifferent {
    param(
        [string]$InstallPath,
        [string]$ConfigFile
    )

    $currentInstall = [System.IO.Path]::GetFullPath($InstallPath)
    $currentConfig = [System.IO.Path]::GetFullPath($ConfigFile)
    $userInstall = [System.IO.Path]::GetFullPath($script:DefaultUserInstallDir)
    $userConfig = [System.IO.Path]::GetFullPath($script:DefaultUserConfigPath)
    if ($currentInstall -ne $userInstall -or $currentConfig -ne $userConfig) {
        Remove-InstalledFiles -InstallPath $userInstall -ConfigFile $userConfig
    }
}


function Test-InteractiveInstaller {
    if ($NonInteractive) {
        return $false
    }
    if ($Interactive) {
        return $true
    }
    return [Environment]::UserInteractive
}

function Read-InstallerValue {
    param(
        [string]$Prompt,
        [string]$Default = "",
        [switch]$Required
    )

    while ($true) {
        $suffix = ""
        if (-not [string]::IsNullOrWhiteSpace($Default)) {
            $suffix = " [$Default]"
        }
        $value = Read-Host "$Prompt$suffix"
        if ([string]::IsNullOrWhiteSpace($value)) {
            $value = $Default
        }
        if (-not $Required -or -not [string]::IsNullOrWhiteSpace($value)) {
            return $value
        }
        Write-Step "$Prompt is required"
    }
}

function Test-EndpointHost {
    param([string]$Value)

    if ([string]::IsNullOrWhiteSpace($Value)) {
        return $false
    }
    $trimmed = $Value.Trim()
    if ($trimmed -match "\s" -or $trimmed.Contains("://") -or $trimmed.IndexOfAny([char[]]"/?#,") -ge 0) {
        return $false
    }
    $bracketed = $trimmed.StartsWith("[") -and $trimmed.EndsWith("]")
    if (($trimmed.StartsWith("[") -ne $trimmed.EndsWith("]")) -or ($bracketed -and -not $trimmed.Contains(":"))) {
        return $false
    }
    if (-not $bracketed -and ([regex]::Matches($trimmed, ":")).Count -eq 1) {
        return $false
    }
    return $true
}

function Test-EndpointPort {
    param([string]$Value)

    $port = 0
    return [int]::TryParse($Value, [ref]$port) -and $port -ge 1 -and $port -le 65535
}

function Read-InstallerPort {
    param(
        [string]$Prompt,
        [string]$Default
    )

    while ($true) {
        $value = Read-InstallerValue -Prompt $Prompt -Default $Default -Required
        if (Test-EndpointPort -Value $value) {
            return $value.Trim()
        }
        Write-Step "$Prompt must be a port between 1 and 65535"
    }
}

function Read-InstallerEndpointHost {
    param(
        [string]$Prompt,
        [string]$Default
    )

    while ($true) {
        $value = Read-InstallerValue -Prompt $Prompt -Default $Default -Required
        if (Test-EndpointHost -Value $value) {
            return $value.Trim()
        }
        Write-Step "$Prompt must be a host name or IP address, not a URL"
    }
}

function Format-EndpointHost {
    param([string]$HostValue)

    $hostText = $HostValue.Trim()
    if ($hostText.StartsWith("[") -and $hostText.EndsWith("]")) {
        return $hostText
    }
    if ($hostText.Contains(":")) {
        return "[$hostText]"
    }
    return $hostText
}

function Split-ApiServerUrl {
    param([string]$Url)

    try {
        $uri = [System.Uri]::new($Url.Trim())
        $port = if ($uri.Port -gt 0) { [string]$uri.Port } else { "443" }
        return @{ Host = $uri.Host; Port = $port }
    } catch {
        return @{ Host = "api.veloramesh.com"; Port = "2053" }
    }
}

function Split-DiscoveryEndpoint {
    param([string]$Endpoint)

    $first = (($Endpoint -split ",")[0]).Trim()
    if ($first.StartsWith("[")) {
        $closing = $first.IndexOf("]")
        if ($closing -gt 0) {
            $hostPart = $first.Substring(1, $closing - 1)
            $rest = $first.Substring($closing + 1)
            $portPart = if ($rest.StartsWith(":")) { $rest.Substring(1) } else { "3478" }
            return @{ Host = $hostPart; Port = $portPart }
        }
    }
    $colonCount = ([regex]::Matches($first, ":")).Count
    if ($colonCount -eq 1) {
        $lastColon = $first.LastIndexOf(":")
        return @{
            Host = $first.Substring(0, $lastColon)
            Port = $first.Substring($lastColon + 1)
        }
    }
    if (-not [string]::IsNullOrWhiteSpace($first)) {
        return @{ Host = $first; Port = "3478" }
    }
    return @{ Host = "edge.veloramesh.com"; Port = "3478" }
}

function Read-ApiServerEndpoint {
    $parts = Split-ApiServerUrl -Url $script:ServerUrl
    $hostValue = Read-InstallerEndpointHost -Prompt "API host/domain" -Default $parts.Host
    $portValue = Read-InstallerPort -Prompt "API HTTPS port" -Default $parts.Port
    $script:ServerUrl = "https://$(Format-EndpointHost -HostValue $hostValue):$portValue"
}

function Read-DiscoveryEndpoint {
    $parts = Split-DiscoveryEndpoint -Endpoint $script:Discovery
    $hostValue = Read-InstallerEndpointHost -Prompt "Edge discovery host/domain" -Default $parts.Host
    $portValue = Read-InstallerPort -Prompt "Edge UDP port" -Default $parts.Port
    $script:Discovery = "$(Format-EndpointHost -HostValue $hostValue):$portValue"
}

function Read-InstallerSecret {
    param([string]$Prompt)

    while ($true) {
        $secure = Read-Host $Prompt -AsSecureString
        $ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
        try {
            $value = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr)
        }
        finally {
            [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr)
        }
        if (-not [string]::IsNullOrWhiteSpace($value)) {
            return $value
        }
        Write-Step "$Prompt is required"
    }
}

function Read-InstallerYesNo {
    param(
        [string]$Prompt,
        [bool]$DefaultYes = $true
    )

    $suffix = if ($DefaultYes) { " [Y/n]" } else { " [y/N]" }
    while ($true) {
        $value = (Read-Host "$Prompt$suffix").Trim().ToLowerInvariant()
        if ($value -eq "") {
            return $DefaultYes
        }
        if ($value -in @("y", "yes", "e", "evet")) {
            return $true
        }
        if ($value -in @("n", "no", "h", "hayir", "hayır")) {
            return $false
        }
    }
}

function Read-InstallerMenuChoice {
    while ($true) {
        Write-Host ""
        Write-Host "VeloraMesh Windows Client Installer"
        Write-Host "1) Guided install/update"
        Write-Host "2) Network operations"
        Write-Host "3) Service operations"
        Write-Host "4) Fresh install/reset client"
        Write-Host "5) Uninstall service, keep files/config"
        Write-Host "6) Purge client installation and local data"
        Write-Host "0) Exit"
        $choice = (Read-Host "Select").Trim()
        switch ($choice) {
            "1" { return "install" }
            "2" { return "network" }
            "3" { return "service" }
            "4" { return "fresh" }
            "5" { return "uninstall" }
            "6" { return "purge" }
            "0" { return "exit" }
            default { Write-Step "unknown selection" }
        }
    }
}

function Confirm-InstallerPhrase {
    param(
        [string]$Message,
        [string]$Phrase
    )

    $answer = Read-Host "$Message Type $Phrase to continue"
    return $answer -eq $Phrase
}

function Test-ShouldShowActionMenu {
    if (-not (Test-InteractiveInstaller)) {
        return $false
    }
    if ($Fresh -or $Uninstall -or $Purge -or $Background -or $Service -or $StartService -or $ReplaceService -or $Tunnel -or $SkipConfig -or $SkipFirewall -or $Status) {
        return $false
    }
    if (-not [string]::IsNullOrWhiteSpace($NetworkName) -or -not [string]::IsNullOrWhiteSpace($JoinSecret) -or -not [string]::IsNullOrWhiteSpace($JoinSecretFile)) {
        return $false
    }
    return $true
}

function Initialize-InteractiveAction {
    if (-not (Test-ShouldShowActionMenu)) {
        return
    }

    switch (Read-InstallerMenuChoice) {
        "install" { return }
        "network" {
            $script:MenuAction = "network"
            return
        }
        "service" {
            $script:MenuAction = "service"
            return
        }
        "fresh" {
            $script:Fresh = $true
            return
        }
        "uninstall" {
            $script:Uninstall = $true
            return
        }
        "purge" {
            $script:Uninstall = $true
            $script:Purge = $true
            return
        }
        "exit" { exit 0 }
    }
}

function Initialize-InteractiveOptions {
    if (-not (Test-InteractiveInstaller) -or $Uninstall) {
        return
    }

    Write-Step "interactive setup"
    $willNeedConfig = -not $SkipConfig -and [string]::IsNullOrWhiteSpace($NetworkName) -and ($Fresh -or -not (Test-Path -LiteralPath $ConfigPath))
    if ($willNeedConfig) {
        Read-ApiServerEndpoint
        $script:NetworkName = Read-InstallerValue -Prompt "Network name (blank to create config only)" -Default ""
        $script:DeviceName = Read-InstallerValue -Prompt "Device name" -Default $DeviceName -Required
        if (-not [string]::IsNullOrWhiteSpace($NetworkName)) {
            $script:JoinSecret = Read-InstallerSecret -Prompt "Network join secret"
            Read-DiscoveryEndpoint
        }
    }

    if (-not $Service -and -not $StartService -and -not $Background -and -not $SkipConfig -and -not $managedServiceInstalled) {
        if ([string]::IsNullOrWhiteSpace($NetworkName)) {
            return
        }
        if (Test-IsAdministrator) {
            if (Read-InstallerYesNo -Prompt "Install and start the background tunnel service?" -DefaultYes $true) {
                $script:Service = $true
                $script:Tunnel = $true
                $script:StartService = $true
            }
        } else {
            Write-Step "background service install requires an elevated PowerShell session; continuing with config-only install"
        }
    }
}

function Test-JoinSecretInputPresent {
    return -not [string]::IsNullOrWhiteSpace($JoinSecret) `
        -or -not [string]::IsNullOrWhiteSpace($JoinSecretFile)
}

function Invoke-NetworkJoinOnce {
    param(
        [string]$ClientPath,
        [string]$ConfigFile
    )

    $secretFile = $JoinSecretFile
    $transientSecretFile = $null
    try {
        if (-not [string]::IsNullOrWhiteSpace($JoinSecret)) {
            if (-not [string]::IsNullOrWhiteSpace($secretFile)) {
                throw "Use either the interactive join secret prompt or -JoinSecretFile"
            }
            $secretDirectory = Join-Path (Split-Path -Parent $ConfigFile) "secrets"
            New-Item -ItemType Directory -Path $secretDirectory -Force | Out-Null
            $item = Get-Item -LiteralPath $secretDirectory -Force
            if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
                throw "refusing to use a reparse-point secret directory: $secretDirectory"
            }
            Set-PrivateDirectoryAcl -Path $secretDirectory
            $transientSecretFile = Join-Path $secretDirectory (".join-secret-{0}.tmp" -f [Guid]::NewGuid().ToString("N"))
            [System.IO.File]::WriteAllText($transientSecretFile, $JoinSecret, [System.Text.UTF8Encoding]::new($false, $true))
            Set-PrivateFileAcl -Path $transientSecretFile
            $secretFile = $transientSecretFile
        }
        if ([string]::IsNullOrWhiteSpace($secretFile)) {
            throw "-JoinSecretFile is required when NetworkName is provided in non-interactive mode"
        }
        $result = Invoke-InstallerClientProcess -ClientPath $ClientPath -Arguments @(
            "network", "join", "-name", $NetworkName, "-join-secret-file", $secretFile,
            "-device-name", $DeviceName, "-server", $ServerUrl, "-config", $ConfigFile)
        if ($result.ExitCode -ne 0) {
            $message = ($result.Output -join [Environment]::NewLine).Trim()
            if ([string]::IsNullOrWhiteSpace($message)) {
                $message = "veloramesh-client network join failed"
            }
            throw $message
        }
    }
    finally {
        if ($null -ne $transientSecretFile) {
            Remove-Item -LiteralPath $transientSecretFile -Force -ErrorAction SilentlyContinue
        }
    }
}

function Invoke-NetworkJoinWithRetry {
    param(
        [string]$ClientPath,
        [string]$ConfigFile
    )

    while ($true) {
        try {
            Write-Step "joining network $NetworkName"
            Invoke-NetworkJoinOnce -ClientPath $ClientPath -ConfigFile $ConfigFile
            return
        }
        catch {
            if (-not (Test-InteractiveInstaller)) {
                throw
            }
            Write-Step "network join failed: $($_.Exception.Message)"
            if (-not (Read-InstallerYesNo -Prompt "Try another network name or join secret?" -DefaultYes $true)) {
                throw
            }
            $script:NetworkName = Read-InstallerValue -Prompt "Network name" -Default $NetworkName -Required
            $script:JoinSecret = Read-InstallerSecret -Prompt "Network join secret"
            $script:JoinSecretFile = ""
        }
    }
}

function Assert-ClientInstalled {
    param([string]$ClientPath)

    if (-not (Test-Path -LiteralPath $ClientPath -PathType Leaf)) {
        throw "veloramesh-client.exe was not found at $ClientPath. Run Guided install/update first."
    }
}

function Invoke-ClientCommand {
    param(
        [string]$ClientPath,
        [string[]]$Arguments
    )

    $result = Invoke-InstallerClientProcess -ClientPath $ClientPath -Arguments $Arguments
    if ($result.ExitCode -ne 0) {
        Write-Step "command exited with code $($result.ExitCode)"
    }
}

function Invoke-InstallerSelfTest {
    param(
        [string]$ClientPath,
        [string]$ConfigFile,
        [string]$Name
    )

    if (-not (Test-Path -LiteralPath $ClientPath -PathType Leaf) -or -not (Test-Path -LiteralPath $ConfigFile -PathType Leaf)) {
        return
    }
    Write-Step "running installer self-test"
    $args = @("doctor", "-config", $ConfigFile, "-timeout", "8s")
    if (-not [string]::IsNullOrWhiteSpace($Name)) {
        $networkId = Get-ConfiguredNetworkId -ConfigFile $ConfigFile -Name $Name
        if (-not [string]::IsNullOrWhiteSpace($networkId)) {
            $args += @("-id", $networkId)
        } else {
            $args += @("-network", $Name)
        }
        if (-not [string]::IsNullOrWhiteSpace($script:Discovery)) {
            $args += @("-discovery", $script:Discovery)
        }
    } else {
        $args += @("-local-only")
    }
    $result = Invoke-InstallerClientProcess -ClientPath $ClientPath -Arguments $args
    if ($result.ExitCode -eq 0) {
        Write-Step "self-test passed"
    } else {
        Write-Step "self-test reported issues; run the doctor command above for details"
    }
}

function Invoke-ClientNetworkMenu {
    param(
        [string]$ClientPath,
        [string]$ConfigFile
    )

    Assert-ClientInstalled -ClientPath $ClientPath
    while ($true) {
        Write-Host ""
        Write-Host "VeloraMesh Windows Client - Network"
        Write-Host "1) Status"
        Write-Host "2) Join network"
        Write-Host "3) List peers"
        Write-Host "4) Disconnect network"
        Write-Host "5) Connect network"
        Write-Host "6) Leave network"
        Write-Host "7) Rename device"
        Write-Host "8) Doctor"
        Write-Host "0) Back"
        $choice = (Read-Host "Select").Trim()
        switch ($choice) {
            "1" {
                Invoke-ClientCommand -ClientPath $ClientPath -Arguments @("network", "status", "-config", $ConfigFile)
            }
            "2" {
                Read-ApiServerEndpoint
                $script:NetworkName = Read-InstallerValue -Prompt "Network name" -Default $NetworkName -Required
                $script:DeviceName = Read-InstallerValue -Prompt "Device name" -Default $DeviceName -Required
                $script:JoinSecret = Read-InstallerSecret -Prompt "Network join secret"
                $script:JoinSecretFile = ""
                Invoke-NetworkJoinWithRetry -ClientPath $ClientPath -ConfigFile $ConfigFile
            }
            "3" {
                $name = Read-InstallerValue -Prompt "Network name" -Default $NetworkName -Required
                Invoke-ClientCommand -ClientPath $ClientPath -Arguments @("network", "peers", "-name", $name, "-config", $ConfigFile)
            }
            "4" {
                $name = Read-InstallerValue -Prompt "Network name" -Default $NetworkName -Required
                Invoke-ClientCommand -ClientPath $ClientPath -Arguments @("network", "disconnect", "-name", $name, "-config", $ConfigFile)
            }
            "5" {
                $name = Read-InstallerValue -Prompt "Network name" -Default $NetworkName -Required
                Invoke-ClientCommand -ClientPath $ClientPath -Arguments @("network", "connect", "-name", $name, "-config", $ConfigFile)
            }
            "6" {
                $name = Read-InstallerValue -Prompt "Network name" -Default $NetworkName -Required
                if (Read-InstallerYesNo -Prompt "Leave network '$name'?" -DefaultYes $false) {
                    Invoke-ClientCommand -ClientPath $ClientPath -Arguments @("network", "leave", "-name", $name, "-config", $ConfigFile)
                }
            }
            "7" {
                $name = Read-InstallerValue -Prompt "New device name" -Default $DeviceName -Required
                Invoke-ClientCommand -ClientPath $ClientPath -Arguments @("device", "rename", "-name", $name, "-config", $ConfigFile)
            }
            "8" {
                $name = Read-InstallerValue -Prompt "Network name (blank for local checks)" -Default ""
                $args = @("doctor", "-config", $ConfigFile)
                if (-not [string]::IsNullOrWhiteSpace($name)) {
                    $args += @("-network", $name)
                    if (-not [string]::IsNullOrWhiteSpace($script:Discovery)) {
                        $args += @("-discovery", $script:Discovery)
                    }
                }
                Invoke-ClientCommand -ClientPath $ClientPath -Arguments $args
            }
            "0" { return }
            default { Write-Step "unknown selection" }
        }
    }
}

function Invoke-ClientServiceMenu {
    param(
        [string]$ClientPath,
        [string]$ConfigFile
    )

    Assert-ClientInstalled -ClientPath $ClientPath
    if (-not (Test-IsAdministrator)) {
        Write-Step "service operations usually require an elevated PowerShell session"
    }
    while ($true) {
        Write-Host ""
        Write-Host "VeloraMesh Windows Client - Service"
        Write-Host "1) Status"
        Write-Host "2) Start"
        Write-Host "3) Stop"
        Write-Host "4) Restart"
        Write-Host "5) Doctor"
        Write-Host "0) Back"
        $choice = (Read-Host "Select").Trim()
        switch ($choice) {
            "1" { Invoke-ClientCommand -ClientPath $ClientPath -Arguments @("service", "status") }
            "2" { Invoke-ClientCommand -ClientPath $ClientPath -Arguments @("service", "start") }
            "3" { Invoke-ClientCommand -ClientPath $ClientPath -Arguments @("service", "stop") }
            "4" {
                Invoke-ClientCommand -ClientPath $ClientPath -Arguments @("service", "stop")
                Invoke-ClientCommand -ClientPath $ClientPath -Arguments @("service", "start")
            }
            "5" { Invoke-ClientCommand -ClientPath $ClientPath -Arguments @("service", "doctor", "-service-config", $ConfigFile) }
            "0" { return }
            default { Write-Step "unknown selection" }
        }
    }
}

if ([string]::IsNullOrWhiteSpace($InstallDir)) {
    throw "InstallDir is required"
}
if ([string]::IsNullOrWhiteSpace($ConfigPath)) {
    throw "ConfigPath is required"
}
if ([string]::IsNullOrWhiteSpace($DeviceName)) {
    $DeviceName = "windows-client"
}
if ([string]::IsNullOrWhiteSpace($ServerUrl)) {
    throw "ServerUrl is required"
}
Initialize-InteractiveAction
if ($Fresh -and $Uninstall) {
    throw "-Fresh cannot be combined with -Uninstall"
}
if ($Purge -and -not $Uninstall) {
    throw "-Purge can only be used with -Uninstall"
}
if ($Background) {
    $Service = $true
    $Tunnel = $true
    $StartService = $true
}
if ($Uninstall) {
    $Service = $true
}
if ($Fresh) {
    $Service = $true
}

$managedServiceInstalled = $null -ne (Get-ManagedClientService)
if ($Service -or $Uninstall -or $managedServiceInstalled -or ($MenuAction -in @("network", "service"))) {
    if (-not (Test-IsAdministrator)) {
        $managedServiceUpdate = $managedServiceInstalled -and ($MenuAction -ne "network")
        if ($Service -or $Uninstall -or $managedServiceUpdate -or ($MenuAction -eq "service")) {
            throw "Service installation, service operations, or uninstall requires an elevated PowerShell session"
        }
        Write-Step "using machine-level install paths; some network actions may require elevation"
    }
    if ($InstallDir -eq $script:DefaultUserInstallDir) {
        $InstallDir = $script:DefaultMachineInstallDir
    }
    if ($ConfigPath -eq $script:DefaultUserConfigPath) {
        $ConfigPath = $script:DefaultMachineConfigPath
    }
}

Initialize-InteractiveOptions

if ($Service -or $Uninstall) {
    if (-not (Test-IsAdministrator)) {
        throw "Service installation, fresh reset, or uninstall requires an elevated PowerShell session"
    }
    if ($InstallDir -eq $script:DefaultUserInstallDir) {
        $InstallDir = $script:DefaultMachineInstallDir
    }
    if ($ConfigPath -eq $script:DefaultUserConfigPath) {
        $ConfigPath = $script:DefaultMachineConfigPath
    }
}

$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$installDirFull = [System.IO.Path]::GetFullPath($InstallDir)
$configPathFull = [System.IO.Path]::GetFullPath($ConfigPath)
$configDir = Split-Path -Parent $configPathFull
$installedClient = Join-Path $installDirFull "veloramesh-client.exe"

while ($MenuAction -in @("network", "service")) {
    if ($MenuAction -eq "network") {
        $script:MenuAction = ""
        Invoke-ClientNetworkMenu -ClientPath $installedClient -ConfigFile $configPathFull
    } elseif ($MenuAction -eq "service") {
        if (-not (Test-IsAdministrator)) {
            throw "Service operations require an elevated PowerShell session"
        }
        $script:MenuAction = ""
        Invoke-ClientServiceMenu -ClientPath $installedClient -ConfigFile $configPathFull
    }
    Initialize-InteractiveAction
}

if ($Fresh -and $Uninstall) {
    throw "-Fresh cannot be combined with -Uninstall"
}
if ($Purge -and -not $Uninstall) {
    throw "-Purge can only be used with -Uninstall"
}
if ($Uninstall -or $Fresh) {
    $Service = $true
}
if (($Service -or $Uninstall) -and -not (Test-IsAdministrator)) {
    throw "Service installation, fresh reset, or uninstall requires an elevated PowerShell session"
}

if ($Uninstall) {
    Write-Step "stopping and uninstalling Windows service if present"
    Invoke-ServiceRemoval -ClientPath $installedClient
    if (-not $SkipFirewall) {
        Remove-FirewallRuleIfPresent
    }
    if ($Purge) {
        Remove-InstalledFiles -InstallPath $installDirFull -ConfigFile $configPathFull
        Remove-DefaultUserFilesIfDifferent -InstallPath $installDirFull -ConfigFile $configPathFull
        Remove-DefaultMachineData
    } else {
        Write-Step "kept installed files and local data; rerun with -Purge to remove them"
    }
    Write-Step "uninstall complete"
    return
}

if ($Fresh) {
    if (-not (Confirm-InstallerPhrase -Message "This removes the Windows service, firewall rule, installed files, config, and service logs." -Phrase "FRESH")) {
        throw "fresh install cancelled"
    }
    Write-Step "resetting existing Windows client installation"
    Invoke-ServiceRemoval -ClientPath $installedClient
    if (-not $SkipFirewall) {
        Remove-FirewallRuleIfPresent
    }
    Remove-InstalledFiles -InstallPath $installDirFull -ConfigFile $configPathFull
    Remove-DefaultMachineData
}

$source = Resolve-ClientBinary -ExplicitPath $SourceClient -ScriptDir $scriptDir

Write-Step "creating directories"
New-Item -ItemType Directory -Path $installDirFull -Force | Out-Null
New-Item -ItemType Directory -Path $configDir -Force | Out-Null
if ($Service -or $managedServiceInstalled -or
    [System.IO.Path]::GetFullPath($configDir).Equals(
        [System.IO.Path]::GetFullPath($script:DefaultMachineDataDir),
        [System.StringComparison]::OrdinalIgnoreCase)) {
    New-Item -ItemType Directory -Path $script:DefaultMachineDataDir -Force | Out-Null
    Protect-DefaultMachineData
}

try {
    Write-Step "installing client binary"
    Install-ClientBinaryAtomically -Source $source -Destination $installedClient

    $sourceDir = Split-Path -Parent $source
    foreach ($optional in @("manifest.json", "RELEASE-NOTES.md", "wintun.dll")) {
        $optionalPath = Join-Path $sourceDir $optional
        if (Test-Path -LiteralPath $optionalPath) {
            Install-PackageFileAtomically -Source $optionalPath -Destination (Join-Path $installDirFull $optional)
        }
    }

    if ($Tunnel -and -not (Test-WintunAvailable -ClientPath $installedClient)) {
        throw "wintun.dll is required for Windows tunnel mode; use the complete VeloraMesh package for this Windows CPU architecture and rerun the installer"
    }

    Write-Step "installed version"
    $result = Invoke-InstallerClientProcess -ClientPath $installedClient -Arguments @("version")
    if ($result.ExitCode -ne 0) {
        throw "veloramesh-client version failed"
    }

    if ($SkipConfig) {
        Write-Step "skipping config initialization"
    } elseif (Test-Path -LiteralPath $configPathFull) {
        Write-Step "keeping existing config $configPathFull"
        if (-not [string]::IsNullOrWhiteSpace($NetworkName) -and (Test-JoinSecretInputPresent)) {
            Invoke-NetworkJoinWithRetry -ClientPath $installedClient -ConfigFile $configPathFull
        }
    } elseif (-not [string]::IsNullOrWhiteSpace($NetworkName)) {
        Invoke-NetworkJoinWithRetry -ClientPath $installedClient -ConfigFile $configPathFull
    } else {
        Write-Step "creating config $configPathFull"
        $result = Invoke-InstallerClientProcess -ClientPath $installedClient -Arguments @(
            "config", "init", "-name", $DeviceName, "-server", $ServerUrl, "-config", $configPathFull)
        if ($result.ExitCode -ne 0) {
            throw "veloramesh-client config init failed"
        }
    }

    if ($Tunnel -and -not $SkipFirewall -and (Test-IsAdministrator)) {
        Write-Step "configuring Windows Firewall for VeloraMesh UDP"
        Ensure-FirewallRule -ClientPath $installedClient -ListenAddress $Listen
    } elseif ($Tunnel -and -not $SkipFirewall) {
        Write-Step "skipping Windows Firewall rule because the installer is not elevated"
    }

    if ($Service) {
        Invoke-LogRotation
        if ($ReplaceService) {
            Write-Step "replacing the Windows service configuration transactionally"
        }
        Write-Step "installing Windows service"
        $serviceArgs = @("service", "install", "-config", $configPathFull, "-service-config", $configPathFull, "-binary", $installedClient, "-listen", $Listen)
        if (-not [string]::IsNullOrWhiteSpace($Advertise)) {
            $serviceArgs += @("-advertise", $Advertise)
        }
        if (-not [string]::IsNullOrWhiteSpace($Discovery)) {
            $serviceArgs += @("-discovery", $Discovery)
        }
        if (-not [string]::IsNullOrWhiteSpace($ForwardBroadcastUDP)) {
            $serviceArgs += @("-forward-broadcast-udp", $ForwardBroadcastUDP)
        }
        if ($Tunnel) {
            $serviceArgs += @("-tunnel")
        }
        if ($ReplaceService) {
            $serviceArgs += @("-replace")
        }
        $shouldStartService = $StartService -or $script:ServiceWasRunning
        if ($shouldStartService) {
            $serviceArgs += @("-start")
        }
        $serviceNetworkId = Get-ConfiguredNetworkId -ConfigFile $configPathFull -Name $NetworkName
        if (-not [string]::IsNullOrWhiteSpace($NetworkName)) {
            if (-not [string]::IsNullOrWhiteSpace($serviceNetworkId)) {
                $serviceArgs += @("-network-id", $serviceNetworkId)
                Write-Step "service network_id=$serviceNetworkId"
            } else {
                $serviceArgs += @("-network-name", $NetworkName)
            }
        }
        $result = Invoke-InstallerClientProcess -ClientPath $installedClient -Arguments $serviceArgs
        if ($result.ExitCode -ne 0) {
            throw "veloramesh-client service install failed"
        }
        if ($shouldStartService) {
            Write-Step "Windows service reached the running state"
        }
        if ($StartService -or $script:ServiceWasRunning -or $Status) {
            Write-Step "service status"
            Invoke-ClientCommand -ClientPath $installedClient -Arguments @("service", "status")
        }
    }

    if (-not $Service -and $script:ServiceWasRunning) {
        Write-Step "restarting existing Windows service"
        Start-Service -Name "VeloraMeshClient" -ErrorAction Stop
    }

    Write-Step "install_dir=$installDirFull"
    Write-Step "client=$installedClient"
    Write-Step "config=$configPathFull"
    $doctorCommand = "& `"$installedClient`" doctor -config `"$configPathFull`""
    if (-not [string]::IsNullOrWhiteSpace($NetworkName)) {
        $doctorNetworkId = Get-ConfiguredNetworkId -ConfigFile $configPathFull -Name $NetworkName
        if (-not [string]::IsNullOrWhiteSpace($doctorNetworkId)) {
            $doctorCommand += " -id `"$doctorNetworkId`""
        } else {
            $doctorCommand += " -network `"$NetworkName`""
        }
        if (-not [string]::IsNullOrWhiteSpace($Discovery)) {
            $doctorCommand += " -discovery `"$Discovery`""
        }
    }
    Write-Step "doctor=$doctorCommand"
    Invoke-InstallerSelfTest -ClientPath $installedClient -ConfigFile $configPathFull -Name $NetworkName
    if ($Service) {
        Write-Step "service=$([bool]$Service)"
        if (-not $StartService -and -not $script:ServiceWasRunning) {
            Write-Step "next: run veloramesh-client service start"
        }
    } elseif ($script:ServiceWasRunning) {
        Write-Step "service=True"
        Write-Step "updated and restarted the existing Windows service"
    } elseif (Test-Path -LiteralPath $configPathFull -PathType Leaf) {
        Write-Step "config-only install/update complete"
    } else {
        Write-Step "next: use veloramesh-client network join to join a network"
    }
    Complete-ClientBinaryUpdate
}
catch {
    Undo-ClientBinaryUpdate -Destination $installedClient
    throw
}
