param(
    [ValidateSet("Debug", "Release")]
    [string]$Configuration = "Release",

    [string]$Server = "",

    [string]$Listen = "127.0.0.1:38580",

    [string]$DiscoveryListen = "",

    [string]$Advertise = "",

    [switch]$Lan,

    [switch]$NoDiscovery,

    [switch]$AllowUnsafePublicRelay,

    [switch]$NoAutoServer,

    [string]$Device = "host-001",

    [string]$Token = "dev-token",

    [string]$TokenFile = "",

    [string]$DeviceTokenFile = "",

    [string]$ClientConfig = "",

    [ValidateSet("disabled", "optional", "required")]
    [string]$TransportEncryption = "disabled",

    [string]$TransportSecret = "",

    [string]$TransportSecretFile = "",

    [string]$ReceiveDir = "",

    [string[]]$AllowViewer = @(),

    [string[]]$GrantInputTo = @(),

    [switch]$DisableRemoteInput,

    [ValidateRange(1, 60)]
    [int]$CaptureFps = 10,

    [ValidateRange(1, 100)]
    [int]$JpegQuality = 70,

    [ValidatePattern('^(primary|all|[0-9]+)$')]
    [string]$CaptureMonitor = "primary",

    [ValidateSet("auto", "gdi", "desktop-duplication", "dxgi")]
    [string]$CaptureBackend = "auto",

    [switch]$TestMode
)

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

function Get-SyncVisionEndpointParts {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Endpoint
    )

    $separator = $Endpoint.LastIndexOf(":")
    if ($separator -le 0 -or $separator -eq $Endpoint.Length - 1) {
        throw "Endpoint must use host:port."
    }

    return [pscustomobject]@{
        Host = $Endpoint.Substring(0, $separator)
        Port = [int]$Endpoint.Substring($separator + 1)
    }
}

function Test-SyncVisionHostLoopback {
    param(
        [Parameter(Mandatory = $true)]
        [string]$HostName
    )

    return $HostName -eq "localhost" -or $HostName -eq "127.0.0.1" -or $HostName.StartsWith("127.")
}

function Test-SyncVisionHostWildcard {
    param(
        [Parameter(Mandatory = $true)]
        [string]$HostName
    )

    return $HostName -eq "0.0.0.0" -or $HostName -eq "::" -or $HostName -eq "*" -or [string]::IsNullOrWhiteSpace($HostName)
}

function Assert-SyncVisionHostTokenAllowed {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Listen,

        [Parameter(Mandatory = $true)]
        [string]$Token
    )

    $parts = Get-SyncVisionEndpointParts -Endpoint $Listen
    if ($Token -eq "dev-token" -and -not (Test-SyncVisionHostLoopback -HostName $parts.Host)) {
        throw "The default dev-token is only allowed for loopback listeners. Set -Token before using -Listen $Listen."
    }
}

function Assert-SyncVisionHostDiscoveryAdvertiseAllowed {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Listen,

        [AllowEmptyString()]
        [string]$Advertise,

        [switch]$NoDiscovery
    )

    $parts = Get-SyncVisionEndpointParts -Endpoint $Listen
    if (-not $NoDiscovery -and [string]::IsNullOrWhiteSpace($Advertise) -and (Test-SyncVisionHostWildcard -HostName $parts.Host)) {
        throw "LAN discovery cannot advertise wildcard listen address '$Listen'. Pass -Advertise <reachable-host:port>, or pass -NoDiscovery for explicit server-address use."
    }
}

function Assert-SyncVisionHostUnsafePublicRelayAcknowledged {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Listen,

        [Parameter(Mandatory = $true)]
        [bool]$AllowUnsafePublicRelay
    )

    $parts = Get-SyncVisionEndpointParts -Endpoint $Listen
    if (-not (Test-SyncVisionHostLoopback -HostName $parts.Host) -and -not $AllowUnsafePublicRelay) {
        throw "Non-loopback raw TCP relay requires -AllowUnsafePublicRelay. Use a trusted network, VPN, or TLS tunnel before exposing SyncVision publicly."
    }
}

