<#
.SYNOPSIS
    SiteKings legacy Windows agent lane.

    This script is intentionally conservative for Windows 7 SP1 era hosts:
    WMI instead of CIM, WebClient instead of newer web cmdlets, and the
    JavaScriptSerializer JSON path instead of PowerShell JSON cmdlets.
#>

param(
    [string]$ServerUrl = "",
    [string]$ApiKey = ""
)

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

try { Add-Type -AssemblyName System.Web.Extensions } catch { }

$Script:Version = "legacy_windows-1.0"
$Script:InstallDir = "$env:ProgramData\SiteKings"
if (-not (Test-Path "$Script:InstallDir\config.json") -and (Test-Path "$env:ProgramData\LCL\config.json")) {
    $Script:InstallDir = "$env:ProgramData\LCL"
}
$Script:ConfigFile = Join-Path $Script:InstallDir "config.json"
$Script:LogDir = Join-Path $Script:InstallDir "logs"
$Script:StatusFile = Join-Path $Script:InstallDir "status.json"
$Script:SyncFlag = Join-Path $Script:InstallDir "sync-now.flag"
$Script:UpdateFlag = Join-Path $Script:InstallDir "update-flag.txt"
$Script:RestartFlag = Join-Path $Script:InstallDir "restart-agent.flag"
$Script:Win10Status = Join-Path $Script:InstallDir "win10_upgrade.status.json"
$Script:Win10Probe = Join-Path $Script:InstallDir "win10-probe.json"
$Script:Win10Log = Join-Path $Script:InstallDir "win10_upgrade.log"
$Script:WatchdogScript = Join-Path $Script:InstallDir "sk-watchdog-legacy.ps1"
$Script:TrayScript = Join-Path $Script:InstallDir "sk-tray-legacy.ps1"

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

function New-Serializer {
    return New-Object System.Web.Script.Serialization.JavaScriptSerializer
}

function ConvertFrom-SkJson {
    param([string]$Json)
    if (-not $Json) { return $null }
    try { return (New-Serializer).DeserializeObject($Json) } catch { return $null }
}

function ConvertTo-SkJson {
    param($Value)
    try { return (New-Serializer).Serialize($Value) } catch { return "{}" }
}

function Get-JsonValue {
    param([string]$Text, [string]$Name)
    if (-not $Text) { return "" }
    $match = [Regex]::Match($Text, '"' + [Regex]::Escape($Name) + '"\s*:\s*"([^"]*)"', "IgnoreCase")
    if ($match.Success) { return $match.Groups[1].Value }
    return ""
}

function Get-FileJsonValue {
    param([string]$Path, [string]$Name)
    if (-not (Test-Path $Path)) { return "" }
    try {
        return Get-JsonValue ([IO.File]::ReadAllText($Path)) $Name
    } catch { return "" }
}

function Test-LegacyServiceRunning {
    param([string]$Name)
    $svc = Get-Service -Name $Name
    return ($svc -and $svc.Status -eq "Running")
}

function Test-LegacyTaskPresent {
    param([string]$Name)
    schtasks.exe /Query /TN $Name | Out-Null
    return ($LASTEXITCODE -eq 0)
}

function Get-LegacyHealth {
    $stage = Get-FileJsonValue $Script:Win10Status "stage"
    $message = Get-FileJsonValue $Script:Win10Status "message"
    if (-not $message) { $message = Get-FileJsonValue $Script:Win10Status "error" }
    return @{
        legacy_recover_service_running = (Test-LegacyServiceRunning "SKRecoverDaemon")
        legacy_agent_task_present = (Test-LegacyTaskPresent "SK Agent")
        legacy_watchdog_task_present = (Test-LegacyTaskPresent "SK Watchdog")
        legacy_tray_task_present = (Test-LegacyTaskPresent "SK Tray")
        legacy_watchdog_script_present = (Test-Path $Script:WatchdogScript)
        legacy_tray_script_present = (Test-Path $Script:TrayScript)
        win10_upgrade_stage = $stage
        win10_upgrade_message = $message
    }
}

function Get-LegacyNetworkInventory {
    $lanIp = ""
    $gateway = ""
    $mac = ""
    $dnsServers = ""
    try {
        $adapters = Get-WmiObject Win32_NetworkAdapterConfiguration -Filter "IPEnabled=True"
        foreach ($adapter in $adapters) {
            if (-not $gateway -and $adapter.DefaultIPGateway) {
                $gateway = [string]$adapter.DefaultIPGateway[0]
            }
            if (-not $mac -and $adapter.MACAddress) {
                $mac = [string]$adapter.MACAddress
            }
            foreach ($candidate in @($adapter.IPAddress)) {
                if (
                    $candidate -and
                    $candidate -match "^\d{1,3}(\.\d{1,3}){3}$" -and
                    $candidate -ne $gateway -and
                    $candidate -ne "0.0.0.0" -and
                    $candidate -notmatch "^127\." -and
                    $candidate -notmatch "^169\.254\."
                ) {
                    $lanIp = [string]$candidate
                    break
                }
            }
            if (-not $dnsServers -and $adapter.DNSServerSearchOrder) {
                $dnsServers = ($adapter.DNSServerSearchOrder -join ", ")
            }
            if ($lanIp) { break }
        }
    } catch { }
    return @{ lan_ip = $lanIp; gateway = $gateway; mac_address = $mac; dns_servers = $dnsServers }
}

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$Level] $Message"
    $line | Out-File -FilePath (Join-Path $Script:LogDir "legacy-agent.log") -Append -Encoding UTF8 -Force
}

function Get-Config {
    $raw = ""
    if (Test-Path $Script:ConfigFile) { $raw = [IO.File]::ReadAllText($Script:ConfigFile) }
    $cfg = ConvertFrom-SkJson $raw
    $server = $ServerUrl
    $key = $ApiKey
    if (-not $server) {
        if ($cfg -and $cfg.ContainsKey("server_url")) { $server = $cfg["server_url"] }
        if (-not $server -and $cfg -and $cfg.ContainsKey("ServerUrl")) { $server = $cfg["ServerUrl"] }
        if (-not $server) { $server = Get-JsonValue $raw "server_url" }
    }
    if (-not $key) {
        if ($cfg -and $cfg.ContainsKey("api_key")) { $key = $cfg["api_key"] }
        if (-not $key -and $cfg -and $cfg.ContainsKey("ApiKey")) { $key = $cfg["ApiKey"] }
        if (-not $key) { $key = Get-JsonValue $raw "api_key" }
    }
    return @{ ServerUrl = ($server.TrimEnd("/")); ApiKey = $key; Hostname = $env:COMPUTERNAME.ToUpper() }
}

