<#
================================================================================
  VelaPanel - Rollback Script   (run ON the production server, as Admin)
================================================================================

  Restores a previously-kept  publish.bak-<ts>  (or publish.failed-<ts>) folder
  as the live  publish  build, then restarts the service and health-checks it.

  This script is normally launched by the panel's "Rollback to Previous Version"
  button via WMI Win32_Process.Create, so that it runs OUTSIDE the VelaPanel
  Windows-service Job Object and survives the Stop-Service it issues. (Doing the
  rollback in-process inside the service kills the rollback the instant the
  service stops - which is why the old in-process rollback hung half-done.)

  Usage:
    powershell -ExecutionPolicy Bypass -File rollback-update.ps1 -BackupName publish.bak-20260619-101811
================================================================================
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string] $BackupName,
    [string] $Root             = "C:\velapanel.com",
    [string] $ServiceName      = "VelaPanel",
    [string] $HealthUrl        = "http://localhost:5100/",
    [int]    $HealthTimeoutSec = 120,
    [string] $AppExeName       = "VelaPanel.exe"
)

$ErrorActionPreference = "Stop"
$ts          = Get-Date -Format "yyyyMMdd-HHmmss"
$LivePath    = Join-Path $Root "publish"
$BackupPath  = Join-Path $Root $BackupName
$DataDir     = Join-Path $Root "data"
$PanelDb     = Join-Path $DataDir "panel.db"
$BackupDir   = Join-Path $DataDir "backups"
$RolledPath  = Join-Path $Root "publish.rolled-$ts"
$logDir      = Join-Path $DataDir "logs"

New-Item -ItemType Directory -Force -Path $logDir, $BackupDir | Out-Null
$transcript = Join-Path $logDir "rollback-$ts.log"
Start-Transcript -Path $transcript -Append | Out-Null

function Info($m) { Write-Host "[*] $m" -ForegroundColor Cyan }
function Ok($m)   { Write-Host "[+] $m" -ForegroundColor Green }
function Warn($m) { Write-Host "[!] $m" -ForegroundColor Yellow }
function Fail($m) { Write-Host "[x] $m" -ForegroundColor Red }

# Lift the management scripts shipped inside the restored build up to the panel root, so
# C:\velapanel.com\ stays in sync with whatever build is now live. Best-effort — a failure
# here must never fail an otherwise-successful rollback.
function Sync-MgmtScripts($fromPublish) {
    try {
        foreach ($s in @("deploy-update.ps1", "rollback-update.ps1")) {
            $src = Join-Path $fromPublish $s
            if (Test-Path $src) { Copy-Item $src (Join-Path $Root $s) -Force; Info "Synced $s to root." }
        }
        $srcTools = Join-Path $fromPublish "tools"
        if (Test-Path $srcTools) {
            $dstTools = Join-Path $Root "tools"
            if (-not (Test-Path $dstTools)) { New-Item -ItemType Directory -Force -Path $dstTools | Out-Null }
            Copy-Item (Join-Path $srcTools "*") $dstTools -Recurse -Force
            Info "Synced tools\ to root."
        }
    } catch { Warn "Management-script sync skipped: $($_.Exception.Message)" }
}

function Test-Healthy {
    # Follow redirects (/ -> /login); any response under HTTP 500 means alive.
    # Do NOT use -MaximumRedirection 0 on PS 5.1 (throws on redirect -> false fail).
    $ProgressPreference = 'SilentlyContinue'
    $deadline = (Get-Date).AddSeconds($HealthTimeoutSec)
    while ((Get-Date) -lt $deadline) {
        $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
        if ($svc -and $svc.Status -eq 'Running') {
            try {
                $r = Invoke-WebRequest -Uri $HealthUrl -UseBasicParsing -TimeoutSec 8 -ErrorAction Stop
                if ([int]$r.StatusCode -lt 500) { Info "Healthy (HTTP $($r.StatusCode))"; return $true }
            } catch {
                $code = $null
                try { $code = [int]$_.Exception.Response.StatusCode } catch {}
                if ($code -and $code -gt 0 -and $code -lt 500) { Info "Healthy (HTTP $code)"; return $true }
            }
        }
        Start-Sleep -Seconds 3
    }
    return $false
}

$swapDone = $false

