<#
.SYNOPSIS
    Lab smoke for SiteKings Legacy Windows (Windows 7 SP1 / Server 2008 R2 era).

    PowerShell 2/3 friendly. Uses only inbox WMI/schtasks/sc/netstat plus
    .NET WebClient + HMAC so the same script can run on old clinical workstations.

.PARAMETER ExerciseSelfRescue
    When set, POST a signed /self-rescue request against the local recover
    daemon (requires -ApiKey or a readable api_key in config.json).

.PARAMETER ExercisePeerRecover
    When set, POST a signed /recover request against the local recover daemon
    to prove the peer-healing contract. This writes restart-agent.flag and may
    run the SK Agent scheduled task.

.PARAMETER ApiKey
    Optional API key used to HMAC-sign /recover and /self-rescue when
    config.json is missing or should be overridden (lab endpoints only).

.PARAMETER OutFile
    Optional path for the result JSON. Defaults to
    <InstallDir>\logs\legacy-windows-lab-smoke.json
#>

param(
    [switch]$ExerciseSelfRescue,
    [switch]$ExercisePeerRecover,
    [string]$ApiKey = "",
    [string]$OutFile = ""
)

$ErrorActionPreference = "SilentlyContinue"
try {
    [Net.ServicePointManager]::SecurityProtocol = [Enum]::ToObject([Net.SecurityProtocolType], 3072)
} catch { }

function Get-InstallDir {
    $candidates = @("$env:ProgramData\SiteKings", "$env:ProgramData\LCL")
    foreach ($candidate in $candidates) {
        if (Test-Path (Join-Path $candidate "config.json")) {
            return $candidate
        }
    }
    return "$env:ProgramData\SiteKings"
}

function Get-ConfigValue {
    param([string]$Name)
    if (-not (Test-Path $ConfigFile)) { return "" }
    $raw = [IO.File]::ReadAllText($ConfigFile)
    $pattern = '"' + [Regex]::Escape($Name) + '"\s*:\s*"([^"]*)"'
    $match = [Regex]::Match($raw, $pattern, "IgnoreCase")
    if ($match.Success) { return $match.Groups[1].Value }
    return ""
}

function Get-SmokeApiKey {
    if (-not [string]::IsNullOrEmpty($ApiKey)) { return $ApiKey }
    return Get-ConfigValue "api_key"
}

function Get-SmokeHostname {
    $hostname = Get-ConfigValue "hostname"
    if ([string]::IsNullOrEmpty($hostname)) {
        try { $hostname = $env:COMPUTERNAME } catch { $hostname = "" }
    }
    return $hostname
}