function Assert-SyncVisionHostTransportSecretAllowed {
    param(
        [ValidateSet("disabled", "optional", "required")]
        [string]$TransportEncryption = "disabled",

        [AllowEmptyString()]
        [string]$TransportSecret = "",

        [AllowEmptyString()]
        [string]$TransportSecretFile = ""
    )

    if (-not [string]::IsNullOrWhiteSpace($TransportSecret) -and
        -not [string]::IsNullOrWhiteSpace($TransportSecretFile)) {
        throw "Pass either -TransportSecret or -TransportSecretFile, not both."
    }
    if ($TransportEncryption -ne "disabled" -and
        [string]::IsNullOrWhiteSpace($TransportSecret) -and
        [string]::IsNullOrWhiteSpace($TransportSecretFile)) {
        throw "TransportEncryption $TransportEncryption requires -TransportSecretFile or -TransportSecret."
    }
}

function Resolve-SyncVisionHostClientEndpoint {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Listen
    )

    $parts = Get-SyncVisionEndpointParts -Endpoint $Listen
    $hostPart = if ($parts.Host -eq "0.0.0.0" -or $parts.Host -eq "*") { "127.0.0.1" } else { $parts.Host }
    return "${hostPart}:$($parts.Port)"
}

function Get-SyncVisionLocalIPv4Addresses {
    $addresses = New-Object System.Collections.Generic.List[string]
    foreach ($adapter in [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces()) {
        if ($adapter.OperationalStatus -ne [System.Net.NetworkInformation.OperationalStatus]::Up) {
            continue
        }
        if ($adapter.NetworkInterfaceType -eq [System.Net.NetworkInformation.NetworkInterfaceType]::Loopback -or
            $adapter.NetworkInterfaceType -eq [System.Net.NetworkInformation.NetworkInterfaceType]::Tunnel) {
            continue
        }

        foreach ($unicast in $adapter.GetIPProperties().UnicastAddresses) {
            if ($unicast.Address.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) {
                $addresses.Add($unicast.Address.ToString())
            }
        }
    }

    return $addresses
}

function Test-SyncVisionUsableLanIPv4Address {
    param(
        [Parameter(Mandatory = $true)]
        [string]$AddressText
    )

    $address = [System.Net.IPAddress]::None
    if (-not [System.Net.IPAddress]::TryParse($AddressText.Trim(), [ref]$address)) {
        return $false
    }
    if ($address.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork) {
        return $false
    }
    if ([System.Net.IPAddress]::IsLoopback($address) -or
        $address.Equals([System.Net.IPAddress]::Any) -or
        $address.Equals([System.Net.IPAddress]::Broadcast) -or
        $address.Equals([System.Net.IPAddress]::None)) {
        return $false
    }

    $bytes = $address.GetAddressBytes()
    return -not ($bytes[0] -eq 169 -and $bytes[1] -eq 254)
}

function Resolve-SyncVisionHostListen {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Listen,

        [switch]$Lan,

        [bool]$ListenExplicit = $false
    )

    if (-not $Lan -or $ListenExplicit) {
        return $Listen
    }

    $parts = Get-SyncVisionEndpointParts -Endpoint $Listen
    return "0.0.0.0:$($parts.Port)"
}

function Resolve-SyncVisionHostDefaultAdvertise {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Listen,

        [string[]]$LocalAddresses = @()
    )

    $addresses = @($LocalAddresses)
    if ($addresses.Count -eq 0) {
        $addresses = @(Get-SyncVisionLocalIPv4Addresses)
    }

    foreach ($address in $addresses) {
        if (Test-SyncVisionUsableLanIPv4Address -AddressText $address) {
            $parts = Get-SyncVisionEndpointParts -Endpoint $Listen
            return "${address}:$($parts.Port)"
        }
    }

    throw "LAN mode could not find a non-loopback IPv4 address to advertise. Pass -Advertise <reachable-host:port> explicitly."
}

function Resolve-SyncVisionHostAdvertise {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Listen,

        [AllowEmptyString()]
        [string]$Advertise,

        [switch]$NoDiscovery,

        [switch]$Lan,

        [string[]]$LocalAddresses = @()
    )

    if ($NoDiscovery -or -not [string]::IsNullOrWhiteSpace($Advertise)) {
        return $Advertise
    }

    if ($Lan) {
        return Resolve-SyncVisionHostDefaultAdvertise -Listen $Listen -LocalAddresses $LocalAddresses
    }

    return $Advertise
}