function New-WebClient {
    param($Config)
    $wc = New-Object System.Net.WebClient
    $wc.Headers.Add("User-Agent", "SK-Agent-Legacy/1.0")
    $wc.Headers.Add("Content-Type", "application/json")
    if ($Config.ApiKey) { $wc.Headers.Add("X-API-Key", $Config.ApiKey) }
    $wc.Headers.Add("X-Hostname", $Config.Hostname)
    return $wc
}

function Invoke-GetJson {
    param($Config, [string]$Path)
    $wc = New-WebClient $Config
    return ConvertFrom-SkJson ($wc.DownloadString($Config.ServerUrl + $Path))
}

function Invoke-PostJson {
    param($Config, [string]$Path, $Payload)
    $wc = New-WebClient $Config
    $body = ConvertTo-SkJson $Payload
    return $wc.UploadString($Config.ServerUrl + $Path, "POST", $body)
}

function Update-Status {
    param([string]$State, [string]$Message)
    $payload = @{
        state = $State
        message = $Message
        lastSync = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
        version = $Script:Version
        hostname = $env:COMPUTERNAME
        agent_type = "legacy_windows"
    }
    [IO.File]::WriteAllText($Script:StatusFile, (ConvertTo-SkJson $payload))
}

function Send-Checkin {
    param($Config)
    $os = Get-WmiObject Win32_OperatingSystem
    $payload = @{
        hostname = $Config.Hostname
        os_info = $os.Caption
        client_version = $Script:Version
        agent_type = "legacy_windows"
    }
    Invoke-PostJson $Config "/api/checkin" $payload | Out-Null
}

function Send-Inventory {
    param($Config)
    $os = Get-WmiObject Win32_OperatingSystem
    $cs = Get-WmiObject Win32_ComputerSystem
    $cpu = Get-WmiObject Win32_Processor | Select-Object -First 1
    $disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'"
    $network = Get-LegacyNetworkInventory
    $payload = @{
        hostname = $Config.Hostname
        os_info = $os.Caption
        os_version = $os.Version
        service_pack = $os.CSDVersion
        architecture = $os.OSArchitecture
        total_ram_mb = [int]($cs.TotalPhysicalMemory / 1MB)
        cpu = $cpu.Name
        disk_free_gb = [math]::Round($disk.FreeSpace / 1GB, 1)
        client_version = $Script:Version
        agent_type = "legacy_windows"
    }
    $payload["lan_ip"] = $network["lan_ip"]
    $payload["gateway"] = $network["gateway"]
    $payload["mac_address"] = $network["mac_address"]
    $payload["dns_servers"] = $network["dns_servers"]
    $health = Get-LegacyHealth
    foreach ($key in $health.Keys) {
        $payload[$key] = $health[$key]
    }
    Invoke-PostJson $Config "/api/inventory" $payload | Out-Null
}

function Send-Logs {
    param($Config)
    $logs = @(
        @{ Path = (Join-Path $Script:LogDir "legacy-agent.log"); Script = "sk-agent-legacy" },
        @{ Path = (Join-Path $Script:LogDir "watchdog.log"); Script = "legacy-watchdog" },
        @{ Path = $Script:Win10Log; Script = "win10-upgrade" }
    )
    foreach ($entry in $logs) {
        $path = $entry["Path"]
        if (-not (Test-Path $path)) { continue }
        $tail = [IO.File]::ReadAllText($path)
        if ($tail.Length -gt 8000) { $tail = $tail.Substring($tail.Length - 8000) }
        Invoke-PostJson $Config "/api/logs" @{ hostname = $Config.Hostname; script = $entry["Script"]; output = $tail } | Out-Null
    }
}

function Complete-Command {
    param($Config, $CommandId, $Result)
    $output = $Result.Output
    $exitCode = [int]$Result.Code
    $status = "completed"
    if ($exitCode -ne 0) { $status = "failed" }
    $payload = @{
        id = $CommandId
        target_hostname = $Config.Hostname
        status = $status
        output = $output
        exit_code = $exitCode
    }
    if ($Result.ContainsKey("ResultKind")) { $payload["result_kind"] = $Result["ResultKind"] }
    if ($Result.ContainsKey("ResultJson")) { $payload["result_json"] = $Result["ResultJson"] }
    Invoke-PostJson $Config "/api/commands/$CommandId/result" $payload | Out-Null
}

function Get-PendingCommands {
    param($Config)
    $commands = Invoke-GetJson $Config ("/api/commands/" + $Config.Hostname)
    if (-not $commands) { return @() }
    if ($commands -is [System.Array]) { return $commands }
    if ($commands.ContainsKey("commands")) { return $commands["commands"] }
    return @()
}

function Get-CommandText {
    param($Command)
    if ($Command -is [string]) { return $Command }
    if ($Command.ContainsKey("command")) { return $Command["command"] }
    return ""
}

function Get-CommandId {
    param($Command)
    if ($Command.ContainsKey("id")) { return $Command["id"] }
    if ($Command.ContainsKey("command_id")) { return $Command["command_id"] }
    return ""
}

function Invoke-Win10Probe {
    $os = Get-WmiObject Win32_OperatingSystem
    $disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'"
    $summary = Get-WmiObject Win32_ComputerSystem
    $bios = Get-WmiObject Win32_BIOS
    $processor = Get-WmiObject Win32_Processor | Select-Object -First 1
    $battery = Get-WmiObject Win32_Battery
    $legacy = ($os.Version -like "6.1*")
    $server = ([string]$os.Caption -match "Server 2008 R2")
    $verdict = "not_legacy_windows"
    if ($server) {
        $verdict = "server_migration_required"
    } elseif ($legacy -and ($os.CSDVersion -notmatch "Service Pack 1|SP1")) {
        $verdict = "missing_sp1"
    } elseif ($legacy -and ($os.CSDVersion -match "Service Pack 1|SP1")) {
        $verdict = "candidate"
    }
    $ramGb = 0
    if ($summary -and $summary.TotalPhysicalMemory) {
        $ramGb = [math]::Round(([double]$summary.TotalPhysicalMemory) / 1GB, 1)
    }
    $acPowerRequired = $false
    if ($battery) { $acPowerRequired = $true }
    $payload = @{
        os_caption = $os.Caption
        service_pack = $os.CSDVersion
        architecture = $os.OSArchitecture
        free_gb = [math]::Round($disk.FreeSpace / 1GB, 1)
        manufacturer = $summary.Manufacturer
        model = $summary.Model
        bios_version = $bios.SMBIOSBIOSVersion
        processor_name = $processor.Name
        total_ram_gb = $ramGb
        ac_power_required = $acPowerRequired
        is_legacy_windows = $legacy
        verdict = $verdict
        notes = @(
            "Windows 10 bridge preflight only; validate clinical vendor drivers before upgrade.",
            "Confirm vendor software, clinical peripherals, backup image, rollback plan, and AC power before launch."
        )
    }
    $json = ConvertTo-SkJson $payload
    [IO.File]::WriteAllText($Script:Win10Status, $json)
    [IO.File]::WriteAllText($Script:Win10Probe, $json)
    return $json
}