try {
    Info "Rollback $ts started  (BackupName=$BackupName, log: $transcript)"

    # ---- 1. Validate ----
    if (-not (Test-Path $BackupPath)) { throw "Backup folder '$BackupPath' not found." }
    if (-not (Test-Path (Join-Path $BackupPath $AppExeName))) { throw "'$BackupPath' has no $AppExeName - not a valid build." }
    if (-not (Test-Path $LivePath)) { throw "Live folder '$LivePath' not found - wrong server?" }

    # Runtime-model guard (self-contained vs framework-dependent must match).
    $liveSelfContained = Test-Path (Join-Path $LivePath 'hostfxr.dll')
    $bakSelfContained  = Test-Path (Join-Path $BackupPath 'hostfxr.dll')
    if ($liveSelfContained -ne $bakSelfContained) {
        throw ("Build model MISMATCH - aborting before any change. " +
               "Live self-contained=$liveSelfContained, Backup self-contained=$bakSelfContained.")
    }
    Ok "Validated: backup build present, runtime model matches (self-contained=$liveSelfContained)."

    # ---- 2. Back up the DB ----
    if (Test-Path $PanelDb) {
        $dbBackup = Join-Path $BackupDir "panel-pre-rollback-$ts.db"
        Copy-Item $PanelDb $dbBackup -Force
        Ok "Backed up panel.db -> $dbBackup"
    } else { Warn "panel.db not found at $PanelDb - skipping DB backup." }

    # ---- 3. Stop ----
    Info "Stopping service '$ServiceName'..."
    Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
    try { (Get-Service $ServiceName).WaitForStatus('Stopped', '00:01:00') } catch {}

    $lingering = Get-Process -Name $AppExeName.Replace('.exe','') -ErrorAction SilentlyContinue
    if ($lingering) {
        Warn "Lingering process found (PID $($lingering.Id)) - force-killing..."
        $lingering | Stop-Process -Force -ErrorAction SilentlyContinue
        Start-Sleep -Seconds 3
    }
    Start-Sleep -Seconds 2
    Ok "Service stopped."

    # ---- 4. Swap: current publish -> publish.rolled-<ts>, backup -> publish ----
    Info "Moving current publish -> $RolledPath"
    Rename-Item -Path $LivePath -NewName (Split-Path $RolledPath -Leaf)
    try {
        Info "Restoring $BackupName -> $LivePath"
        Move-Item -Path $BackupPath -Destination $LivePath
        $swapDone = $true
        Ok "Backup build is now live."

        # Preserve production's machine-specific appsettings.json from the build
        # we just moved aside (real SigningSecret, SMTP/DB credentials, etc.).
        $oldCfg = Join-Path $RolledPath "appsettings.json"
        $newCfg = Join-Path $LivePath "appsettings.json"
        if (Test-Path $oldCfg) {
            Copy-Item $oldCfg $newCfg -Force
            Ok "Preserved production appsettings.json."
        } else {
            Warn "No appsettings.json to preserve - the backup's own config will be used."
        }
    } catch {
        Warn "Restore failed: $($_.Exception.Message) - putting current build back."
        if (-not (Test-Path $LivePath)) { Rename-Item -Path $RolledPath -NewName (Split-Path $LivePath -Leaf) }
        throw
    }

    # ---- 5. Start ----
    Info "Starting service '$ServiceName'..."
    $startErr = $null
    try { Start-Service -Name $ServiceName -ErrorAction Stop }
    catch { $startErr = $_.Exception.Message; Warn "Start-Service failed: $startErr" }
    Ok "Waiting up to ${HealthTimeoutSec}s for healthy..."

    # ---- 6. Health check / un-rollback ----
    if (-not $startErr -and (Test-Healthy)) {
        Ok "HEALTHY - rollback successful. Live build is now $BackupName (restored at $ts)."
        Sync-MgmtScripts $LivePath   # keep root deploy/rollback/tools in sync with the live build
        Ok "Previous build kept at $RolledPath for safety."
    }
    else {
        if ($startErr) { Fail "Service failed to start ($startErr) - UNDOING rollback." }
        else { Fail "Panel did NOT become healthy in ${HealthTimeoutSec}s - UNDOING rollback." }
        Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
        try { (Get-Service $ServiceName).WaitForStatus('Stopped', '00:01:00') } catch {}
        if ($swapDone) {
            $badPath = Join-Path $Root "publish.rollfailed-$ts"
            if (Test-Path $LivePath) { Rename-Item -Path $LivePath -NewName (Split-Path $badPath -Leaf) }
            Rename-Item -Path $RolledPath -NewName (Split-Path $LivePath -Leaf)
            Ok "Restored the pre-rollback build to $LivePath (bad rollback kept at $badPath)."
        }
        Start-Service -Name $ServiceName -ErrorAction SilentlyContinue
        if (Test-Healthy) { Ok "Original build healthy again - rollback was undone." }
        else { Fail "Restart not healthy - CHECK THE SERVER NOW. Log: $transcript" }
        throw "Rollback failed and was undone."
    }
}
catch {
    Fail $_.Exception.Message
    Stop-Transcript | Out-Null
    exit 1
}

Stop-Transcript | Out-Null
exit 0