function New-SyncVisionHostServerArguments {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Listen,

        [Parameter(Mandatory = $true)]
        [string]$Token,

        [string]$TokenFile = "",

        [string]$ConfigPath = "",

        [string]$DiscoveryListen = "",

        [string]$Advertise = "",

        [bool]$AllowUnsafePublicRelay = $false,

        [ValidateSet("disabled", "optional", "required")]
        [string]$TransportEncryption = "disabled",

        [string]$TransportSecret = "",

        [string]$TransportSecretFile = "",

        [switch]$NoDiscovery
    )

    if (-not [string]::IsNullOrWhiteSpace($TokenFile)) {
        if ([string]::IsNullOrWhiteSpace($ConfigPath)) {
            throw "ConfigPath is required when TokenFile is used for the local server."
        }

        return @("--config", $ConfigPath)
    }

    $arguments = @("--listen", $Listen, "--token", $Token)
    if ($NoDiscovery) {
        $arguments += "--no-discovery"
    }
    if ($DiscoveryListen) {
        $arguments += @("--discovery-listen", $DiscoveryListen)
    }
    if ($Advertise) {
        $arguments += @("--advertise", $Advertise)
    }
    if ($AllowUnsafePublicRelay) {
        $arguments += "--allow-unsafe-public-relay"
    }
    if ($TransportEncryption -ne "disabled" -or $PSBoundParameters.ContainsKey("TransportEncryption")) {
        $arguments += @("--transport-encryption", $TransportEncryption)
    }
    if (-not [string]::IsNullOrWhiteSpace($TransportSecret)) {
        $arguments += @("--transport-secret", $TransportSecret)
    }
    if (-not [string]::IsNullOrWhiteSpace($TransportSecretFile)) {
        $arguments += @("--transport-secret-file", $TransportSecretFile)
    }

    return $arguments
}

function New-SyncVisionHostServerConfigText {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Listen,

        [Parameter(Mandatory = $true)]
        [string]$TokenFile,

        [string]$DiscoveryListen = "",

        [string]$Advertise = "",

        [bool]$AllowUnsafePublicRelay = $false,

        [ValidateSet("disabled", "optional", "required")]
        [string]$TransportEncryption = "disabled",

        [string]$TransportSecret = "",

        [string]$TransportSecretFile = "",

        [switch]$NoDiscovery
    )

    if (-not [string]::IsNullOrWhiteSpace($TransportSecret) -and
        -not [string]::IsNullOrWhiteSpace($TransportSecretFile)) {
        throw "Pass either -TransportSecret or -TransportSecretFile, not both."
    }

    $lines = @(
        "listen=$Listen",
        "token_file=$TokenFile"
    )
    if ($DiscoveryListen) {
        $lines += "discovery_listen=$DiscoveryListen"
    }
    if ($Advertise) {
        $lines += "advertise=$Advertise"
    }
    $lines += "discovery_enabled=$(if ($NoDiscovery) { "false" } else { "true" })"
    if ($AllowUnsafePublicRelay) {
        $lines += "allow_unsafe_public_relay=true"
    }
    if ($TransportEncryption -ne "disabled" -or $PSBoundParameters.ContainsKey("TransportEncryption")) {
        $lines += "transport_encryption=$TransportEncryption"
    }
    if (-not [string]::IsNullOrWhiteSpace($TransportSecretFile)) {
        $lines += "transport_secret_file=$TransportSecretFile"
    }
    elseif (-not [string]::IsNullOrWhiteSpace($TransportSecret)) {
        $lines += "transport_secret=$TransportSecret"
    }

    return $lines -join [Environment]::NewLine
}

function New-SyncVisionHostServerConfigPath {
    param(
        [Parameter(Mandatory = $true)]
        [string]$RepoRoot
    )

    $runtimeDir = Join-Path $RepoRoot "build\runtime"
    New-Item -ItemType Directory -Force -Path $runtimeDir | Out-Null
    return Join-Path $runtimeDir ("start-host-server-$PID-$([Guid]::NewGuid().ToString("N")).conf")
}

function Write-SyncVisionHostServerConfig {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [string]$Text
    )

    [IO.File]::WriteAllText($Path, ($Text.TrimEnd() + [Environment]::NewLine), [Text.UTF8Encoding]::new($false))
}

function Resolve-SyncVisionTokenFilePath {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        throw "Token file was not found: $Path"
    }

    return (Resolve-Path -LiteralPath $Path).Path
}

function Resolve-SyncVisionTokenFromFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        throw "Token file was not found: $Path"
    }

    $tokenText = ([IO.File]::ReadAllText($Path)).Trim()
    if ([string]::IsNullOrWhiteSpace($tokenText)) {
        throw "Token file cannot be empty: $Path"
    }

    return $tokenText
}

