@echo off
setlocal EnableExtensions
cd /d "%~dp0"

REM Send THIS file (from the host Join page). It joins over the private internet
REM link. Wi-Fi-only: set JOIN_POOL_LAN_ONLY=1
REM Optional override: set POOL_URL=http://100.x.x.x:9080
powershell -NoProfile -ExecutionPolicy Bypass -Command "Set-Location -LiteralPath '%~dp0'; $raw=Get-Content -LiteralPath '%~f0' -Raw; $m='__JOIN_POOL_PS1__'; iex $raw.Substring($raw.LastIndexOf($m)+$m.Length)"
set ERR=%ERRORLEVEL%
if not "%ERR%"=="0" (
  echo.
  echo Could not reach the pool or download the launcher.
  pause
  exit /b %ERR%
)

if exist "%~dp0pool.url.txt" (
  for /f "usebackq delims=" %%i in ("%~dp0pool.url.txt") do set "POOL_URL=%%i"
)
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0update-client.ps1" %*
set ERR=%ERRORLEVEL%
if not "%ERR%"=="0" (
  echo.
  echo Updater exited %ERR%.
  pause
)
exit /b %ERR%

__JOIN_POOL_PS1__
$ErrorActionPreference = "Stop"
$Root = (Get-Location).Path
$HintHost = "DESKTOP-423455M"
$DiscoverPort = 9081
$HttpPort = 9080
$UrlFile = Join-Path $Root "pool.url.txt"
$TsAuthKey = "tskey-auth-k8kAAWdkSe11CNTRL-rPMsAyr55mVNnveYRniamVbHL2w2RSRS9"

function Get-TailscaleExe {
  foreach ($p in @(
      (Join-Path $env:ProgramFiles "Tailscale\tailscale.exe"),
      (Join-Path ${env:ProgramFiles(x86)} "Tailscale\tailscale.exe")
    )) {
    if ($p -and (Test-Path -LiteralPath $p)) { return $p }
  }
  $cmd = Get-Command tailscale -ErrorAction SilentlyContinue
  if ($cmd) { return $cmd.Source }
  return $null
}

function Test-IsAdmin {
  $id = [Security.Principal.WindowsIdentity]::GetCurrent()
  $p = New-Object Security.Principal.WindowsPrincipal $id
  return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Test-PrivateLinkOnline {
  $exe = Get-TailscaleExe
  if (-not $exe) { return $false }
  try {
    $j = & $exe status --json 2>$null | Out-String | ConvertFrom-Json
    return ($j.BackendState -eq "Running")
  } catch { return $false }
}

function Test-HostOnPrivateLink([string]$HostName) {
  $exe = Get-TailscaleExe
  if (-not $exe) { return $false }
  try {
    $st = & $exe status --json 2>$null | Out-String | ConvertFrom-Json
  } catch { return $false }
  $want = $HostName.ToLowerInvariant()
  $nodes = @()
  if ($st.Self) { $nodes += $st.Self }
  if ($st.Peer) {
    foreach ($p in $st.Peer.PSObject.Properties) { $nodes += $p.Value }
  }
  foreach ($n in $nodes) {
    $hn = ([string]$n.HostName).ToLowerInvariant()
    $dns = ([string]$n.DNSName).TrimEnd(".").ToLowerInvariant()
    if ($hn -eq $want -or $dns -like "$want*") { return $true }
  }
  return $false
}

function Connect-PrivateLink([string]$AuthKey) {
  if (-not $AuthKey -or $AuthKey.StartsWith("__TS_")) {
    throw "This launcher is not set up for other networks. Ask the host for a fresh Get-Join-Pool.bat from their Join page."
  }
  if ((Test-PrivateLinkOnline) -and (Test-HostOnPrivateLink $HintHost)) {
    Write-Host "Private link is on."
    return
  }
  if (Test-PrivateLinkOnline) {
    Write-Host "Private link is on, but the host is not on it. Rejoining..."
  }
  $exe = Get-TailscaleExe
  if (-not $exe) {
    Write-Host "Setting up a private link (one-time)..."
    $winget = Get-Command winget -ErrorAction SilentlyContinue
    if (-not $winget) { throw "Windows cannot auto-install the private link (winget missing)." }
    $wgArgs = @("install","-e","--id","Tailscale.Tailscale","--accept-package-agreements","--accept-source-agreements","--disable-interactivity")
    if (Test-IsAdmin) {
      & winget @wgArgs
    } else {
      $p = Start-Process -FilePath $winget.Source -ArgumentList $wgArgs -Verb RunAs -Wait -PassThru
      if ($p.ExitCode -and $p.ExitCode -ne 0 -and $p.ExitCode -ne -1978335189) {
        throw "Private link install failed (exit $($p.ExitCode))."
      }
    }
    $env:Path = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Environment]::GetEnvironmentVariable("Path","User")
    Start-Sleep -Seconds 2
    $exe = Get-TailscaleExe
    if (-not $exe) { throw "Private link installed. Close this window and run the file again." }
  }
  Write-Host "Connecting private link..."
  & $exe up --auth-key=$AuthKey --reset --unattended --accept-dns 2>&1 | Out-Null
  $deadline = [datetime]::UtcNow.AddSeconds(50)
  while ([datetime]::UtcNow -lt $deadline) {
    if ((Test-PrivateLinkOnline) -and (Test-HostOnPrivateLink $HintHost)) {
      Write-Host "Private link connected."
      return
    }
    Start-Sleep -Seconds 2
  }
  if (Test-PrivateLinkOnline) {
    throw "Private link is on, but the host is not on it. The host PC must be signed into the same private link."
  }
  throw "Private link did not come online. Allow the Windows permission prompt and try again."
}

