param(
    [string]$Server = "",

    [string]$Token = "",

    [string]$TokenFile = "",

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

    [string]$TransportSecret = "",

    [string]$TransportSecretFile = "",

    [int]$TimeoutMs = 5000,

    [int]$MinHosts = 0,

    [int]$MinViewers = 0,

    [UInt64]$MinRelayedFrames = 0,

    [UInt64]$MinRelayedInputs = 0,

    [UInt64]$MinRelayedClipboards = 0,

    [UInt64]$MinRelayedFileMessages = 0,

    [switch]$AllowPrivateRelayAddress,

    [switch]$TestMode
)

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

function ConvertFrom-SyncVisionPublicRelayStatusPayload {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Payload
    )

    $status = @{}
    foreach ($rawLine in ($Payload -split "`r?`n")) {
        $line = $rawLine.Trim()
        if (-not $line) {
            continue
        }

        $separator = $line.IndexOf("=")
        if ($separator -le 0) {
            throw "Invalid SyncVision status line: $rawLine"
        }

        $key = $line.Substring(0, $separator).Trim()
        $value = $line.Substring($separator + 1).Trim()
        $status[$key] = $value
    }

    return $status
}

function Get-SyncVisionPublicRelayHost {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Server
    )

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

    return $Server.Substring(0, $separator).Trim().Trim("[", "]")
}

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

    return (Get-SyncVisionPublicRelayHostScope -HostName $HostName) -eq "Local"
}

function Test-SyncVisionPrivateRelayAddress {
    param(
        [Parameter(Mandatory = $true)]
        [System.Net.IPAddress]$Address
    )

    if ($Address.Equals([System.Net.IPAddress]::Any) -or
        $Address.Equals([System.Net.IPAddress]::IPv6Any) -or
        $Address.Equals([System.Net.IPAddress]::Broadcast) -or
        $Address.Equals([System.Net.IPAddress]::None) -or
        [System.Net.IPAddress]::IsLoopback($Address)) {
        return $true
    }

    $bytes = $Address.GetAddressBytes()
    if ($Address.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) {
        return (
            $bytes[0] -eq 0 -or
            $bytes[0] -eq 10 -or
            $bytes[0] -eq 127 -or
            ($bytes[0] -eq 169 -and $bytes[1] -eq 254) -or
            ($bytes[0] -eq 172 -and $bytes[1] -ge 16 -and $bytes[1] -le 31) -or
            ($bytes[0] -eq 192 -and $bytes[1] -eq 168) -or
            ($bytes[0] -eq 100 -and $bytes[1] -ge 64 -and $bytes[1] -le 127)
        )
    }

    if ($Address.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6) {
        return (
            $Address.IsIPv6LinkLocal -or
            $Address.IsIPv6SiteLocal -or
            $Address.IsIPv6Multicast -or
            (($bytes[0] -band 0xfe) -eq 0xfc)
        )
    }

    return $false
}

function Resolve-SyncVisionPublicRelayHostAddresses {
    param(
        [Parameter(Mandatory = $true)]
        [string]$HostName
    )

    $normalized = $HostName.Trim()
    if ([string]::IsNullOrWhiteSpace($normalized) -or $normalized -eq "*") {
        return @([System.Net.IPAddress]::Any)
    }

    $ipAddress = [System.Net.IPAddress]::None
    if ([System.Net.IPAddress]::TryParse($normalized, [ref]$ipAddress)) {
        return @($ipAddress)
    }

    try {
        return @([System.Net.Dns]::GetHostAddresses($normalized))
    }
    catch {
        return @()
    }
}

function Get-SyncVisionPublicRelayHostScope {
    param(
        [Parameter(Mandatory = $true)]
        [string]$HostName
    )

    $addresses = @(Resolve-SyncVisionPublicRelayHostAddresses -HostName $HostName)
    if ($addresses.Count -eq 0) {
        return "Unknown"
    }

    foreach ($address in $addresses) {
        if (Test-SyncVisionPrivateRelayAddress -Address $address) {
            return "Local"
        }
    }

    return "Public"
}

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

        [switch]$AllowPrivateRelayAddress
    )

    if ($AllowPrivateRelayAddress) {
        return
    }

    $hostName = Get-SyncVisionPublicRelayHost -Server $Server
    $hostScope = Get-SyncVisionPublicRelayHostScope -HostName $hostName
    if ($hostScope -eq "Unknown") {
        throw "Public relay verification endpoint '$Server' hostname could not be resolved. Use -AllowPrivateRelayAddress only when intentionally using this verifier for LAN/non-public validation."
    }
    if ($hostScope -eq "Local") {
        throw "Public relay verification endpoint '$Server' is local, wildcard, or private. Use -AllowPrivateRelayAddress only when intentionally using this verifier for LAN/non-public validation."
    }
}

function Get-SyncVisionPublicRelayStatusCounter {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$Status,

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

    if (-not $Status.ContainsKey($Name)) {
        throw "Status payload did not include '$Name'."
    }

    $actual = [UInt64]0
    if (-not [UInt64]::TryParse([string]$Status[$Name], [ref]$actual)) {
        throw "Status counter '$Name' was not an unsigned integer: $($Status[$Name])"
    }

    return $actual
}