function Resolve-SyncVisionSecretFilePath {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [string]$Name
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        throw "$Name was not found: $Path"
    }

    $resolvedPath = (Resolve-Path -LiteralPath $Path).Path
    $secretText = ([IO.File]::ReadAllText($resolvedPath)).Trim()
    if ([string]::IsNullOrWhiteSpace($secretText)) {
        throw "$Name cannot be empty: $Path"
    }

    return $resolvedPath
}

function New-SyncVisionHostClientArguments {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Device,

        [string]$Token = "dev-token",

        [string]$TokenFile = "",

        [string]$DeviceTokenFile = "",

        [string]$ClientConfig = "",

        [ValidateSet("disabled", "optional", "required")]
        [string]$TransportEncryption = "disabled",

        [string]$TransportSecret = "",

        [string]$TransportSecretFile = "",

        [string]$Server = "",

        [string]$ReceiveDir = "",

        [string[]]$AllowViewer = @(),

        [string[]]$GrantInputTo = @(),

        [switch]$DisableRemoteInput,

        [ValidateRange(1, 60)]
        [int]$CaptureFps = 10,

        [ValidateRange(1, 100)]
        [int]$JpegQuality = 70,

        [ValidatePattern('^(primary|all|[0-9]+)$')]
        [string]$CaptureMonitor = "primary",

        [ValidateSet("auto", "gdi", "desktop-duplication", "dxgi")]
        [string]$CaptureBackend = "auto"
    )

    $arguments = @("--mode", "host", "--device", $Device)
    if (-not [string]::IsNullOrWhiteSpace($ClientConfig)) {
        $arguments += @("--config", $ClientConfig)
    }
    elseif (-not [string]::IsNullOrWhiteSpace($TokenFile)) {
        $arguments += @("--token-file", $TokenFile)
    }
    else {
        $arguments += @("--token", $Token)
    }
    if (-not [string]::IsNullOrWhiteSpace($DeviceTokenFile)) {
        $arguments += @("--device-token-file", $DeviceTokenFile)
    }
    if ($TransportEncryption -ne "disabled" -or $PSBoundParameters.ContainsKey("TransportEncryption")) {
        $arguments += @("--transport-encryption", $TransportEncryption)
    }
    if (-not [string]::IsNullOrWhiteSpace($TransportSecret)) {
        $arguments += @("--transport-secret", $TransportSecret)
    }
    if (-not [string]::IsNullOrWhiteSpace($TransportSecretFile)) {
        $arguments += @("--transport-secret-file", $TransportSecretFile)
    }
    if ($Server) {
        $arguments += @("--server", $Server)
    }
    if ($ReceiveDir) {
        $arguments += @("--receive-dir", $ReceiveDir)
    }
    foreach ($viewer in $AllowViewer) {
        if (-not [string]::IsNullOrWhiteSpace($viewer)) {
            $arguments += @("--allow-viewer", $viewer)
        }
    }
    foreach ($viewer in $GrantInputTo) {
        if (-not [string]::IsNullOrWhiteSpace($viewer)) {
            $arguments += @("--grant-input-to", $viewer)
        }
    }
    if ($DisableRemoteInput) {
        $arguments += "--disable-remote-input"
    }
    $hasClientConfig = -not [string]::IsNullOrWhiteSpace($ClientConfig)
    if (-not $hasClientConfig -or $PSBoundParameters.ContainsKey("CaptureFps")) {
        $arguments += @("--capture-fps", ([string]$CaptureFps))
    }
    if (-not $hasClientConfig -or $PSBoundParameters.ContainsKey("JpegQuality")) {
        $arguments += @("--jpeg-quality", ([string]$JpegQuality))
    }
    if (-not $hasClientConfig -or $PSBoundParameters.ContainsKey("CaptureMonitor")) {
        $arguments += @("--capture-monitor", $CaptureMonitor)
    }
    if (-not $hasClientConfig -or $PSBoundParameters.ContainsKey("CaptureBackend")) {
        $arguments += @("--capture-backend", $CaptureBackend)
    }

    return $arguments
}