function Get-CommandJsonArg {
    param([string]$Command, [string]$Name)
    $jsonStart = $Command.IndexOf("{")
    if ($jsonStart -ge 0) {
        $payload = ConvertFrom-SkJson ($Command.Substring($jsonStart))
        if ($payload) {
            foreach ($key in $payload.Keys) {
                if ([string]::Compare([string]$key, $Name, $true) -eq 0) {
                    if ($null -eq $payload[$key]) { return "" }
                    return [string]$payload[$key]
                }
            }
        }
    }
    $match = [Regex]::Match($Command, '"' + [Regex]::Escape($Name) + '"\s*:\s*"((?:\\.|[^"\\])*)"', "IgnoreCase")
    if ($match.Success) {
        $wrapped = '{"v":"' + $match.Groups[1].Value + '"}'
        $decoded = ConvertFrom-SkJson $wrapped
        if ($decoded -and $decoded.ContainsKey("v")) { return [string]$decoded["v"] }
        $value = $match.Groups[1].Value
        $value = $value -replace '\\/', '/'
        $value = $value -replace '\\"', '"'
        $value = $value -replace '\\r', "`r"
        $value = $value -replace '\\n', "`n"
        $value = $value -replace '\\t', "`t"
        $value = $value -replace '\\\\', '\'
        return $value
    }
    return ""
}

function Invoke-Win10MediaUpgrade {
    param($Config, [string]$Command)
    $url = Get-CommandJsonArg $Command "url"
    $sha = Get-CommandJsonArg $Command "sha256"
    $probeVerdict = Get-CommandJsonArg $Command "probe_verdict"
    $ackBackup = Get-CommandJsonArg $Command "ack_backup"
    $ackRollback = Get-CommandJsonArg $Command "ack_rollback"
    $ackVendorSoftware = Get-CommandJsonArg $Command "ack_vendor_software"
    $ackClinicalEquipment = Get-CommandJsonArg $Command "ack_clinical_equipment"
    $customerScopedEnablement = Get-CommandJsonArg $Command "customer_scoped_enablement"
    $backupEvidence = Get-CommandJsonArg $Command "backup_evidence"
    $rollbackPlan = Get-CommandJsonArg $Command "rollback_plan"
    $vendorSoftwareReview = Get-CommandJsonArg $Command "vendor_software_review"
    $clinicalEquipmentReview = Get-CommandJsonArg $Command "clinical_equipment_review"
    if (-not $url) {
        [IO.File]::WriteAllText($Script:Win10Status, '{"stage":"preflight_failed","error":"missing media url"}')
        return @{ Output = "Windows 10 bridge missing media URL"; Code = 1 }
    }
    if ($probeVerdict -ne "candidate") {
        [IO.File]::WriteAllText($Script:Win10Status, '{"stage":"preflight_failed","error":"Windows 10 bridge requires latest probe verdict candidate before media launch"}')
        return @{ Output = "Windows 10 bridge requires latest probe verdict candidate before media launch"; Code = 1 }
    }
    if ($ackBackup -notmatch "^(1|true|yes|on|checked)$" -or $ackRollback -notmatch "^(1|true|yes|on|checked)$" -or $ackVendorSoftware -notmatch "^(1|true|yes|on|checked)$" -or $ackClinicalEquipment -notmatch "^(1|true|yes|on|checked)$") {
        [IO.File]::WriteAllText($Script:Win10Status, '{"stage":"preflight_failed","error":"missing Windows 10 bridge acknowledgement"}')
        return @{ Output = "Windows 10 bridge missing required operator acknowledgements"; Code = 1 }
    }
    if (-not $customerScopedEnablement -or -not $backupEvidence -or -not $rollbackPlan -or -not $vendorSoftwareReview -or -not $clinicalEquipmentReview) {
        [IO.File]::WriteAllText($Script:Win10Status, '{"stage":"preflight_failed","error":"missing Windows 10 bridge evidence note"}')
        return @{ Output = "Windows 10 bridge missing required evidence notes"; Code = 1 }
    }
    $helper = Join-Path $Script:InstallDir "upgrade_win10_media.ps1"
    $helperUrl = $Config.ServerUrl + "/api/client/download/upgrade_win10_media.ps1"
    try {
        [IO.File]::WriteAllText($Script:Win10Status, '{"stage":"helper_download","message":"downloading Windows 10 bridge helper"}')
        $wc = New-WebClient $Config
        $wc.DownloadFile($helperUrl, $helper)
    } catch {
        [IO.File]::WriteAllText($Script:Win10Status, '{"stage":"preflight_failed","error":"helper download failed"}')
        return @{ Output = "Windows 10 bridge helper download failed: " + $_.Exception.Message; Code = 1 }
    }
    try {
        [Environment]::SetEnvironmentVariable("SK_WIN10_MEDIA_URL", $url, "Process")
        [Environment]::SetEnvironmentVariable("SK_WIN10_MEDIA_SHA256", $sha, "Process")
        [Environment]::SetEnvironmentVariable("SK_WIN10_PROBE_VERDICT", $probeVerdict, "Process")
        [Environment]::SetEnvironmentVariable("SK_WIN10_ACK_BACKUP", $ackBackup, "Process")
        [Environment]::SetEnvironmentVariable("SK_WIN10_ACK_ROLLBACK", $ackRollback, "Process")
        [Environment]::SetEnvironmentVariable("SK_WIN10_ACK_VENDOR_SOFTWARE", $ackVendorSoftware, "Process")
        [Environment]::SetEnvironmentVariable("SK_WIN10_ACK_CLINICAL_EQUIPMENT", $ackClinicalEquipment, "Process")
        [Environment]::SetEnvironmentVariable("SK_WIN10_CUSTOMER_SCOPED_ENABLEMENT", $customerScopedEnablement, "Process")
        [Environment]::SetEnvironmentVariable("SK_WIN10_BACKUP_EVIDENCE", $backupEvidence, "Process")
        [Environment]::SetEnvironmentVariable("SK_WIN10_ROLLBACK_PLAN", $rollbackPlan, "Process")
        [Environment]::SetEnvironmentVariable("SK_WIN10_VENDOR_SOFTWARE_REVIEW", $vendorSoftwareReview, "Process")
        [Environment]::SetEnvironmentVariable("SK_WIN10_CLINICAL_EQUIPMENT_REVIEW", $clinicalEquipmentReview, "Process")
        "Windows 10 bridge helper starting; helper will run setup.exe only after guarded preflight." | Out-File -FilePath $Script:Win10Log -Append -Encoding UTF8
        $quotedHelper = '"' + $helper + '"'
        $args = "-NoProfile -ExecutionPolicy Bypass -File $quotedHelper"
        $process = Start-Process -FilePath "powershell.exe" -ArgumentList $args -WindowStyle Hidden -PassThru -ErrorAction Stop
        if (-not $process) { throw "helper process did not start" }
        $status = @{
            stage = "helper_started"
            message = "Windows 10 bridge helper launched"
            helper = $helper
            pid = $process.Id
            media_url = $url
        }
        [IO.File]::WriteAllText($Script:Win10Status, (ConvertTo-SkJson $status))
        return @{ Output = "Windows 10 bridge helper launched (pid=$($process.Id)). Monitor win10_upgrade.status.json."; Code = 0 }
    } catch {
        [IO.File]::WriteAllText($Script:Win10Status, '{"stage":"preflight_failed","error":"helper launch failed"}')
        return @{ Output = "Windows 10 bridge helper launch failed: " + $_.Exception.Message; Code = 1 }
    }
}

