#Requires -RunAsAdministrator
$ErrorActionPreference = "Stop"

. (Join-Path $PSScriptRoot "_env.ps1")
$env:CLAUDE_PROXY_HOME = $ProductHome
$Host.UI.RawUI.WindowTitle = "Claude Proxy Control - start"
try { chcp 65001 | Out-Null; [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) } catch {}

# Progress for GUI (merged/start-progress.json) — console window optional/hidden
$script:progressPath = $null
$script:progressLines = New-Object System.Collections.ArrayList
$script:progressWarns = New-Object System.Collections.ArrayList
$script:progressFails = New-Object System.Collections.ArrayList
$script:progressStep = ""
$script:progressTitle = "Запуск…"
$script:progressPct = 0

function Save-Progress {
  param(
    [ValidateSet("running", "ok", "fail")][string]$Phase = "running",
    [string]$Step = "",
    [string]$Title = "",
    [string]$Detail = "",
    [int]$Pct = -1,
    [string]$Summary = ""
  )
  if (-not $script:progressPath) { return }
  if ($Step) { $script:progressStep = $Step }
  if ($Title) { $script:progressTitle = $Title }
  if ($Pct -ge 0) { $script:progressPct = $Pct }
  $copyBits = @()
  if ($script:progressFails.Count -gt 0) { $copyBits += ($script:progressFails -join "`n") }
  if ($script:progressWarns.Count -gt 0) { $copyBits += ($script:progressWarns -join "`n") }
  $obj = [ordered]@{
    phase      = $Phase
    step       = $script:progressStep
    title      = $script:progressTitle
    detail     = $Detail
    pct        = [int]$script:progressPct
    lines      = @($script:progressLines | Select-Object -Last 40)
    warns      = @($script:progressWarns)
    fails      = @($script:progressFails)
    summary    = $Summary
    copy_text  = ($copyBits -join "`n`n")
    time       = (Get-Date).ToString("o")
  }
  try {
    ($obj | ConvertTo-Json -Depth 6) | Set-Content -Path $script:progressPath -Encoding UTF8
  } catch {}
}

function Write-Step([string]$msg) {
  Write-Host ""
  Write-Host "==> $msg" -ForegroundColor Cyan
  [void]$script:progressLines.Add("==> $msg")
  $friendly = $msg
  $pct = $script:progressPct
  $step = $script:progressStep
  if ($msg -match '^(\d+)/(\d+)\s+(.*)$') {
    $n = [int]$Matches[1]
    $t = [int]$Matches[2]
    $rest = [string]$Matches[3]
    $step = "$n/$t"
    $pct = [int](100 * $n / [Math]::Max($t, 1))
    $friendly = switch -Regex ($rest) {
      'инструмент'          { "Проверяю программы и файлы…" }
      'proxy\.env|Squid для' { "Читаю прокси с сервера…" }
      'системный прокси'    { "Отключаю системный прокси Windows…" }
      'settings\.json'      { "Проверяю настройки Claude Code…" }
      'Конфликты'           { "Проверяю конфликты VPN…" }
      'Очистка'             { "Чищу старый туннель…" }
      'Домашний IP'         { "Смотрю домашний IP…" }
      'Снимок Sota|ловец'   { "Проверяю узел Sota…" }
      'Чужие TUN'           { "Проверяю чужие VPN…" }
      'Сборка конфига'      { "Собираю маршруты…" }
      'синтаксиса'          { "Проверяю конфиг…" }
      'Запуск туннеля'      { "Поднимаю туннель…" }
      'адаптера TUN'        { "Жду сетевой адаптер…" }
      'Проверка Squid'      { "Проверяю Squid для Claude…" }
      'VPN-узла'            { "Проверяю VPN-узел…" }
      'Запуск Claude'       { "Запускаю Claude Desktop…" }
      'Пост-проверка'       { "Проверяю, что Claude идёт через Squid…" }
      'status\.json|Write status' { "Сохраняю статус…" }
      'Итог'                { "Готово" }
      default               { $rest }
    }
  }
  Save-Progress -Phase running -Step $step -Title $friendly -Detail $msg -Pct $pct
}
function Write-Ok([string]$msg) {
  Write-Host "  [OK]   $msg" -ForegroundColor Green
  [void]$script:progressLines.Add("[OK] $msg")
  Save-Progress -Phase running -Detail $msg
}
function Write-Warn([string]$msg) {
  Write-Host "  [WARN] $msg" -ForegroundColor Yellow
  [void]$script:progressLines.Add("[WARN] $msg")
  [void]$script:progressWarns.Add($msg)
  Save-Progress -Phase running -Detail $msg
}
function Write-Fail([string]$msg) {
  Write-Host "  [FAIL] $msg" -ForegroundColor Red
  [void]$script:progressLines.Add("[FAIL] $msg")
  [void]$script:progressFails.Add($msg)
  Save-Progress -Phase running -Detail $msg
}
function Write-Info([string]$msg) {
  Write-Host "  [..]   $msg" -ForegroundColor Gray
  [void]$script:progressLines.Add("[..] $msg")
}
function Write-Explain([string]$msg) { Write-Host ("         " + $msg) -ForegroundColor DarkGray }

function Wait-EnterOrSkip {
  if ($env:CLAUDE_PROXY_NOPAUSE) { return }
  Pause
}

function Get-IpGeoBrief([string]$Ip) {
  if (-not $Ip) { return "" }
  try {
    $r = (& curl.exe -s -m 6 "http://ip-api.com/json/$Ip`?fields=status,country,countryCode,city" 2>$null | Out-String).Trim()
    if (-not $r) { return "" }
    $j = $r | ConvertFrom-Json
    if ($j.status -ne "success") { return "" }
    $city = if ($j.city) { ", " + $j.city } else { "" }
    return ($j.country + $city + " [" + $j.countryCode + "]")
  } catch { return "" }
}

function Get-AgeText([datetime]$When) {
  $span = (Get-Date) - $When
  if ($span.TotalMinutes -lt 2) { return "только что" }
  if ($span.TotalMinutes -lt 60) { return ([int]$span.TotalMinutes).ToString() + " мин назад" }
  if ($span.TotalHours -lt 24) { return ([int]$span.TotalHours).ToString() + " ч назад" }
  return ([int]$span.TotalDays).ToString() + " дн назад"
}

