# Invoke-ShopdbShadow.ps1 -- run one shopdb SHADOW cycle, from the manifest.
#
# Shadow = fetch the shopdb manifest, diff it against the share manifest, report
# the cycle to shopdb. The engine still installs FROM THE SHARE, so behaviour is
# unchanged. It is the observable step before any cutover.
#
# WHY THIS RUNS AS A MANIFEST ENTRY AND NOT A SCHEDULED TASK
#
# The first version registered a separate 15-minute task. That cannot work:
# GE-Enforce.ps1 mounts the SFLD share for the length of its own cycle and
# unmounts it at the end (mounted 12:10:02, unmounted 12:10:27). A task on its
# own schedule therefore wakes up with the drive gone, hands the engine a path
# that no longer resolves, and reports 0 installed / 0 skipped / 0 failed - a
# silent nothing indistinguishable from a healthy no-op. It also meant two
# cadences that could drift apart, and a registration that had to be healed.
#
# Running here removes all of it: the share is mounted because the enforce cycle
# is what invoked us, the cadence is the fleet's own, and there is no task.
#
# Runs as SYSTEM under GE-Enforce, from an entry gated to the test bays.
# Fail-safe: exits 0 on every path - shadowing must never stop a bay enforcing.

param(
    # This site's ShopDB. Optional: a bay that already carries a BaseUrl in the
    # registry keeps it, so the fleet needs this only on the first cycle or when
    # the address changes. There is deliberately NO default - see below.
    [string] $BaseUrl = ''
)

$ErrorActionPreference = 'Continue'

# RE-ENTRANCY GUARD. This entry lives IN the manifest the shadow run enforces,
# so without it the thing recurses without bound: engine runs the manifest ->
# reaches this entry -> we invoke the runner -> the runner runs the engine
# against the SAME manifest -> reaches this entry again. Measured on the test
# bays 2026-08-13: 347 nested cycles in 36 minutes, one every five seconds,
# until the share unmounted. The task-based predecessor never hit this because
# it registered a task instead of invoking the runner.
#
# An environment variable, because it is inherited by every child process and
# therefore covers the nested engine and runner without a file to clean up or a
# stale lock to age out. Set for THIS process tree only.
if ($env:SHOPDB_SHADOW_ACTIVE -eq '1') { exit 0 }
$env:SHOPDB_SHADOW_ACTIVE = '1'

$InstallDir = 'C:\Program Files\GE\Shopfloor'
$LegacyTask = 'ShopDB GE-Enforce (shadow)'

function Write-ShadowLog {
    # To a file as well as the host: the engine records only "ps1: <path>" and
    # an exit code for a PS1 entry, so Write-Host reaches nothing, and with the
    # fail-safe exit 0 a silent early-out looks exactly like success.
    param([string]$Message)
    $line = "[{0}] [shadow] {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
    Write-Host $line
    try {
        $dir = 'C:\Logs\Shopfloor'
        if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
        Add-Content -LiteralPath (Join-Path $dir ('shadow-{0}.log' -f (Get-Date -Format yyyyMMdd))) -Value $line
    } catch { }
}

try {
    # Retire the scheduled task the earlier version left behind. Bays that got
    # it would otherwise keep firing a run that cannot see the share, reporting
    # 0/0/0 over the top of the real result from this one.
    $stale = Get-ScheduledTask -TaskName $LegacyTask -ErrorAction SilentlyContinue
    if ($stale) {
        Unregister-ScheduledTask -TaskName $LegacyTask -Confirm:$false -ErrorAction SilentlyContinue
        Write-ShadowLog "removed the superseded '$LegacyTask' task (it ran outside the share mount)."
    }

    # Scope is this script's own directory name, never hardcoded: the same file
    # ships from more than one scope and a wrong value would shadow the wrong
    # manifest silently.
    $scopeDir = Split-Path -Parent $PSScriptRoot
    $scope = Split-Path -Leaf $scopeDir
    $shareManifest = Join-Path $scopeDir 'manifest.json'

    $runner = Join-Path $InstallDir 'Invoke-ShopdbEnforce.ps1'
    $engine = Join-Path $InstallDir 'lib\Install-FromManifest.ps1'
    foreach ($required in @($runner, $engine, $shareManifest)) {
        if (-not (Test-Path -LiteralPath $required)) {
            Write-ShadowLog "MISSING $required - skipping this cycle."
            exit 0
        }
    }

    # THE BAY'S OWN VALUE WINS. This used to hold one site's ShopDB URL as a
    # compiled-in constant and write it whenever the registry disagreed. At the
    # site it was written for that reads as "heal drift"; anywhere else it reads
    # as "overwrite this site's address on every enforce cycle", and the site
    # cannot win because the cycle repeats. A second plant could not point its
    # own bays at its own server.
    #
    # So: an explicitly passed -BaseUrl seeds the value, an existing registry
    # value is never touched, and with neither there is nothing honest to write.
    $regPath = 'HKLM:\SOFTWARE\GE\ShopDB'
    if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
    $current = (Get-ItemProperty -Path $regPath -Name BaseUrl -ErrorAction SilentlyContinue).BaseUrl
    if (-not $current) {
        if (-not $BaseUrl) {
            Write-ShadowLog "no BaseUrl in $regPath and none passed - set it, or pass -BaseUrl in the manifest entry. Skipping this cycle."
            exit 0
        }
        Set-ItemProperty -Path $regPath -Name BaseUrl -Value $BaseUrl
        Write-ShadowLog "BaseUrl seeded as $BaseUrl"
    } elseif ($BaseUrl -and $BaseUrl -ne $current) {
        # Say it, do not do it. A changed address is a real event, and it should
        # be a deliberate one, not a side effect of whatever the manifest last
        # carried.
        Write-ShadowLog "BaseUrl in registry is $current; -BaseUrl passed $BaseUrl. Keeping the registry value."
    }

    Write-ShadowLog "shadowing $scope against $shareManifest"
    & $runner -Scope $scope -EnginePath $engine -ShadowMode -ShareManifestPath $shareManifest
    Write-ShadowLog "cycle complete (runner exit $LASTEXITCODE)."
    exit 0
}
catch {
    Write-ShadowLog "FAILED: $_"
    exit 0
}
finally {
    # Clear it so the NEXT enforce cycle shadows again. Without this the guard
    # would latch for the life of the process tree and shadow would run once.
    $env:SHOPDB_SHADOW_ACTIVE = $null
}