function Format-SyncVisionHostArgumentsForDisplay {
    param(
        [Parameter(Mandatory = $true)]
        [string[]]$Arguments
    )

    $display = New-Object System.Collections.Generic.List[string]
    $redactNext = $false
    foreach ($argument in $Arguments) {
        if ($redactNext) {
            $display.Add("<redacted>")
            $redactNext = $false
            continue
        }

        if ($argument -eq "--token" -or $argument -eq "--transport-secret") {
            $display.Add($argument)
            $redactNext = $true
            continue
        }

        if ($argument -match '^--token=' -or $argument -match '^--transport-secret=') {
            $display.Add(($argument -split '=', 2)[0] + "=<redacted>")
            continue
        }

        if ($argument.IndexOfAny([char[]]" `t`"") -ge 0) {
            $display.Add('"' + ($argument -replace '"', '\"') + '"')
        }
        else {
            $display.Add($argument)
        }
    }

    return $display -join " "
}

function Test-SyncVisionTcpEndpoint {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Endpoint,

        [int]$TimeoutMs = 250
    )

    $parts = Get-SyncVisionEndpointParts -Endpoint $Endpoint
    $client = [System.Net.Sockets.TcpClient]::new()
    try {
        $connectTask = $client.ConnectAsync($parts.Host, $parts.Port)
        return $connectTask.Wait($TimeoutMs) -and $client.Connected
    }
    catch {
        return $false
    }
    finally {
        $client.Close()
    }
}

function New-SyncVisionHostFrame {
    param(
        [Parameter(Mandatory = $true)]
        [UInt16]$Type,

        [Parameter(Mandatory = $true)]
        [byte[]]$Payload
    )

    $bytes = New-Object byte[] (12 + $Payload.Length)
    $bytes[0] = [byte][char]'S'
    $bytes[1] = [byte][char]'V'
    $bytes[2] = [byte][char]'P'
    $bytes[3] = [byte][char]'1'
    [BitConverter]::GetBytes([UInt16]1).CopyTo($bytes, 4)
    [BitConverter]::GetBytes($Type).CopyTo($bytes, 6)
    [BitConverter]::GetBytes([UInt32]$Payload.Length).CopyTo($bytes, 8)
    if ($Payload.Length -gt 0) {
        $Payload.CopyTo($bytes, 12)
    }

    return $bytes
}

function Read-SyncVisionHostExact {
    param(
        [Parameter(Mandatory = $true)]
        [System.Net.Sockets.NetworkStream]$Stream,

        [Parameter(Mandatory = $true)]
        [int]$Length
    )

    $buffer = New-Object byte[] $Length
    $offset = 0
    while ($offset -lt $Length) {
        $read = $Stream.Read($buffer, $offset, $Length - $offset)
        if ($read -le 0) {
            throw "Socket closed while reading $Length bytes."
        }

        $offset += $read
    }

    return $buffer
}

function Read-SyncVisionHostFrame {
    param(
        [Parameter(Mandatory = $true)]
        [System.Net.Sockets.TcpClient]$Client
    )

    $stream = $Client.GetStream()
    $header = Read-SyncVisionHostExact -Stream $stream -Length 12
    $magic = [Text.Encoding]::ASCII.GetString($header, 0, 4)
    if ($magic -ne "SVP1") {
        throw "Unexpected SyncVision frame magic: $magic"
    }

    $type = [BitConverter]::ToUInt16($header, 6)
    $length = [BitConverter]::ToUInt32($header, 8)
    if ($length -gt 65536) {
        throw "SyncVision status frame payload is too large."
    }

    $payload = Read-SyncVisionHostExact -Stream $stream -Length ([int]$length)
    return [pscustomobject]@{
        Type = $type
        Payload = [Text.Encoding]::UTF8.GetString($payload)
    }
}