function Find-Python {
  foreach ($name in @("python.exe", "python")) {
    $cmd = Get-Command $name -ErrorAction SilentlyContinue
    if ($cmd -and (Test-Path $cmd.Source)) { return $cmd.Source }
  }
  $candidates = @(
    "E:\Python\python.exe",
    "$env:LOCALAPPDATA\Programs\Python\Python313\python.exe",
    "$env:LOCALAPPDATA\Programs\Python\Python312\python.exe",
    "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
    "C:\Python313\python.exe",
    "C:\Python312\python.exe"
  )
  foreach ($p in $candidates) { if (Test-Path $p) { return $p } }
  $hit = Get-ChildItem "E:\Python", "C:\Python*" -Filter "python.exe" -ErrorAction SilentlyContinue |
    Select-Object -First 1
  if ($hit) { return $hit.FullName }
  return $null
}

function Save-Status([hashtable]$Data) {
  $path = Join-Path $mergedDir "status.json"
  $Data["time"] = (Get-Date).ToString("o")
  ($Data | ConvertTo-Json -Depth 6) | Set-Content -Path $path -Encoding UTF8
}

function Test-CurlRetry([string[]]$ArgsList, [scriptblock]$OkTest, [string]$Name, [int]$Tries = 3) {
  for ($i = 1; $i -le $Tries; $i++) {
    $out = (& curl.exe @ArgsList 2>$null | Out-String).Trim()
    if (& $OkTest $out) { return $out }
    Write-Info ($Name + " try " + $i + "/" + $Tries + " => [" + $out + "]")
    if ($i -lt $Tries) { Start-Sleep -Seconds 2 }
  }
  return $null
}

function Clear-OrphanWintun {
  # Zombie Wintun devices cause: "Cannot create a file when that file already exists"
  #
  # This used to be a second, unscoped copy of the cleanup that matched EVERY Wintun
  # device on the machine - which meant Start tore down the user's other VPN (Sota,
  # AmneziaWG, WireGuard, Tailscale) along with our own leftovers. Delegate to the single
  # scoped implementation in _env.ps1: ours, or genuinely orphaned, and nothing else.
  $removed = Clear-CpcOrphanWintun
  if ($removed -eq 0) {
    Write-Info "no orphan Wintun PnP devices to remove"
    return
  }
  Write-Ok ("orphan/own Wintun devices removed: " + $removed)
  $left = @(Get-CpcTunDevices | Where-Object { $_.AdapterName -match $CpcAdapterPattern })
  if ($left.Count -gt 0) {
    Note-Warn ("our Wintun devices still left: " + $left.Count + " (will use unique TUN name)")
  }
}

function New-TunName {
  # Unique name every run avoids stuck Wintun name collision
  return (New-CpcTunName)
}

function Test-SingBoxHealthy {
  $proc = @(Get-CpcSingBoxProcesses)
  if (-not $proc) { return $false }
  if (Test-Path $log) {
    $tail = Get-Content $log -Tail 30 -ErrorAction SilentlyContinue | Out-String
    if ($tail -match 'FATAL|Cannot create a file when that file already exists') { return $false }
  }
  return $true
}

function Ask-YesNo([string]$Prompt, [string]$Default = "N") {
  $def = $Default.ToUpperInvariant()
  # GUI / silent start: never hang on Read-Host
  if ($env:CLAUDE_PROXY_NOPAUSE) {
    Write-Info ("auto-answer " + $def + " (CLAUDE_PROXY_NOPAUSE): " + $Prompt)
    return ($def -eq "Y")
  }
  $hint = if ($def -eq "Y") { "[Y/n]" } else { "[y/N]" }
  $ans = Read-Host ($Prompt + " " + $hint)
  if ([string]::IsNullOrWhiteSpace($ans)) { $ans = $def }
  return ($ans.Trim().ToUpperInvariant() -eq "Y")
}

function Get-SotaNodeMeta([string]$CapturePath) {
  $meta = @{ server = $null; port = 0; ok = $false }
  if (-not (Test-Path $CapturePath)) { return $meta }
  try {
    $j = Get-Content $CapturePath -Raw -Encoding UTF8 | ConvertFrom-Json
    $v = @($j.outbounds | Where-Object { $_.type -eq "vless" } | Select-Object -First 1)
    if ($v.Count -gt 0) {
      $meta.server = [string]$v[0].server
      $meta.port = [int]$v[0].server_port
      $meta.ok = ($meta.server -and $meta.port -gt 0)
    }
  } catch {}
  return $meta
}