function Escape-JsonString {
    param([string]$Value)
    if ($null -eq $Value) { return "" }
    $s = [string]$Value
    $s = $s.Replace("\", "\\")
    $s = $s.Replace('"', '\"')
    $s = $s.Replace("`r", "\r")
    $s = $s.Replace("`n", "\n")
    $s = $s.Replace("`t", "\t")
    return $s
}

function New-Check {
    param(
        [string]$Name,
        [bool]$Ok,
        [string]$Detail
    )
    return @{
        Name = $Name
        Ok = $Ok
        Detail = $Detail
    }
}

function Test-LegacyLabOsIdentity {
    param(
        [string]$Caption,
        [string]$Version,
        [string]$ServicePack
    )
    return ((Get-LegacyLabOsKind $Caption $Version $ServicePack) -ne "unsupported")
}

function Get-LegacyLabOsKind {
    param(
        [string]$Caption,
        [string]$Version,
        [string]$ServicePack
    )
    $hay = (($Caption + " " + $ServicePack + " " + $Version).ToLowerInvariant())
    $isSp1 = ($hay.IndexOf("service pack 1") -ge 0) -or ($hay.IndexOf("sp1") -ge 0) -or ($hay.IndexOf("6.1.7601") -ge 0)
    if (($hay.IndexOf("server 2008 r2") -ge 0) -and $isSp1) { return "server_2008_r2" }
    if ((($hay.IndexOf("windows 7") -ge 0) -or ($hay.IndexOf("windows nt 6.1") -ge 0)) -and $isSp1) { return "windows_7_sp1" }
    return "unsupported"
}

function Test-ScheduledTaskName {
    param([string]$TaskName)
    $out = & schtasks.exe /Query /TN $TaskName 2>&1 | Out-String
    $ok = ($LASTEXITCODE -eq 0) -or ($out -match [Regex]::Escape($TaskName))
    return New-Check "task:$TaskName" $ok ($out.Trim())
}

function Test-ServiceName {
    param([string]$ServiceName)
    $out = & sc.exe query $ServiceName 2>&1 | Out-String
    $ok = $out -match "SERVICE_NAME:\s+$ServiceName" -or $out -match "STATE"
    if ($out -match "FAILED|1060|does not exist") { $ok = $false }
    return New-Check "service:$ServiceName" $ok ($out.Trim())
}

function Test-ProcessCommandLine {
    param(
        [string]$CheckName,
        [string[]]$Needles
    )
    $matches = @()
    try {
        $processes = Get-WmiObject Win32_Process
        foreach ($proc in $processes) {
            $name = [string]$proc.Name
            $commandLine = [string]$proc.CommandLine
            $hay = ($name + " " + $commandLine).ToLowerInvariant()
            foreach ($needle in $Needles) {
                if ($hay.IndexOf(([string]$needle).ToLowerInvariant()) -ge 0) {
                    $matches += (($name + " " + $commandLine).Trim())
                    break
                }
            }
        }
    } catch {
        return New-Check $CheckName $false $_.Exception.Message
    }
    $ok = ($matches.Count -gt 0)
    $detail = if ($ok) { ($matches -join " | ") } else { "no matching Win32_Process command line" }
    return New-Check $CheckName $ok $detail
}

function Get-HmacSha256Hex {
    param(
        [string]$Key,
        [string]$Message
    )
    $enc = New-Object System.Text.UTF8Encoding
    $hmac = New-Object System.Security.Cryptography.HMACSHA256 ($enc.GetBytes($Key))
    try {
        $hash = $hmac.ComputeHash($enc.GetBytes($Message))
        return ([BitConverter]::ToString($hash) -replace "-", "").ToLowerInvariant()
    } finally {
        $hmac.Dispose()
    }
}

function Invoke-HttpText {
    param(
        [string]$Method,
        [string]$Url,
        [string]$Body = "",
        [hashtable]$Headers = @{}
    )
    $client = New-Object System.Net.WebClient
    $client.Encoding = [System.Text.Encoding]::UTF8
    $client.Headers.Add("User-Agent", "SK-Legacy-Windows-Lab-Smoke/1.0")
    foreach ($key in $Headers.Keys) {
        $client.Headers[$key] = [string]$Headers[$key]
    }
    try {
        if ($Method -eq "GET") {
            return $client.DownloadString($Url)
        }
        $client.Headers["Content-Type"] = "application/json"
        return $client.UploadString($Url, $Method, $Body)
    } finally {
        $client.Dispose()
    }
}

function Clear-RecoveryFlags {
    param([bool]$IncludeUpdateFlag)
    if (Test-Path $RestartFlag) { Remove-Item $RestartFlag -Force }
    if ($IncludeUpdateFlag -and (Test-Path $UpdateFlag)) { Remove-Item $UpdateFlag -Force }
}

function Convert-FromLooseJson {
    param([string]$Text)
    if ([string]::IsNullOrEmpty($Text)) { return $null }
    try {
        $serializer = New-Object System.Web.Script.Serialization.JavaScriptSerializer
        return $serializer.DeserializeObject($Text)
    } catch {
        return $null
    }
}

$InstallDir = Get-InstallDir
$ConfigFile = Join-Path $InstallDir "config.json"
$LogDir = Join-Path $InstallDir "logs"
$AgentScript = Join-Path $InstallDir "sk-agent-legacy.ps1"
$WatchdogScript = Join-Path $InstallDir "sk-watchdog-legacy.ps1"
$TrayScript = Join-Path $InstallDir "sk-tray-legacy.ps1"
$StatusFile = Join-Path $InstallDir "status.json"
$WatchdogLog = Join-Path $LogDir "watchdog.log"
$RestartFlag = Join-Path $InstallDir "restart-agent.flag"
$UpdateFlag = Join-Path $InstallDir "update-flag.txt"
$Win10Status = Join-Path $InstallDir "win10_upgrade.status.json"
$DefaultOut = Join-Path $LogDir "legacy-windows-lab-smoke.json"
if ([string]::IsNullOrEmpty($OutFile)) { $OutFile = $DefaultOut }

if (-not (Test-Path $InstallDir)) {
    New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
}
if (-not (Test-Path $LogDir)) {
    New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
}

$checks = New-Object System.Collections.ArrayList
$started = Get-Date

# OS identity
$osCaption = ""
$osVersion = ""
$osServicePack = ""
try {
    $os = Get-WmiObject Win32_OperatingSystem
    if ($os) {
        $osCaption = [string]$os.Caption
        $osVersion = [string]$os.Version
        $osServicePack = [string]$os.CSDVersion
    }
} catch {
    $osCaption = "unknown"
}
$osKind = Get-LegacyLabOsKind $osCaption $osVersion $osServicePack
$osIdentityOk = Test-LegacyLabOsIdentity $osCaption $osVersion $osServicePack
$osIdentityDetail = "$osCaption $osServicePack ($osVersion)"
if (-not $osIdentityOk) {
    $osIdentityDetail = $osIdentityDetail + " - unsupported OS; expected Windows 7 SP1 or Windows Server 2008 R2 SP1"
}
[void]$checks.Add((New-Check "os_identity" $osIdentityOk $osIdentityDetail))

# Scheduled tasks (legacy install uses these names)
[void]$checks.Add((Test-ScheduledTaskName "SK Agent"))
[void]$checks.Add((Test-ScheduledTaskName "SK Watchdog"))
[void]$checks.Add((Test-ScheduledTaskName "SK Tray"))

# Recover service + listener
[void]$checks.Add((Test-ServiceName "SKRecoverDaemon"))
$netstat = & netstat.exe -ano 2>&1 | Out-String
$listenOk = $netstat -match ":47778\s"
[void]$checks.Add((New-Check "recover_listen_47778" $listenOk "netstat.exe -ano scan for :47778"))

$firewallText = & netsh advfirewall firewall show rule name="SiteKings Recover Daemon" 2>&1 | Out-String
$firewallRuleOk = $false
if ($firewallText -match "LocalPort:\s*47778" -or $firewallText -match "localport=47778" -or $firewallText -match "\b47778\b") {
    if (($firewallText -match "Protocol:\s*TCP" -or $firewallText -match "protocol=TCP") -and ($firewallText -match "Enabled:\s*Yes" -or $firewallText -match "enable=yes")) {
        $firewallRuleOk = $true
    }
}
[void]$checks.Add((New-Check "firewall:SKRecoverDaemon 47778" $firewallRuleOk 'netsh advfirewall firewall show rule name="SiteKings Recover Daemon"'))

# Legacy runtime scripts present
foreach ($pair in @(
    @("sk-agent-legacy.ps1", $AgentScript),
    @("sk-watchdog-legacy.ps1", $WatchdogScript),
    @("sk-tray-legacy.ps1", $TrayScript)
)) {
    $present = Test-Path $pair[1]
    [void]$checks.Add((New-Check ("file:" + $pair[0]) $present $pair[1]))
}

[void]$checks.Add((Test-ProcessCommandLine "process:sk-agent-legacy" @("sk-agent-legacy.ps1")))
[void]$checks.Add((Test-ProcessCommandLine "process:sk-tray" @("sk-tray-legacy.ps1", "sk-tray.exe")))

$watchdogLogOk = (Test-Path $WatchdogLog)
try {
    if ($watchdogLogOk) {
        $watchdogLogOk = ((Get-Item $WatchdogLog).Length -gt 0)
    }
} catch {
    $watchdogLogOk = $false
}
[void]$checks.Add((New-Check "watchdog.log" $watchdogLogOk $WatchdogLog))

# Local recover health
$healthText = ""
$healthOk = $false
try {
    $healthText = Invoke-HttpText -Method "GET" -Url "http://127.0.0.1:47778/health"
    $healthOk = ($healthText -match "legacy_windows") -or ($healthText -match '"ok"\s*:\s*true') -or ($healthText.Length -gt 0)
} catch {
    $healthText = $_.Exception.Message
}
[void]$checks.Add((New-Check "recover_health" $healthOk $healthText))

# Optional signed /recover peer-healing proof
$peerRecoverDetail = "skipped (pass -ExercisePeerRecover to exercise)"
$peerRecoverOk = $false
if ($ExercisePeerRecover) {
    $peerRecoverOk = $false
    $apiKey = Get-SmokeApiKey
    $hostname = Get-SmokeHostname
    if ([string]::IsNullOrEmpty($apiKey)) {
        $peerRecoverDetail = "api_key missing (pass -ApiKey or set config.json); cannot sign /recover"
    } elseif ([string]::IsNullOrEmpty($hostname)) {
        $peerRecoverDetail = "hostname missing; cannot sign /recover"
    } else {
        $ts = [int][double]::Parse((Get-Date -UFormat %s))
        $recoverMsg = "$($hostname):$ts"
        $recoverSig = Get-HmacSha256Hex -Key $apiKey -Message $recoverMsg
        $body = "{`"target_hostname`":`"$hostname`",`"ts`":$ts,`"sig`":`"$recoverSig`"}"
        try {
            Clear-RecoveryFlags $false
            $resp = Invoke-HttpText -Method "POST" -Url "http://127.0.0.1:47778/recover" -Body $body
            $peerRecoverDetail = $resp
            $flagOk = Test-Path $RestartFlag
            $peerRecoverOk = $flagOk -and ($resp -match '"ok"\s*:\s*true')
            if (-not $flagOk) {
                $peerRecoverDetail = $peerRecoverDetail + " (restart-agent.flag not present)"
            } else {
                $peerRecoverDetail = $peerRecoverDetail + " (fresh restart-agent.flag observed)"
            }
        } catch {
            $peerRecoverDetail = $_.Exception.Message
        }
    }
}
[void]$checks.Add((New-Check "peer_recover" $peerRecoverOk $peerRecoverDetail))

