[CmdletBinding()]
param([string] $NodeExecutable = '')

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

$repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..'))
$wrapperPath = Join-Path $repositoryRoot 'scripts\Invoke-LegacyPgmParityRun.ps1'
$stagePath = Join-Path $repositoryRoot 'scripts\LegacyPgmParityStage.mjs'
$protocolPath = Join-Path $repositoryRoot 'src\MBN_STOCK_WEBVIEW.LegacyBridge\LegacyBridgeProtocol.cs'
$playoutUiPath = Join-Path $repositoryRoot 'src\MBN_STOCK_WEBVIEW.LegacyParityApp\Web\playout-ui.js'
$appUiPath = Join-Path $repositoryRoot 'src\MBN_STOCK_WEBVIEW.LegacyParityApp\Web\app.js'

foreach ($path in @($wrapperPath, $stagePath, $protocolPath, $playoutUiPath, $appUiPath)) {
    if (-not [IO.File]::Exists($path)) { throw "Required parity contract file is missing: $path" }
}

if ([string]::IsNullOrWhiteSpace($NodeExecutable)) {
    $command = Get-Command node.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
    if ($null -ne $command) {
        $NodeExecutable = $command.Source
    } else {
        $NodeExecutable = Join-Path $env:USERPROFILE '.cache\codex-runtimes\codex-primary-runtime\dependencies\node\bin\node.exe'
    }
}
$NodeExecutable = [IO.Path]::GetFullPath($NodeExecutable)
if (-not [IO.File]::Exists($NodeExecutable)) { throw "Node executable is missing: $NodeExecutable" }

$tokens = $null
$parseErrors = $null
[Management.Automation.Language.Parser]::ParseFile($wrapperPath, [ref]$tokens, [ref]$parseErrors) | Out-Null
if ($parseErrors.Count -ne 0) {
    throw "PowerShell syntax failed at line $($parseErrors[0].Extent.StartLineNumber): $($parseErrors[0].Message)"
}
& $NodeExecutable --check $stagePath
if ($LASTEXITCODE -ne 0) { throw 'Node syntax check failed.' }

$wrapper = [IO.File]::ReadAllText($wrapperPath, [Text.Encoding]::UTF8)
$stage = [IO.File]::ReadAllText($stagePath, [Text.Encoding]::UTF8)
$protocol = [IO.File]::ReadAllText($protocolPath, [Text.Encoding]::UTF8)
$playoutUi = [IO.File]::ReadAllText($playoutUiPath, [Text.Encoding]::UTF8)
$appUi = [IO.File]::ReadAllText($appUiPath, [Text.Encoding]::UTF8)

$forbidden = @(
    'Wickedness\.MBNStockWebView\.LegacyParity_[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+_',
    '(?i)\$(?:Pgm|App)ProcessId\s*=\s*[0-9]{3,}',
    'MBNWEB-20260718-DEV-LIVE-02',
    'legacy-named-[0-9]',
    'MBN_STOCK_WEBVIEW\.LegacyParityApp\.Playout\.dll',
    'C:\\Users\\[^\\]+',
    'C:\\Tornado2'
)
foreach ($pattern in $forbidden) {
    if ([regex]::IsMatch($wrapper + "`n" + $stage, $pattern)) {
        throw "A stale runtime/package/named-playlist pin was found: $pattern"
    }
}