function Test-SyncVisionHostServerReady {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Endpoint,

        [Parameter(Mandatory = $true)]
        [string]$Token,

        [int]$TimeoutMs = 500,

        [ValidateSet("disabled", "optional", "required")]
        [string]$TransportEncryption = "disabled",

        [string]$TransportSecret = "",

        [string]$TransportSecretFile = ""
    )

    if ($TransportEncryption -ne "disabled") {
        $statusScript = Join-Path $PSScriptRoot "status-server.ps1"
        $probeTimeoutMs = [Math]::Max(1000, $TimeoutMs)
        $probeArgs = @(
            "-NoProfile",
            "-ExecutionPolicy",
            "Bypass",
            "-File",
            $statusScript,
            "-Probe",
            "-Server",
            $Endpoint,
            "-Token",
            $Token,
            "-TimeoutMs",
            ([string]$probeTimeoutMs),
            "-TransportEncryption",
            $TransportEncryption
        )
        if (-not [string]::IsNullOrWhiteSpace($TransportSecretFile)) {
            $probeArgs += @("-TransportSecretFile", $TransportSecretFile)
        }
        elseif (-not [string]::IsNullOrWhiteSpace($TransportSecret)) {
            $probeArgs += @("-TransportSecret", $TransportSecret)
        }

        $probeOut = [IO.Path]::GetTempFileName()
        $probeErr = [IO.Path]::GetTempFileName()
        try {
            $probe = Start-Process -FilePath "powershell.exe" `
                -ArgumentList $probeArgs `
                -RedirectStandardOutput $probeOut `
                -RedirectStandardError $probeErr `
                -Wait `
                -PassThru `
                -WindowStyle Hidden
            return $probe.ExitCode -eq 0
        }
        finally {
            Remove-Item -LiteralPath $probeOut, $probeErr -Force -ErrorAction SilentlyContinue
        }
    }

    $parts = Get-SyncVisionEndpointParts -Endpoint $Endpoint
    $client = [System.Net.Sockets.TcpClient]::new()
    try {
        $connectTask = $client.ConnectAsync($parts.Host, $parts.Port)
        if (-not $connectTask.Wait($TimeoutMs)) {
            return $false
        }

        $client.ReceiveTimeout = $TimeoutMs
        $client.SendTimeout = $TimeoutMs
        $payload = [Text.Encoding]::UTF8.GetBytes("token=$Token`n")
        $frame = New-SyncVisionHostFrame -Type 11 -Payload $payload
        $stream = $client.GetStream()
        $stream.Write($frame, 0, $frame.Length)
        $stream.Flush()

        $response = Read-SyncVisionHostFrame -Client $client
        return $response.Type -eq 12
    }
    catch {
        return $false
    }
    finally {
        $client.Close()
    }
}

function Wait-SyncVisionHostServerReady {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Endpoint,

        [Parameter(Mandatory = $true)]
        [string]$Token,

        [int]$TimeoutMs = 3000,

        [ValidateSet("disabled", "optional", "required")]
        [string]$TransportEncryption = "disabled",

        [string]$TransportSecret = "",

        [string]$TransportSecretFile = ""
    )

    $deadline = [DateTime]::UtcNow.AddMilliseconds($TimeoutMs)
    while ([DateTime]::UtcNow -lt $deadline) {
        if (Test-SyncVisionHostServerReady `
                -Endpoint $Endpoint `
                -Token $Token `
                -TimeoutMs 250 `
                -TransportEncryption $TransportEncryption `
                -TransportSecret $TransportSecret `
                -TransportSecretFile $TransportSecretFile) {
            return
        }

        Start-Sleep -Milliseconds 100
    }

    throw "Timed out waiting for SyncVision status response at $Endpoint."
}

function Get-SyncVisionLogTail {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    if (-not (Test-Path -LiteralPath $Path)) {
        return "<missing: $Path>"
    }

    return (Get-Content -LiteralPath $Path -Tail 40) -join [Environment]::NewLine
}

if ($TestMode) {
    return
}

if (-not [string]::IsNullOrWhiteSpace($TokenFile) -and $PSBoundParameters.ContainsKey("Token")) {
    throw "Pass either -Token or -TokenFile, not both."
}
if (-not [string]::IsNullOrWhiteSpace($ClientConfig) -and
    (-not [string]::IsNullOrWhiteSpace($TokenFile) -or $PSBoundParameters.ContainsKey("Token"))) {
    throw "Pass either -ClientConfig or token options, not both."
}
if (-not [string]::IsNullOrWhiteSpace($TransportSecret) -and -not [string]::IsNullOrWhiteSpace($TransportSecretFile)) {
    throw "Pass either -TransportSecret or -TransportSecretFile, not both."
}
if ($Lan -and $NoDiscovery) {
    throw "Pass either -Lan or -NoDiscovery, not both."
}

$resolvedTokenFile = if (-not [string]::IsNullOrWhiteSpace($TokenFile)) {
    Resolve-SyncVisionTokenFilePath -Path $TokenFile
}
else {
    ""
}
$resolvedDeviceTokenFile = if (-not [string]::IsNullOrWhiteSpace($DeviceTokenFile)) {
    if (-not (Test-Path -LiteralPath $DeviceTokenFile -PathType Leaf)) {
        throw "Device token file was not found: $DeviceTokenFile"
    }
    (Resolve-Path -LiteralPath $DeviceTokenFile).Path
}
else {
    ""
}
$resolvedTransportSecretFile = if (-not [string]::IsNullOrWhiteSpace($TransportSecretFile)) {
    Resolve-SyncVisionSecretFilePath -Path $TransportSecretFile -Name "TransportSecretFile"
}
else {
    ""
}
Assert-SyncVisionHostTransportSecretAllowed `
    -TransportEncryption $TransportEncryption `
    -TransportSecret $TransportSecret `
    -TransportSecretFile $resolvedTransportSecretFile
$effectiveToken = if (-not [string]::IsNullOrWhiteSpace($TokenFile)) {
    Resolve-SyncVisionTokenFromFile -Path $resolvedTokenFile
}
else {
    $Token
}

$effectiveListen = Resolve-SyncVisionHostListen `
    -Listen $Listen `
    -Lan:$Lan `
    -ListenExplicit:$PSBoundParameters.ContainsKey("Listen")