function Invoke-LegacyLabSmoke {
    param($Config)
    $script = Join-Path $Script:InstallDir "legacy-windows-lab-smoke.ps1"
    $report = Join-Path $Script:LogDir "legacy-windows-lab-smoke.json"
    $smokeUrl = $Config.ServerUrl + "/api/client/download/legacy-windows-lab-smoke.ps1"
    try {
        $wc = New-WebClient $Config
        $wc.DownloadFile($smokeUrl, $script)
    } catch {
        return @{ Output = "Legacy lab smoke download failed: " + $_.Exception.Message; Code = 1 }
    }

    try {
        $quotedScript = '"' + $script + '"'
        $args = "-NoProfile -ExecutionPolicy Bypass -File $quotedScript -ExercisePeerRecover -ExerciseSelfRescue"
        $process = Start-Process -FilePath "powershell.exe" -ArgumentList $args -WindowStyle Hidden -Wait -PassThru -ErrorAction Stop
        $code = 0
        if ($process) { $code = [int]$process.ExitCode }
        $output = "Legacy lab smoke completed with exit code $code"
        $result = @{ Output = $output; Code = $code }
        if (Test-Path $report) {
            $json = [IO.File]::ReadAllText($report)
            if ($json) {
                $result = @{
                    Output = $json
                    Code = $code
                    ResultKind = "legacy_windows_smoke"
                    ResultJson = $json
                }
            }
        }
        return $result
    } catch {
        return @{ Output = "Legacy lab smoke failed: " + $_.Exception.Message; Code = 1 }
    }
}

function Invoke-LegacyLabCollect {
    param($Config)
    $script = Join-Path $Script:InstallDir "legacy-windows-lab-collect.ps1"
    $smokeScript = Join-Path $Script:InstallDir "legacy-windows-lab-smoke.ps1"
    $collectUrl = $Config.ServerUrl + "/api/client/download/legacy-windows-lab-collect.ps1"
    $smokeUrl = $Config.ServerUrl + "/api/client/download/legacy-windows-lab-smoke.ps1"
    $outRoot = Join-Path $Script:LogDir "legacy-windows-lab-evidence"
    if (-not (Test-Path $outRoot)) {
        New-Item -ItemType Directory -Path $outRoot -Force | Out-Null
    }
    $stamp = Get-Date -Format "yyyyMMdd-HHmmss"
    $outDir = Join-Path $outRoot ("legacy-windows-lab-evidence-" + $stamp)
    $manifest = Join-Path $outDir "legacy-windows-lab-collector-manifest.json"
    try {
        $wc = New-WebClient $Config
        $wc.DownloadFile($collectUrl, $script)
        $wc.DownloadFile($smokeUrl, $smokeScript)
    } catch {
        return @{ Output = "Legacy lab collect download failed: " + $_.Exception.Message; Code = 1 }
    }

    try {
        $quotedScript = '"' + $script + '"'
        $quotedOut = '"' + $outDir + '"'
        $args = "-NoProfile -ExecutionPolicy Bypass -File $quotedScript -OutDir $quotedOut -ExercisePeerRecover -ExerciseSelfRescue"
        $process = Start-Process -FilePath "powershell.exe" -ArgumentList $args -WindowStyle Hidden -Wait -PassThru -ErrorAction Stop
        $code = 0
        if ($process) { $code = [int]$process.ExitCode }
        $output = "Legacy lab collect completed with exit code $code; evidence folder: $outDir"
        $result = @{ Output = $output; Code = $code }
        if (Test-Path $manifest) {
            $json = [IO.File]::ReadAllText($manifest)
            if ($json) {
                $result = @{
                    Output = $json
                    Code = $code
                    ResultKind = "legacy_windows_collect"
                    ResultJson = $json
                }
            }
        }
        return $result
    } catch {
        return @{ Output = "Legacy lab collect failed: " + $_.Exception.Message; Code = 1 }
    }
}

