<#
.SYNOPSIS
    Registers the v2 Windows service on a node: OtOpcUaHost (fused binary, .NET 10)
    and optionally OtOpcUaWonderwareHistorian (Wonderware historian sidecar, net48 x86).

.DESCRIPTION
    v2 consolidates the legacy OtOpcUa + OtOpcUaAdmin services into a single role-gated
    OtOpcUaHost binary. The -Roles parameter sets the OTOPCUA_ROLES service env so
    Program.cs decides what to mount (admin / driver / both). The Wonderware historian
    sidecar logic is unchanged from v1; install it with -InstallWonderwareHistorian.

    Galaxy access flows through the mxaccessgw sibling repo (separate service); see
    docs/v2/Galaxy.ParityRig.md for the gateway setup.

.PARAMETER InstallRoot
    Where the binaries live (typically C:\Program Files\OtOpcUa). The OtOpcUaHost
    service runs OtOpcUa.Host.exe from this directory; publish the Host project there
    with `dotnet publish -c Release -r win-x64 --self-contained` first.

.PARAMETER ServiceAccount
    Service account SID or DOMAIN\name. The OtOpcUaHost service runs under this account.

.PARAMETER Roles
    Comma-separated cluster roles for this node. One of:
    - "admin,driver"  — single-node dev or all-in-one production node
    - "admin"          — admin-only HA pair member (Blazor + control-plane singletons)
    - "driver"         — driver-only node (OPC UA endpoint + per-node actors)
    Written to the service env as OTOPCUA_ROLES.

.PARAMETER HttpPort
    HTTP port for the AdminUI + auth endpoints. Default 9000. Written as ASPNETCORE_URLS.
    Ignored on driver-only nodes (no Blazor surface).

.PARAMETER InstallWonderwareHistorian
    Gate the OtOpcUaWonderwareHistorian sidecar install. Off by default; set when the
    deployment uses the Wonderware historian for history reads + alarm-event persistence.

.PARAMETER HistorianSharedSecret
    Per-process secret passed to the historian sidecar via env var. Generated freshly
    per install when not supplied.

.EXAMPLE
    .\Install-Services.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' `
        -ServiceAccount 'OTOPCUA\svc-otopcua' -Roles 'admin,driver'