# Optional signed /self-rescue
$selfRescueDetail = "skipped (pass -ExerciseSelfRescue to exercise)"
$selfRescueOk = $false
if ($ExerciseSelfRescue) {
    $selfRescueOk = $false
    $apiKey = Get-SmokeApiKey
    $hostname = Get-SmokeHostname
    if ([string]::IsNullOrEmpty($apiKey)) {
        $selfRescueDetail = "api_key missing (pass -ApiKey or set config.json); cannot sign /self-rescue"
    } else {
        $ts = [int][double]::Parse((Get-Date -UFormat %s))
        $osFamily = "windows"
        $targetVersion = "legacy-lab-smoke"
        $msg = "$hostname|$ts|$osFamily|$targetVersion"
        $sig = Get-HmacSha256Hex -Key $apiKey -Message $msg
        $body = "{`"os_family`":`"$osFamily`",`"target_version`":`"$targetVersion`",`"server_url`":`"`"}"
        try {
            Clear-RecoveryFlags $true
            $resp = Invoke-HttpText -Method "POST" -Url "http://127.0.0.1:47778/self-rescue" -Body $body -Headers @{
                "X-SK-Timestamp" = "$ts"
                "X-SK-Signature" = $sig
            }
            $selfRescueDetail = $resp
            $flagOk = (Test-Path $RestartFlag) -and (Test-Path $UpdateFlag)
            $selfRescueOk = $flagOk -and ($resp -match '"ok"\s*:\s*true')
            if (-not $flagOk) {
                $selfRescueDetail = $selfRescueDetail + " (restart-agent.flag/update-flag.txt not both present)"
            } else {
                $selfRescueDetail = $selfRescueDetail + " (fresh restart-agent.flag and update-flag.txt observed)"
            }
        } catch {
            $selfRescueDetail = $_.Exception.Message
        }
    }
}
[void]$checks.Add((New-Check "self_rescue" $selfRescueOk $selfRescueDetail))