function Invoke-LegacyInstallTray {
    param($Config)
    $tray = $Script:TrayScript
    $launcher = Join-Path $Script:InstallDir "sk-tray-legacy.vbs"
    $trayUrl = $Config.ServerUrl + "/api/client/download/sk-tray-legacy.ps1"
    try {
        $wc = New-WebClient $Config
        $wc.DownloadFile($trayUrl, $tray)
    } catch {
        if (-not (Test-Path $tray)) {
            return @{ Output = "SK Tray legacy repair failed: sk-tray-legacy.ps1 download failed: " + $_.Exception.Message; Code = 1 }
        }
    }

    try {
        $ps = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $tray + '"'
        $vbs = 'CreateObject("WScript.Shell").Run "' + ($ps -replace '"','""') + '", 0, False'
        [IO.File]::WriteAllText($launcher, $vbs)

        $taskAction = "wscript.exe `"$launcher`""
        $createOut = schtasks.exe /Create /TN "SK Tray" /SC ONLOGON /TR $taskAction /F 2>&1 | Out-String
        $createCode = $LASTEXITCODE
        $runOut = schtasks.exe /Run /TN "SK Tray" 2>&1 | Out-String
        $runCode = $LASTEXITCODE
        $code = 0
        if ($createCode -ne 0 -and $runCode -ne 0) { $code = 1 }
        return @{ Output = "SK Tray legacy repair`n$createOut`n$runOut"; Code = $code }
    } catch {
        return @{ Output = "SK Tray legacy repair failed: " + $_.Exception.Message; Code = 1 }
    }
}

function Get-LegacyNetworkContext {
    $networkType = ""
    $ssid = ""
    try {
        $adapter = Get-WmiObject Win32_NetworkAdapterConfiguration -Filter "IPEnabled=True" | Select-Object -First 1
        if ($adapter -and $adapter.Description) {
            if ($adapter.Description -match "Wireless|Wi-Fi|WiFi|802\.11") { $networkType = "wireless" }
            else { $networkType = "lan" }
        }
        if ($networkType -eq "wireless") {
            $raw = netsh wlan show interfaces 2>$null
            foreach ($line in $raw) {
                if ($line -match "^\s*SSID\s*:\s*(.+)$" -and $line -notmatch "BSSID") {
                    $ssid = $Matches[1].Trim()
                    break
                }
            }
        }
    } catch { }
    return @{ network_type = $networkType; network_ssid = $ssid }
}

function Measure-LegacyDownloadMbps {
    param([string]$Url)
    $wc = New-Object System.Net.WebClient
    $sw = New-Object Diagnostics.Stopwatch
    $sw.Start()
    $bytes = $wc.DownloadData($Url)
    $sw.Stop()
    if (-not $bytes -or $sw.Elapsed.TotalSeconds -le 0) { return 0 }
    return [math]::Round((([double]$bytes.Length * 8) / $sw.Elapsed.TotalSeconds / 1000000), 1)
}

function Measure-LegacyUploadMbps {
    param([string]$Url, [int]$Bytes)
    $wc = New-Object System.Net.WebClient
    $payload = New-Object byte[] $Bytes
    $sw = New-Object Diagnostics.Stopwatch
    $sw.Start()
    $wc.UploadData($Url, "POST", $payload) | Out-Null
    $sw.Stop()
    if ($sw.Elapsed.TotalSeconds -le 0) { return 0 }
    return [math]::Round((([double]$Bytes * 8) / $sw.Elapsed.TotalSeconds / 1000000), 1)
}

function Invoke-LegacySpeedtest {
    param($Config)
    $pingMs = 0
    $downloadMbps = 0
    $uploadMbps = 0
    $output = @()
    try {
        $sw = New-Object Diagnostics.Stopwatch
        $wc = New-Object System.Net.WebClient
        $sw.Start()
        $wc.DownloadData("https://speed.cloudflare.com/__down?bytes=1") | Out-Null
        $sw.Stop()
        $pingMs = [math]::Round($sw.Elapsed.TotalMilliseconds, 1)
        $output += ("Ping: {0} ms" -f $pingMs)
    } catch {
        $output += ("Ping failed: " + $_.Exception.Message)
    }
    try {
        $downloadMbps = Measure-LegacyDownloadMbps "https://speed.cloudflare.com/__down?bytes=5000000"
        $output += ("Download: {0} Mbps" -f $downloadMbps)
    } catch {
        $output += ("Download test failed: " + $_.Exception.Message)
    }
    try {
        $uploadMbps = Measure-LegacyUploadMbps "https://speed.cloudflare.com/__up" 1000000
        $output += ("Upload: {0} Mbps" -f $uploadMbps)
    } catch {
        $output += ("Upload test failed: " + $_.Exception.Message)
    }
    if ($downloadMbps -gt 0 -or $uploadMbps -gt 0) {
        $network = Get-LegacyNetworkContext
        $payload = @{
            hostname = $Config.Hostname
            download_mbps = $downloadMbps
            upload_mbps = $uploadMbps
            ping_ms = $pingMs
            test_server = "cloudflare"
            agent_version = $Script:Version
            network_type = $network["network_type"]
            network_ssid = $network["network_ssid"]
        }
        try { Invoke-PostJson $Config "/api/agent/speedtest" $payload | Out-Null } catch { }
        return @{ Output = ($output -join "`n"); Code = 0 }
    }
    return @{ Output = ($output -join "`n"); Code = 1 }
}

function Invoke-LegacyPowerAction {
    param([string]$Action)
    if ($Action -eq "reboot") {
        shutdown.exe /r /t 60 /c "SiteKings requested restart" 2>&1 | Out-String
        return @{ Output = "Restart scheduled in 60 seconds"; Code = $LASTEXITCODE }
    }
    if ($Action -eq "shutdown") {
        shutdown.exe /s /t 60 /c "SiteKings requested shutdown" 2>&1 | Out-String
        return @{ Output = "Shutdown scheduled in 60 seconds"; Code = $LASTEXITCODE }
    }
    return @{ Output = "Unsupported power action"; Code = 1 }
}

function Invoke-LegacyGetNtpSource {
    $source = w32tm.exe /query /source 2>&1 | Out-String
    $status = w32tm.exe /query /status 2>&1 | Out-String
    return @{ Output = ("NTP source`n" + $source + "`nNTP status`n" + $status); Code = $LASTEXITCODE }
}

function Invoke-LegacySetNtpSource {
    param([string]$Source)
    $clean = ($Source -replace '"', '').Trim()
    if (-not $clean -or $clean.Length -gt 160 -or ($clean -notmatch "^[A-Za-z0-9._,\- ]+$")) {
        return @{ Output = "NTP source rejected: invalid source"; Code = 1 }
    }
    $peerArg = "/manualpeerlist:$clean"
    $config = w32tm.exe /config $peerArg /syncfromflags:manual /update 2>&1 | Out-String
    $resync = w32tm.exe /resync 2>&1 | Out-String
    $verify = w32tm.exe /query /source 2>&1 | Out-String
    return @{ Output = ("NTP source set to $clean`n$config`n$resync`nCurrent source`n$verify"); Code = $LASTEXITCODE }
}

function Invoke-LegacyKillProcess {
    param([string]$Target)
    $clean = ($Target -replace '"', '').Trim()
    if (-not $clean -or $clean.Length -gt 80) {
        return @{ Output = "Process kill rejected: missing target"; Code = 1 }
    }
    $blockedNames = @("system", "idle", "csrss.exe", "wininit.exe", "winlogon.exe", "services.exe", "lsass.exe", "smss.exe", "powershell.exe", "sk-agent.exe", "sk-recover.exe")
    $lower = $clean.ToLower()
    if ($blockedNames -contains $lower) {
        return @{ Output = "Process kill rejected: protected process"; Code = 1 }
    }
    if ($clean -match "^\d+$") {
        if ([int]$clean -eq $PID) {
            return @{ Output = "Process kill rejected: refusing to kill legacy agent host"; Code = 1 }
        }
        $out = taskkill.exe /PID $clean /F 2>&1 | Out-String
        return @{ Output = $out; Code = $LASTEXITCODE }
    }
    if ($clean -notmatch "^[A-Za-z0-9._\-]+(\.exe)?$") {
        return @{ Output = "Process kill rejected: invalid process target"; Code = 1 }
    }
    $out = taskkill.exe /IM $clean /F 2>&1 | Out-String
    return @{ Output = $out; Code = $LASTEXITCODE }
}

