#Requires -RunAsAdministrator
# Full apply: kill Claude -> stop old TUN -> start new TUN from prebuilt (or rebuild) config.
#
# ASCII-only on purpose. Windows PowerShell 5.1 reads a BOM-less UTF-8 script as the
# ANSI codepage, which turned a non-ASCII character inside a string into a parse error
# and took down the whole apply. Keeping this file ASCII makes it immune regardless of
# how it was copied.
#
# Heartbeat BEFORE _env.ps1 so we can see "script started" even if env fails.
$ErrorActionPreference = "Continue"
$bootHome = $env:CLAUDE_PROXY_HOME
if (-not $bootHome) { $bootHome = Join-Path $env:USERPROFILE ".claude-proxy-control" }
$bootMerged = Join-Path $bootHome "merged"
$bootLogs = Join-Path $bootHome "logs"
New-Item -ItemType Directory -Force -Path $bootLogs, $bootMerged | Out-Null
$bootTrace = Join-Path $bootLogs "cpc-trace.log"
$bootResult = Join-Path $bootMerged "reload-result.json"
$bootRequest = Join-Path $bootMerged "reload-request.json"

# Carry the caller's token into the very first result write, so a waiter can already
# tell "my run started" apart from "somebody else's leftover result".
$bootToken = $env:CPC_RELOAD_TOKEN
if (-not $bootToken) {
  try {
    if (Test-Path $bootRequest) {
      $bootToken = [string]((Get-Content $bootRequest -Raw -Encoding UTF8 | ConvertFrom-Json).token)
    }
  } catch { $bootToken = "" }
}
if (-not $bootToken) { $bootToken = "" }
try {
  $bootLine = "$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ss.fff') [RELOAD] INFO  reload.boot  script entered PSScriptRoot=$PSScriptRoot token=$bootToken"
  Add-Content -Path $bootTrace -Value $bootLine -Encoding UTF8
  @{
    ok     = $false
    detail = "elevated-reload-started"
    tun    = ""
    token  = $bootToken
    time   = (Get-Date).ToString("o")
  } | ConvertTo-Json | Set-Content -Path $bootResult -Encoding UTF8
} catch {}

$ErrorActionPreference = "Stop"
try { chcp 65001 | Out-Null } catch {}
. (Join-Path $PSScriptRoot "_env.ps1")
$env:CLAUDE_PROXY_HOME = $ProductHome
$env:CLAUDE_PROXY_NOPAUSE = "1"

$cfg = Join-Path $MergedDir "vpn-claude-config.json"
$statusPath = Join-Path $MergedDir "status.json"
$log = Join-Path $MergedDir "vpn-claude-tun.log"
$builder = Join-Path $MergedDir "build_sota_merged_config.py"
$reqPath = Join-Path $MergedDir "reload-request.json"
$resultPath = Join-Path $MergedDir "reload-result.json"
$traceDir = Join-Path $ProductHome "logs"
$tracePath = Join-Path $traceDir "cpc-trace.log"
New-Item -ItemType Directory -Force -Path $traceDir | Out-Null

# Token of the request this run is serving. Echoed back in reload-result so the UI can
# prove the result belongs to its own request and not to a concurrent or stale one.
$script:ReloadToken = $bootToken

function Write-CpcTrace([string]$Step, [string]$Message = "", [string]$Level = "INFO") {
  $ts = Get-Date -Format "yyyy-MM-ddTHH:mm:ss.fff"
  $line = "$ts [RELOAD] $Level  $Step  $Message"
  try { Add-Content -Path $tracePath -Value $line -Encoding UTF8 } catch {}
  Write-Host $line
}

function Write-ReloadResult {
  # Never use a param named Pid - it collides with the automatic $PID variable in
  # Windows PowerShell 5.1 and throws "Cannot overwrite variable Pid".
  param(
    [Parameter(Mandatory = $true)][bool]$Ok,
    [Parameter(Mandatory = $true)][string]$Detail,
    [string]$TunName = "",
    [int]$SingBoxProcessId = 0
  )
  $obj = @{
    ok         = $Ok
    detail     = $Detail
    tun        = $TunName
    singBoxPid = $SingBoxProcessId
    token      = $script:ReloadToken
    time       = (Get-Date).ToString("o")
  }
  ($obj | ConvertTo-Json -Depth 5) | Set-Content -Path $resultPath -Encoding UTF8
  Write-CpcTrace "reload.result" ("ok=$Ok detail=$Detail tun=$TunName singBoxPid=$SingBoxProcessId token=$script:ReloadToken")
}

