# Unified stack teardown: Docker containers (if any) + ALL native leftovers.
# Used by stop.ps1 / restart.ps1 / start.ps1 / start-host.ps1.
# Console messages are ASCII-only to avoid GBK mojibake.
param(
    [switch]$Quiet
)

$ErrorActionPreference = "SilentlyContinue"
$Root = Split-Path -Parent $PSScriptRoot
if (-not (Test-Path (Join-Path $Root "start.ps1"))) {
    # allow calling from repo root as -File scripts\Stop-Stack.ps1
    if (Test-Path (Join-Path $PSScriptRoot "..\start.ps1")) {
        $Root = Resolve-Path (Join-Path $PSScriptRoot "..")
    }
}
Set-Location -LiteralPath $Root
$LogDir = Join-Path $Root ".runtime\logs"

function Write-Info([string]$msg) {
    if (-not $Quiet) { Write-Host $msg }
}

function Kill-Tree([int]$ProcessId) {
    if ($ProcessId -le 0) { return }
    # Skip self / critical system
    if ($ProcessId -eq $PID) { return }
    & taskkill.exe /F /T /PID $ProcessId 2>$null | Out-Null
    Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue
}

function Kill-Port([int]$Port) {
    $pids = @()
    Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | ForEach-Object {
        $pids += [int]$_.OwningProcess
    }
    $net = & netstat.exe -ano 2>$null
    foreach ($line in $net) {
        if ($line -notmatch [regex]::Escape(":${Port}")) { continue }
        if ($line -notmatch "LISTENING") { continue }
        $parts = ($line -split "\s+") | Where-Object { $_ }
        $procId = 0
        if ([int]::TryParse($parts[-1], [ref]$procId) -and $procId -gt 0) {
            $pids += $procId
        }
    }
    foreach ($procId in ($pids | Select-Object -Unique)) {
        try {
            $proc = Get-Process -Id $procId -ErrorAction Stop
        } catch {
            continue
        }
        Write-Info ("  free port {0} (pid={1} {2})" -f $Port, $procId, $proc.ProcessName)
        Kill-Tree $procId
    }
}

function Kill-ByCommand([string]$ProcessName, [string]$Pattern) {
    # WMI/CIM 在 Docker 半挂或本机负载高时会卡死；用限时 Job，超时则跳过
    $job = Start-Job -ScriptBlock {
        param($ProcessName, $Pattern)
        Get-CimInstance Win32_Process -Filter ("Name='{0}'" -f $ProcessName) -ErrorAction SilentlyContinue | Where-Object {
            $_.CommandLine -and ($_.CommandLine -match $Pattern)
        } | ForEach-Object { [int]$_.ProcessId }
    } -ArgumentList $ProcessName, $Pattern
    if (-not (Wait-Job $job -Timeout 5)) {
        Stop-Job $job -ErrorAction SilentlyContinue
        Remove-Job $job -Force -ErrorAction SilentlyContinue
        return
    }
    $ids = @(Receive-Job $job -ErrorAction SilentlyContinue)
    Remove-Job $job -Force -ErrorAction SilentlyContinue
    foreach ($procId in $ids) {
        if ($procId -gt 0) {
            Write-Info ("  kill {0} pid={1}" -f $ProcessName, $procId)
            Kill-Tree $procId
        }
    }
}