# Status / flag / Win10 bridge artifacts (presence is informational; missing is not a hard fail unless both OS and agent look healthy)
$statusPresent = Test-Path $StatusFile
[void]$checks.Add((New-Check "status.json" $statusPresent $StatusFile))
$win10Present = Test-Path $Win10Status
[void]$checks.Add((New-Check "win10_upgrade.status.json" $win10Present "$Win10Status (created after __WIN10_PROBE__ / media upgrade)"))

# Summarize
$failed = @()
$optionalInformational = @("win10_upgrade.status.json")
foreach ($c in $checks) {
    if ((-not $c.Ok) -and ($optionalInformational -notcontains $c.Name)) { $failed += $c.Name }
}
$overallOk = ($failed.Count -eq 0)
$finished = Get-Date
$durationSec = [int](($finished - $started).TotalSeconds)

# Manual JSON writer (PS2-safe string builder)
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine("{")
[void]$sb.AppendLine('  "schema": "legacy-windows-lab-smoke",')
[void]$sb.AppendLine('  "ok": ' + ($(if ($overallOk) { "true" } else { "false" })) + ",")
[void]$sb.AppendLine('  "hostname": "' + (Escape-JsonString $env:COMPUTERNAME) + '",')
[void]$sb.AppendLine('  "os_kind": "' + (Escape-JsonString $osKind) + '",')
[void]$sb.AppendLine('  "os_caption": "' + (Escape-JsonString $osCaption) + '",')
[void]$sb.AppendLine('  "os_version": "' + (Escape-JsonString $osVersion) + '",')
[void]$sb.AppendLine('  "os_service_pack": "' + (Escape-JsonString $osServicePack) + '",')
[void]$sb.AppendLine('  "install_dir": "' + (Escape-JsonString $InstallDir) + '",')
[void]$sb.AppendLine('  "started": "' + (Escape-JsonString $started.ToString("o")) + '",')
[void]$sb.AppendLine('  "finished": "' + (Escape-JsonString $finished.ToString("o")) + '",')
[void]$sb.AppendLine('  "duration_seconds": ' + $durationSec + ",")
[void]$sb.AppendLine('  "exercise_peer_recover": ' + ($(if ($ExercisePeerRecover) { "true" } else { "false" })) + ",")
[void]$sb.AppendLine('  "exercise_self_rescue": ' + ($(if ($ExerciseSelfRescue) { "true" } else { "false" })) + ",")
[void]$sb.AppendLine('  "failed": [')
for ($i = 0; $i -lt $failed.Count; $i++) {
    $comma = if ($i -lt ($failed.Count - 1)) { "," } else { "" }
    [void]$sb.AppendLine('    "' + (Escape-JsonString $failed[$i]) + '"' + $comma)
}
[void]$sb.AppendLine("  ],")
[void]$sb.AppendLine('  "checks": [')
for ($i = 0; $i -lt $checks.Count; $i++) {
    $c = $checks[$i]
    $comma = if ($i -lt ($checks.Count - 1)) { "," } else { "" }
    $okLit = if ($c.Ok) { "true" } else { "false" }
    [void]$sb.AppendLine("    {")
    [void]$sb.AppendLine('      "name": "' + (Escape-JsonString $c.Name) + '",')
    [void]$sb.AppendLine('      "ok": ' + $okLit + ",")
    [void]$sb.AppendLine('      "detail": "' + (Escape-JsonString $c.Detail) + '"')
    [void]$sb.AppendLine("    }" + $comma)
}
[void]$sb.AppendLine("  ]")
[void]$sb.AppendLine("}")

$jsonText = $sb.ToString()
[IO.File]::WriteAllText($OutFile, $jsonText)

Write-Host "Legacy Windows lab smoke complete. ok=$overallOk"
Write-Host "Report: $OutFile"
if (-not $overallOk) {
    Write-Host ("Failed checks: " + ($failed -join ", "))
    exit 1
}
exit 0