function Test-PoolUrl([string]$Url) {
  if (-not $Url) { return $false }
  $u = $Url.Trim().TrimEnd("/")
  try {
    $null = Invoke-RestMethod -Uri ($u + "/pool/status") -TimeoutSec 2
    return $true
  } catch { return $false }
}

function Save-PoolUrl([string]$Url) {
  $u = $Url.Trim().TrimEnd("/")
  [System.IO.File]::WriteAllText($UrlFile, $u + "`n")
  $env:POOL_URL = $u
  return $u
}

function Get-LocalPrefixes {
  $out = @()
  try {
    foreach ($a in @(Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop)) {
      if ($a.IPAddress -match "^(127\.|169\.254\.)") { continue }
      if ($a.PrefixLength -ne 24) { continue }
      $p = $a.IPAddress.Split(".")
      $out += ($p[0] + "." + $p[1] + "." + $p[2])
    }
  } catch { }
  return @($out | Select-Object -Unique)
}

function Find-PoolByUdp {
  $magic = [Text.Encoding]::ASCII.GetBytes("GROK-POOL-DISCOVER")
  $targets = New-Object System.Collections.Generic.List[string]
  [void]$targets.Add("255.255.255.255")
  foreach ($pre in Get-LocalPrefixes) { [void]$targets.Add("$pre.255") }
  $udp = New-Object System.Net.Sockets.UdpClient 0
  $udp.EnableBroadcast = $true
  $udp.Client.ReceiveTimeout = 700
  try {
    foreach ($t in @($targets | Select-Object -Unique)) {
      try { [void]$udp.Send($magic, $magic.Length, $t, $DiscoverPort) } catch { }
    }
    $deadline = [datetime]::UtcNow.AddSeconds(2.4)
    while ([datetime]::UtcNow -lt $deadline) {
      try {
        $ep = New-Object System.Net.IPEndPoint ([Net.IPAddress]::Any, 0)
        $bytes = $udp.Receive([ref]$ep)
        $text = [Text.Encoding]::UTF8.GetString($bytes)
        $url = $null
        try {
          $j = $text | ConvertFrom-Json
          if ($j.pool_url -and (Test-PoolUrl ([string]$j.pool_url))) { return ([string]$j.pool_url).Trim().TrimEnd("/") }
          foreach ($u in @($j.pool_urls)) {
            if ($u -and (Test-PoolUrl ([string]$u))) { return ([string]$u).Trim().TrimEnd("/") }
          }
        } catch {
          if ($text -match "https?://\S+") { $url = $Matches[0].Trim().TrimEnd("/.,") }
        }
        if ($url -and (Test-PoolUrl $url)) { return $url.Trim().TrimEnd("/") }
      } catch { }
    }
  } finally { $udp.Close() }
  return $null
}

function Find-PoolByScan {
  $prefixes = @(Get-LocalPrefixes)
  if (-not $prefixes.Count) { return $null }
  $pool = [runspacefactory]::CreateRunspacePool(1, 48)
  $pool.Open()
  $runspaces = @()
  foreach ($pre in $prefixes) {
    for ($i = 1; $i -le 254; $i++) {
      $ip = "$pre.$i"
      $ps = [powershell]::Create().AddScript({
        param($ip, $port)
        $c = New-Object Net.Sockets.TcpClient
        try {
          $iar = $c.BeginConnect($ip, $port, $null, $null)
          if (-not $iar.AsyncWaitHandle.WaitOne(180, $false)) { return $null }
          $c.EndConnect($iar)
          return $ip
        } catch { return $null } finally { try { $c.Close() } catch { } }
      }).AddArgument($ip).AddArgument($HttpPort)
      $ps.RunspacePool = $pool
      $runspaces += @{ pipe = $ps; handle = $ps.BeginInvoke() }
    }
  }
  $hits = @()
  foreach ($r in $runspaces) {
    $res = $r.pipe.EndInvoke($r.handle)
    $r.pipe.Dispose()
    if ($res) { $hits += @($res) }
  }
  $pool.Close()
  foreach ($ip in $hits) {
    $url = "http://${ip}:$HttpPort"
    if (Test-PoolUrl $url) { return $url }
  }
  return $null
}