$effectiveAdvertise = Resolve-SyncVisionHostAdvertise `
    -Listen $effectiveListen `
    -Advertise $Advertise `
    -NoDiscovery:$NoDiscovery `
    -Lan:$Lan
$effectiveDiscoveryListen = if ($Lan -and [string]::IsNullOrWhiteSpace($DiscoveryListen)) {
    "0.0.0.0:38581"
}
else {
    $DiscoveryListen
}

$repoRoot = Split-Path -Parent $PSScriptRoot
$runtimeDir = Join-Path $repoRoot "build\x64\$Configuration"
$privateClientExe = Join-Path $runtimeDir "private-runtime\SyncVisionClient.exe"
$clientExe = if (Test-Path -LiteralPath $privateClientExe -PathType Leaf) {
    $privateClientExe
}
else {
    Join-Path $runtimeDir "SyncVisionClient.exe"
}
$serverExe = Join-Path $runtimeDir "SyncVisionServer.exe"

if (-not (Test-Path -LiteralPath $clientExe)) {
    throw "SyncVisionClient.exe was not found at $clientExe. Run scripts\build.ps1 -Configuration $Configuration first."
}

$startedServer = $null
$serverOutLog = ""
$serverErrLog = ""
$clientServer = $Server
$serverConfigPath = ""