$requiredWrapperTokens = @(
    'Get-AppxPackage -Name $ExpectedPackageName',
    '$package.IsDevelopmentMode -ne $true',
    "'src\MBN_STOCK_WEBVIEW.LegacyParityApp\bin\x64\Debug'",
    '$debugSegments[1] -cne ''win-x64''',
    '$debugSegments[2] -cne ''AppX''',
    'Find-UniqueProcessByPath',
    'Get-EstablishedPgmConnection',
    '[Net.IPAddress]::IsLoopback($localAddress)',
    '[Net.IPAddress]::IsLoopback($remoteAddress)',
    '-PgmProcessId $pgmProcess.Id',
    '-RemotePort $connection.LocalPort',
    "'MBN_STOCK_WEBVIEW.LegacyParityApp.exe'",
    "'MBN_STOCK_WEBVIEW.LegacyParityApp.dll'",
    "'MBN_STOCK_WEBVIEW.Playout.dll'",
    "[string] `$ExpectedPgmExecutable = ''",
    '$env:MBN_STOCK_PGM_EXECUTABLE',
    "'..\MBN_STOCK_N\MBN_STOCK_N\bin\Debug\Cuts'",
    'Use-ManifestDiscoverySettings -Manifest $manifest',
    'Assert-PinsExact -Pinned $manifest.pins',
    'SetTopMostForCapture',
    'CopyFromScreen',
    '[AllowEmptyString()] [string] $Text',
    '[Diagnostics.ProcessStartInfo]::new()',
    '$start.RedirectStandardOutput = $true',
    '$start.RedirectStandardError = $true',
    '$stdoutTask = $process.StandardOutput.ReadToEndAsync()',
    '$exitCode = [int]$process.ExitCode',
    'STOPPED.json',
    'FileMode]::CreateNew',
    "'^(?!0000-)\d{4}-'",
    "'RunNext'",
    'VgAtAFMAdABvAGMAawAgAJ3JjK0VyPS8ocGczdzCpMJc0SAAZgBvAHIAIADkuXzHvawcyFQAVgAgACgAMgA2AC4AMAAzAC4AMgA2ACkA'
)
foreach ($token in $requiredWrapperTokens) {
    if (-not $wrapper.Contains($token)) { throw "Wrapper safety contract is missing: $token" }
}
if ($wrapper.Contains('SetForegroundWindow')) {
    throw 'Evidence capture must not steal keyboard focus from either application.'
}
if ($wrapper.Contains('Start-Process -FilePath $nodePath')) {
    throw 'Windows PowerShell Start-Process cannot provide a trustworthy Node exit code on this workstation.'
}
if (@([regex]::Matches($wrapper, '\[AllowEmptyString\(\)\]\s*\[string\]\s*\$Text')).Count -ne 2) {
    throw 'Both empty Network evidence writes and empty Network delta counts must remain supported.'
}

$requiredStageTokens = @(
    "'search-stocks'", "'select-stock'", "'cut-pointer-down'", "'drop-selected-cuts'",
    "'activate-playlist-row'", "'take-in'", "'next-playout'", "'take-out'",
    "'open-manual-financial'", "'search-manual-financial'",
    "'activate-manual-financial-result'", "'select-comparison-pair'", "'delete-selected-comparison-pair'",
    "'playout-take-in'", "'playout-next'", "'playout-take-out'",
    'Input.dispatchMouseEvent', 'OUTCOME_UNKNOWN', 'current.state.currentCode === code',
    'payload?.allowAppend === true', 'physicalDragStockCut(client, physicalIndex, label)',
    "types[0] !== 'cut-pointer-down'", "types[1] !== 'drop-selected-cuts'",
    'comparisonPairRows(comparison)', 'deleteTemporaryComparisonPair(client, description, baseline, addedPairId)',
    'restoreComparisonSelection(client, selectedPairId)', 'savedPairBaseline',
    'resolved: row.isSceneResolved === true', 'row.resolved !== true',
    'const FIXED_VISIBLE_ROW_IDENTITIES = new Map([', 'expected.market != null && row.market !== expected.market',
    "['5077', { market: '\ucf54\uc2a4\ud53c', stockName: '\uc9c0\uc218', graphicType: '6\uc885\ubaa9 \ud604\uc7ac\uac00', subtype: '\ucf54\uc2a4\ud53c \uc2dc\uac00\ucd1d\uc561' }]",
    "['5088', { market: '\ucf54\uc2a4\ud53c', stockName: '\uc9c0\uc218', graphicType: '12\uc885\ubaa9 \ud604\uc7ac\uac00', subtype: '\ucf54\uc2a4\ud53c \uc2dc\uac00\ucd1d\uc561' }]",
    "second.pageText !== '1/20'", "result.state.rows.some(row => row.pageText !== '1/20')",
    "current.state.pageCount === 20", "current.state.itemCount === 120",
    "current.state.itemCount === 240", "const STATE_TIMEOUT_MS = 180_000",
    "case 'take-out-recovery-8001'", "case 'take-out-recovery-5077-page2'", "case 'resume-30-manifest'"
)
foreach ($token in $requiredStageTokens) {
    if (-not $stage.Contains($token)) { throw "One-shot stage contract is missing: $token" }
}
if ($stage.Contains("postAndWait(client, 'cut-pointer-down'") -or
    $stage.Contains("postAndWait(client, 'drop-selected-cuts'")) {
    throw 'Stock cut setup must use the actual pointer drag path instead of direct native messages.'
}
if (@([regex]::Matches($stage, 'allowMissingState:\s*true')).Count -ne 1 -or
    $stage -notmatch "Boolean\(value\.state\),\s*'initial native state',\s*STATE_TIMEOUT_MS,\s*\{\s*allowMissingState:\s*true\s*\}" -or
    $stage -notmatch "PROBE_VIOLATION[\s\S]*?const state = result\.state;\s*if \(allowMissingState && !state\) return;[\s\S]*?LIVE_STATE_MISMATCH") {
    throw 'Only bootstrap may tolerate the probe-to-native-state initialization gap.'
}
if (@([regex]::Matches($stage, '\bgraphicCode\b')).Count -ne 2) {
    throw 'The runner must not infer hidden cut aliases from WebView playlist state.'
}
if (-not $protocol.Contains('row.IsSceneResolved')) {
    throw 'Latest serialized playlist state no longer exposes IsSceneResolved.'
}