# ----- 1) Docker first (do NOT return; native may still be running) -----
# Docker Desktop 半挂时 `docker info` 会无限阻塞，必须限时。
$dockerOk = $false
try {
    $outFile = Join-Path $env:TEMP "aijz-docker-info.out"
    $errFile = Join-Path $env:TEMP "aijz-docker-info.err"
    $p = Start-Process -FilePath "docker" -ArgumentList @("info") -NoNewWindow -PassThru `
        -RedirectStandardOutput $outFile -RedirectStandardError $errFile
    if (-not $p.WaitForExit(8000)) {
        try { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue } catch {}
        Write-Info "  docker info timeout (8s); skip compose down"
        $dockerOk = $false
    } else {
        $dockerOk = ($p.ExitCode -eq 0)
    }
} catch {
    $dockerOk = $false
}

if ($dockerOk) {
    Write-Info "==> docker compose down --remove-orphans"
    & docker compose down --remove-orphans 2>$null
    # also stop any leftover named containers from older compose projects
    $names = @("ai-web-1", "ai-ai-1", "ai-gateway-1", "ai-platform-1", "ai-postgres-1")
    foreach ($n in $names) {
        & docker rm -f $n 2>$null | Out-Null
    }
}

# ----- 2) Watchdog first (otherwise it revives ai) -----
Write-Info "==> stopping native processes..."
$watchPidFile = Join-Path $LogDir "watchdog.pid"
if (Test-Path $watchPidFile) {
    $wid = Get-Content $watchPidFile -ErrorAction SilentlyContinue
    if ($wid) {
        Write-Info ("  kill watchdog pid={0}" -f $wid)
        Kill-Tree ([int]$wid)
    }
    Remove-Item $watchPidFile -Force -ErrorAction SilentlyContinue
}
Kill-ByCommand "powershell.exe" "watch-services\.ps1"
Kill-ByCommand "pwsh.exe" "watch-services\.ps1"
Kill-ByCommand "powershell.exe" "start-host\.ps1"
Kill-ByCommand "powershell.exe" "Restart-AiOnly|restart_ai_only|hard_restart"

# ----- 3) Pid files -----
foreach ($name in @("ai-service.pid", "platform.pid", "gateway.pid", "web.pid", "watchdog.pid")) {
    $pf = Join-Path $LogDir $name
    if (Test-Path $pf) {
        $pidText = Get-Content $pf -ErrorAction SilentlyContinue
        if ($pidText) {
            Write-Info ("  kill {0} -> {1}" -f $name, $pidText)
            Kill-Tree ([int]$pidText)
        }
        Remove-Item $pf -Force -ErrorAction SilentlyContinue
    }
}

# ----- 4) By process name / cmdline -----
Get-Process -Name "platform", "gateway" -ErrorAction SilentlyContinue | ForEach-Object {
    Write-Info ("  kill {0} pid={1}" -f $_.ProcessName, $_.Id)
    Kill-Tree ([int]$_.Id)
}

# uvicorn reloader = parent + workers
Kill-ByCommand "python.exe" "uvicorn"
Kill-ByCommand "python.exe" "app:app"
Kill-ByCommand "python.exe" "shot_worker\.py"
Kill-ByCommand "python.exe" "fidelity_loop|FIDELITY_SHOT"
Kill-ByCommand "python.exe" "ai-service"

# vite / node tooling for this repo
Kill-ByCommand "node.exe" "vite"
Kill-ByCommand "node.exe" "esbuild"
Kill-ByCommand "cmd.exe" "npm.*vite|vite\.js"

# Playwright Chromium leftovers from fidelity shots（限时，避免 CIM 卡死）
Kill-ByCommand "chrome.exe" "ms-playwright|playwright"
Kill-ByCommand "chromium.exe" "ms-playwright|playwright"

# ----- 5) Ports (authoritative) -----
foreach ($port in 8888, 8001, 8180, 5173) {
    Kill-Port $port
}

Start-Sleep -Seconds 2

# second pass for stubborn reloaders
Kill-ByCommand "python.exe" "uvicorn|app:app|shot_worker"
Kill-ByCommand "node.exe" "vite"
foreach ($port in 8888, 8001, 8180, 5173) {
    Kill-Port $port
}
Start-Sleep -Seconds 1

# ----- 6) Verify (only live processes count) -----
$still = @()
foreach ($port in 8888, 8001, 8180, 5173) {
    Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | ForEach-Object {
        $proc = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
        if ($proc) {
            $still += (":{0} pid={1} {2}" -f $port, $_.OwningProcess, $proc.ProcessName)
        }
    }
}

if ($still.Count -gt 0) {
    Write-Host "WARN  still listening after stop:" -ForegroundColor Yellow
    $still | ForEach-Object { Write-Host ("  {0}" -f $_) }
    exit 1
}

Write-Info "OK  stopped (docker + native; ports 8888/8001/8180/5173 free)"
exit 0