$lock = Enter-CpcReloadLock -TimeoutSec 120
if ($null -eq $lock) {
  Write-CpcTrace "reload.lock" "another elevated reload is still running - refusing to run concurrently" "ERROR"
  Write-ReloadResult -Ok $false -Detail "another reload is already running"
  exit 4
}
Write-CpcTrace "reload.lock" "acquired"

try {
  Write-CpcTrace "reload.begin" ("home=$ProductHome script=$PSCommandPath token=$script:ReloadToken")
  Write-Host "Apply routes: restart TUN (+ close/reopen Claude from UI)..." -ForegroundColor Cyan

  # 1) Close Claude first so it cannot leak while TUN restarts.
  #    The UI reopens it in its finally block, on success and on failure alike.
  Write-CpcTrace "reload.claude" "Closing Claude processes..."
  Get-Process -Name "Claude", "claude", "cowork-svc" -ErrorAction SilentlyContinue |
    ForEach-Object {
      Write-CpcTrace "reload.claude.kill" ("pid=$($_.Id) name=$($_.ProcessName)")
      Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
    }
  Start-Sleep -Seconds 1

  $sbExe = Find-CpcSingBoxExe
  Write-CpcTrace "reload.singbox" ("exe=$sbExe")
  if (-not $sbExe) {
    Write-ReloadResult -Ok $false -Detail "sing-box.exe missing under $SingBoxRoot"
    throw "sing-box.exe missing under $SingBoxRoot"
  }
  $sbDir = Split-Path $sbExe -Parent

  $prebuilt = $false
  $tunName = $null
  if (Test-Path $reqPath) {
    try {
      $req = Get-Content $reqPath -Raw -Encoding UTF8 | ConvertFrom-Json
      Write-CpcTrace "reload.request" ("built=$($req.built) tun=$($req.tun) token=$($req.token) time=$($req.time)")
      if ($req.token) { $script:ReloadToken = [string]$req.token }
      if ($req.built -and $req.tun -and (Test-Path $cfg)) {
        $raw = Get-Content $cfg -Raw -Encoding UTF8
        if ($raw -match [regex]::Escape([string]$req.tun)) {
          $prebuilt = $true
          $tunName = [string]$req.tun
          Write-CpcTrace "reload.prebuilt" ("USING prebuilt config TUN=$tunName")
        } else {
          Write-CpcTrace "reload.prebuilt" "tun name not in config - will rebuild" "WARN"
        }
      }
    } catch {
      Write-CpcTrace "reload.request" ("unreadable: $_") "WARN"
    }
    # Consume the request. It used to live forever, so any later elevated reload would
    # silently re-apply the last build instead of the current app rules.
    try {
      Remove-Item -LiteralPath $reqPath -Force -ErrorAction SilentlyContinue
      Write-CpcTrace "reload.request" "consumed reload-request.json"
    } catch {}
  } else {
    Write-CpcTrace "reload.request" "reload-request.json missing" "WARN"
  }

  if (-not $prebuilt) {
    if (-not (Test-Path $builder)) {
      Write-ReloadResult -Ok $false -Detail "builder missing"
      throw "builder missing"
    }
    $capture = Join-Path $MergedDir "sota-captured\latest.json"
    if (-not (Test-Path $capture)) {
      Write-ReloadResult -Ok $false -Detail "no Sota capture"
      throw "no Sota capture"
    }
    $python = Find-CpcPython
    if (-not $python) {
      Write-ReloadResult -Ok $false -Detail "python missing"
      throw "python missing"
    }
    $tunName = New-CpcTunName
    $env:VPN_CLAUDE_TUN_NAME = $tunName
    $desk = $null
    $dp = Join-Path $ProductHome "desktop-path.txt"
    if (Test-Path $dp) {
      $desk = (Get-Content $dp -Raw -Encoding UTF8).Trim().Trim([char]0xFEFF)
    }
    if ($desk) { $env:PYTHONPATH = $desk }
    Write-CpcTrace "reload.rebuild" ("python=$python tun=$tunName PYTHONPATH=$desk")
    & $python $builder
    Write-CpcTrace "reload.rebuild.done" ("exit=$LASTEXITCODE")
    if ($LASTEXITCODE -ne 0) {
      Write-ReloadResult -Ok $false -Detail "build failed"
      throw "build failed"
    }
  }

  $env:VPN_CLAUDE_TUN_NAME = $tunName
  Write-CpcTrace "reload.check" ("sing-box check -c $cfg tun=$tunName")
  & $sbExe check -c $cfg
  Write-CpcTrace "reload.check.done" ("exit=$LASTEXITCODE")
  if ($LASTEXITCODE -ne 0) {
    Write-ReloadResult -Ok $false -Detail "sing-box check failed"
    throw "sing-box check failed"
  }

  $prev = $null
  if (Test-Path $statusPath) {
    try { $prev = Get-Content $statusPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch {}
  }
  Write-CpcTrace "reload.prev_status" ("tun=$($prev.tun) pid=$($prev.singBoxPid)")

  # From here until the new TUN is Up there are no routes of ours. Close the
  # window with a firewall rule so traffic stalls instead of escaping unrouted.
  $ksGap = [System.Diagnostics.Stopwatch]::StartNew()
  $ksOn = Enable-CpcKillSwitch
  if ($ksOn) {
    Write-CpcTrace "reload.killswitch" "outbound blocked for the rebuild"
  } else {
    Write-CpcTrace "reload.killswitch" "could NOT arm firewall rule - rebuild runs unprotected" "WARN"
  }

  Write-CpcTrace "reload.stop" "Stopping old CPC sing-box..."
  $stopped = Stop-CpcSingBox
  Write-CpcTrace "reload.stop.done" ("stopped_count=$stopped")

  # Wait for the fact, not for a fixed two seconds: the adapter normally goes
  # away with the process that owned it, and the heavy PnP sweep is only worth
  # running when it did not.
  $gone = Wait-CpcAdapterGone -TimeoutMs 2500
  Write-CpcTrace "reload.adapter_gone" ("gone=$gone after $($ksGap.ElapsedMilliseconds)ms")
  if (-not $gone) {
    $cleared = Clear-CpcOrphanWintun
    Write-CpcTrace "reload.orphan" ("Clear-CpcOrphanWintun removed=$cleared")
    Get-NetAdapter -ErrorAction SilentlyContinue | Where-Object { $_.Name -match $CpcAdapterPattern } | ForEach-Object {
      Write-CpcTrace "reload.disable_adapter" ("name=$($_.Name) status=$($_.Status)")
      try { Disable-NetAdapter -Name $_.Name -Confirm:$false -ErrorAction SilentlyContinue } catch {}
    }
  }

  # Keep one rotation of the previous run, and stop the log growing without bound.
  if (Test-Path $log) {
    try {
      $sizeMb = [math]::Round((Get-Item $log).Length / 1MB, 1)
      Write-CpcTrace "reload.log_rotate" ("previous tun log ${sizeMb}MB -> .bak")
    } catch {}
    Move-Item $log "$log.bak" -Force -ErrorAction SilentlyContinue
  }
  $outLog = Join-Path $MergedDir "vpn-claude-tun-out.log"

  $started = $false
  $sbPid = 0
  for ($attempt = 1; $attempt -le 3; $attempt++) {
    Write-CpcTrace "reload.start.attempt" ("n=$attempt/3 tun=$tunName")
    [void](Stop-CpcSingBox)
    Start-Sleep -Seconds 1
    if ($attempt -gt 1) {
      [void](Clear-CpcOrphanWintun)
      # Rename the interface inside the ALREADY VERIFIED config instead of rebuilding.
      # A rebuild here could produce a different rule set than the one the UI checked,
      # and the token (not the interface name) is what identifies this request.
      $newTun = New-CpcTunName
      try {
        $rawCfg = [System.IO.File]::ReadAllText($cfg)
        $rawCfg = $rawCfg.Replace($tunName, $newTun)
        $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
        [System.IO.File]::WriteAllText($cfg, $rawCfg, $utf8NoBom)
        Write-CpcTrace "reload.retry_tun" ("renamed interface $tunName -> $newTun (config unchanged otherwise)")
        $tunName = $newTun
        $env:VPN_CLAUDE_TUN_NAME = $tunName
      } catch {
        Write-CpcTrace "reload.retry_tun" ("rename failed: $_") "ERROR"
        Write-ReloadResult -Ok $false -Detail "could not rename TUN interface for retry"
        throw "could not rename TUN interface for retry"
      }
      & $sbExe check -c $cfg
      if ($LASTEXITCODE -ne 0) {
        Write-ReloadResult -Ok $false -Detail "sing-box check failed after rename"
        throw "sing-box check failed after rename"
      }
    }

    Write-CpcTrace "reload.start.process" ("Start-Process sing-box tun=$tunName")
    $proc = Start-Process -FilePath $sbExe -ArgumentList @("run", "-c", $cfg) -WorkingDirectory $sbDir -WindowStyle Hidden `
      -RedirectStandardError $log -RedirectStandardOutput $outLog -PassThru
    $startedPid = 0
    if ($proc) { $startedPid = [int]$proc.Id }
    Write-CpcTrace "reload.start.pid" ("sing-box pid=$startedPid")

    for ($i = 1; $i -le 30; $i++) {
      Start-Sleep -Seconds 1
      # Track the process we actually started, not "any sing-box on the machine".
      $alive = $null
      if ($startedPid -gt 0) {
        $alive = Get-Process -Id $startedPid -ErrorAction SilentlyContinue
      }
      if (-not $alive) {
        if ($i -eq 1 -or ($i % 5) -eq 0) {
          Write-CpcTrace "reload.wait" ("t=${i}s our sing-box (pid=$startedPid) not running")
        }
        continue
      }
      if (Test-Path $log) {
        $tail = Get-Content $log -Tail 40 -ErrorAction SilentlyContinue | Out-String
        if ($tail -match 'FATAL|Cannot create a file when that file already exists') {
          Write-CpcTrace "reload.fatal" ("sing-box FATAL at t=${i}s - retry") "ERROR"
          Write-CpcTrace "reload.fatal.tail" ($tail.Substring(0, [Math]::Min(400, $tail.Length))) "ERROR"
          [void](Stop-CpcSingBox)
          break
        }
      }
      $ad = Get-NetAdapter -Name $tunName -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq "Up" }
      if ($ad) {
        $started = $true
        $sbPid = $startedPid
        Write-CpcTrace "reload.up" ("TUN Up name=$tunName pid=$sbPid after ${i}s")
        break
      }
      if (($i % 5) -eq 0) {
        Write-CpcTrace "reload.wait" ("t=${i}s pid=$startedPid adapter=$tunName not Up yet")
      }
    }
    if ($started) { break }
  }

  if (-not $started) {
    Write-CpcTrace "reload.fail" "TUN did not come back Up" "ERROR"
    Write-ReloadResult -Ok $false -Detail "TUN did not come back Up after reload"
    throw "TUN did not come back Up after reload"
  }

  # Routes are ours again - lift the block as early as possible, before the
  # bookkeeping below, so the pause is no longer than it has to be.
  Disable-CpcKillSwitch
  Write-CpcTrace "reload.killswitch" ("lifted, no-route window was $($ksGap.ElapsedMilliseconds)ms")

  # Report route.final as it actually is in the config we just loaded, instead of
  # asserting "block" and hoping.
  $routeFinal = Get-CpcRouteFinal -ConfigPath $cfg
  try {
    $obj = @{
      ok            = $true
      mode          = $(if ($prev -and $prev.mode) { [string]$prev.mode } else { "CPC+SotaVPN" })
      vpnModeActive = $true
      vpnProbeOk    = $(if ($prev -and $null -ne $prev.vpnProbeOk) { [bool]$prev.vpnProbeOk } else { $true })
      tun           = $tunName
      singBoxPid    = $sbPid
      vpnEgress     = $(if ($prev) { [string]$prev.vpnEgress } else { "" })
      homeEgress    = $(if ($prev) { [string]$prev.homeEgress } else { "" })
      squidHost     = $(if ($prev) { [string]$prev.squidHost } else { "" })
      routeFinal    = $routeFinal
      time          = (Get-Date).ToString("o")
      reloaded      = $true
      appliedRoutes = $true
    }
    ($obj | ConvertTo-Json -Depth 6) | Set-Content -Path $statusPath -Encoding UTF8
    Write-CpcTrace "reload.status" ("wrote status.json tun=$tunName pid=$sbPid routeFinal=$routeFinal")
  } catch {
    Write-CpcTrace "reload.status" ("WARN: $_") "WARN"
  }

  Write-ReloadResult -Ok $true -Detail ("TUN restarted: $tunName") -TunName $tunName -SingBoxProcessId $sbPid
  Write-CpcTrace "reload.ok" ("TUN=$tunName pid=$sbPid - UI will reopen Claude")
  Write-Host ("OK: TUN=$tunName pid=$sbPid - UI will reopen Claude") -ForegroundColor Green
  exit 0
} finally {
  # Last line of defence: every failure path above leaves through here, so the
  # machine can never be left behind a block rule we forgot to remove.
  if (Remove-CpcKillSwitchRule) {
    Write-CpcTrace "reload.killswitch" "removed in finally (rebuild did not finish cleanly)" "WARN"
  }
  Exit-CpcReloadLock $lock
  Write-CpcTrace "reload.lock" "released"
}
