[CmdletBinding()]
param(
    [string]$RepoRoot = "C:\Users\dohertj2\Desktop\lmxopcua",
    [string]$GatewayRoot = "C:\Users\dohertj2\Desktop\mxaccessgw",
    [string]$PublishRoot = "C:\publish",
    [switch]$SkipBackup,
    [switch]$WhatIf
)

# PR D.1 — refresh C:\publish + restart services for the alarms-over-gateway
# epic. Stops services in reverse-dependency order (OtOpcUa →
# OtOpcUaWonderwareHistorian → MxAccessGw), refreshes binaries from the
# repos, then starts in forward order. A timestamped backup of the existing
# C:\publish trees lands under C:\publish\.backup-YYYY-MM-DD\ unless
# -SkipBackup is supplied.
#
# Designed to run as a single elevated PowerShell session on the deploy host
# (the dev rig today; production refresh is a separate runbook).

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

function Step([string]$Message) {
    Write-Host ""
    Write-Host "==> $Message" -ForegroundColor Cyan
}

function Run([scriptblock]$Block, [string]$Description) {
    if ($WhatIf) {
        Write-Host "  (skip) $Description" -ForegroundColor DarkYellow
        return
    }
    Write-Host "  $Description"
    & $Block
}

function Test-NssmService([string]$Name) {
    $svc = Get-Service -Name $Name -ErrorAction SilentlyContinue
    return $null -ne $svc
}

# ------------------------------------------------------------------------
# Step 1: Stop in reverse dependency order
# ------------------------------------------------------------------------

Step "Stopping services (OtOpcUaHost > OtOpcUaWonderwareHistorian > MxAccessGw)"

foreach ($name in @('OtOpcUaHost', 'OtOpcUaWonderwareHistorian', 'MxAccessGw')) {
    if (Test-NssmService $name) {
        Run { Stop-Service $name -Force -ErrorAction SilentlyContinue } "stop $name"
    }
    else {
        Write-Host "  ($name not installed; skipping)" -ForegroundColor DarkGray
    }
}

if (-not $WhatIf) {
    Start-Sleep -Seconds 3
    Get-Process MxGateway.Server, MxGateway.Worker, OtOpcUa.Host, OtOpcUa.Driver.Historian.Wonderware -ErrorAction SilentlyContinue |
        ForEach-Object {
            Write-Host "  killing residual process $($_.ProcessName) (PID=$($_.Id))" -ForegroundColor DarkYellow
            Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue
        }
}

# ------------------------------------------------------------------------
# Step 2: Backup existing C:\publish trees
# ------------------------------------------------------------------------

if (-not $SkipBackup -and (Test-Path $PublishRoot)) {
    $backupRoot = Join-Path $PublishRoot ".backup-$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))"
    Step "Backing up $PublishRoot → $backupRoot"

    Run {
        New-Item -ItemType Directory -Path $backupRoot | Out-Null
        foreach ($subdir in @('mxaccessgw', 'lmxopcua')) {
            $src = Join-Path $PublishRoot $subdir
            if (Test-Path $src) {
                Copy-Item -Recurse -Path $src -Destination (Join-Path $backupRoot $subdir)
            }
        }
    } "snapshot publish dirs (rollback target)"
}
else {
    Write-Host "  (backup skipped)" -ForegroundColor DarkGray
}

# ------------------------------------------------------------------------
# Step 3: Refresh mxaccessgw binaries (Track A output)
# ------------------------------------------------------------------------

Step "Building + copying mxaccessgw binaries from $GatewayRoot"

Run {
    & dotnet build "$GatewayRoot\src\MxGateway.Worker" -c Release | Out-Null
    & dotnet build "$GatewayRoot\src\MxGateway.Server" -c Release | Out-Null
} "dotnet build (Worker x86 net48 + Server net10.0)"

Run {
    $serverDest = Join-Path $PublishRoot "mxaccessgw\Server"
    $workerDest = Join-Path $PublishRoot "mxaccessgw\Worker"
    if (-not (Test-Path $serverDest)) { New-Item -ItemType Directory -Path $serverDest -Force | Out-Null }
    if (-not (Test-Path $workerDest)) { New-Item -ItemType Directory -Path $workerDest -Force | Out-Null }
    Copy-Item -Recurse -Force "$GatewayRoot\src\MxGateway.Server\bin\Release\net10.0\*" $serverDest
    Copy-Item -Recurse -Force "$GatewayRoot\src\MxGateway.Worker\bin\x86\Release\net48\*" $workerDest
} "copy gateway server + worker outputs"

# ------------------------------------------------------------------------
# Step 4: Refresh OtOpcUa + Wonderware historian sidecar
# ------------------------------------------------------------------------

Step "Publishing OtOpcUa.Host + Wonderware historian sidecar from $RepoRoot"

Run {
    & dotnet publish "$RepoRoot\src\Server\ZB.MOM.WW.OtOpcUa.Host" `
        -c Release -o (Join-Path $PublishRoot "lmxopcua") | Out-Null
    & dotnet publish "$RepoRoot\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware" `
        -c Release -o (Join-Path $PublishRoot "lmxopcua\WonderwareHistorian") | Out-Null
} "dotnet publish (Host + sidecar)"

# ------------------------------------------------------------------------
# Step 4b: Assert the Wonderware historian sidecar deploy is COMPLETE
# ------------------------------------------------------------------------
# A partial/stale sidecar deploy (e.g. a hand-copy that bypassed the
# `dotnet publish` above) silently drops the net48 binding-redirect
# .exe.config and the transitive runtime DLLs MessagePack needs — most
# notably System.Memory.dll. The sidecar then JIT-load-faults inside
# TcpFrameServer.RunOneConnectionAsync ("FileNotFoundException: System.Memory,
# Version=4.0.1.2") and NSSM crash-loops it (exit 2 every ~2 min, ~120 s of
# retry backoff before it gives up). Fail loudly here so an incomplete deploy
# is caught at publish time instead of by a production crash-loop.
# (Root-caused 2026-06-12; these four files have no alternate resolution path,
# unlike the AVEVA SDK DLLs which the Historian install can also supply.)