try {
    if (-not $clientServer -and -not $NoAutoServer -and [string]::IsNullOrWhiteSpace($ClientConfig)) {
        if (-not (Test-Path -LiteralPath $serverExe)) {
            throw "SyncVisionServer.exe was not found at $serverExe. Run scripts\build.ps1 -Configuration $Configuration first."
        }

        Assert-SyncVisionHostTokenAllowed -Listen $effectiveListen -Token $effectiveToken
        Assert-SyncVisionHostDiscoveryAdvertiseAllowed -Listen $effectiveListen -Advertise $effectiveAdvertise -NoDiscovery:$NoDiscovery
        Assert-SyncVisionHostUnsafePublicRelayAcknowledged -Listen $effectiveListen -AllowUnsafePublicRelay ([bool]$AllowUnsafePublicRelay)
        $clientServer = Resolve-SyncVisionHostClientEndpoint -Listen $effectiveListen

        if (Test-SyncVisionHostServerReady `
                -Endpoint $clientServer `
                -Token $effectiveToken `
                -TimeoutMs 500 `
                -TransportEncryption $TransportEncryption `
                -TransportSecret $TransportSecret `
                -TransportSecretFile $resolvedTransportSecretFile) {
            Write-Host "Using existing local SyncVision server at $clientServer"
        }
        elseif (Test-SyncVisionTcpEndpoint -Endpoint $clientServer -TimeoutMs 250) {
            throw "A TCP listener is already using $clientServer, but it did not answer the SyncVision status probe with the supplied token."
        }
        else {
            $logDir = Join-Path $repoRoot "build\logs"
            New-Item -ItemType Directory -Force -Path $logDir | Out-Null
            $serverOutLog = Join-Path $logDir "start-host-server.out.log"
            $serverErrLog = Join-Path $logDir "start-host-server.err.log"
            foreach ($path in @($serverOutLog, $serverErrLog)) {
                if (Test-Path -LiteralPath $path) {
                    Remove-Item -LiteralPath $path
                }
            }

            if ($resolvedTokenFile) {
                $serverConfigPath = New-SyncVisionHostServerConfigPath -RepoRoot $repoRoot
                $serverConfigText = New-SyncVisionHostServerConfigText `
                    -Listen $effectiveListen `
                    -TokenFile $resolvedTokenFile `
                    -DiscoveryListen $effectiveDiscoveryListen `
                    -Advertise $effectiveAdvertise `
                    -AllowUnsafePublicRelay ([bool]$AllowUnsafePublicRelay) `
                    -TransportEncryption $TransportEncryption `
                    -TransportSecret $TransportSecret `
                    -TransportSecretFile $resolvedTransportSecretFile `
                    -NoDiscovery:$NoDiscovery
                Write-SyncVisionHostServerConfig -Path $serverConfigPath -Text $serverConfigText
            }

            $serverArguments = New-SyncVisionHostServerArguments `
                -Listen $effectiveListen `
                -Token $effectiveToken `
                -TokenFile $resolvedTokenFile `
                -ConfigPath $serverConfigPath `
                -DiscoveryListen $effectiveDiscoveryListen `
                -Advertise $effectiveAdvertise `
                -AllowUnsafePublicRelay ([bool]$AllowUnsafePublicRelay) `
                -TransportEncryption $TransportEncryption `
                -TransportSecret $TransportSecret `
                -TransportSecretFile $resolvedTransportSecretFile `
                -NoDiscovery:$NoDiscovery

            Write-Host "Starting local SyncVisionServer.exe $(Format-SyncVisionHostArgumentsForDisplay -Arguments $serverArguments)"
            $startedServer = Start-Process -FilePath $serverExe `
                -ArgumentList $serverArguments `
                -RedirectStandardOutput $serverOutLog `
                -RedirectStandardError $serverErrLog `
                -PassThru `
                -WindowStyle Hidden

            try {
                Wait-SyncVisionHostServerReady `
                    -Endpoint $clientServer `
                    -Token $effectiveToken `
                    -TimeoutMs 5000 `
                    -TransportEncryption $TransportEncryption `
                    -TransportSecret $TransportSecret `
                    -TransportSecretFile $resolvedTransportSecretFile
            }
            catch {
                $serverStatus = if ($startedServer.HasExited) { "exited=$($startedServer.ExitCode)" } else { "still running" }
                throw "$($_.Exception.Message) Server process $serverStatus. stdout:`n$(Get-SyncVisionLogTail -Path $serverOutLog)`nstderr:`n$(Get-SyncVisionLogTail -Path $serverErrLog)"
            }
        }
    }

    $clientArgumentParameters = @{
        Device = $Device
        Token = $Token
        TokenFile = $TokenFile
        DeviceTokenFile = $resolvedDeviceTokenFile
        ClientConfig = $ClientConfig
        Server = $clientServer
        ReceiveDir = $ReceiveDir
        AllowViewer = $AllowViewer
        GrantInputTo = $GrantInputTo
        DisableRemoteInput = $DisableRemoteInput
    }
    if ($PSBoundParameters.ContainsKey("CaptureFps")) {
        $clientArgumentParameters.CaptureFps = $CaptureFps
    }
    if ($PSBoundParameters.ContainsKey("JpegQuality")) {
        $clientArgumentParameters.JpegQuality = $JpegQuality
    }
    if ($PSBoundParameters.ContainsKey("CaptureMonitor")) {
        $clientArgumentParameters.CaptureMonitor = $CaptureMonitor
    }
    if ($PSBoundParameters.ContainsKey("CaptureBackend")) {
        $clientArgumentParameters.CaptureBackend = $CaptureBackend
    }
    if ($PSBoundParameters.ContainsKey("TransportEncryption")) {
        $clientArgumentParameters.TransportEncryption = $TransportEncryption
    }
    if ($PSBoundParameters.ContainsKey("TransportSecret")) {
        $clientArgumentParameters.TransportSecret = $TransportSecret
    }
    if ($PSBoundParameters.ContainsKey("TransportSecretFile")) {
        $clientArgumentParameters.TransportSecretFile = $resolvedTransportSecretFile
    }

    $arguments = New-SyncVisionHostClientArguments @clientArgumentParameters
    $serverText = if ($ClientConfig) { "client config $ClientConfig" } elseif ($clientServer) { $clientServer } else { "LAN discovery, fallback 127.0.0.1:38580" }
    Write-Host "Starting SyncVisionClient.exe host device=$Device server=$serverText"
    & $clientExe @arguments
    $exitCode = if ($null -ne $LASTEXITCODE) { $LASTEXITCODE } else { 0 }
}
finally {
    if ($startedServer -and -not $startedServer.HasExited) {
        Stop-Process -Id $startedServer.Id -Force
        $startedServer.WaitForExit()
    }
    if ($serverConfigPath -and (Test-Path -LiteralPath $serverConfigPath)) {
        Remove-Item -LiteralPath $serverConfigPath -Force -ErrorAction SilentlyContinue
    }
}

if ($exitCode -ne 0) {
    exit $exitCode
}
