# Independent Windows NSIS upgrade (detached from Python/Electron agent).
#
# Usage:
#   windows_exe_upgrade.ps1 -InstallerPath <path> [-UpgradeId <id>] [-TargetVersion <ver>] [-DryRun]
#
# Logs: %USERPROFILE%\.workstatus\logs\windows_force_upgrade.log

param(
    [Parameter(Mandatory = $true)]
    [string]$InstallerPath,
    [string]$UpgradeId = "unknown",
    [string]$TargetVersion = "",
    [switch]$DryRun
)

$ErrorActionPreference = "Continue"
$LogDir = Join-Path $env:USERPROFILE ".workstatus\logs"
$LogFile = Join-Path $LogDir "windows_force_upgrade.log"
$SingleInstancePort = 12345
$IpcPort = 54321
$TmpInstallerGlob = Join-Path $env:TEMP "ws_force_upgrade_*.exe"

$ProcessImageNames = @(
    "Workstatus-Desktop",
    "workstatus-desktop",
    "Workstatus",
    "Workstatus.exe"
)

$UninstallDisplayNameExact = @(
    "Workstatus Desktop",
    "Workstatus-Desktop",
    "Workstatus"
)

$DesktopInstallCandidates = @(
    (Join-Path $env:LOCALAPPDATA "Programs\Workstatus-Desktop"),
    (Join-Path $env:LOCALAPPDATA "Programs\workstatus-desktop")
)

$DesktopExeCandidates = @(
    (Join-Path $env:LOCALAPPDATA "Programs\Workstatus-Desktop\Workstatus-Desktop.exe"),
    (Join-Path $env:LOCALAPPDATA "Programs\workstatus-desktop\workstatus-desktop.exe"),
    (Join-Path $env:LOCALAPPDATA "Programs\Workstatus-Desktop\workstatus-desktop.exe")
)

function Write-UpgradeLog {
    param([string]$Message)
    $ts = Get-Date -Format "yyyy-MM-ddTHH:mm:ssK"
    $line = "[upgrade] $ts $Message"
    try {
        New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
        Add-Content -Path $LogFile -Value $line -Encoding UTF8
    } catch {
        Write-Host $line
    }
}

function Test-ValidInstaller {
    param([string]$Path)
    if (-not (Test-Path -LiteralPath $Path)) {
        Write-UpgradeLog "validate: missing file $Path"
        return $false
    }
    try {
        $fs = [System.IO.File]::OpenRead($Path)
        try {
            $header = New-Object byte[] 2
            if ($fs.Read($header, 0, 2) -lt 2) {
                Write-UpgradeLog "validate: file too small"
                return $false
            }
            if ($header[0] -ne 0x4D -or $header[1] -ne 0x5A) {
                Write-UpgradeLog "validate: not a PE executable (missing MZ header)"
                return $false
            }
            $readLen = [Math]::Min(2097152, [int]$fs.Length)
            $buf = New-Object byte[] $readLen
            [void]$fs.Read($buf, 0, $readLen)
            $text = [System.Text.Encoding]::ASCII.GetString($buf)
            if ($text -notmatch 'Nullsoft') {
                Write-UpgradeLog "validate: PE file is not NSIS - API must return desktop NSIS setup (filename may still be Workstatus.exe)"
                return $false
            }
        } finally {
            $fs.Dispose()
        }
        $size = (Get-Item -LiteralPath $Path).Length
        Write-UpgradeLog "validate: NSIS installer ok size=$size bytes"
        return $true
    } catch {
        Write-UpgradeLog "validate: error reading $Path : $_"
        return $false
    }
}

function Stop-ListenersOnPort {
    param([int]$Port)
    Write-UpgradeLog "stopping listeners on TCP $Port"
    try {
        $conns = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue
        foreach ($c in $conns) {
            $procId = $c.OwningProcess
            if ($procId -and $procId -ne $PID) {
                Write-UpgradeLog "Stop-ListenersOnPort: graceful stop PID $procId (port $Port)"
                Stop-Process -Id $procId -ErrorAction SilentlyContinue
                Start-Sleep -Seconds 2
                if (Get-Process -Id $procId -ErrorAction SilentlyContinue) {
                    Write-UpgradeLog "Stop-ListenersOnPort: force stop PID $procId"
                    Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
                }
            }
        }
    } catch {
        Write-UpgradeLog "Stop-ListenersOnPort: Get-NetTCPConnection failed, falling back to netstat: $_"
        try {
            $lines = netstat -ano | Select-String ":$Port\s"
            foreach ($line in $lines) {
                $parts = ($line -replace '\s+', ' ').Trim().Split(' ')
                $procId = $parts[-1]
                if ($procId -match '^\d+$' -and [int]$procId -ne $PID) {
                    taskkill /PID $procId /T 2>$null | Out-Null
                    Start-Sleep -Seconds 1
                    taskkill /PID $procId /F /T 2>$null | Out-Null
                }
            }
        } catch {
            Write-UpgradeLog "Stop-ListenersOnPort: netstat fallback failed: $_"
        }
    }
    Start-Sleep -Seconds 1
}