foreach ($messageType in @(
        'search-stocks', 'select-stock', 'cut-pointer-down', 'drop-selected-cuts',
        'activate-playlist-row', 'take-in', 'next-playout', 'take-out',
        'open-manual-financial', 'search-manual-financial',
        'activate-manual-financial-result', 'select-comparison-pair', 'delete-selected-comparison-pair')) {
    if (-not $protocol.Contains('"' + $messageType + '"') -and
        -not $playoutUi.Contains('"' + $messageType + '"') -and
        -not $appUi.Contains('"' + $messageType + '"')) {
        throw "Latest source no longer exposes native message '$messageType'."
    }
}
foreach ($buttonId in @('playout-take-in', 'playout-next', 'playout-take-out')) {
    if (-not $playoutUi.Contains('"' + $buttonId + '"')) {
        throw "Latest playout UI no longer exposes '$buttonId'."
    }
}
if (-not $appUi.Contains('element.dataset.rowId = row.rowId') -or
    -not $appUi.Contains('rowHeader.className = "playlist-row-header"')) {
    throw 'Latest playlist row/header DOM contract changed.'
}

$packageBuildRoot = Join-Path $repositoryRoot 'src\MBN_STOCK_WEBVIEW.LegacyParityApp\bin\x64\Debug'
$fixtureRoots = @()
if ([IO.Directory]::Exists($packageBuildRoot)) {
    foreach ($manifestFile in @(Get-ChildItem -LiteralPath $packageBuildRoot -Recurse -File `
            -Filter 'AppxManifest.xml' -ErrorAction Stop)) {
        $candidateRoot = $manifestFile.Directory.FullName
        $relative = $candidateRoot.Substring([IO.Path]::GetFullPath($packageBuildRoot).TrimEnd('\').Length).TrimStart('\')
        $segments = @($relative.Split('\') | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
        if ($segments.Count -eq 3 -and
            $segments[0] -match '^net[0-9]+\.[0-9]+-windows[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' -and
            $segments[1] -ceq 'win-x64' -and $segments[2] -ceq 'AppX' -and
            [IO.File]::Exists((Join-Path $candidateRoot 'MBN_STOCK_WEBVIEW.LegacyParityApp.exe')) -and
            [IO.File]::Exists((Join-Path $candidateRoot 'MBN_STOCK_WEBVIEW.LegacyParityApp.dll')) -and
            [IO.File]::Exists((Join-Path $candidateRoot 'MBN_STOCK_WEBVIEW.Playout.dll'))) {
            $fixtureRoots += $candidateRoot
        }
    }
}
if ($fixtureRoots.Count -lt 1) {
    throw 'No exact x64 Debug target-framework/win-x64/AppX runtime-file fixture was found.'
}
$runtimeFixtureRoot = @($fixtureRoots | Sort-Object)[-1]
$runtimeFixtureFiles = @(
    'MBN_STOCK_WEBVIEW.LegacyParityApp.exe',
    'MBN_STOCK_WEBVIEW.LegacyParityApp.dll',
    'MBN_STOCK_WEBVIEW.Playout.dll'
)
$runtimeFixtureHashes = @($runtimeFixtureFiles | ForEach-Object {
    $fixturePath = Join-Path $runtimeFixtureRoot $_
    [ordered]@{
        name = $_
        length = [IO.FileInfo]::new($fixturePath).Length
        sha256 = (Get-FileHash -LiteralPath $fixturePath -Algorithm SHA256).Hash
    }
})

$expectedRoutes = @(
    '5016', '50160', '5078', '8067', '5086', '50860', '5023', '5024',
    '5077', '5082', '5083', '5084', '5085', '5088', '6067', '8035',
    '5011', '8003', '5037', '8001', '5026', '5087', '5029', '8032',
    '5032', '5076', '5079', '5080', '5081', '5025'
)
$routeBlock = [regex]::Match(
    $stage,
    'const THIRTY_CODES = \[(?<body>.*?)\];',
    [Text.RegularExpressions.RegexOptions]::Singleline)
if (-not $routeBlock.Success) { throw 'THIRTY_CODES block is missing.' }
$actualRoutes = @([regex]::Matches($routeBlock.Groups['body'].Value, "'(?<code>[0-9]+)'") |
    ForEach-Object { $_.Groups['code'].Value })
if (($actualRoutes -join ',') -cne ($expectedRoutes -join ',')) {
    throw 'The 30-route validation order changed.'
}

$probeFixedIdBlock = [regex]::Match(
    $stage,
    'const fixedIds = new Set\(\[(?<body>.*?)\]\);',
    [Text.RegularExpressions.RegexOptions]::Singleline)
if (-not $probeFixedIdBlock.Success) { throw 'The probe fixed-action allowlist is missing.' }
foreach ($actionId in @('fixed-072', 'fixed-073', 'fixed-074', 'fixed-075')) {
    if (-not $probeFixedIdBlock.Groups['body'].Value.Contains("'$actionId'")) {
        throw "The S6001 physical action is not probe-allowlisted: $actionId"
    }
}

foreach ($token in @(
        'S6001_ASSET_INDEPENDENT_ACTIONS',
        "actionId: 'fixed-072'",
        "actionId: 'fixed-073'",
        "actionId: 'fixed-074'",
        "actionId: 'fixed-075'",
        "subtype: 'DubaiCrude'",
        "subtype: 'WtiCrude'",
        "subtype: 'BrentCrude'",
        "subtype: 'Gold'",
        "physicalSelectCatalogTab(client, 'overseas')",
        '#market-tabs button[data-tab-id=',
        'current.probe.nativeStateCount > nativeStateBefore',
        "current.dom.activeWorkspace === 'catalog'",
        'current.dom.catalogVisible === true',
        "types[0] !== 'select-tab'",
        'physicalDoubleClickAndWait(client, selector',
        "current.state.currentCode === '6001'")) {
    if (-not $stage.Contains($token)) {
        throw "The asset-independent s6001 physical-input contract is missing: $token"
    }
}

$staticJson = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $wrapperPath -Action StaticAudit
if ($LASTEXITCODE -ne 0) { throw 'Wrapper StaticAudit failed.' }
$static = $staticJson | ConvertFrom-Json
if ($static.ok -ne $true -or $static.authoritativeCutCount -ne 33 -or
    $static.authoritativeAssetCount -ne 2 -or
    $static.workflowStageCounts.Core5001 -ne 6 -or
    $static.workflowStageCounts.ThirtyRoutes -ne 125 -or
    $static.workflowStageCounts.PagedBoundary -ne 24 -or
    $static.workflowStageCounts.S6001AssetIndependent -ne 18 -or
    $static.workflowStageCounts.Recovery8001 -ne 1 -or
    $static.workflowStageCounts.Recovery5077Page2 -ne 1 -or
    $static.workflowStageCounts.ThirtyRoutes20To29 -ne 41 -or
    $static.runtimeProcessesQueried -ne $false -or $static.cdpAttached -ne $false -or
    $static.nativeMessagesSent -ne 0) {
    throw 'Wrapper StaticAudit result is not exact.'
}
if (@($static.authoritativeCutHashes).Count -ne 33 -or
    @($static.authoritativeCutHashes | Where-Object {
            [string]::IsNullOrWhiteSpace([string]$_.path) -or
            [long]$_.length -le 0 -or
            [string]$_.sha256 -notmatch '^[0-9A-F]{64}$'
        }).Count -ne 0) {
    throw 'Wrapper StaticAudit cut identities contain a null or invalid value.'
}
if (@($static.authoritativeAssetHashes).Count -ne 2 -or
    @($static.authoritativeAssetHashes | Where-Object {
            [string]::IsNullOrWhiteSpace([string]$_.path) -or
            [long]$_.length -le 0 -or
            [string]$_.sha256 -notmatch '^[0-9A-F]{64}$'
        }).Count -ne 0) {
    throw 'Wrapper StaticAudit asset identities contain a null or invalid value.'
}

[ordered]@{
    ok = $true
    checkedAtUtc = [DateTime]::UtcNow.ToString('o')
    powershellSyntax = 'passed'
    nodeSyntax = 'passed'
    latestBridgeAndDomContract = 'passed'
    hardcodedRuntimeProcessIds = 0
    hardcodedPackageFullNames = 0
    namedPlaylistDependencies = 0
    developmentDebugRuntimeFixture = $runtimeFixtureRoot
    developmentDebugRuntimeFiles = $runtimeFixtureHashes
    routeCount = $actualRoutes.Count
    workflowStageCounts = $static.workflowStageCounts
    runtimeProcessesQueried = $false
    cdpAttached = $false
    nativeMessagesSent = 0
} | ConvertTo-Json -Depth 8 -Compress