function Invoke-LegacyBrowseDir {
    param([string]$Path)
    $path = ($Path -replace '"', '').Trim()
    if (-not $path) { $path = "C:\" }
    if ($path.Length -gt 260 -or ($path -match "[\*\?]")) {
        return @{ Output = "Browse failed: invalid path"; Code = 1 }
    }
    if (-not (Test-Path $path)) {
        return @{ Output = "Browse failed: path not found"; Code = 1 }
    }
    $rows = @()
    try {
        foreach ($item in (Get-ChildItem -LiteralPath $path -Force | Select-Object -First 500)) {
            $size = 0
            if (-not $item.PSIsContainer -and $item.Length) { $size = [int64]$item.Length }
            $rows += @{
                name = $item.Name
                size = $size
                modified = $item.LastWriteTime.ToString("yyyy-MM-ddTHH:mm:ss")
                is_dir = [bool]$item.PSIsContainer
                path = $item.FullName
            }
        }
        return @{ Output = (ConvertTo-SkJson $rows); Code = 0 }
    } catch {
        return @{ Output = ("Browse failed: " + $_.Exception.Message); Code = 1 }
    }
}

function Format-LegacyBytes {
    param([int64]$Bytes)
    if ($Bytes -ge 1GB) { return ("{0:N2} GB" -f ($Bytes / 1GB)) }
    if ($Bytes -ge 1MB) { return ("{0:N2} MB" -f ($Bytes / 1MB)) }
    if ($Bytes -ge 1KB) { return ("{0:N2} KB" -f ($Bytes / 1KB)) }
    return ("{0} B" -f $Bytes)
}

function Invoke-LegacyTopFiles {
    param([string]$CommandText)
    $root = Get-CommandJsonArg $CommandText "path"
    if (-not $root) { $root = (Get-LegacyCommandSuffix $CommandText "__TOP_FILES__") }
    if (-not $root) { $root = "C:\Users" }
    $root = ($root -replace '"', '').Trim()
    if ($root.Length -gt 260 -or ($root -match "[\*\?]") -or (-not (Test-Path $root))) {
        return @{ Output = "Top Files scan rejected: invalid root"; Code = 1 }
    }
    $minSize = 50MB
    $scanned = 0
    $candidates = @()
    try {
        foreach ($item in (Get-ChildItem -LiteralPath $root -Recurse -Force | Where-Object { -not $_.PSIsContainer })) {
            $scanned += 1
            if ($item.Length -ge $minSize) {
                $candidates += @{ path = $item.FullName; size = [int64]$item.Length }
            }
        }
    } catch { }
    $files = @()
    $rank = 1
    foreach ($file in ($candidates | Sort-Object -Property size -Descending | Select-Object -First 20)) {
        $files += @{
            rank = $rank
            path = $file.path
            size_bytes = $file.size
            size_formatted = (Format-LegacyBytes $file.size)
        }
        $rank += 1
    }
    $payload = @{
        files = $files
        scanned_count = $scanned
        roots = @($root)
        min_size = $minSize
        truncated = ($files.Count -ge 20)
    }
    return @{
        Output = ("Top Files scan completed: {0} files over 50 MB from {1} scanned files." -f $files.Count, $scanned)
        Code = 0
        ResultKind = "top_files"
        ResultJson = (ConvertTo-SkJson $payload)
    }
}

function Invoke-LegacyInventorySync {
    param($Config)
    Send-Checkin $Config
    Send-Inventory $Config
    Send-Logs $Config
    return @{ Output = "Legacy inventory sync completed"; Code = 0 }
}

function Invoke-LegacyListServices {
    $rows = @()
    foreach ($svc in (Get-WmiObject Win32_Service | Sort-Object DisplayName)) {
        $rows += @{
            name = $svc.Name
            display_name = $svc.DisplayName
            status = $svc.State
            start_type = $svc.StartMode
            path = $svc.PathName
            account = $svc.StartName
        }
    }
    return ConvertTo-SkJson @{ services = $rows; count = @($rows).Count }
}

function Invoke-LegacyListProcesses {
    $rows = @()
    foreach ($proc in (Get-WmiObject Win32_Process | Sort-Object WorkingSetSize -Descending | Select-Object -First 100)) {
        $user = ""
        try {
            $owner = $proc.GetOwner()
            if ($owner -and $owner.User) { $user = $owner.User }
        } catch { }
        $mem = 0
        if ($proc.WorkingSetSize) { $mem = [math]::Round(([double]$proc.WorkingSetSize) / 1MB, 1) }
        $rows += @{
            pid = $proc.ProcessId
            name = $proc.Name
            cpu = 0
            mem_mb = $mem
            user = $user
            title = ""
            command_line = $proc.CommandLine
            path = $proc.ExecutablePath
        }
    }
    return ConvertTo-SkJson $rows
}

function Invoke-LegacyListDrivers {
    $rows = @()
    foreach ($driver in (Get-WmiObject Win32_PnPSignedDriver | Where-Object { $_.DeviceName } | Sort-Object DeviceName)) {
        $date = ""
        if ($driver.DriverDate) {
            try {
                $date = ([Management.ManagementDateTimeConverter]::ToDateTime($driver.DriverDate)).ToString("yyyy-MM-dd")
            } catch { $date = "" }
        }
        $rows += @{
            device = $driver.DeviceName
            driver = $driver.DriverVersion
            provider = $driver.DriverProviderName
            class = $driver.DeviceClass
            signed = if ($driver.IsSigned) { "Yes" } else { "No" }
            date = $date
            inf = $driver.InfName
        }
    }
    return ConvertTo-SkJson $rows
}

function Invoke-LegacyDiscoverPrinters {
    param($Config)
    $rows = @()
    foreach ($printer in (Get-WmiObject Win32_Printer)) {
        $ip = ""
        $isNetwork = $false
        if ($printer.PortName -match '(\d+\.\d+\.\d+\.\d+)') {
            $ip = $Matches[1]
            $isNetwork = $true
        } elseif ($printer.Network) {
            $isNetwork = $true
        }
        try {
            $portName = ($printer.PortName -replace "'", "''")
            $port = Get-WmiObject Win32_TCPIPPrinterPort -Filter "Name='$portName'"
            if ($port -and $port.HostAddress) {
                $ip = $port.HostAddress
                $isNetwork = $true
            }
        } catch { }
        $manufacturer = ""
        $model = ""
        if ($printer.DriverName) {
            $parts = $printer.DriverName -split " "
            if ($parts.Count -gt 0) { $manufacturer = $parts[0] }
            $model = $printer.DriverName
        }
        $status = "unknown"
        switch ($printer.PrinterStatus) {
            1 { $status = "other" }
            2 { $status = "unknown" }
            3 { $status = "idle" }
            4 { $status = "printing" }
            5 { $status = "warmup" }
            default {
                if ($printer.WorkOffline) { $status = "offline" }
                else { $status = "online" }
            }
        }
        $rows += @{
            name = $printer.Name
            ip = $ip
            mac = ""
            manufacturer = $manufacturer
            model = $model
            location = if ($printer.Location) { $printer.Location } else { "" }
            status = $status
            is_network = $isNetwork
            driver = if ($printer.DriverName) { $printer.DriverName } else { "" }
            port = $printer.PortName
        }
    }
    $payload = @{ hostname = $Config.Hostname; printers = $rows }
    try { Invoke-PostJson $Config "/api/printers" $payload | Out-Null } catch { }
    return ConvertTo-SkJson @{ printers = $rows; count = @($rows).Count }
}

function Add-LegacyStartupRow {
    param([ref]$Rows, [string]$Name, [string]$Command, [string]$Location, [string]$User, [bool]$Removable)
    if (-not $Name) { return }
    $Rows.Value += @{
        name = $Name
        command = if ($Command) { $Command } else { "" }
        location = if ($Location) { $Location } else { "" }
        user = if ($User) { $User } else { "" }
        removable = $Removable
    }
}

function Invoke-LegacyListStartup {
    $rows = @()
    $seen = @{}
    foreach ($item in (Get-WmiObject Win32_StartupCommand | Sort-Object Name)) {
        $key = ($item.Location + "|" + $item.Name)
        if ($seen.ContainsKey($key)) { continue }
        $seen[$key] = $true
        Add-LegacyStartupRow ([ref]$rows) $item.Name $item.Command $item.Location $item.User $true
    }

    $runKeys = @(
        "HKLM:\Software\Microsoft\Windows\CurrentVersion\Run",
        "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Run",
        "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run"
    )
    foreach ($path in $runKeys) {
        if (-not (Test-Path $path)) { continue }
        $props = Get-ItemProperty -Path $path
        foreach ($prop in $props.PSObject.Properties) {
            if ($prop.Name -like "PS*") { continue }
            $key = ($path + "|" + $prop.Name)
            if ($seen.ContainsKey($key)) { continue }
            $seen[$key] = $true
            Add-LegacyStartupRow ([ref]$rows) $prop.Name ([string]$prop.Value) $path $env:USERNAME $true
        }
    }

    $startupFolders = @(
        [Environment]::GetFolderPath("Startup"),
        [Environment]::GetFolderPath("CommonStartup")
    )
    foreach ($folder in $startupFolders) {
        if (-not $folder -or -not (Test-Path $folder)) { continue }
        foreach ($file in (Get-ChildItem -Path $folder | Where-Object { -not $_.PSIsContainer })) {
            $key = ($folder + "|" + $file.Name)
            if ($seen.ContainsKey($key)) { continue }
            $seen[$key] = $true
            Add-LegacyStartupRow ([ref]$rows) $file.BaseName $file.FullName $folder $env:USERNAME $true
        }
    }
    return ConvertTo-SkJson $rows
}

function Add-LegacyEventSection {
    param([ref]$Lines, [string]$Title, $Events)
    $Lines.Value += ""
    $Lines.Value += ("## " + $Title)
    if (-not $Events) {
        $Lines.Value += "No matching events in the last 24 hours."
        return
    }
    foreach ($evt in $Events) {
        $message = ""
        if ($evt.Message) { $message = ($evt.Message -replace "\s+", " ").Trim() }
        if ($message.Length -gt 500) { $message = $message.Substring(0, 500) + "..." }
        $Lines.Value += ("{0:u} [{1}] EventID={2} Source={3}" -f $evt.TimeGenerated, $evt.EntryType, $evt.InstanceId, $evt.Source)
        if ($message) { $Lines.Value += ("  " + $message) }
    }
}

function Invoke-LegacyPullEventLogs {
    $since = (Get-Date).AddHours(-24)
    $lines = @("--- Windows Event Logs (last 24 hours) ---")
    try {
        $system = Get-EventLog -LogName System -After $since -EntryType Error,Warning -Newest 80
        Add-LegacyEventSection ([ref]$lines) "System errors and warnings" $system
    } catch {
        $lines += ""
        $lines += "## System errors and warnings"
        $lines += ("Unavailable: " + $_.Exception.Message)
    }
    try {
        $application = Get-EventLog -LogName Application -After $since -EntryType Error,Warning -Newest 80
        Add-LegacyEventSection ([ref]$lines) "Application errors and warnings" $application
    } catch {
        $lines += ""
        $lines += "## Application errors and warnings"
        $lines += ("Unavailable: " + $_.Exception.Message)
    }
    try {
        $security = Get-EventLog -LogName Security -After $since -Newest 80 | Where-Object {
            $_.InstanceId -eq 4625 -or $_.InstanceId -eq 4740 -or $_.InstanceId -eq 4672 -or $_.InstanceId -eq 4720 -or $_.InstanceId -eq 4726 -or $_.InstanceId -eq 4732 -or $_.InstanceId -eq 4733
        }
        Add-LegacyEventSection ([ref]$lines) "Security failed logons and account changes" $security
    } catch {
        $lines += ""
        $lines += "## Security failed logons and account changes"
        $lines += ("Unavailable: " + $_.Exception.Message)
    }
    try {
        $reboots = Get-EventLog -LogName System -After $since -Newest 80 | Where-Object {
            $_.InstanceId -eq 41 -or $_.InstanceId -eq 6005 -or $_.InstanceId -eq 6006 -or $_.InstanceId -eq 6008 -or $_.InstanceId -eq 1074
        }
        Add-LegacyEventSection ([ref]$lines) "Boot, shutdown, and unexpected restart events" $reboots
    } catch {
        $lines += ""
        $lines += "## Boot, shutdown, and unexpected restart events"
        $lines += ("Unavailable: " + $_.Exception.Message)
    }
    return ($lines -join "`n")
}

function Get-LegacyCommandSuffix {
    param([string]$CommandText, [string]$Prefix)
    if (-not $CommandText.StartsWith($Prefix)) { return "" }
    $suffix = $CommandText.Substring($Prefix.Length)
    if ($suffix.StartsWith(":")) { $suffix = $suffix.Substring(1) }
    return $suffix.Trim()
}

function Invoke-LegacyServiceAction {
    param([string]$Action, [string]$ServiceName)
    if (-not $ServiceName -or ($ServiceName -notmatch "^[A-Za-z0-9._\-]+$")) {
        return @{ Output = "Service action rejected: invalid service name"; Code = 1 }
    }
    if ($Action -eq "start") {
        $out = sc.exe start "$ServiceName" 2>&1 | Out-String
        return @{ Output = "Service action start $ServiceName`n$out"; Code = $LASTEXITCODE }
    }
    if ($Action -eq "stop") {
        $out = sc.exe stop "$ServiceName" 2>&1 | Out-String
        return @{ Output = "Service action stop $ServiceName`n$out"; Code = $LASTEXITCODE }
    }
    if ($Action -eq "restart") {
        $stopOut = sc.exe stop "$ServiceName" 2>&1 | Out-String
        Start-Sleep -Seconds 3
        $startOut = sc.exe start "$ServiceName" 2>&1 | Out-String
        $code = $LASTEXITCODE
        return @{ Output = "Service action restart $ServiceName`n$stopOut`n$startOut"; Code = $code }
    }
    return @{ Output = "Service action rejected: unsupported action"; Code = 1 }
}

function Invoke-LegacyCommand {
    param($Config, [string]$CommandText)
    if ($CommandText -eq "__PING__") {
        return @{ Output = "pong legacy_windows " + $env:COMPUTERNAME; Code = 0 }
    }
    if ($CommandText -eq "__VERSION__") {
        return @{ Output = $Script:Version; Code = 0 }
    }
    if ($CommandText -eq "__SHIP_LOGS__") {
        Send-Logs $Config
        return @{ Output = "Legacy logs shipped"; Code = 0 }
    }
    if ($CommandText -like "__REBOOT__*") {
        return Invoke-LegacyPowerAction "reboot"
    }
    if ($CommandText -like "__SHUTDOWN__*") {
        return Invoke-LegacyPowerAction "shutdown"
    }
    if ($CommandText -like "__SPEEDTEST__*") {
        return Invoke-LegacySpeedtest $Config
    }
    if ($CommandText -like "__GET_NTP_SOURCE__*") {
        return Invoke-LegacyGetNtpSource
    }
    if ($CommandText -like "__SET_NTP_SOURCE__*") {
        return Invoke-LegacySetNtpSource (Get-LegacyCommandSuffix $CommandText "__SET_NTP_SOURCE__")
    }
    if ($CommandText -like "__KILL_PROCESS__*") {
        return Invoke-LegacyKillProcess (Get-LegacyCommandSuffix $CommandText "__KILL_PROCESS__")
    }
    if ($CommandText -like "__BROWSE_DIR__*") {
        return Invoke-LegacyBrowseDir (Get-LegacyCommandSuffix $CommandText "__BROWSE_DIR__")
    }
    if ($CommandText -like "__TOP_FILES__*") {
        return Invoke-LegacyTopFiles $CommandText
    }
    if ($CommandText -eq "__SYNC__" -or $CommandText -eq "__INVENTORY__") {
        return Invoke-LegacyInventorySync $Config
    }
    if ($CommandText -like "__LIST_SERVICES__*") {
        return @{ Output = (Invoke-LegacyListServices); Code = 0 }
    }
    if ($CommandText -like "__LIST_PROCESSES__*") {
        return @{ Output = (Invoke-LegacyListProcesses); Code = 0 }
    }
    if ($CommandText -like "__LIST_DRIVERS__*") {
        return @{ Output = (Invoke-LegacyListDrivers); Code = 0 }
    }
    if ($CommandText -like "__DISCOVER_PRINTERS__*") {
        return @{ Output = (Invoke-LegacyDiscoverPrinters $Config); Code = 0 }
    }
    if ($CommandText -like "__LIST_STARTUP__*") {
        return @{ Output = (Invoke-LegacyListStartup); Code = 0 }
    }
    if ($CommandText -like "__PULL_EVENT_LOGS__*") {
        return @{ Output = (Invoke-LegacyPullEventLogs); Code = 0 }
    }
    if ($CommandText -like "__START_SERVICE__*") {
        return Invoke-LegacyServiceAction "start" (Get-LegacyCommandSuffix $CommandText "__START_SERVICE__")
    }
    if ($CommandText -like "__STOP_SERVICE__*") {
        return Invoke-LegacyServiceAction "stop" (Get-LegacyCommandSuffix $CommandText "__STOP_SERVICE__")
    }
    if ($CommandText -like "__RESTART_SERVICE__*") {
        return Invoke-LegacyServiceAction "restart" (Get-LegacyCommandSuffix $CommandText "__RESTART_SERVICE__")
    }
    if ($CommandText -like "__WIN10_PROBE__*") {
        return @{ Output = (Invoke-Win10Probe); Code = 0 }
    }
    if ($CommandText -like "__UPGRADE_WIN10_MEDIA__*") {
        return Invoke-Win10MediaUpgrade $Config $CommandText
    }
    if ($CommandText -like "__LEGACY_LAB_SMOKE__*") {
        return Invoke-LegacyLabSmoke $Config
    }
    if ($CommandText -like "__LEGACY_LAB_COLLECT__*") {
        return Invoke-LegacyLabCollect $Config
    }
    if ($CommandText -like "__INSTALL_TRAY__*") {
        return Invoke-LegacyInstallTray $Config
    }
    if ($CommandText -like "__RESTART_AGENT__*") {
        [IO.File]::WriteAllText($Script:RestartFlag, "restart requested")
        return @{ Output = "restart-agent.flag written"; Code = 0 }
    }
    $out = cmd.exe /c $CommandText 2>&1 | Out-String
    return @{ Output = $out; Code = $LASTEXITCODE }
}

function Run-Once {
    $config = Get-Config
    if (-not $config.ServerUrl -or -not $config.ApiKey) {
        Write-Log "missing server_url or api_key" "ERROR"
        Update-Status "error" "missing config"
        return
    }
    Update-Status "syncing" "legacy agent check-in"
    Send-Checkin $config
    Send-Inventory $config
    foreach ($cmd in (Get-PendingCommands $config)) {
        $cmdId = Get-CommandId $cmd
        $text = Get-CommandText $cmd
        if (-not $cmdId -or -not $text) { continue }
        $result = Invoke-LegacyCommand $config $text
        Complete-Command $config $cmdId $result
    }
    if (Test-Path $Script:SyncFlag) { Remove-Item $Script:SyncFlag -Force }
    if (Test-Path $Script:UpdateFlag) { Write-Log "update-flag.txt observed; waiting for explicit update command" }
    Send-Logs $config
    Update-Status "idle" "legacy agent healthy"
}

while ($true) {
    try { Run-Once } catch { Write-Log $_.Exception.Message "ERROR"; Update-Status "error" $_.Exception.Message }
    Start-Sleep -Seconds 60
}