function Stop-WorkstatusProcesses {
    Write-UpgradeLog "stopping Workstatus processes (graceful then force)"
    foreach ($name in $ProcessImageNames) {
        $procs = Get-Process -Name $name -ErrorAction SilentlyContinue
        foreach ($p in $procs) {
            if ($p.Id -eq $PID) { continue }
            Write-UpgradeLog "graceful CloseMainWindow PID=$($p.Id) name=$($p.ProcessName)"
            try { $null = $p.CloseMainWindow() } catch {}
        }
    }
    Start-Sleep -Seconds 4
    foreach ($name in $ProcessImageNames) {
        $procs = Get-Process -Name $name -ErrorAction SilentlyContinue
        foreach ($p in $procs) {
            if ($p.Id -eq $PID) { continue }
            Write-UpgradeLog "force Stop-Process PID=$($p.Id) name=$($p.ProcessName)"
            Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue
        }
    }
    Start-Sleep -Seconds 2
}

function Get-UninstallEntries {
    $entries = @()
    $roots = @(
        "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
        "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
        "HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
    )
    foreach ($root in $roots) {
        $items = Get-ItemProperty $root -ErrorAction SilentlyContinue
        if (-not $items) { continue }
        if ($items -isnot [array]) { $items = @($items) }
        foreach ($item in $items) {
            $dn = $item.DisplayName
            if (-not $dn) { continue }
            if ($UninstallDisplayNameExact -notcontains $dn) { continue }
            if (-not $item.UninstallString) { continue }
            $entries += [PSCustomObject]@{
                DisplayName     = $dn
                UninstallString = $item.UninstallString
                QuietUninstall  = $item.QuietUninstallString
            }
        }
    }
    return $entries | Sort-Object DisplayName -Unique
}