function Test-VpnNodeLive([string]$Server, [int]$Port, [string]$HomeIp) {
  # Under ask/block policy, plain curl.exe is often BLOCKED (no app rule yet).
  # So primary health = TCP to VLESS node. Full egress is verified later when an app has channel=VPN.
  $result = @{
    ok = $false
    tcpOk = $false
    egress = $null
    reason = ""
  }
  if (-not $Server -or $Port -le 0) {
    $result.reason = "no server/port in capture"
    return $result
  }
  try {
    $tnc = Test-NetConnection -ComputerName $Server -Port $Port -WarningAction SilentlyContinue
    $result.tcpOk = [bool]$tnc.TcpTestSucceeded
  } catch {
    $result.tcpOk = $false
  }
  if (-not $result.tcpOk) {
    $result.reason = "TCP to VPN node failed ($Server`:$Port)"
    return $result
  }

  $vlessOk = $false
  if (Test-Path $log) {
    $vlessOk = [bool](Select-String -Path $log -Pattern "outbound/vless\[sota-vless\]" -Quiet -ErrorAction SilentlyContinue)
  }

  # Optional egress: only meaningful if curl already has channel=VPN; otherwise ignore empty/home
  $eg = ((& curl.exe -s -m 8 https://ifconfig.me 2>$null) | Out-String).Trim()
  if ($eg -match '^\d{1,3}(\.\d{1,3}){3}$') {
    $result.egress = $eg
    if ($HomeIp -and ($eg -eq $HomeIp)) {
      # curl likely went direct/home (no VPN rule) — not proof that node is dead
      Write-Info "curl egress=$eg (home) — игнор: у curl вероятно нет канала VPN; TCP узла OK"
    } elseif ($HomeIp -and ($eg -ne $HomeIp)) {
      $geo = Get-IpGeoBrief $eg
      $result.ok = $true
      if ($geo) { $result.reason = "VPN ok: выход $eg ($geo), узел $Server`:$Port" }
      else { $result.reason = "VPN ok: выход $eg, узел $Server`:$Port" }
      return $result
    }
  }

  $result.ok = $true
  if ($vlessOk) {
    $result.reason = "VPN node TCP ok ($Server`:$Port), в логе уже есть vless"
  } else {
    $result.reason = "VPN node TCP ok ($Server`:$Port). Полный выход проверим, когда приложение пойдёт с каналом VPN"
  }
  return $result
}

$dir = $ProductHome
$mergedDir = $MergedDir
New-Item -ItemType Directory -Force -Path $mergedDir | Out-Null
$cfg = Join-Path $mergedDir "vpn-claude-config.json"
$log = Join-Path $mergedDir "vpn-claude-tun.log"
$runLog = Join-Path $mergedDir "last-start.log"
$statusPath = Join-Path $mergedDir "status.json"
$script:progressPath = Join-Path $mergedDir "start-progress.json"
Save-Progress -Phase running -Step "0/15" -Title "Запуск… подтверди UAC если спросит" -Detail "starting" -Pct 0
$sotaCapture = Join-Path $mergedDir "sota-captured\latest.json"
$useSotaVpn = Test-Path $sotaCapture
# Один сборщик на оба случая - см. пояснение на шаге 5c.
$builder = Join-Path $mergedDir "build_sota_merged_config.py"
$modeName = $(if ($useSotaVpn) { "CPC+SotaVPN" } else { "CPC-без-VPN" })
$proxyEnv = Join-Path $dir "proxy.env"
$sbDir = Get-ChildItem (Join-Path $dir "sing-box") -Directory -Filter "sing-box-*-windows-amd64" |
  Sort-Object Name -Descending | Select-Object -First 1
if (-not $sbDir) { throw "sing-box folder not found under $dir\sing-box" }
$sbExe = Join-Path $sbDir.FullName "sing-box.exe"
$tunName = New-TunName
$env:VPN_CLAUDE_TUN_NAME = $tunName
Write-Info ("planned TUN name: " + $tunName)

try { Start-Transcript -Path $runLog -Force | Out-Null } catch {}

$script:failCount = 0
$script:warnCount = 0
function Assert-Check([bool]$ok, [string]$okMsg, [string]$failMsg) {
  if ($ok) { Write-Ok $okMsg }
  else { Write-Fail $failMsg; $script:failCount++ }
}
function Note-Warn([string]$msg) { Write-Warn $msg; $script:warnCount++ }

trap {
  Write-Host ""
  Write-Fail ("Script error: " + $_.Exception.Message)
  Write-Host "Run log: $runLog" -ForegroundColor Yellow
  if (Test-Path $log) { Write-Host "TUN log (tail):" -ForegroundColor Yellow; Get-Content $log -Tail 30 }
  try { Save-Status @{ ok = $false; error = $_.Exception.Message; failCount = $script:failCount } } catch {}
  try {
    Save-Progress -Phase fail -Title "Ошибка запуска" -Detail $_.Exception.Message -Summary $_.Exception.Message
  } catch {}
  try { Stop-Transcript | Out-Null } catch {}
  Wait-EnterOrSkip
  exit 1
}

Write-Host ""
Write-Host "============================================================" -ForegroundColor White
Write-Host "  Окно диагностики запуска. Не закрывайте до конца." -ForegroundColor Yellow
if ($useSotaVpn) {
  Write-Host "  РЕЖИМ: Claude + VPN (узел из снимка Sota)" -ForegroundColor White
  Write-Host "  Claude  -> Squid (отдельный прокси для Claude)" -ForegroundColor White
  Write-Host "  Остальное -> по правилам в UI (Дом / VPN / блок; без правила = спрос)" -ForegroundColor White
} else {
  Write-Host "  РЕЖИМ: только Claude через Squid" -ForegroundColor White
  Write-Host "  Claude  -> Squid" -ForegroundColor White
  Write-Host "  Остальное -> домашний интернет (без VPN)" -ForegroundColor White
}
Write-Host "============================================================" -ForegroundColor White
Write-Explain "Squid = ваш VPS-прокси, нужен чтобы Claude не ловил geo-block."
Write-Explain "VPN-узел = копия сервера из Sota; приложение Sota при этом должно быть Disconnect."
Write-Host ("  Лог запуска: {0}" -f $runLog) -ForegroundColor DarkGray
Write-Host ("  Время: {0}" -f (Get-Date)) -ForegroundColor DarkGray

# --- 0. preflight ---
Write-Step "0/15 Проверка инструментов и файлов"
Write-Explain "Нужны: sing-box (туннель), сборщик конфига, proxy.env (логин Squid), Python, curl."
Assert-Check (Test-Path $sbExe) ("sing-box: " + $sbExe) "sing-box.exe missing"
Assert-Check (Test-Path $builder) ("builder: " + $builder) ("builder missing: " + $builder)
if ($useSotaVpn) {
  Write-Ok ("Снимок Sota найден: " + $sotaCapture)
  Write-Explain "Это копия узла после Connect в Sota — её и использует наш VPN."
} else {
  Write-Ok "Снимка Sota нет -> режим Claude-only (остальное через домашний IP)"
}
Assert-Check (Test-Path $proxyEnv) ("proxy.env: " + $proxyEnv) "proxy.env missing"
Assert-Check ($null -ne (Get-Command curl.exe -ErrorAction SilentlyContinue)) "curl.exe found" "curl.exe missing"
$python = Find-Python
Assert-Check ($null -ne $python) ("Python: " + $python) "Python missing"
if ($script:failCount -gt 0) { throw "Preflight failed" }

# --- 1. proxy.env ---
Write-Step "1/15 Чтение proxy.env (Squid для Claude)"
Write-Explain "Отсюда берём адрес/логин Squid. Пароль в лог не печатаем."
if (-not (Test-Path $proxyEnv)) { throw "Missing $proxyEnv" }
$envMap = @{}
Get-Content $proxyEnv | ForEach-Object {
  if ($_ -match '^\s*#' -or $_ -notmatch '=') { return }
  $k, $v = $_.Split('=', 2)
  $envMap[$k.Trim()] = $v.Trim()
}
foreach ($k in @('PROXY_HOST','PROXY_PORT','PROXY_USER','PROXY_PASS')) {
  if (-not $envMap.ContainsKey($k) -or [string]::IsNullOrWhiteSpace($envMap[$k])) { throw "proxy.env missing $k" }
}
$portNum = 0
Assert-Check ([int]::TryParse($envMap.PROXY_PORT, [ref]$portNum)) ("port=" + $envMap.PROXY_PORT) "PROXY_PORT not a number"
Write-Ok ("Squid: {0}:{1} user={2} (пароль скрыт)" -f $envMap.PROXY_HOST, $envMap.PROXY_PORT, $envMap.PROXY_USER)

# --- 2. system proxy OFF ---
Write-Step "2/15 Выключить системный прокси Windows"
Write-Explain "Системный прокси мешает: гоняет ВСЁ через прокси. Нам нужен только Claude через Squid."
$reg = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings"
Set-ItemProperty $reg -Name ProxyEnable -Type DWord -Value 0
Set-ItemProperty $reg -Name ProxyServer -Value ""
Remove-ItemProperty $reg -Name AutoConfigURL -ErrorAction SilentlyContinue
$pe = (Get-ItemProperty $reg -Name ProxyEnable).ProxyEnable
Assert-Check ($pe -eq 0) "ProxyEnable=0 (системный прокси ВЫКЛ)" ("ProxyEnable still=" + $pe)

# --- 3. old Claude settings proxy ---
Write-Step "3/15 Проверка Claude Code settings.json"
Write-Explain "Старый HTTP_PROXY/104 ломал Code mode. Должно быть чисто."
$settingsPath = Join-Path $env:USERPROFILE ".claude\settings.json"
if (Test-Path $settingsPath) {
  $raw = Get-Content $settingsPath -Raw -ErrorAction SilentlyContinue
  if ($raw -match '141\.11\.197\.104' -or $raw -match 'HTTP_PROXY' -or $raw -match 'HTTPS_PROXY') {
    $bak = Join-Path $mergedDir ("settings.json.bak-" + (Get-Date -Format "yyyyMMdd-HHmmss"))
    Copy-Item $settingsPath $bak -Force
    Set-Content -Path $settingsPath -Value "{}" -Encoding UTF8
    Write-Ok ("очистили settings.json, backup=" + $bak)
  } else {
    Write-Ok "settings.json чистый"
  }
} else {
  Write-Ok "settings.json нет (это нормально)"
}

# --- 4. conflicts ---
Write-Step "4/15 Конфликты VPN/TUN"
Write-Explain "Два TUN сразу (наш + Connect в Sota) конфликтуют. Само окно Sota может быть открыто — это ОК."
$conflict = @()
$hiddify = Get-Service -Name "HiddifyTunnelService" -ErrorAction SilentlyContinue
if ($hiddify -and $hiddify.Status -eq "Running") { $conflict += "HiddifyTunnelService=Running" }
$sota = Get-Service -Name "SOTA-DAEMON" -ErrorAction SilentlyContinue
$sotaTunUp = [bool](Get-NetAdapter -Name "tun0" -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq "Up" })
if ($sota -and $sota.Status -eq "Running") {
  if ($sotaTunUp) {
    $conflict += "Sota CONNECTED (tun0 Up) — ОБЯЗАТЕЛЬНО Disconnect в Sota!"
  } else {
    Note-Warn "Sota приложение/служба запущены, но Connect выключен (tun0 нет) — обычно ОК"
    Write-Explain "WARN не ошибка: можно оставить Sota открытой, главное не жать Connect."
  }
}
if ((Get-Process -Name "SotaVPN" -ErrorAction SilentlyContinue) -and $sotaTunUp) {
  $conflict += "SotaVPN.exe + tun0 Up (активный Connect)"
}
$upTun = Get-NetAdapter -ErrorAction SilentlyContinue | Where-Object {
  $_.Status -eq "Up" -and (
    $_.Name -match 'tun|wintun|sing|hiddify|vpn-claude|claude-tun|OpenVPN|Tap' -or
    $_.InterfaceDescription -match 'Wintun|sing-tun|TAP|OpenVPN|Hiddify|WireGuard'
  )
}
foreach ($a in @($upTun)) { $conflict += ("UP adapter: " + $a.Name) }
$cpcSb = @(Get-CpcSingBoxProcesses)
$othSb = @(Get-OtherSingBoxProcesses)
if ($cpcSb.Count) { $conflict += "CPC sing-box already running (будет остановлен)" }
if ($othSb.Count) { $conflict += "другой sing-box running — конфликт риск" }
if ($conflict.Count -gt 0) { foreach ($c in $conflict) { Note-Warn $c } } else { Write-Ok "опасных конфликтов нет" }

# --- 5. stop old tunnels ---
Write-Step "5/15 Очистка старого туннеля"
Write-Explain "Гасим прошлый sing-box и «зомби»-адаптеры Wintun, чтобы новый TUN поднялся чисто."
$otherSb = @(Get-OtherSingBoxProcesses)
if ($otherSb.Count -gt 0) {
  Write-Warn ("Другой sing-box уже запущен (не CPC): " + $otherSb.Count +  " — останови личный туннель, иначе возможен конфликт TUN")
}
[void](Stop-CpcSingBox)
Start-Sleep -Seconds 2
Get-NetAdapter -ErrorAction SilentlyContinue |
  Where-Object { $_.Name -match 'claude-tun|claude-squid|vpn-claude|^vc\d' } |
  ForEach-Object {
    Write-Info ("disable adapter " + $_.Name)
    try { Disable-NetAdapter -Name $_.Name -Confirm:$false -ErrorAction SilentlyContinue } catch {}
  }
Clear-OrphanWintun
Write-Ok "старый sing-box/TUN очищен"

# Baseline home IP (after our TUN is down) for later VPN probe
Write-Step "5b/15 Домашний IP (эталон)"
Write-Explain "Снимаем ваш обычный выход в интернет БЕЗ нашего VPN. Потом сравним: если после VPN IP тот же — узел мёртвый."
$homeEgress = ((& curl.exe -s -m 12 https://ifconfig.me 2>$null) | Out-String).Trim()
if ($homeEgress) {
  $homeGeo = Get-IpGeoBrief $homeEgress
  if ($homeGeo) { Write-Ok ("домашний выход = $homeEgress ($homeGeo)") }
  else { Write-Ok ("домашний выход = " + $homeEgress) }
} else { Note-Warn "не удалось измерить домашний IP (проверка VPN будет слабее)" }

# Capture / watcher status
Write-Step "5c/15 Снимок Sota + ловец"
Write-Explain "Ловец в фоне копирует temp-конфиг Sota при Connect. Потом Disconnect — а копия остаётся у нас."
$watcherStatus = Join-Path $mergedDir "sota-captured\watcher-status.json"
$watcherAlive = @(Get-CimInstance Win32_Process -Filter "Name='powershell.exe'" -ErrorAction SilentlyContinue |
  Where-Object { $_.CommandLine -match 'Watch-SotaCapture\.ps1' }).Count -gt 0
if ($watcherAlive) { Write-Ok "ловец узлов Sota работает (фоновая задача)" }
else { Note-Warn "ловец НЕ запущен — поставьте: Desktop → Install Sota Capture Watcher" }
if (Test-Path $watcherStatus) {
  try {
    $ws = Get-Content $watcherStatus -Raw -Encoding UTF8 | ConvertFrom-Json
    if ($ws.lastCapture) {
      $age = ""
      try { $age = " (" + (Get-AgeText ([datetime]$ws.lastCapture)) + ")" } catch {}
      Write-Info ("последний авто-снимок: " + $ws.lastCapture + $age)
      Write-Info ("узел в снимке: " + $ws.server + ":" + $ws.port)
    }
  } catch {}
}
# Refresh mode decision after cleanup (capture may have appeared).
#
# Сборщик ВСЕГДА один и тот же. Раньше без снимка Sota скрипт уходил на
# build_claude_only_config.py, а там route.final = "direct": всё незнакомое шло
# домой напрямую, без каналов, без блокировки, без защиты самой программы. То
# есть на машине без Sota продукт делал ровно обратное тому, что обещает.
# build_sota_merged_config.py умеет собираться без единого узла: канал VPN
# просто недоступен, приложения на нём уходят в block, остальное работает.
$useSotaVpn = Test-Path $sotaCapture
$builder = Join-Path $mergedDir "build_sota_merged_config.py"
if ($useSotaVpn) {
  $modeName = "CPC+SotaVPN"
  $nodeMeta = Get-SotaNodeMeta $sotaCapture
  Write-Ok ("будем использовать узел: " + $nodeMeta.server + ":" + $nodeMeta.port)
  Write-Explain "Это входной адрес узла. Страна выхода может отличаться — смотрите шаг 11b (egress)."
} else {
  # И никаких вопросов. Sota - источник одного канала, а не условие запуска.
  # Вопрос с ответом по умолчанию "N" в скрытом окне отвечал сам себе "нет" и
  # ронял весь старт на этом месте: пользователь видел, что туннель "сбрасывается
  # на пятом шаге", и понять причину было неоткуда.
  $modeName = "CPC-без-VPN"
  Note-Warn "Снимка Sota нет - канал VPN недоступен, приложения на нём будут заблокированы"
  Write-Explain "Остальное работает: Claude через Squid, домашние приложения напрямую, незнакомые закрыты."
  Write-Explain "Чтобы включить VPN позже: Sota Connect (2-3 сек) -> Disconnect, затем «Перезапустить TUN»."
}

# --- 6. stop other TUN services that fight with us ---
Write-Step "6/15 Чужие TUN-службы"
Write-Explain "Hiddify мы уже не используем. Sota Connect во время нашего режима лучше не включать."
if ($hiddify -and $hiddify.Status -eq "Running") {
  Stop-Service -Name "HiddifyTunnelService" -Force -ErrorAction SilentlyContinue
  Set-Content -Path (Join-Path $mergedDir "hiddify-was-running.flag") -Value "1"
  Start-Sleep -Seconds 2
  Write-Ok "HiddifyTunnelService остановлен"
} else {
  Write-Ok "HiddifyTunnelService не запущен"
}
if ($sotaTunUp) {
  Note-Warn "Сейчас Sota CONNECTED — перед стартом нашего TUN лучше Disconnect"
} else {
  Write-Ok "Sota Connect сейчас выключен (можно продолжать)"
}

# --- 7. build config ---
if ($useSotaVpn) {
  Write-Step "7/15 Сборка конфига: Claude→Squid, остальное→VPN"
  Write-Explain "Пишем правила маршрутизации в vpn-claude-config.json."
} else {
  Write-Step "7/15 Сборка конфига: Claude→Squid, остальное→дом"
  Write-Explain "VPN-узла нет — остальные приложения пойдут напрямую."
}
& $python $builder
if ($LASTEXITCODE -ne 0) { throw ("builder failed: " + $builder) }
Assert-Check (Test-Path $cfg) ("config written: " + $cfg) "config missing after build"
$meta = & $python -c "import json,pathlib;c=json.loads(pathlib.Path(r'$cfg').read_text(encoding='utf-8'));rules=c.get('route',{}).get('rules',[]);r=json.dumps(rules);udp=any(isinstance(x,dict) and x.get('network')=='udp' and x.get('outbound')=='block' for x in rules);vless=sum(1 for o in c.get('outbounds',[]) if o.get('type')=='vless');print(int('claude-squid' in r));print(int('Claude.exe' in r or 'claude.exe' in r));print(int(udp));print(c['route'].get('final',''));print(vless)"
$rc = @($meta)
Assert-Check ($rc[0] -eq "1") "правило Claude → claude-squid есть" "config missing claude-squid rule"
Assert-Check ($rc[1] -eq "1") "правило по процессу Claude.exe есть" "config missing Claude process rule"
Assert-Check ($rc[2] -eq "1") "UDP Claude заблокирован (защита от QUIC в обход Squid)" "config missing UDP block"
$routeFinal = $rc[3]
$vlessN = [int]$rc[4]
if ($useSotaVpn) {
  Assert-Check ($vlessN -ge 1) ("VPN-узлов из снимка: " + $vlessN) "no VLESS nodes from Sota capture"
  Assert-Check ($routeFinal -eq "block") ("неизвестные приложения → BLOCK/ask (final=" + $routeFinal + ")") ("route.final unexpected: " + $routeFinal)
  Write-Ok "Claude→Squid; apps→direct/VPN/proxy; неизвестные→block (спросим в UI)"
} else {
  Assert-Check ($routeFinal -eq "direct") ("остальные приложения → дом (final=direct)") ("route.final unexpected: " + $routeFinal)
  Write-Ok "VPN-узлов в этом запуске нет"
}

# Report the leak-hardening switches so the run shows what protection is actually on.
$hard = & $python -c "import json,pathlib;c=json.loads(pathlib.Path(r'$cfg').read_text(encoding='utf-8'));i=c['inbounds'][0];a=i.get('address') or [];print(int(bool(i.get('strict_route'))));print(int(any(':' in str(x) for x in a)));print(c.get('dns',{}).get('final',''));print(int(bool((c.get('experimental') or {}).get('clash_api'))))"
$hc = @($hard)
if ($hc[0] -eq "1") { Write-Ok "strict_route=on (приложение не обойдёт туннель через свой интерфейс)" }
else { Note-Warn "strict_route=off - приложение может обойти туннель мимо правил" }
if ($hc[1] -eq "1") { Write-Ok "IPv6 заведён в туннель (иначе блок/прокси утекают по IPv6)" }
else { Note-Warn "IPv6 не в туннеле - на сети с IPv6 возможна утечка мимо правил" }
if ($hc[2] -eq "secure") { Write-Ok "DNS шифрованный (DoH) - провайдер не видит домены" }
else { Note-Warn ("DNS не шифрован (dns.final=" + $hc[2] + ")") }
if ($hc[3] -eq "1") { Write-Ok "Clash API включён - живая проверка каналов доступна" }

# --- 8. validate ---
Write-Step "8/15 Проверка синтаксиса конфига"
Write-Explain "sing-box check — конфиг валидный до реального старта туннеля."
& $sbExe check -c $cfg
if ($LASTEXITCODE -ne 0) { throw "sing-box check failed" }
Write-Ok "синтаксис конфига OK"

# --- 9. start TUN with retry ---
Write-Step "9/15 Запуск туннеля (TUN)"
Write-Explain "Поднимаем виртуальный сетевой адаптер. Уникальное имя каждый раз — чтобы не застрять на зомби-Wintun."
function Start-TunOnce {
  if (Test-Path $log) { Move-Item $log "$log.bak" -Force -ErrorAction SilentlyContinue }
  Write-Info ("starting sing-box with TUN name=" + $tunName)
  $arg = "run -c `"$cfg`""
  Start-Process -FilePath $sbExe -ArgumentList $arg -WorkingDirectory $sbDir.FullName -WindowStyle Hidden `
    -RedirectStandardError $log -RedirectStandardOutput (Join-Path $mergedDir "vpn-claude-tun-out.log")
  # Wintun create can take >10s; FATAL often appears around 15s on collision
  for ($w = 1; $w -le 20; $w++) {
    Start-Sleep -Seconds 1
    if (-not (Get-CpcSingBoxProcesses)) {
      Write-Info ("sing-box exited early at " + $w + "s")
      return $false
    }
    if (Test-Path $log) {
      $tail = Get-Content $log -Tail 20 -ErrorAction SilentlyContinue | Out-String
      if ($tail -match 'FATAL|Cannot create a file when that file already exists') {
        Write-Info "FATAL in TUN log - treating start as failed"
        $otherSb = @(Get-OtherSingBoxProcesses)
if ($otherSb.Count -gt 0) {
  Write-Warn ("Другой sing-box уже запущен (не CPC): " + $otherSb.Count +  " — останови личный туннель, иначе возможен конфликт TUN")
}
[void](Stop-CpcSingBox)
        return $false
      }
      if ($tail -match 'started|inbound/tun') {
        # keep waiting a bit for adapter, process still alive is enough here
      }
    }
    $ad = Get-NetAdapter -Name $tunName -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq "Up" }
    if ($ad) { return $true }
  }
  return (Test-SingBoxHealthy)
}

$started = $false
for ($attempt = 1; $attempt -le 3; $attempt++) {
  Write-Info ("TUN start attempt " + $attempt + "/3 name=" + $tunName)
  $started = Start-TunOnce
  if ($started) { break }
  Note-Warn ("attempt " + $attempt + " failed")
  if (Test-Path $log) { Get-Content $log -Tail 12 | ForEach-Object { Write-Info $_ } }
  $otherSb = @(Get-OtherSingBoxProcesses)
if ($otherSb.Count -gt 0) {
  Write-Warn ("Другой sing-box уже запущен (не CPC): " + $otherSb.Count +  " — останови личный туннель, иначе возможен конфликт TUN")
}
[void](Stop-CpcSingBox)
  Start-Sleep -Seconds 2
  Clear-OrphanWintun
  $tunName = New-TunName
  $env:VPN_CLAUDE_TUN_NAME = $tunName
  & $python $builder | Out-Null
  & $sbExe check -c $cfg | Out-Null
}
$sbProcObj = Get-CpcSingBoxProcesses | Select-Object -First 1; $sbProc = if ($sbProcObj) { Get-Process -Id $sbProcObj.ProcessId -ErrorAction SilentlyContinue } else { $null }
Assert-Check ($started -and $null -ne $sbProc) ("sing-box pid=" + $(if ($sbProc) { $sbProc.Id } else { "?" }) + " tun=" + $tunName) "sing-box failed to start after retries"

# --- 10. adapter ---
Write-Step "10/15 Ждём появления сетевого адаптера TUN"
Write-Explain "Адаптер Up = туннель реально поднялся в Windows."
$adapter = $null
for ($i = 1; $i -le 25; $i++) {
  if (-not (Test-SingBoxHealthy)) {
    throw ("sing-box died while waiting for adapter. See log: " + $log)
  }
  $adapter = Get-NetAdapter -Name $tunName -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq "Up" }
  if ($adapter) { break }
  # also accept any Up adapter matching our prefix if rename happened
  if (-not $adapter) {
    $adapter = Get-NetAdapter -ErrorAction SilentlyContinue | Where-Object {
      $_.Status -eq "Up" -and ($_.Name -eq $tunName -or $_.InterfaceDescription -match 'sing-tun|Wintun')
    } | Select-Object -First 1
  }
  Write-Info ("waiting adapter " + $tunName + " " + $i + "/25")
  Start-Sleep -Seconds 1
}
if (-not $adapter) {
  if (Test-Path $log) { Get-Content $log -Tail 40 }
  throw ("TUN adapter failed: " + $tunName)
}
Write-Ok ("Adapter " + $adapter.Name + " is Up (requested=" + $tunName + ")")

# --- 11. network checks ---
Write-Step "11/15 Проверка Squid (путь Claude)"
Write-Explain "Это проверка ПРОКСИ для Claude, не VPN. hello от Anthropic + выходной IP должен быть IP Squid."
# IMPORTANT: do not use format "http://{0}:{1}@{2}" - in PowerShell "@{" starts a hashtable
$proxyUrl = 'http://' + $envMap.PROXY_USER + ':' + $envMap.PROXY_PASS + '@' + $envMap.PROXY_HOST + ':' + $envMap.PROXY_PORT

$hello = Test-CurlRetry -ArgsList @("-s","-m","25","-x",$proxyUrl,"https://api.anthropic.com/api/hello") -Name "Anthropic hello" -OkTest { param($x) $x -match "hello" }
Assert-Check ($null -ne $hello) ("Anthropic hello через Squid => " + $hello) "Anthropic hello via Squid FAILED"

$egress = Test-CurlRetry -ArgsList @("-s","-m","25","-x",$proxyUrl,"https://ifconfig.me") -Name "egress" -OkTest { param($x) $x -eq $envMap.PROXY_HOST }
Assert-Check ($null -ne $egress) ("выход Claude/Squid = " + $egress + " (должен совпасть с PROXY_HOST)") ("Unexpected Squid egress")

# --- 11b. VPN node live probe (Sota mode) ---
$vpnModeActive = $useSotaVpn
$script:vpnProbeOk = $true
$script:vpnEgress = $null
if ($useSotaVpn) {
  Write-Step "11b/15 Проверка VPN-узла (доступность сервера Sota)"
  Write-Explain "Главное — TCP до узла. curl без правила канала часто BLOCK — это не значит, что VPN мёртв."
  Write-Explain ("Эталон дома (справочно): " + $homeEgress)
  $nodeMeta = Get-SotaNodeMeta $sotaCapture
  Write-Info ("проверяем узел " + $nodeMeta.server + ":" + $nodeMeta.port + " ...")
  $probe = Test-VpnNodeLive -Server $nodeMeta.server -Port $nodeMeta.port -HomeIp $homeEgress
  $script:vpnEgress = $probe.egress
  if ($probe.ok) {
    Write-Ok $probe.reason
    Write-Explain "Claude → Squid. Остальные приложения: только по правилам в UI (Дом / VPN / блок)."
    $script:vpnProbeOk = $true
  } else {
    $script:vpnProbeOk = $false
    Write-Fail ("VPN-узел НЕ работает: " + $probe.reason)
    Write-Host ""
    Write-Host "  Claude через Squid уже OK." -ForegroundColor Yellow
    Write-Host "  Политика приложений:" -ForegroundColor Yellow
    Write-Host "    • без правила → блок + спрос в программе" -ForegroundColor Yellow
    Write-Host "    • канал VPN → тоже блок, пока узел мёртв (не утечёт в домашний IP)" -ForegroundColor Yellow
    Write-Host "  Варианты:" -ForegroundColor Yellow
    Write-Host "    Y = продолжить так (рекомендуется)" -ForegroundColor Yellow
    Write-Host "    N = остановить → новый снимок Sota (Connect→Disconnect)" -ForegroundColor Yellow
    Write-Host ""
    # Default Y — do NOT fall back to "all apps via home IP" (old model)
    if (Ask-YesNo "Продолжить запуск без рабочего VPN-узла?" "Y") {
      Write-Warn "Продолжаем: Claude OK; VPN-канал недоступен до нового снимка Sota"
      # Rebuild so fail-closed (vpn apps → block) is in the live config
      try {
        & $python $builder | Out-Null
        if ($LASTEXITCODE -eq 0) {
          Write-Info "config rebuilt (VPN apps → block while node down)"
        }
      } catch {
        Write-Warn ("rebuild warn: " + $_.Exception.Message)
      }
      $vpnModeActive = $true
      Note-Warn "Сделайте новый снимок Sota, когда будет удобно — иначе приложения на VPN останутся без интернета"
    } else {
      $otherSb = @(Get-OtherSingBoxProcesses)
      if ($otherSb.Count -gt 0) {
        Write-Warn ("Другой sing-box уже запущен (не CPC): " + $otherSb.Count)
      }
      [void](Stop-CpcSingBox)
      Write-Host ""
      Write-Host "  Остановлено. Обновите узел:" -ForegroundColor Cyan
      Write-Host "    1) Ловец узлов должен работать" -ForegroundColor Cyan
      Write-Host "    2) Sota: Connect → 2–3 сек → Disconnect" -ForegroundColor Cyan
      Write-Host "    3) Снова «Запустить Claude»" -ForegroundColor Cyan
      Write-Host ""
      Save-Status @{
        ok = $false
        error = "VPN node dead; user aborted for fresh Sota capture"
        homeEgress = $homeEgress
        vpnServer = $nodeMeta.server
        vpnPort = $nodeMeta.port
      }
      try { Stop-Transcript | Out-Null } catch {}
      if (-not $env:CLAUDE_PROXY_NOPAUSE) { Pause }
      exit 2
    }
  }
} else {
  Write-Ok "VPN probe skipped (Claude-only / home mode)"
}

# --- 12. launch Claude ---
Write-Step "12/15 Запуск Claude Desktop"
Write-Explain "Открываем приложение Claude. Его трафик должен идти в claude-squid (следующий шаг проверит лог)."
if ($script:failCount -gt 0) {
  Write-Fail ($script:failCount.ToString() + " check(s) failed - Claude NOT launched")
  Save-Status @{ ok = $false; failCount = $script:failCount; warnCount = $script:warnCount; tun = $tunName }
  Save-Progress -Phase fail -Title "Запуск не удался" -Detail "$($script:failCount) ошибка(и) — Claude не запущен" -Summary "Исправь FAIL и нажми «Запустить Claude» снова."
  Write-Host "Fix FAIL lines, then run shortcut again." -ForegroundColor Yellow
  Write-Host "TUN log: $log" -ForegroundColor Yellow
  Write-Host "Run log: $runLog" -ForegroundColor Yellow
  try { Stop-Transcript | Out-Null } catch {}
  Wait-EnterOrSkip
  exit 1
}

# Туннель и Claude - разные вещи. CPC_SKIP_CLAUDE=1 поднимает маршрутизацию и на
# этом останавливается: на машине, где Claude не установлен, шаг 12 валил весь
# запуск, и туннель считался неудавшимся, хотя он уже работал.
$skipClaude = ($env:CPC_SKIP_CLAUDE -eq "1")
if ($skipClaude) {
  Write-Ok "Режим «только туннель» - Claude не запускаем"
  Write-Explain "Маршрутизация уже работает. Claude открывается отдельной кнопкой, когда он установлен."
} else {
  Get-Process -Name claude -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
  Start-Sleep -Seconds 1
  Start-Process "explorer.exe" "shell:AppsFolder\Claude_pzs8sxrjxfjjc!Claude"
  Start-Sleep -Seconds 4
  $claude = @(Get-Process -Name claude -ErrorAction SilentlyContinue)
  Assert-Check ($claude.Count -gt 0) ("Claude process started (" + $claude.Count + ")") "Claude did not start"
}

# --- 13. post route check ---
Write-Step "13/15 Пост-проверка: Claude реально идёт в Squid"
if ($skipClaude) {
  Write-Ok "Пропускаем - Claude не запускали"
} else {
  Write-Explain "Ищем в логе строку outbound/http[claude-squid]. Это главный признак, что Claude не ушёл в VPN/дом."
  $routeSeen = $false
  for ($i = 1; $i -le 12; $i++) {
    if ((Test-Path $log) -and (Select-String -Path $log -Pattern "outbound/http\[claude-squid\]" -Quiet)) {
      $routeSeen = $true
      $hit = Select-String -Path $log -Pattern "outbound/http\[claude-squid\]" | Select-Object -Last 1
      Write-Info $hit.Line.Trim()
      break
    }
    Write-Info ("waiting Claude->Squid in log " + $i + "/12")
    Start-Sleep -Seconds 2
  }
  if ($routeSeen) { Write-Ok "TUN log shows outbound/http[claude-squid]" }
  else { Note-Warn "claude-squid not seen yet - send a chat message in Claude, then run Diagnose" }
}

# --- 14. status ---
Write-Step "14/15 Write status.json"
Save-Status @{
  ok = ($script:failCount -eq 0)
  failCount = $script:failCount
  warnCount = $script:warnCount
  tun = $tunName
  singBoxPid = $(if ($sbProc) { $sbProc.Id } else { $null })
  squidHost = $envMap.PROXY_HOST
  routeFinal = $routeFinal
  mode = $modeName
  homeEgress = $homeEgress
  vpnEgress = $script:vpnEgress
  vpnProbeOk = $script:vpnProbeOk
  vpnModeActive = $vpnModeActive
  claudeProcesses = $claude.Count
  claudeViaSquidSeen = $routeSeen
}
Write-Ok ("status: " + $statusPath)

# --- 15. summary ---
Write-Step "15/15 Итог — что сейчас работает"
Write-Host ""
if ($vpnModeActive) {
  Write-Host "  УСПЕХ: Claude + VPN активны" -ForegroundColor Green
} else {
  Write-Host "  УСПЕХ: Claude через Squid активен (без VPN / fallback)" -ForegroundColor Green
}
Write-Host ("  Предупреждений (WARN): " + $script:warnCount + "  |  ошибок (FAIL): " + $script:failCount) -ForegroundColor Yellow
Write-Host ""
Write-Host "  Куда идёт трафик:" -ForegroundColor White
Write-Host ("    Claude / Code / cowork  =>  Squid " + $envMap.PROXY_HOST) -ForegroundColor Green
if ($vpnModeActive) {
  Write-Host ("    Назначенные apps       =>  Direct или VPN (по правилам в UI)") -ForegroundColor Green
  Write-Host ("    Без правила            =>  BLOCK + спрос в программе (final=" + $routeFinal + ")") -ForegroundColor Yellow
} else {
  Write-Host ("    Остальные приложения    =>  ДОМАШНИЙ интернет (final=" + $routeFinal + ")") -ForegroundColor Yellow
}
Write-Host "    Системный прокси Windows =>  ВЫКЛ" -ForegroundColor Green
Write-Host ""
Write-Host "  Адреса (простыми словами):" -ForegroundColor White
$homeGeo2 = Get-IpGeoBrief $homeEgress
$vpnGeo2 = Get-IpGeoBrief $script:vpnEgress
Write-Host ("    Домашний IP (эталон) : " + $(if ($homeEgress) { $homeEgress } else { "?" }) + $(if ($homeGeo2) { " — $homeGeo2" } else { "" })) -ForegroundColor Gray
if ($vpnModeActive -and $script:vpnEgress) {
  Write-Host ("    VPN выход сейчас    : " + $script:vpnEgress + $(if ($vpnGeo2) { " — $vpnGeo2" } else { "" })) -ForegroundColor Green
  Write-Explain "Если открыть whatismyip в браузере — должен быть VPN-выход, не домашний."
} elseif (-not $vpnModeActive) {
  Write-Host ("    VPN выход           : выключен (остальное = дом " + $homeEgress + ")") -ForegroundColor Yellow
}
Write-Host ("    Туннель Windows     : " + $tunName + "  |  режим: " + $modeName) -ForegroundColor Gray
Write-Host ""
Write-Host "  Как пользоваться дальше:" -ForegroundColor White
Write-Host "    • Пишите в Claude как обычно" -ForegroundColor Gray
Write-Host "    • Пока этот режим ВКЛ — в Sota НЕ жмите Connect" -ForegroundColor Yellow
Write-Host "    • Обновить узел VPN: Sota Connect→Disconnect, потом снова Claude + VPN.lnk" -ForegroundColor Gray
Write-Host "    • Остановить всё: Stop Claude + VPN.bat (закроет туннель и Claude)" -ForegroundColor Gray
Write-Host "    • Диагностика: Diagnose Claude + VPN.bat" -ForegroundColor Gray
Write-Host ""
Write-Host "  Что значат WARN:" -ForegroundColor White
Write-Host "    • Sota приложение запущено, но Disconnect — обычно нормально" -ForegroundColor Gray
Write-Host "    • Опасно только если у Sota активный Connect (адаптер tun0 Up)" -ForegroundColor Gray
Write-Host ""
Write-Host ("  Логи: TUN=" + $log) -ForegroundColor DarkGray
Write-Host ("        RUN=" + $runLog) -ForegroundColor DarkGray
Write-Host ("        status.json=" + $statusPath) -ForegroundColor DarkGray
Write-Host ""
Write-Host "Все обязательные проверки пройдены. Окно можно закрыть." -ForegroundColor Green
Write-Host ""
$okSummary = if ($vpnModeActive) {
  "УСПЕХ: Claude + VPN. Claude → Squid $($envMap.PROXY_HOST). VPN выход: $($script:vpnEgress). Дом: $homeEgress"
} else {
  "УСПЕХ: Claude через Squid $($envMap.PROXY_HOST). VPN выкл."
}
if ($script:warnCount -gt 0) {
  $okSummary += " · WARN: $($script:warnCount)"
}
Save-Progress -Phase ok -Step "15/15" -Title "Готово — Claude работает" -Detail $okSummary -Pct 100 -Summary $okSummary
try { Stop-Transcript | Out-Null } catch {}
Wait-EnterOrSkip
exit 0