function Assert-SyncVisionPublicRelayMinimumCounter {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$Status,

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

        [UInt64]$Minimum = 0
    )

    if ($Minimum -eq 0) {
        return
    }

    $actual = Get-SyncVisionPublicRelayStatusCounter -Status $Status -Name $Name
    if ($actual -lt $Minimum) {
        throw "Status counter '$Name' was $actual, expected at least $Minimum."
    }
}

function Assert-SyncVisionPublicRelayStatusThresholds {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$Status,

        [int]$MinHosts = 0,

        [int]$MinViewers = 0,

        [UInt64]$MinRelayedFrames = 0,

        [UInt64]$MinRelayedInputs = 0,

        [UInt64]$MinRelayedClipboards = 0,

        [UInt64]$MinRelayedFileMessages = 0
    )

    if (-not $Status.ContainsKey("status") -or $Status["status"] -ne "ok") {
        $actualStatus = if ($Status.ContainsKey("status")) { $Status["status"] } else { "<missing>" }
        throw "Public relay status was '$actualStatus', expected 'ok'."
    }

    Assert-SyncVisionPublicRelayMinimumCounter -Status $Status -Name "hosts" -Minimum ([UInt64]$MinHosts)
    Assert-SyncVisionPublicRelayMinimumCounter -Status $Status -Name "viewers" -Minimum ([UInt64]$MinViewers)
    Assert-SyncVisionPublicRelayMinimumCounter -Status $Status -Name "relayed_frames" -Minimum $MinRelayedFrames
    Assert-SyncVisionPublicRelayMinimumCounter -Status $Status -Name "relayed_inputs" -Minimum $MinRelayedInputs
    Assert-SyncVisionPublicRelayMinimumCounter -Status $Status -Name "relayed_clipboards" -Minimum $MinRelayedClipboards
    Assert-SyncVisionPublicRelayMinimumCounter -Status $Status -Name "relayed_file_messages" -Minimum $MinRelayedFileMessages
}

function Format-SyncVisionPublicRelayProbeResult {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Server,

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

        [switch]$AllowPrivateRelayAddress
    )

    if ($AllowPrivateRelayAddress) {
        return @(
            "SyncVision non-public relay probe passed.",
            "Server: $Server",
            "ProbeScope: LAN/non-public validation only; this does not prove external public reachability.",
            "LiveStatus:",
            $Payload
        )
    }

    return @(
        "SyncVision public relay probe passed.",
        "Server: $Server",
        "ProbeScope: external client network running this script; this proves reachability only from this client network path.",
        "LiveStatus:",
        $Payload
    )
}

function Resolve-SyncVisionPublicRelayToken {
    param(
        [AllowEmptyString()]
        [string]$Token = "",

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

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

    if (-not [string]::IsNullOrWhiteSpace($TokenFile)) {
        if (-not (Test-Path -LiteralPath $TokenFile -PathType Leaf)) {
            throw "Public relay token file was not found: $TokenFile"
        }

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

        return $tokenText
    }

    if (-not [string]::IsNullOrWhiteSpace($Token)) {
        return $Token
    }

    throw "Public relay verification requires -TokenFile or -Token."
}

if ($TestMode) {
    return
}

$relayEndpoint = $Server
$tokenArgument = $Token
$tokenFileArgument = $TokenFile
$transportEncryptionArgument = $TransportEncryption
$transportSecretArgument = $TransportSecret
$transportSecretFileArgument = $TransportSecretFile
$probeTimeoutMs = $TimeoutMs

. (Join-Path $PSScriptRoot "status-server.ps1") -TestMode

if ([string]::IsNullOrWhiteSpace($relayEndpoint)) {
    throw "Public relay verification requires -Server."
}
Assert-SyncVisionPublicRelayEndpointAllowed -Server $relayEndpoint -AllowPrivateRelayAddress:$AllowPrivateRelayAddress
$probeToken = Resolve-SyncVisionPublicRelayToken -Token $tokenArgument -TokenFile $tokenFileArgument
$statusFrame = Read-SyncVisionStatus `
    -Endpoint $relayEndpoint `
    -Token $probeToken `
    -TimeoutMs $probeTimeoutMs `
    -TransportEncryption $transportEncryptionArgument `
    -TransportSecret $transportSecretArgument `
    -TransportSecretFile $transportSecretFileArgument
if ($statusFrame.Type -ne 12) {
    throw "SyncVision public relay probe returned unexpected status frame type $($statusFrame.Type): $($statusFrame.Payload)"
}

$status = ConvertFrom-SyncVisionPublicRelayStatusPayload -Payload $statusFrame.Payload
Assert-SyncVisionPublicRelayStatusThresholds `
    -Status $status `
    -MinHosts $MinHosts `
    -MinViewers $MinViewers `
    -MinRelayedFrames $MinRelayedFrames `
    -MinRelayedInputs $MinRelayedInputs `
    -MinRelayedClipboards $MinRelayedClipboards `
    -MinRelayedFileMessages $MinRelayedFileMessages

Format-SyncVisionPublicRelayProbeResult `
    -Server $relayEndpoint `
    -Payload $statusFrame.Payload `
    -AllowPrivateRelayAddress:$AllowPrivateRelayAddress |
    ForEach-Object { Write-Host $_ }