function Invoke-SilentUninstall {
    param([string]$UninstallString, [string]$QuietUninstall, [string]$DisplayName)
    $cmd = if ($QuietUninstall) { $QuietUninstall } else { $UninstallString }
    if (-not $cmd) {
        Write-UpgradeLog "uninstall: no command for $DisplayName"
        return
    }
    Write-UpgradeLog "uninstall: starting $DisplayName => $cmd"
    if ($cmd -match 'msiexec(\.exe)?') {
        if ($cmd -notmatch '/quiet' -and $cmd -notmatch '/qn') {
            $cmd = "$cmd /quiet /norestart"
        }
    } elseif ($cmd -notmatch '/S\b' -and $cmd -notmatch '/silent' -and $cmd -notmatch '/quiet') {
        $cmd = "$cmd /S"
    }
    try {
        $proc = Start-Process -FilePath "cmd.exe" -ArgumentList @("/c", $cmd) -Wait -PassThru -WindowStyle Hidden `
            -RedirectStandardOutput $LogFile -RedirectStandardError $LogFile
        Write-UpgradeLog "uninstall: $DisplayName finished exit=$($proc.ExitCode)"
    } catch {
        Write-UpgradeLog "uninstall: ERROR $DisplayName : $_"
    }
    Start-Sleep -Seconds 2
}

function Remove-WorkstatusStartupShortcut {
    $startupFolder = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup"
    $shortcut = Join-Path $startupFolder "Workstatus.lnk"
    if (Test-Path -LiteralPath $shortcut) {
        Write-UpgradeLog "startup: removing $shortcut"
        Remove-Item -LiteralPath $shortcut -Force -ErrorAction SilentlyContinue
    }
}

function Remove-AppDataWorkstatusCompletely {
    <#
    Remove entire %APPDATA%\Workstatus (legacy agent, DB, logs, python venv copy).
    Matches NSIS preInit in build/installer.nsh — required so Electron does not keep launching old Workstatus.exe.
    #>
    $legacyDir = Join-Path $env:APPDATA "Workstatus"
    if (-not (Test-Path -LiteralPath $legacyDir)) {
        Write-UpgradeLog "appdata: no $legacyDir folder (already clean)"
        return
    }

    Write-UpgradeLog "appdata: removing entire tree $legacyDir"
    $maxAttempts = 5
    for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
        try {
            Remove-Item -LiteralPath $legacyDir -Recurse -Force -ErrorAction Stop
            if (-not (Test-Path -LiteralPath $legacyDir)) {
                Write-UpgradeLog "appdata: removed $legacyDir (attempt $attempt)"
                return
            }
        } catch {
            Write-UpgradeLog "appdata: remove attempt $attempt failed: $_"
        }
        Start-Sleep -Seconds 2
    }

    if (Test-Path -LiteralPath $legacyDir) {
        Write-UpgradeLog "WARN: $legacyDir still exists after $maxAttempts attempts (files may be locked)"
        Get-ChildItem -LiteralPath $legacyDir -ErrorAction SilentlyContinue | ForEach-Object {
            Write-UpgradeLog "appdata: remaining $($_.FullName)"
        }
    }
}

function Remove-StaleInstallDirs {
    foreach ($dir in $DesktopInstallCandidates) {
        if (Test-Path -LiteralPath $dir) {
            Write-UpgradeLog "removing stale install dir $dir"
            Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}

function Install-NewBuild {
    param([string]$Path)
    Write-UpgradeLog "install: silent NSIS $Path"
    try {
        $proc = Start-Process -FilePath $Path -ArgumentList @("/S") -Wait -PassThru -WindowStyle Hidden
        Write-UpgradeLog "install: finished exit=$($proc.ExitCode)"
        return ($proc.ExitCode -eq 0)
    } catch {
        Write-UpgradeLog "install: ERROR $_"
        return $false
    }
}

function Start-InstalledDesktopApp {
    foreach ($exe in $DesktopExeCandidates) {
        if (Test-Path -LiteralPath $exe) {
            Write-UpgradeLog "start: launching $exe"
            Start-Process -FilePath $exe -WindowStyle Normal
            return $true
        }
    }
    Write-UpgradeLog "WARN: installed exe not found in expected paths"
    return $false
}

function Remove-OldTempInstallers {
    param([string]$KeepPath)
    $keepResolved = $null
    if ($KeepPath) {
        try { $keepResolved = (Resolve-Path -LiteralPath $KeepPath).Path } catch {}
    }
    Get-ChildItem -Path $env:TEMP -Filter "ws_force_upgrade_*.exe" -ErrorAction SilentlyContinue | ForEach-Object {
        if ($keepResolved -and $_.FullName -eq $keepResolved) { return }
        Write-UpgradeLog "cleanup: removing old temp installer $($_.FullName)"
        Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue
    }
}

# --- main ---
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
Write-UpgradeLog "===== windows exe upgrade start upgrade_id=$UpgradeId target_version=$TargetVersion ====="
Write-UpgradeLog "user=$env:USERNAME installer=$InstallerPath"

if (-not (Test-Path -LiteralPath $InstallerPath)) {
    Write-UpgradeLog "ERROR installer not found: $InstallerPath"
    exit 1
}
$InstallerPath = (Resolve-Path -LiteralPath $InstallerPath).Path

if (-not (Test-ValidInstaller -Path $InstallerPath)) {
    Write-UpgradeLog "ERROR aborting: invalid installer"
    exit 1
}

if ($DryRun) {
    Write-UpgradeLog "DRY RUN: spawn test OK (skipping stop/uninstall/install). upgrade_id=$UpgradeId"
    Write-UpgradeLog "===== windows exe upgrade end upgrade_id=$UpgradeId (dry run) ====="
    exit 0
}

Remove-OldTempInstallers -KeepPath $InstallerPath

Stop-ListenersOnPort -Port $SingleInstancePort
Stop-ListenersOnPort -Port $IpcPort
Stop-WorkstatusProcesses

$entries = Get-UninstallEntries
if ($entries.Count -eq 0) {
    Write-UpgradeLog "uninstall: no Add/Remove Programs entries matched (will remove dirs manually)"
} else {
    foreach ($e in $entries) {
        Invoke-SilentUninstall -UninstallString $e.UninstallString -QuietUninstall $e.QuietUninstall -DisplayName $e.DisplayName
    }
}

Remove-LegacyAppDataInstall
Remove-StaleInstallDirs

if (-not (Install-NewBuild -Path $InstallerPath)) {
    Write-UpgradeLog "ERROR silent install failed"
    exit 1
}

Remove-Item -LiteralPath $InstallerPath -Force -ErrorAction SilentlyContinue
Write-UpgradeLog "removed temp installer"

# Desktop app may copy a fresh python-build into AppData on first launch (expected).
Start-InstalledDesktopApp | Out-Null
Write-UpgradeLog "===== windows exe upgrade end upgrade_id=$UpgradeId ====="
exit 0