if (-not $WhatIf) {
    Step "Verifying Wonderware historian sidecar deploy is complete"

    $sidecarDir = Join-Path $PublishRoot "lmxopcua\WonderwareHistorian"
    $sidecarRequired = @(
        'OtOpcUa.Driver.Historian.Wonderware.exe',
        'OtOpcUa.Driver.Historian.Wonderware.exe.config', # net48 binding redirects
        'System.Memory.dll',                              # MessagePack's load-bearing dep
        'MessagePack.dll'
    )

    $missing = $sidecarRequired | Where-Object { -not (Test-Path (Join-Path $sidecarDir $_)) }

    if ($missing.Count -gt 0) {
        $msg = "Wonderware historian sidecar deploy at '$sidecarDir' is INCOMPLETE — missing: " +
               "$($missing -join ', '). It would crash-loop with a FileNotFoundException in TcpFrameServer. " +
               "Re-run the sidecar 'dotnet publish' into that folder (Step 4) — never hand-copy a partial build output."
        throw $msg
    }

    Write-Host "  sidecar deploy OK — all $($sidecarRequired.Count) load-bearing files present" -ForegroundColor DarkGreen
}

# ------------------------------------------------------------------------
# Step 5: Service env block — ensure OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED
# is set on the Wonderware historian service (PR C.2 toggle).
# Note: the TCP transport env (OTOPCUA_HISTORIAN_TCP_PORT, OTOPCUA_HISTORIAN_BIND,
# OTOPCUA_HISTORIAN_TLS_ENABLED, OTOPCUA_HISTORIAN_TLS_CERT) and the Windows
# Firewall rule are set once at install time by Install-Services.ps1 and are
# not touched here on refresh; re-run Install-Services.ps1 to change them.
# OTOPCUA_ALLOWED_SID is no longer used (TCP transport retired the named pipe).
# ------------------------------------------------------------------------

if (Test-NssmService 'OtOpcUaWonderwareHistorian') {
    Step "Ensuring OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED is set on the historian service"

    Run {
        $existing = nssm get OtOpcUaWonderwareHistorian AppEnvironmentExtra
        if ($existing -notmatch 'OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED') {
            $combined = $existing + "`r`nOTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=true"
            nssm set OtOpcUaWonderwareHistorian AppEnvironmentExtra $combined | Out-Null
            Write-Host "    appended OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=true" -ForegroundColor DarkGreen
        }
        else {
            Write-Host "    already present; leaving service env block untouched"
        }
    } "patch service env block"
}

# ------------------------------------------------------------------------
# Step 6: Start in forward dependency order
# ------------------------------------------------------------------------

Step "Starting services (MxAccessGw > OtOpcUaWonderwareHistorian > OtOpcUaHost)"

foreach ($pair in @(
    @{ Name = 'MxAccessGw';                  Wait = 4 },
    @{ Name = 'OtOpcUaWonderwareHistorian';  Wait = 4 },
    @{ Name = 'OtOpcUaHost';                 Wait = 8 }
)) {
    $name = $pair.Name
    if (Test-NssmService $name) {
        Run { Start-Service $name } "start $name"
        if (-not $WhatIf) { Start-Sleep -Seconds $pair.Wait }
    }
    else {
        Write-Host "  ($name not installed; skipping)" -ForegroundColor DarkGray
    }
}

# ------------------------------------------------------------------------
# Step 7: Smoke verification
# ------------------------------------------------------------------------

Step "Smoke verification"

if (-not $WhatIf) {
    foreach ($name in @('MxAccessGw', 'OtOpcUaWonderwareHistorian', 'OtOpcUaHost')) {
        if (Test-NssmService $name) {
            $status = (Get-Service $name).Status
            $color = if ($status -eq 'Running') { 'Green' } else { 'Red' }
            Write-Host "  $name = $status" -ForegroundColor $color
        }
    }

    foreach ($port in @(5120, 4840, 4841)) {
        $listening = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue
        $color = if ($listening) { 'Green' } else { 'DarkYellow' }
        Write-Host "  TCP $port listening = $($null -ne $listening)" -ForegroundColor $color
    }

    Write-Host ""
    Write-Host "  Recent log tails:" -ForegroundColor DarkCyan
    $tails = @(
        "$PublishRoot\lmxopcua\logs\otopcua-*.log",
        "$PublishRoot\mxaccessgw\stdout.log",
        "$env:ProgramData\OtOpcUa\historian-wonderware-*.log"
    )
    foreach ($pattern in $tails) {
        $latest = Get-ChildItem -Path $pattern -ErrorAction SilentlyContinue |
            Sort-Object LastWriteTime -Descending |
            Select-Object -First 1
        if ($null -ne $latest) {
            Write-Host ""
            Write-Host "  --- $($latest.FullName) (last 10 lines) ---" -ForegroundColor DarkGray
            Get-Content $latest.FullName -Tail 10 | ForEach-Object { Write-Host "    $_" }
        }
    }
}

Write-Host ""
Write-Host "Refresh complete." -ForegroundColor Green
Write-Host ""
Write-Host "Next: run the functional verification scenarios from"
Write-Host "  docs\plans\alarms-over-gateway.md §Track D §6 'Functional verification'"
Write-Host "  - Galaxy-native alarm raise"
Write-Host "  - Scripted alarm → AVEVA Historian round-trip"
Write-Host "  - Sub-attribute fallback path with IAlarmSource disabled"