.EXAMPLE
    .\Install-Services.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' `
        -ServiceAccount 'OTOPCUA\svc-otopcua' -Roles 'driver' `
        -InstallWonderwareHistorian
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory)] [string]$InstallRoot,
    [Parameter(Mandatory)] [string]$ServiceAccount,
    [Parameter(Mandatory)] [ValidateSet('admin', 'driver', 'admin,driver', 'driver,admin')]
                          [string]$Roles,
    [int]$HttpPort = 9000,

    # Wonderware historian sidecar. Optional; gates the OtOpcUaWonderwareHistorian
    # service. Secret + pipe defaults match the server's Historian:Wonderware appsettings.
    [switch]$InstallWonderwareHistorian,
    [string]$HistorianSharedSecret,
    [string]$HistorianPipeName = 'OtOpcUaWonderwareHistorian',
    [string]$HistorianServer = 'localhost',
    [int]$HistorianPort = 32568,
    [string[]]$AvevaServiceDependencies = @('NmxSvc', 'aaBootstrap', 'aaGR')
)

$ErrorActionPreference = 'Stop'

if (-not (Test-Path "$InstallRoot\OtOpcUa.Host.exe")) {
    Write-Error "OtOpcUa.Host.exe not found at $InstallRoot — copy the publish output first"
    exit 1
}

function New-SharedSecret {
    $bytes = New-Object byte[] 32
    [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
    return [Convert]::ToBase64String($bytes)
}
if ($InstallWonderwareHistorian -and -not $HistorianSharedSecret) {
    $HistorianSharedSecret = New-SharedSecret
}

if ($InstallWonderwareHistorian -and -not (Test-Path "$InstallRoot\WonderwareHistorian\OtOpcUa.Driver.Historian.Wonderware.exe")) {
    Write-Error "OtOpcUa.Driver.Historian.Wonderware.exe not found at $InstallRoot\WonderwareHistorian — copy the publish output first"
    exit 1
}

# Resolve the SID — the IPC ACL needs the SID, not the down-level name.
$sid = if ($ServiceAccount.StartsWith('S-1-')) {
    $ServiceAccount
} else {
    (New-Object System.Security.Principal.NTAccount $ServiceAccount).Translate([System.Security.Principal.SecurityIdentifier]).Value
}

# --- OtOpcUaWonderwareHistorian sidecar (optional, unchanged from v1) -------
$historianDepend = $null
if ($InstallWonderwareHistorian) {
    $historianEnv = @(
        "OTOPCUA_HISTORIAN_PIPE=$HistorianPipeName"
        "OTOPCUA_ALLOWED_SID=$sid"
        "OTOPCUA_HISTORIAN_SECRET=$HistorianSharedSecret"
        "OTOPCUA_HISTORIAN_ENABLED=true"
        "OTOPCUA_HISTORIAN_ALARM_WRITE_ENABLED=true"
        "OTOPCUA_HISTORIAN_SERVER=$HistorianServer"
        "OTOPCUA_HISTORIAN_PORT=$HistorianPort"
    )

    Write-Host "Installing OtOpcUaWonderwareHistorian..."
    & sc.exe create OtOpcUaWonderwareHistorian binPath= "`"$InstallRoot\WonderwareHistorian\OtOpcUa.Driver.Historian.Wonderware.exe`"" `
        DisplayName= 'OtOpcUa Wonderware Historian Sidecar (out-of-process aahClient)' `
        start= auto `
        depend= ($AvevaServiceDependencies -join '/') `
        obj= $ServiceAccount | Out-Null
    & sc.exe config OtOpcUaWonderwareHistorian start= delayed-auto | Out-Null

    $svcKey = "HKLM:\SYSTEM\CurrentControlSet\Services\OtOpcUaWonderwareHistorian"
    Set-ItemProperty -Path $svcKey -Name 'Environment' -Type MultiString -Value $historianEnv

    & sc.exe failure OtOpcUaWonderwareHistorian reset= 86400 actions= restart/5000/restart/30000/restart/60000 | Out-Null

    $historianDepend = 'OtOpcUaWonderwareHistorian'
}

# --- OtOpcUaHost (the fused v2 binary) --------------------------------------
$normalisedRoles = ($Roles -split ',' | ForEach-Object { $_.Trim() } | Sort-Object -Unique) -join ','

$hasAdmin = $normalisedRoles -split ',' -contains 'admin'

$hostEnv = @(
    "OTOPCUA_ROLES=$normalisedRoles",
    'DOTNET_ENVIRONMENT=Production'
)
if ($hasAdmin) {
    $hostEnv += "ASPNETCORE_URLS=http://+:$HttpPort"
}

$hostDepends = @()
if ($historianDepend) { $hostDepends += $historianDepend }

Write-Host "Installing OtOpcUaHost (roles=$normalisedRoles)..."
$createArgs = @(
    'create', 'OtOpcUaHost',
    'binPath=', "`"$InstallRoot\OtOpcUa.Host.exe`"",
    'DisplayName=', "OtOpcUa Host ($normalisedRoles)",
    'start=', 'auto',
    'obj=', $ServiceAccount
)
if ($hostDepends.Count -gt 0) {
    $createArgs += @('depend=', ($hostDepends -join '/'))
}
& sc.exe @createArgs | Out-Null

# Env block via registry MultiString (sc.exe doesn't take env directly).
$svcKey = "HKLM:\SYSTEM\CurrentControlSet\Services\OtOpcUaHost"
Set-ItemProperty -Path $svcKey -Name 'Environment' -Type MultiString -Value $hostEnv

# Restart-on-failure: 5s, 30s, 60s; reset counter after a clean 24h run.
& sc.exe failure OtOpcUaHost reset= 86400 actions= restart/5000/restart/30000/restart/60000 | Out-Null

Write-Host ""
Write-Host "Installed OtOpcUaHost:"
Write-Host "  Roles:        $normalisedRoles"
if ($hasAdmin) { Write-Host "  HTTP port:    $HttpPort" }
Write-Host "  Binary:       $InstallRoot\OtOpcUa.Host.exe"
Write-Host "  Account:      $ServiceAccount"
Write-Host ""
Write-Host "Start with:"
if ($InstallWonderwareHistorian) { Write-Host "  sc.exe start OtOpcUaWonderwareHistorian" }
Write-Host "  sc.exe start OtOpcUaHost"
if ($InstallWonderwareHistorian) {
    Write-Host ""
    Write-Host "Wonderware historian shared secret (configure into appsettings.json Historian:Wonderware:SharedSecret):"
    Write-Host "  $HistorianSharedSecret"
}
Write-Host ""
Write-Host "NOTE: Galaxy access flows through mxaccessgw — install + run that separately"
Write-Host "      per docs/v2/Galaxy.ParityRig.md. OtOpcUaHost connects via the"
Write-Host "      Galaxy.Gateway section of appsettings.json (default http://localhost:5120)."