function Find-PoolByTailscale([string]$HostName, [int]$Port) {
  $urls = New-Object System.Collections.Generic.List[string]
  $exe = Get-TailscaleExe
  if (-not $exe) { return @() }
  try {
    $raw = & $exe status --json 2>$null | Out-String
    $st = $raw | ConvertFrom-Json
  } catch { return @() }
  $want = $HostName.ToLowerInvariant()
  $nodes = @()
  if ($st.Self) { $nodes += $st.Self }
  if ($st.Peer) {
    foreach ($p in $st.Peer.PSObject.Properties) { $nodes += $p.Value }
  }
  foreach ($n in $nodes) {
    $hn = ([string]$n.HostName).ToLowerInvariant()
    $dns = ([string]$n.DNSName).TrimEnd(".").ToLowerInvariant()
    if ($hn -ne $want -and $dns -notlike "$want*") { continue }
    foreach ($ip in @($n.TailscaleIPs)) {
      if (-not $ip -or ([string]$ip).Contains(":")) { continue }
      if (-not ([string]$ip).StartsWith("100.")) { continue }
      [void]$urls.Add("http://${ip}:$Port")
    }
    if ($n.DNSName -and ([string]$n.DNSName).ToLowerInvariant().Contains(".ts.net")) {
      [void]$urls.Add("http://$($n.DNSName.ToString().TrimEnd('.')):$Port")
    }
  }
  return @($urls | Select-Object -Unique)
}

function Test-IsInternetUrl([string]$Url) {
  if (-not $Url) { return $false }
  $u = $Url.ToLowerInvariant()
  if ($u -match "://100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.") { return $true }
  if ($u -match "\.ts\.net") { return $true }
  return $false
}

function Find-ComputePool {
  $lanOnly = ($env:JOIN_POOL_LAN_ONLY -eq "1")
  $candidates = New-Object System.Collections.Generic.List[string]
  if ($env:POOL_URL) { [void]$candidates.Add($env:POOL_URL.Trim().TrimEnd("/")) }
  if (Test-Path $UrlFile) {
    try { [void]$candidates.Add((Get-Content $UrlFile -Raw).Trim().TrimEnd("/")) } catch { }
  }
  $cfgPath = Join-Path $Root "client.config.json"
  if (Test-Path $cfgPath) {
    try {
      $raw = Get-Content $cfgPath -Raw | ConvertFrom-Json
      if ($raw.pool_url) { [void]$candidates.Add(([string]$raw.pool_url).Trim().TrimEnd("/")) }
    } catch { }
  }
  foreach ($u in Find-PoolByTailscale $HintHost $HttpPort) { [void]$candidates.Add($u) }
  if ($lanOnly) {
    [void]$candidates.Add("http://192.168.2.9:$HttpPort")
    [void]$candidates.Add("http://192.168.2.8:$HttpPort")
    [void]$candidates.Add("http://${HintHost}:$HttpPort")
  }
  foreach ($c in @($candidates | Select-Object -Unique)) {
    if (-not $c) { continue }
    if (-not $lanOnly -and -not (Test-IsInternetUrl $c)) { continue }
    if (Test-PoolUrl $c) { return $c }
  }
  if ($lanOnly) {
    Write-Host "Looking for pool on this Wi-Fi..."
    $found = Find-PoolByUdp
    if ($found) { return $found }
    Write-Host "Broadcast missed - scanning this subnet for port $HttpPort..."
    return (Find-PoolByScan)
  }
  return $null
}

Write-Host ""
Write-Host "  Compute Pool"
Write-Host "  Folder: $Root"
Write-Host ""

$hasKey = $TsAuthKey -and -not $TsAuthKey.StartsWith("__TS_")
$lanOnly = ($env:JOIN_POOL_LAN_ONLY -eq "1")
if (-not $lanOnly) {
  if (-not $hasKey) {
    Write-Host "This file cannot join over the internet."
    Write-Host "Download Get-Join-Pool.bat from the host Join page and run that."
    exit 1
  }
  Connect-PrivateLink $TsAuthKey
}

$pool = Find-ComputePool
if (-not $pool) {
  if ($lanOnly) {
    Write-Host "Cannot find the compute pool on this Wi-Fi."
  } else {
    Write-Host "Cannot find the compute pool over the private link."
    Write-Host "The host PC must be online on that same private link."
  }
  exit 1
}

Save-PoolUrl $pool | Out-Null
Write-Host "Pool: $pool"

Write-Host "Refreshing launcher..."
foreach ($name in @("update-client.ps1", "pool-settings.ps1", "ensure-link.ps1")) {
  $out = Join-Path $Root $name
  Invoke-WebRequest -Uri ($pool + "/" + $name) -OutFile $out -UseBasicParsing -TimeoutSec 30
  Write-Host "  $name"
}
exit 0
