#Requires -Version 5.1

[CmdletBinding()]
param(
    [string] $LegacyRuntimeSourceRoot,

    [switch] $NoFolderPicker,

    [string] $DatabaseIniPath,

    [switch] $SkipDatabaseProfile,

    [switch] $ReplaceRuntimeBinding,

    [switch] $ReplaceDatabaseProfile,

    [switch] $ConfigureDevelopmentLive,

    [string] $PlayoutHost,

    [int] $PlayoutPort,

    [string] $NativeSha256,

    [string] $InteropSha256,

    [switch] $PinRegisteredK3D,

    [switch] $ReplaceK3DPin,

    [ValidateRange(0, 2147483647)]
    [Nullable[int]] $OutputChannel = $null,

    [switch] $ReplaceLiveConfig,

    [switch] $SkipBuild
)

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

$maximumDatabaseIniLength = 1MB
$maximumManifestLength = 4MB
$expectedResAssetCount = 34
$manifestFileName = 'LegacyRuntimeBundle.manifest.json'
$archiveFileName = 'LegacyRuntimeBundle.zip'
$archiveHashFileName = 'LegacyRuntimeBundle.zip.sha256'

function Get-NormalizedFullPath {
    param([Parameter(Mandatory = $true)][string] $Path)

    return [IO.Path]::GetFullPath($Path)
}

function Test-PathIsWithin {
    param(
        [Parameter(Mandatory = $true)][string] $Root,
        [Parameter(Mandatory = $true)][string] $Candidate
    )

    $normalizedRoot = (Get-NormalizedFullPath -Path $Root).TrimEnd(
        [IO.Path]::DirectorySeparatorChar,
        [IO.Path]::AltDirectorySeparatorChar)
    $normalizedCandidate = Get-NormalizedFullPath -Path $Candidate
    return $normalizedCandidate.StartsWith(
        $normalizedRoot + [IO.Path]::DirectorySeparatorChar,
        [StringComparison]::OrdinalIgnoreCase)
}

function Test-SamePath {
    param(
        [Parameter(Mandatory = $true)][string] $Left,
        [Parameter(Mandatory = $true)][string] $Right
    )

    return (Get-NormalizedFullPath -Path $Left).TrimEnd('\', '/').Equals(
        (Get-NormalizedFullPath -Path $Right).TrimEnd('\', '/'),
        [StringComparison]::OrdinalIgnoreCase)
}

function Test-DirectCutsAndResRootCandidate {
    param(
        [Parameter(Mandatory = $true)][string] $Path,
        [Parameter(Mandatory = $true)][string] $RepositoryRoot
    )

    try {
        $candidate = Get-NormalizedFullPath -Path $Path
        if (-not (Test-Path -LiteralPath $candidate -PathType Container)) {
            return $false
        }

        $cuts = Get-NormalizedFullPath -Path (Join-Path $candidate 'Cuts')
        $res = Get-NormalizedFullPath -Path (Join-Path $candidate 'Res')
        $hasDirectRoots = (Test-Path -LiteralPath $cuts -PathType Container) -and
            (Test-Path -LiteralPath $res -PathType Container) -and
            (Test-SamePath -Left ([IO.Path]::GetDirectoryName($cuts)) -Right $candidate) -and
            (Test-SamePath -Left ([IO.Path]::GetDirectoryName($res)) -Right $candidate)
        if (-not $hasDirectRoots) {
            return $false
        }

        Assert-LegacyRuntimeRoot `
            -Root $candidate `
            -RepositoryRoot $RepositoryRoot
        return $true
    }
    catch {
        return $false
    }
}

function Assert-NoReparsePointInExistingAncestry {
    param(
        [Parameter(Mandatory = $true)][string] $Path,
        [Parameter(Mandatory = $true)][string] $Description
    )

    $current = Get-NormalizedFullPath -Path $Path
    while (-not [string]::IsNullOrWhiteSpace($current)) {
        if (Test-Path -LiteralPath $current) {
            $attributes = [IO.File]::GetAttributes($current)
            if (($attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
                throw "$Description traverses a reparse point: $current"
            }
        }

        $trimmed = $current.TrimEnd('\', '/')
        $parent = [IO.Path]::GetDirectoryName($trimmed)
        if ([string]::IsNullOrWhiteSpace($parent) -or
            $parent.Equals($current, [StringComparison]::OrdinalIgnoreCase)) {
            break
        }

        $current = $parent
    }
}

function Assert-NoReparsePointInTree {
    param(
        [Parameter(Mandatory = $true)][string] $Root,
        [Parameter(Mandatory = $true)][string] $Description
    )

    Assert-NoReparsePointInExistingAncestry -Path $Root -Description $Description
    foreach ($item in @(Get-ChildItem -LiteralPath $Root -Recurse -Force)) {
        if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
            throw "$Description contains a reparse point: $($item.FullName)"
        }
    }
}

function Assert-FixedLocalPath {
    param(
        [Parameter(Mandatory = $true)][string] $Path,
        [Parameter(Mandatory = $true)][string] $Description
    )

    $fullPath = Get-NormalizedFullPath -Path $Path
    if ($fullPath.StartsWith('\\', [StringComparison]::Ordinal)) {
        throw "$Description must be on a local fixed drive."
    }

    $pathRoot = [IO.Path]::GetPathRoot($fullPath)
    if ([string]::IsNullOrWhiteSpace($pathRoot)) {
        throw "$Description does not have a filesystem root."
    }

    $drive = New-Object IO.DriveInfo($pathRoot)
    if (-not $drive.IsReady -or $drive.DriveType -ne [IO.DriveType]::Fixed) {
        throw "$Description must be on a ready local fixed drive."
    }
}

function Assert-RegularFile {
    param(
        [Parameter(Mandatory = $true)][string] $Path,
        [Parameter(Mandatory = $true)][string] $Description
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        throw "$Description was not found as an ordinary file: $Path"
    }

    Assert-NoReparsePointInExistingAncestry -Path $Path -Description $Description
    $item = Get-Item -LiteralPath $Path -Force
    if ($item.PSIsContainer -or
        ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
        throw "$Description must be an ordinary file: $Path"
    }

    return $item
}

function Get-UserOnlyAcl {
    $identity = [Security.Principal.WindowsIdentity]::GetCurrent().Name
    $acl = New-Object Security.AccessControl.FileSecurity
    $acl.SetAccessRuleProtection($true, $false)
    $rule = New-Object Security.AccessControl.FileSystemAccessRule(
        $identity,
        [Security.AccessControl.FileSystemRights]::FullControl,
        [Security.AccessControl.AccessControlType]::Allow)
    $acl.AddAccessRule($rule)
    return $acl
}

function Set-UserOnlyDirectoryAcl {
    param([Parameter(Mandatory = $true)][string] $Path)

    $identity = [Security.Principal.WindowsIdentity]::GetCurrent().Name
    $acl = New-Object Security.AccessControl.DirectorySecurity
    $acl.SetAccessRuleProtection($true, $false)
    $rule = New-Object Security.AccessControl.FileSystemAccessRule(
        $identity,
        [Security.AccessControl.FileSystemRights]::FullControl,
        ([Security.AccessControl.InheritanceFlags]::ContainerInherit -bor
            [Security.AccessControl.InheritanceFlags]::ObjectInherit),
        [Security.AccessControl.PropagationFlags]::None,
        [Security.AccessControl.AccessControlType]::Allow)
    $acl.AddAccessRule($rule)
    [IO.Directory]::SetAccessControl($Path, $acl)
}

function Test-UserOnlyFileAcl {
    param([Parameter(Mandatory = $true)][string] $Path)

    try {
        $identity = [Security.Principal.WindowsIdentity]::GetCurrent().Name
        $acl = [IO.File]::GetAccessControl($Path)
        if (-not $acl.AreAccessRulesProtected) {
            return $false
        }

        $rules = @($acl.GetAccessRules(
            $true,
            $false,
            [Security.Principal.NTAccount]))
        return $rules.Count -eq 1 -and
            $rules[0].IdentityReference.Value.Equals(
                $identity,
                [StringComparison]::OrdinalIgnoreCase) -and
            $rules[0].AccessControlType -eq
                [Security.AccessControl.AccessControlType]::Allow -and
            (($rules[0].FileSystemRights -band
                [Security.AccessControl.FileSystemRights]::FullControl) -eq
                [Security.AccessControl.FileSystemRights]::FullControl)
    }
    catch {
        return $false
    }
}

function Get-FileAccessSddl {
    param([Parameter(Mandatory = $true)][string] $Path)

    $acl = [IO.File]::GetAccessControl(
        $Path,
        [Security.AccessControl.AccessControlSections]::Access)
    return $acl.GetSecurityDescriptorSddlForm(
        [Security.AccessControl.AccessControlSections]::Access)
}

function Assert-ExactObjectProperties {
    param(
        [Parameter(Mandatory = $true)][object] $Value,
        [Parameter(Mandatory = $true)][string[]] $Names,
        [Parameter(Mandatory = $true)][string] $Description
    )

    if ($null -eq $Value -or $Value -isnot [pscustomobject]) {
        throw "$Description must be one PSCustomObject."
    }

    $actualNames = @($Value.PSObject.Properties | ForEach-Object {
        [string] $_.Name
    })
    if ($actualNames.Count -ne $Names.Count) {
        throw "$Description returned an unexpected property count."
    }
    for ($index = 0; $index -lt $Names.Count; $index++) {
        if (-not $actualNames[$index].Equals(
                $Names[$index],
                [StringComparison]::Ordinal)) {
            throw "$Description returned unexpected, reordered, or case-changed properties."
        }
    }
}

function Test-ExactUtf8Text {
    param(
        [Parameter(Mandatory = $true)][string] $Path,
        [Parameter(Mandatory = $true)][string] $ExpectedText
    )

    $actualBytes = [IO.File]::ReadAllBytes($Path)
    $expectedBytes = (New-Object Text.UTF8Encoding($false)).GetBytes($ExpectedText)
    if ($actualBytes.Length -ne $expectedBytes.Length) {
        return $false
    }
    for ($index = 0; $index -lt $expectedBytes.Length; $index++) {
        if ($actualBytes[$index] -ne $expectedBytes[$index]) {
            return $false
        }
    }
    return $true
}

function New-FileRollbackSnapshot {
    param(
        [Parameter(Mandatory = $true)][string] $Path,
        [Parameter(Mandatory = $true)][string] $BackupDirectory,
        [Parameter(Mandatory = $true)][string] $Description
    )

    $destination = Get-NormalizedFullPath -Path $Path
    if (-not (Test-Path -LiteralPath $destination)) {
        return [pscustomobject]@{
            Path = $destination
            Existed = $false
            Length = [long] 0
            Sha256 = $null
            AccessSddl = $null
            BackupPath = $null
            OriginalBytes = $null
        }
    }

    $item = Assert-RegularFile -Path $destination -Description $Description
    $sourceHash = (
        Get-FileHash -LiteralPath $destination -Algorithm SHA256
    ).Hash.ToUpperInvariant()
    $accessSddl = Get-FileAccessSddl -Path $destination
    $backupPath = Join-Path $BackupDirectory (
        [Guid]::NewGuid().ToString('N') + '.rollback')
    [IO.File]::Copy($destination, $backupPath, $false)

    # A credential backup is protected immediately after the copy, before its
    # content is read or used for any later rollback decision.
    [IO.File]::SetAccessControl($backupPath, (Get-UserOnlyAcl))
    $backupItem = Assert-RegularFile `
        -Path $backupPath `
        -Description "$Description rollback backup"
    $backupHash = (
        Get-FileHash -LiteralPath $backupPath -Algorithm SHA256
    ).Hash.ToUpperInvariant()
    if ($backupItem.Length -ne $item.Length -or
        -not $backupHash.Equals($sourceHash, [StringComparison]::Ordinal) -or
        -not (Test-UserOnlyFileAcl -Path $backupPath)) {
        throw "$Description rollback backup failed its immediate ACL/hash verification."
    }

    return [pscustomobject]@{
        Path = $destination
        Existed = $true
        Length = [long] $backupItem.Length
        Sha256 = $backupHash
        AccessSddl = $accessSddl
        BackupPath = $backupPath
        OriginalBytes = [IO.File]::ReadAllBytes($backupPath)
    }
}

function Restore-FileRollbackSnapshot {
    param(
        [Parameter(Mandatory = $true)][pscustomobject] $Snapshot,
        [Parameter(Mandatory = $true)][string] $Description
    )

    $destination = Get-NormalizedFullPath -Path ([string] $Snapshot.Path)
    $directory = [IO.Path]::GetDirectoryName($destination)
    if (-not [bool] $Snapshot.Existed) {
        if (Test-Path -LiteralPath $destination) {
            [void](Assert-RegularFile -Path $destination -Description $Description)
            [IO.File]::Delete($destination)
        }
        if (Test-Path -LiteralPath $destination) {
            throw "$Description rollback could not remove a newly created file."
        }
        return
    }

    [IO.Directory]::CreateDirectory($directory) | Out-Null
    Assert-NoReparsePointInExistingAncestry -Path $directory -Description $Description
    $temporaryPath = Join-Path $directory (
        '.' + [IO.Path]::GetFileName($destination) + '.' +
        [Guid]::NewGuid().ToString('N') + '.restore')
    try {
        [IO.File]::WriteAllBytes(
            $temporaryPath,
            [byte[]] $Snapshot.OriginalBytes)
        [IO.File]::SetAccessControl($temporaryPath, (Get-UserOnlyAcl))
        $temporaryItem = Assert-RegularFile `
            -Path $temporaryPath `
            -Description "$Description rollback temporary file"
        $temporaryHash = (
            Get-FileHash -LiteralPath $temporaryPath -Algorithm SHA256
        ).Hash.ToUpperInvariant()
        if ($temporaryItem.Length -ne [long] $Snapshot.Length -or
            -not $temporaryHash.Equals(
                [string] $Snapshot.Sha256,
                [StringComparison]::Ordinal) -or
            -not (Test-UserOnlyFileAcl -Path $temporaryPath)) {
            throw "$Description rollback bytes failed verification."
        }

        $originalAcl = New-Object Security.AccessControl.FileSecurity
        $originalAcl.SetSecurityDescriptorSddlForm(
            [string] $Snapshot.AccessSddl,
            [Security.AccessControl.AccessControlSections]::Access)
        [IO.File]::SetAccessControl($temporaryPath, $originalAcl)

        if (Test-Path -LiteralPath $destination) {
            [void](Assert-RegularFile -Path $destination -Description $Description)
            [IO.File]::Replace($temporaryPath, $destination, $null)
            $temporaryPath = $null
        }
        else {
            [IO.File]::Move($temporaryPath, $destination)
            $temporaryPath = $null
        }
        [IO.File]::SetAccessControl($destination, $originalAcl)

        $restoredItem = Assert-RegularFile `
            -Path $destination `
            -Description "$Description restored file"
        $restoredHash = (
            Get-FileHash -LiteralPath $destination -Algorithm SHA256
        ).Hash.ToUpperInvariant()
        $restoredSddl = Get-FileAccessSddl -Path $destination
        if ($restoredItem.Length -ne [long] $Snapshot.Length -or
            -not $restoredHash.Equals(
                [string] $Snapshot.Sha256,
                [StringComparison]::Ordinal) -or
            -not $restoredSddl.Equals(
                [string] $Snapshot.AccessSddl,
                [StringComparison]::Ordinal)) {
            throw "$Description rollback did not restore the exact bytes and ACL."
        }
    }
    finally {
        if (-not [string]::IsNullOrWhiteSpace($temporaryPath) -and
            (Test-Path -LiteralPath $temporaryPath)) {
            [IO.File]::Delete($temporaryPath)
        }
    }
}

function Remove-OwnedTemporaryDirectory {
    param(
        [Parameter(Mandatory = $true)][string] $Parent,
        [Parameter(Mandatory = $true)][string] $Path,
        [Parameter(Mandatory = $true)][string] $Description
    )

    if (-not (Test-Path -LiteralPath $Path)) {
        return
    }
    if (-not (Test-PathIsWithin -Root $Parent -Candidate $Path) -or
        [IO.Path]::GetFileName($Path) -notmatch '^[0-9a-f]{32}$') {
        throw "Refusing to clean an unexpected $Description path: $Path"
    }
    Assert-NoReparsePointInTree -Root $Path -Description $Description
    [IO.Directory]::Delete($Path, $true)
    if (Test-Path -LiteralPath $Path) {
        throw "$Description was not completely removed."
    }
}

function Remove-DevelopmentLiveAuthorizationFailClosed {
    param([Parameter(Mandatory = $true)][string] $Path)

    $authorizationPath = Get-NormalizedFullPath -Path $Path
    Assert-NoReparsePointInExistingAncestry `
        -Path $authorizationPath `
        -Description 'Development Live authorization'
    if (-not (Test-Path -LiteralPath $authorizationPath)) {
        return
    }
    [void](Assert-RegularFile `
        -Path $authorizationPath `
        -Description 'Development Live authorization')

    $invalidatedPath = (
        $authorizationPath + '.invalidated.' + [Guid]::NewGuid().ToString('N'))
    try {
        [IO.File]::Move($authorizationPath, $invalidatedPath)
    }
    catch {
        $moveError = $_
        try {
            [IO.File]::SetAccessControl($authorizationPath, (Get-UserOnlyAcl))
            [IO.File]::Delete($authorizationPath)
        }
        catch {
            throw [InvalidOperationException]::new(
                'Development Live authorization could not be invalidated at its exact path.',
                $moveError.Exception)
        }
        if (Test-Path -LiteralPath $authorizationPath) {
            throw [InvalidOperationException]::new(
                'Development Live authorization remains at its exact path after invalidation.',
                $moveError.Exception)
        }
        return
    }

    try {
        [IO.File]::SetAccessControl($invalidatedPath, (Get-UserOnlyAcl))
        [IO.File]::Delete($invalidatedPath)
    }
    catch {
        throw [InvalidOperationException]::new(
            'The authorization exact path is invalidated, but its protected tombstone could not be removed.',
            $_.Exception)
    }
    if (Test-Path -LiteralPath $authorizationPath) {
        throw 'Development Live authorization was recreated during fail-closed invalidation.'
    }
}

function Select-LegacyRuntimeRoot {
    param(
        [Parameter(Mandatory = $true)][string] $RepositoryRoot,
        [AllowNull()][string] $RequestedRoot,
        [switch] $DisablePicker
    )

    if (-not [string]::IsNullOrWhiteSpace($RequestedRoot)) {
        $requestedPath = Get-NormalizedFullPath -Path $RequestedRoot
        $requestedLeaf = [IO.Path]::GetFileName($requestedPath.TrimEnd('\', '/'))
        if ($requestedLeaf.Equals('Cuts', [StringComparison]::OrdinalIgnoreCase) -or
            $requestedLeaf.Equals('Res', [StringComparison]::OrdinalIgnoreCase)) {
            $requestedPath = Get-NormalizedFullPath -Path (
                [IO.Path]::GetDirectoryName($requestedPath))
        }
        return $requestedPath
    }

    $siblingCandidate = Get-NormalizedFullPath -Path (
        Join-Path $RepositoryRoot '..\MBN_STOCK_N\MBN_STOCK_N\bin\Debug')
    if (Test-DirectCutsAndResRootCandidate `
            -Path $siblingCandidate `
            -RepositoryRoot $RepositoryRoot) {
        return $siblingCandidate
    }

    if ($DisablePicker) {
        throw (
            "The exact sibling runtime was not found at '$siblingCandidate'. " +
            'Pass -LegacyRuntimeSourceRoot explicitly.')
    }

    Add-Type -AssemblyName System.Windows.Forms
    $dialog = New-Object System.Windows.Forms.FolderBrowserDialog
    try {
        $dialog.Description = (
            'Select the bin\Debug folder that directly contains both Cuts and Res.')
        $dialog.ShowNewFolderButton = $false
        if ($dialog.ShowDialog() -ne [Windows.Forms.DialogResult]::OK) {
            throw 'Legacy runtime folder selection was cancelled.'
        }

        $selectedPath = Get-NormalizedFullPath -Path $dialog.SelectedPath
        $selectedLeaf = [IO.Path]::GetFileName($selectedPath.TrimEnd('\', '/'))
        if ($selectedLeaf.Equals('Cuts', [StringComparison]::OrdinalIgnoreCase) -or
            $selectedLeaf.Equals('Res', [StringComparison]::OrdinalIgnoreCase)) {
            $selectedPath = Get-NormalizedFullPath -Path (
                [IO.Path]::GetDirectoryName($selectedPath))
        }
        return $selectedPath
    }
    finally {
        $dialog.Dispose()
    }
}

function Assert-LegacyRuntimeRoot {
    param(
        [Parameter(Mandatory = $true)][string] $Root,
        [Parameter(Mandatory = $true)][string] $RepositoryRoot
    )

    if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
        throw "Legacy runtime source root was not found: $Root"
    }

    if ($Root.Contains('$') -or
        $Root.Contains('%') -or
        $Root.Contains('@') -or
        $Root.Contains(';') -or
        $Root.Contains("'")) {
        throw 'Legacy runtime source root contains characters reserved by MSBuild.'
    }

    Assert-FixedLocalPath -Path $Root -Description 'Legacy runtime source root'
    Assert-NoReparsePointInExistingAncestry `
        -Path $Root `
        -Description 'Legacy runtime source root'

    if ((Test-SamePath -Left $Root -Right $RepositoryRoot) -or
        (Test-PathIsWithin -Root $RepositoryRoot -Candidate $Root) -or
        (Test-PathIsWithin -Root $Root -Candidate $RepositoryRoot)) {
        throw 'Legacy runtime source root must be outside and must not overlap this repository.'
    }

    $cutsRoot = Get-NormalizedFullPath -Path (Join-Path $Root 'Cuts')
    $resRoot = Get-NormalizedFullPath -Path (Join-Path $Root 'Res')
    if (-not (Test-Path -LiteralPath $cutsRoot -PathType Container) -or
        -not (Test-Path -LiteralPath $resRoot -PathType Container) -or
        -not (Test-SamePath -Left ([IO.Path]::GetDirectoryName($cutsRoot)) -Right $Root) -or
        -not (Test-SamePath -Left ([IO.Path]::GetDirectoryName($resRoot)) -Right $Root)) {
        throw 'The selected folder must be the single common parent of Cuts and Res.'
    }
    Assert-NoReparsePointInExistingAncestry `
        -Path $cutsRoot `
        -Description 'Cuts source directory'
    Assert-NoReparsePointInExistingAncestry `
        -Path $resRoot `
        -Description 'Res source directory'
}

function Get-ExistingRuntimeBinding {
    param(
        [Parameter(Mandatory = $true)][string] $RepositoryRoot
    )

    $propsPath = Join-Path $RepositoryRoot 'Directory.Build.local.props'
    if (-not (Test-Path -LiteralPath $propsPath)) {
        return $null
    }

    [void](Assert-RegularFile -Path $propsPath -Description 'Local build props')
    [xml] $xml = Get-Content -LiteralPath $propsPath -Raw -Encoding UTF8
    $projects = @($xml.SelectNodes("/*[local-name()='Project']"))
    $groups = @($xml.SelectNodes(
        "/*[local-name()='Project']/*[local-name()='PropertyGroup']"))
    $roots = @($xml.SelectNodes(
        "/*[local-name()='Project']/*[local-name()='PropertyGroup']/*[local-name()='LegacyRuntimeSourceRoot']"))
    $modes = @($xml.SelectNodes(
        "/*[local-name()='Project']/*[local-name()='PropertyGroup']/*[local-name()='LegacyRuntimeAssetsMode']"))
    if ($projects.Count -ne 1 -or $projects[0].Attributes.Count -ne 0 -or
        $groups.Count -ne 1 -or $groups[0].Attributes.Count -ne 0 -or
        $roots.Count -ne 1 -or $roots[0].Attributes.Count -ne 0 -or
        $modes.Count -ne 1 -or $modes[0].Attributes.Count -ne 0 -or
        @($groups[0].ChildNodes | Where-Object {
            $_.NodeType -eq [Xml.XmlNodeType]::Element
        }).Count -ne 2 -or
        -not ([string] $modes[0].InnerText).Equals(
            'Required',
            [StringComparison]::Ordinal)) {
        throw 'Existing Directory.Build.local.props is not the exact generated Required runtime binding.'
    }

    $boundRoot = Get-NormalizedFullPath -Path ([string] $roots[0].InnerText)
    return [pscustomobject]@{
        PropsPath = $propsPath
        BoundRoot = $boundRoot
    }
}

function Test-JsonIntegerValue {
    param([AllowNull()][object] $Value)

    return $Value -is [byte] -or
        $Value -is [int16] -or
        $Value -is [int32] -or
        $Value -is [int64] -or
        $Value -is [uint16] -or
        $Value -is [uint32]
}

function Get-VerifiedRuntimeManifest {
    param([Parameter(Mandatory = $true)][string] $ManifestPath)

    $manifestItem = Assert-RegularFile `
        -Path $ManifestPath `
        -Description 'Generated runtime manifest'
    if ($manifestItem.Length -le 0 -or
        $manifestItem.Length -gt $maximumManifestLength) {
        throw 'Generated runtime manifest is empty or exceeds the 4 MiB safety limit.'
    }

    $rawManifest = Get-Content -LiteralPath $ManifestPath -Raw -Encoding UTF8
    try {
        $manifest = $rawManifest | ConvertFrom-Json
    }
    catch {
        throw [InvalidDataException]::new(
            'Generated runtime manifest is not valid JSON.',
            $_.Exception)
    }
    Assert-ExactObjectProperties `
        -Value $manifest `
        -Names @(
            'schemaVersion',
            'bundleType',
            'fileCount',
            'cutsFileCount',
            'resFileCount',
            'files') `
        -Description 'Generated runtime manifest'

    if ($manifest.schemaVersion -isnot [int32] -or
        [int] $manifest.schemaVersion -ne 1 -or
        $manifest.bundleType -isnot [string] -or
        [string] $manifest.bundleType -cne
            'MBN_STOCK_WEBVIEW.LegacyRuntimeBundle' -or
        $manifest.fileCount -isnot [int32] -or
        $manifest.cutsFileCount -isnot [int32] -or
        $manifest.resFileCount -isnot [int32]) {
        throw 'Generated runtime manifest header types or values are invalid.'
    }

    $records = @($manifest.files)
    $canonicalRecords = New-Object 'System.Collections.Generic.List[object]'
    $seenPaths = New-Object 'System.Collections.Generic.HashSet[string]' (
        [StringComparer]::OrdinalIgnoreCase)
    $cutsCount = 0
    $resCount = 0
    foreach ($record in $records) {
        Assert-ExactObjectProperties `
            -Value $record `
            -Names @('path', 'length', 'sha256') `
            -Description 'Generated runtime manifest file record'
        if ($record.path -isnot [string] -or
            -not (Test-JsonIntegerValue -Value $record.length) -or
            [long] $record.length -lt 0 -or
            $record.sha256 -isnot [string] -or
            [string] $record.sha256 -cnotmatch '^[0-9A-F]{64}$') {
            throw 'Generated runtime manifest file record types are invalid.'
        }

        $relativePath = [string] $record.path
        if ([string]::IsNullOrWhiteSpace($relativePath) -or
            $relativePath.Contains('\') -or
            $relativePath.StartsWith('/', [StringComparison]::Ordinal) -or
            $relativePath.EndsWith('/', [StringComparison]::Ordinal) -or
            $relativePath.Split('/') -contains '..' -or
            -not $seenPaths.Add($relativePath)) {
            throw "Generated runtime manifest contains an unsafe or duplicate path: $relativePath"
        }
        if ($relativePath.StartsWith('Cuts/', [StringComparison]::Ordinal)) {
            $cutsCount++
        }
        elseif ($relativePath.StartsWith('Res/', [StringComparison]::Ordinal)) {
            $resCount++
        }
        else {
            throw "Generated runtime manifest contains a file outside Cuts/Res: $relativePath"
        }
        if ([IO.Path]::GetFileName($relativePath).Equals(
                'MmoneyCoder.ini',
                [StringComparison]::OrdinalIgnoreCase)) {
            throw 'Generated runtime manifest contains the credential-bearing database INI.'
        }

        $canonicalRecords.Add([ordered]@{
            path = $relativePath
            length = [long] $record.length
            sha256 = [string] $record.sha256
        })
    }

    if ([int] $manifest.fileCount -ne $records.Count -or
        [int] $manifest.cutsFileCount -ne $cutsCount -or
        [int] $manifest.resFileCount -ne $resCount -or
        $cutsCount -le 0 -or
        $resCount -ne $expectedResAssetCount) {
        throw 'Generated runtime manifest counts do not match its exact file set.'
    }

    $canonicalManifest = [ordered]@{
        schemaVersion = 1
        bundleType = 'MBN_STOCK_WEBVIEW.LegacyRuntimeBundle'
        fileCount = [int] $records.Count
        cutsFileCount = [int] $cutsCount
        resFileCount = [int] $resCount
        files = @($canonicalRecords.ToArray())
    }
    $expectedText = (
        Get-CanonicalJsonText -Value $canonicalManifest -Depth 6
    ) + [Environment]::NewLine
    if (-not (Test-ExactUtf8Text `
            -Path $ManifestPath `
            -ExpectedText $expectedText)) {
        throw (
            'Generated runtime manifest is not the exact canonical JSON contract; ' +
            'duplicate/extra/case/type/format changes are rejected.')
    }

    return [pscustomobject]@{
        Path = Get-NormalizedFullPath -Path $ManifestPath
        Sha256 = (
            Get-FileHash -LiteralPath $ManifestPath -Algorithm SHA256
        ).Hash.ToUpperInvariant()
        FileCount = [int] $records.Count
        CutsFileCount = [int] $cutsCount
        ResFileCount = [int] $resCount
        Records = @($canonicalRecords.ToArray())
        CanonicalText = $expectedText
    }
}

function Assert-InstalledRuntimeMatchesManifest {
    param(
        [Parameter(Mandatory = $true)][string] $InstalledRoot,
        [Parameter(Mandatory = $true)][string] $ManifestPath
    )

    $verifiedManifest = Get-VerifiedRuntimeManifest -ManifestPath $ManifestPath
    Assert-NoReparsePointInTree `
        -Root $InstalledRoot `
        -Description 'Installed runtime bundle'
    $expectedPaths = New-Object 'System.Collections.Generic.HashSet[string]' (
        [StringComparer]::OrdinalIgnoreCase)
    foreach ($record in @($verifiedManifest.Records)) {
        $relativePath = [string] $record.path
        if ([string]::IsNullOrWhiteSpace($relativePath) -or
            $relativePath.Contains('\') -or
            $relativePath.StartsWith('/', [StringComparison]::Ordinal) -or
            $relativePath.Split('/') -contains '..') {
            throw "Generated runtime manifest contains an unsafe path: $relativePath"
        }

        $path = Join-Path $InstalledRoot $relativePath.Replace('/', '\')
        if (-not (Test-PathIsWithin -Root $InstalledRoot -Candidate $path)) {
            throw "Generated runtime manifest escaped the installed root: $relativePath"
        }
        if (-not $expectedPaths.Add($relativePath)) {
            throw "Generated runtime manifest contains a duplicate path: $relativePath"
        }
        $item = Assert-RegularFile -Path $path -Description 'Installed runtime payload'
        if ([long] $item.Length -ne [long] $record.length -or
            -not (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.Equals(
                [string] $record.sha256,
                [StringComparison]::OrdinalIgnoreCase)) {
            throw "Installed runtime payload differs from the selected source: $relativePath"
        }
    }

    [void] $expectedPaths.Add($manifestFileName)
    $actualPaths = New-Object 'System.Collections.Generic.HashSet[string]' (
        [StringComparer]::OrdinalIgnoreCase)
    $rootPrefix = $InstalledRoot.TrimEnd('\', '/') + '\'
    foreach ($file in @(Get-ChildItem `
            -LiteralPath $InstalledRoot `
            -File `
            -Recurse `
            -Force)) {
        $relativePath = $file.FullName.Substring($rootPrefix.Length).Replace('\', '/')
        if (-not $actualPaths.Add($relativePath) -or
            -not $expectedPaths.Contains($relativePath)) {
            throw "Installed runtime bundle contains an unexpected file: $relativePath"
        }
    }
    if ($actualPaths.Count -ne $expectedPaths.Count) {
        throw 'Installed runtime bundle does not match the exact manifest file set.'
    }

    $installedManifestPath = Join-Path $InstalledRoot $manifestFileName
    $installedManifestTextMatches = Test-ExactUtf8Text `
        -Path $installedManifestPath `
        -ExpectedText ([string] $verifiedManifest.CanonicalText)
    $installedManifestHash = (
        Get-FileHash `
            -LiteralPath $installedManifestPath `
            -Algorithm SHA256
    ).Hash.ToUpperInvariant()
    if (-not $installedManifestTextMatches -or
        -not $installedManifestHash.Equals(
            [string] $verifiedManifest.Sha256,
            [StringComparison]::Ordinal)) {
        throw 'Installed runtime manifest differs from the exact generated manifest.'
    }
}

function Get-VerifiedBundleCreatorResult {
    param(
        [Parameter(Mandatory = $true)][object] $Result,
        [Parameter(Mandatory = $true)][string] $BundleOutput
    )

    Assert-ExactObjectProperties `
        -Value $Result `
        -Names @(
            'OutputDirectory',
            'ArchivePath',
            'ArchiveSha256',
            'ArchiveHashPath',
            'ManifestPath',
            'FileCount',
            'CutsFileCount',
            'ResFileCount') `
        -Description 'Runtime bundle creation result'
    foreach ($propertyName in @(
            'OutputDirectory',
            'ArchivePath',
            'ArchiveSha256',
            'ArchiveHashPath',
            'ManifestPath')) {
        if ($Result.$propertyName -isnot [string] -or
            [string]::IsNullOrWhiteSpace([string] $Result.$propertyName)) {
            throw "Runtime bundle creation result property '$propertyName' has an invalid type."
        }
    }
    foreach ($propertyName in @('FileCount', 'CutsFileCount', 'ResFileCount')) {
        if ($Result.$propertyName -isnot [int32]) {
            throw "Runtime bundle creation result property '$propertyName' must be Int32."
        }
    }
    if ([string] $Result.ArchiveSha256 -cnotmatch '^[0-9A-F]{64}$') {
        throw 'Runtime bundle creation result has a non-canonical archive SHA-256.'
    }

    $outputRoot = Get-NormalizedFullPath -Path $BundleOutput
    $archivePath = Get-NormalizedFullPath -Path ([string] $Result.ArchivePath)
    $hashPath = Get-NormalizedFullPath -Path ([string] $Result.ArchiveHashPath)
    $manifestPath = Get-NormalizedFullPath -Path ([string] $Result.ManifestPath)
    if (-not (Test-SamePath `
            -Left ([string] $Result.OutputDirectory) `
            -Right $outputRoot) -or
        -not (Test-SamePath `
            -Left $archivePath `
            -Right (Join-Path $outputRoot $archiveFileName)) -or
        -not (Test-SamePath `
            -Left $hashPath `
            -Right (Join-Path $outputRoot $archiveHashFileName)) -or
        -not (Test-SamePath `
            -Left $manifestPath `
            -Right (Join-Path $outputRoot $manifestFileName)) -or
        -not (Test-PathIsWithin -Root $outputRoot -Candidate $archivePath) -or
        -not (Test-PathIsWithin -Root $outputRoot -Candidate $hashPath) -or
        -not (Test-PathIsWithin -Root $outputRoot -Candidate $manifestPath)) {
        throw 'Runtime bundle creation result paths escaped or changed the exact output contract.'
    }
    Assert-NoReparsePointInTree `
        -Root $outputRoot `
        -Description 'Created runtime bundle output'
    [void](Assert-RegularFile -Path $archivePath -Description 'Created runtime archive')
    [void](Assert-RegularFile -Path $hashPath -Description 'Created runtime archive hash')
    $actualArchiveHash = (
        Get-FileHash -LiteralPath $archivePath -Algorithm SHA256
    ).Hash.ToUpperInvariant()
    if (-not $actualArchiveHash.Equals(
            [string] $Result.ArchiveSha256,
            [StringComparison]::Ordinal)) {
        throw 'Runtime bundle creation result archive SHA-256 does not match its file.'
    }
    $expectedHashText = (
        [string] $Result.ArchiveSha256 + '  ' + $archiveFileName +
        [Environment]::NewLine)
    if (-not (Test-ExactUtf8Text `
            -Path $hashPath `
            -ExpectedText $expectedHashText)) {
        throw 'Runtime bundle creation result hash sidecar is not canonical.'
    }

    $manifest = Get-VerifiedRuntimeManifest -ManifestPath $manifestPath
    if ([int] $Result.FileCount -ne [int] $manifest.FileCount -or
        [int] $Result.CutsFileCount -ne [int] $manifest.CutsFileCount -or
        [int] $Result.ResFileCount -ne [int] $manifest.ResFileCount -or
        [int] $Result.FileCount -ne
            ([int] $Result.CutsFileCount + [int] $Result.ResFileCount)) {
        throw 'Runtime bundle creation result counts do not match its manifest.'
    }

    return [pscustomobject]@{
        Result = $Result
        OutputDirectory = $outputRoot
        ArchivePath = $archivePath
        ArchiveSha256 = $actualArchiveHash
        Manifest = $manifest
    }
}

function Get-VerifiedBundleInitializerResult {
    param(
        [Parameter(Mandatory = $true)][object] $Result,
        [Parameter(Mandatory = $true)][pscustomobject] $VerifiedBundle,
        [Parameter(Mandatory = $true)][string] $ShadowRepositoryRoot,
        [Parameter(Mandatory = $true)][string] $RuntimeStorageRoot
    )

    Assert-ExactObjectProperties `
        -Value $Result `
        -Names @(
            'InstalledRoot',
            'ArchivePath',
            'ArchiveSha256',
            'ManifestSha256',
            'FileCount',
            'CutsFileCount',
            'ResFileCount',
            'LocalBuildPropsPath') `
        -Description 'Runtime bundle initialization result'
    foreach ($propertyName in @(
            'InstalledRoot',
            'ArchivePath',
            'ArchiveSha256',
            'ManifestSha256',
            'LocalBuildPropsPath')) {
        if ($Result.$propertyName -isnot [string] -or
            [string]::IsNullOrWhiteSpace([string] $Result.$propertyName)) {
            throw "Runtime bundle initialization result property '$propertyName' has an invalid type."
        }
    }
    foreach ($propertyName in @('FileCount', 'CutsFileCount', 'ResFileCount')) {
        if ($Result.$propertyName -isnot [int32]) {
            throw "Runtime bundle initialization result property '$propertyName' must be Int32."
        }
    }
    if ([string] $Result.ArchiveSha256 -cnotmatch '^[0-9A-F]{64}$' -or
        [string] $Result.ManifestSha256 -cnotmatch '^[0-9A-F]{64}$') {
        throw 'Runtime bundle initialization result contains a non-canonical SHA-256.'
    }

    $installedRoot = Get-NormalizedFullPath -Path ([string] $Result.InstalledRoot)
    $localPropsPath = Get-NormalizedFullPath -Path (
        [string] $Result.LocalBuildPropsPath)
    $archivePathMatches = Test-SamePath `
        -Left ([string] $Result.ArchivePath) `
        -Right ([string] $VerifiedBundle.ArchivePath)
    if (-not $archivePathMatches -or
        -not ([string] $Result.ArchiveSha256).Equals(
            [string] $VerifiedBundle.ArchiveSha256,
            [StringComparison]::Ordinal) -or
        -not ([string] $Result.ManifestSha256).Equals(
            [string] $VerifiedBundle.Manifest.Sha256,
            [StringComparison]::Ordinal) -or
        -not (Test-PathIsWithin `
            -Root $RuntimeStorageRoot `
            -Candidate $installedRoot) -or
        -not ([IO.Path]::GetDirectoryName($installedRoot)).Equals(
            (Get-NormalizedFullPath -Path $RuntimeStorageRoot).TrimEnd('\', '/'),
            [StringComparison]::OrdinalIgnoreCase) -or
        -not ([IO.Path]::GetFileName($installedRoot)).Equals(
            [string] $VerifiedBundle.ArchiveSha256,
            [StringComparison]::Ordinal) -or
        -not (Test-SamePath `
            -Left $localPropsPath `
            -Right (Join-Path $ShadowRepositoryRoot 'Directory.Build.local.props')) -or
        -not (Test-PathIsWithin `
            -Root $ShadowRepositoryRoot `
            -Candidate $localPropsPath)) {
        throw 'Runtime bundle initialization result paths or hashes violate containment.'
    }
    if ([int] $Result.FileCount -ne [int] $VerifiedBundle.Manifest.FileCount -or
        [int] $Result.CutsFileCount -ne [int] $VerifiedBundle.Manifest.CutsFileCount -or
        [int] $Result.ResFileCount -ne [int] $VerifiedBundle.Manifest.ResFileCount) {
        throw 'Runtime bundle initialization result counts differ from the verified manifest.'
    }

    Assert-NoReparsePointInTree `
        -Root $installedRoot `
        -Description 'Initialized runtime bundle'
    [void](Assert-RegularFile `
        -Path $localPropsPath `
        -Description 'Generated local runtime binding')
    Assert-InstalledRuntimeMatchesManifest `
        -InstalledRoot $installedRoot `
        -ManifestPath ([string] $VerifiedBundle.Manifest.Path)
    $shadowBinding = Get-ExistingRuntimeBinding `
        -RepositoryRoot $ShadowRepositoryRoot `
        -RuntimeStorageRoot $RuntimeStorageRoot
    if ($null -eq $shadowBinding -or
        -not (Test-SamePath `
            -Left $shadowBinding.InstalledRoot `
            -Right $installedRoot) -or
        -not $shadowBinding.ManifestSha256.Equals(
            [string] $VerifiedBundle.Manifest.Sha256,
            [StringComparison]::Ordinal)) {
        throw 'Generated local runtime binding does not match the initialized bundle.'
    }

    return [pscustomobject]@{
        InstalledRoot = $installedRoot
        ArchiveSha256 = [string] $Result.ArchiveSha256
        ManifestSha256 = [string] $Result.ManifestSha256
        FileCount = [int] $Result.FileCount
        CutsFileCount = [int] $Result.CutsFileCount
        ResFileCount = [int] $Result.ResFileCount
        LocalBuildPropsPath = $localPropsPath
    }
}

function Get-VerifiedK3DRegistration {
    param([Parameter(Mandatory = $true)][object[]] $Reports)

    if ($Reports.Count -ne 1) {
        throw 'The read-only K3D x64 registration inspection did not return one result.'
    }
    $registration = $Reports[0]
    Assert-ExactObjectProperties `
        -Value $registration `
        -Names @(
            'Status',
            'RegistryHive',
            'RegistryView',
            'CurrentUserOverrides',
            'TypeLibId',
            'TypeLibVersion',
            'TypeLibTarget',
            'TypeLibPath',
            'TypeLibMachine',
            'Classes',
            'PowerShellProcessIs64Bit',
            'ComActivated') `
        -Description 'K3D registration inspection result'
    foreach ($propertyName in @(
            'Status',
            'RegistryHive',
            'RegistryView',
            'CurrentUserOverrides',
            'TypeLibId',
            'TypeLibVersion',
            'TypeLibTarget',
            'TypeLibPath',
            'TypeLibMachine')) {
        if ($registration.$propertyName -isnot [string]) {
            throw "K3D registration property '$propertyName' must be a string."
        }
    }
    if ($registration.PowerShellProcessIs64Bit -isnot [bool] -or
        $registration.ComActivated -isnot [bool] -or
        [string] $registration.Status -cne 'Valid' -or
        [string] $registration.RegistryHive -cne 'HKLM' -or
        [string] $registration.RegistryView -cne 'Registry64' -or
        [string] $registration.CurrentUserOverrides -cne 'None' -or
        [string] $registration.TypeLibId -cne
            '{2B7F2D64-3A8D-401C-BE73-5C0747BA342C}' -or
        [string] $registration.TypeLibVersion -cne '1.0' -or
        [string] $registration.TypeLibTarget -cne 'win64' -or
        [string] $registration.TypeLibMachine -cne 'AMD64' -or
        $registration.PowerShellProcessIs64Bit -ne $true -or
        $registration.ComActivated -ne $false) {
        throw 'The read-only K3D x64 registration inspection result is invalid.'
    }

    $expectedClasses = [ordered]@{
        KAEngine = [ordered]@{
            ClassId = '{D756CDBE-AA31-42B2-9CC7-018753CA61BF}'
            ProgId = 'K3DAsyncEngine.KAEngine.1'
        }
        KAEventHandler = [ordered]@{
            ClassId = '{39828C77-EFF0-4E59-979B-8673C028C718}'
            ProgId = 'K3DAsyncEngine.KAEventHandler.1'
        }
    }
    $classes = @($registration.Classes)
    if ($classes.Count -ne 2) {
        throw 'K3D registration inspection must return exactly two classes.'
    }
    $seenClassNames = New-Object 'System.Collections.Generic.HashSet[string]' (
        [StringComparer]::Ordinal)
    foreach ($classReport in $classes) {
        Assert-ExactObjectProperties `
            -Value $classReport `
            -Names @(
                'Name',
                'ClassId',
                'ProgId',
                'ServerPath',
                'ServerMachine',
                'ThreadingModel',
                'ReciprocalMapping') `
            -Description 'K3D class registration result'
        foreach ($propertyName in @(
                'Name',
                'ClassId',
                'ProgId',
                'ServerPath',
                'ServerMachine',
                'ThreadingModel')) {
            if ($classReport.$propertyName -isnot [string]) {
                throw "K3D class registration property '$propertyName' must be a string."
            }
        }
        if ($classReport.ReciprocalMapping -isnot [bool]) {
            throw 'K3D class ReciprocalMapping must be Boolean.'
        }
        $name = [string] $classReport.Name
        if (-not $expectedClasses.Contains($name) -or
            -not $seenClassNames.Add($name) -or
            [string] $classReport.ClassId -cne
                [string] $expectedClasses[$name].ClassId -or
            [string] $classReport.ProgId -cne
                [string] $expectedClasses[$name].ProgId -or
            [string] $classReport.ServerMachine -cne 'AMD64' -or
            [string] $classReport.ThreadingModel -cne 'Apartment' -or
            $classReport.ReciprocalMapping -ne $true) {
            throw 'The read-only K3D x64 class registration shape is invalid.'
        }
    }

    return $registration
}

function Set-RuntimeBindingAtomically {
    param(
        [Parameter(Mandatory = $true)][string] $GeneratedPropsPath,
        [Parameter(Mandatory = $true)][string] $RepositoryRoot,
        [switch] $Replace
    )

    [void](Assert-RegularFile `
        -Path $GeneratedPropsPath `
        -Description 'Generated local runtime binding')
    $generatedHash = (
        Get-FileHash -LiteralPath $GeneratedPropsPath -Algorithm SHA256
    ).Hash.ToUpperInvariant()
    $destination = Join-Path $RepositoryRoot 'Directory.Build.local.props'
    $temporary = Join-Path $RepositoryRoot (
        '.Directory.Build.local.props.' + [Guid]::NewGuid().ToString('N') + '.tmp')
    $backup = Join-Path $RepositoryRoot (
        '.Directory.Build.local.props.' + [Guid]::NewGuid().ToString('N') + '.bak')
    $preserveBackup = $false
    try {
        [IO.File]::Copy($GeneratedPropsPath, $temporary, $false)
        if (Test-Path -LiteralPath $destination) {
            if (-not $Replace) {
                throw 'Refusing to replace the existing runtime binding.'
            }
            [IO.File]::Replace($temporary, $destination, $backup)
            $temporary = $null
            try {
                [xml] $verification = Get-Content `
                    -LiteralPath $destination `
                    -Raw `
                    -Encoding UTF8
                if ($null -eq $verification.Project.PropertyGroup.LegacyRuntimeSourceRoot -or
                    [string] $verification.Project.PropertyGroup.LegacyRuntimeAssetsMode -cne
                        'Required' -or
                    -not (Get-FileHash `
                        -LiteralPath $destination `
                        -Algorithm SHA256).Hash.Equals(
                            $generatedHash,
                            [StringComparison]::Ordinal)) {
                    throw 'The atomically replaced runtime binding failed verification.'
                }
            }
            catch {
                try {
                    [IO.File]::Replace($backup, $destination, $null)
                    $backup = $null
                }
                catch {
                    $preserveBackup = $true
                    throw
                }
                throw
            }
        }
        else {
            [IO.File]::Move($temporary, $destination)
            $temporary = $null
            if (-not (Get-FileHash `
                    -LiteralPath $destination `
                    -Algorithm SHA256).Hash.Equals(
                        $generatedHash,
                        [StringComparison]::Ordinal)) {
                [IO.File]::Delete($destination)
                throw 'The atomically installed runtime binding failed verification.'
            }
        }
    }
    finally {
        if (-not [string]::IsNullOrWhiteSpace($temporary) -and
            (Test-Path -LiteralPath $temporary)) {
            [IO.File]::Delete($temporary)
        }
        if (-not $preserveBackup -and
            -not [string]::IsNullOrWhiteSpace($backup) -and
            (Test-Path -LiteralPath $backup)) {
            [IO.File]::Delete($backup)
        }
    }
}

function New-DirectRuntimeBindingFile {
    param(
        [Parameter(Mandatory = $true)][string] $RuntimeRoot,
        [Parameter(Mandatory = $true)][string] $OutputDirectory
    )

    $path = Join-Path $OutputDirectory 'Directory.Build.local.props'
    $settings = New-Object Xml.XmlWriterSettings
    $settings.Encoding = New-Object Text.UTF8Encoding($false)
    $settings.Indent = $true
    $settings.NewLineChars = [Environment]::NewLine
    $settings.NewLineHandling = [Xml.NewLineHandling]::Replace
    $writer = [Xml.XmlWriter]::Create($path, $settings)
    try {
        $writer.WriteStartDocument()
        $writer.WriteStartElement('Project')
        $writer.WriteStartElement('PropertyGroup')
        $writer.WriteElementString(
            'LegacyRuntimeSourceRoot',
            (Get-NormalizedFullPath -Path $RuntimeRoot))
        $writer.WriteElementString('LegacyRuntimeAssetsMode', 'Required')
        $writer.WriteEndElement()
        $writer.WriteEndElement()
        $writer.WriteEndDocument()
    }
    finally {
        $writer.Dispose()
    }

    [void](Assert-RegularFile `
        -Path $path `
        -Description 'Generated direct runtime binding')
    return $path
}

function Get-CanonicalJsonText {
    param(
        [Parameter(Mandatory = $true)][object] $Value,
        [Parameter(Mandatory = $true)][int] $Depth
    )

    return ($Value | ConvertTo-Json -Depth $Depth)
}

function Get-RuntimeFoldersPreflight {
    param(
        [Parameter(Mandatory = $true)][string] $LocalAppDataRoot,
        [Parameter(Mandatory = $true)][string] $CutsDirectory,
        [Parameter(Mandatory = $true)][string] $ResourceDirectory,
        [switch] $AllowReplacement
    )

    $configurationRoot = Join-Path $LocalAppDataRoot 'MBN_STOCK_WEBVIEW\Config'
    $path = Join-Path $configurationRoot 'runtime-folders.local.json'
    $contract = [ordered]@{
        schemaVersion = 2
        sceneDirectory = $CutsDirectory
        resourceDirectory = $ResourceDirectory
        backgroundDirectory = $null
        navigationExpanded = $true
        colorTheme = 'system'
        viewMode = 'automatic'
        startWorkspace = 'stockCut'
    }
    $json = Get-CanonicalJsonText -Value $contract -Depth 3
    $exists = Test-Path -LiteralPath $path
    $matches = $exists -and
        (Test-ExistingJsonContract -Path $path -ExpectedJson $json)
    if ($exists -and -not $matches -and -not $AllowReplacement) {
        throw (
            'The stored Cuts/Res paths differ from the selected folders. ' +
            'Re-run with -ReplaceRuntimeBinding.')
    }

    return [pscustomobject]@{
        NeedsWrite = [bool] (-not $matches)
        NeedsReplace = [bool] $exists
        Path = $path
        Json = $json
    }
}

function Test-ExistingJsonContract {
    param(
        [Parameter(Mandatory = $true)][string] $Path,
        [Parameter(Mandatory = $true)][string] $ExpectedJson
    )

    if (-not (Test-Path -LiteralPath $Path)) {
        return $false
    }
    try {
        $item = Assert-RegularFile -Path $Path -Description 'Development Live configuration'
        if ($item.Length -le 0 -or $item.Length -gt 1MB -or
            -not (Test-UserOnlyFileAcl -Path $Path)) {
            return $false
        }
        # Exact UTF-8 bytes are the contract. This rejects duplicate/extra keys,
        # case changes, type coercion, reordered properties, alternate whitespace,
        # and a BOM instead of normalizing them through ConvertFrom-Json.
        return Test-ExactUtf8Text -Path $Path -ExpectedText $ExpectedJson
    }
    catch {
        return $false
    }
}

function Assert-ExistingJsonContract {
    param(
        [Parameter(Mandatory = $true)][string] $Path,
        [Parameter(Mandatory = $true)][string] $ExpectedJson,
        [Parameter(Mandatory = $true)][string] $Description
    )

    if (-not (Test-ExistingJsonContract `
            -Path $Path `
            -ExpectedJson $ExpectedJson)) {
        throw "$Description failed its exact JSON/ACL verification."
    }
}

function Get-DevelopmentLivePreflight {
    param(
        [Parameter(Mandatory = $true)][string] $ConfigurationRoot,
        [Parameter(Mandatory = $true)][string] $HostName,
        [Parameter(Mandatory = $true)][int] $Port,
        [Parameter(Mandatory = $true)][string] $ApprovedNativeHash,
        [Parameter(Mandatory = $true)][string] $ApprovedInteropHash,
        [AllowNull()][Nullable[int]] $Channel,
        [switch] $AllowReplacement
    )

    $sceneAllowlist = @(
        '5001', '5006', '5011', '5016', '50160', '5023', '5024', '5025',
        '5026', '5029', '5032', '5037', '5068', '5070', '5072', '5074',
        '5076', '5077', '5078', '5079', '5080', '5081', '5082', '5083',
        '5084', '5085', '5086', '50860', '5087', '5088', '6001', '6067',
        '8001', '8002', '8003', '8018', '8032', '8035', '8040', '8046',
        '8051', '8056', '8061', '8067', 'N5001')
    $playout = [ordered]@{
        mode = 'Live'
        host = $HostName
        port = $Port
        tcpMode = 1
        clientPort = 0
        sceneDirectory = $null
        outputChannel = $Channel
        layoutIndex = 10
        legacySceneFadeDuration = 6
        legacySceneBackgroundKind = 'None'
        legacySceneBackgroundAssetPath = $null
        legacySceneBackgroundVideoLoopCount = 2004
        legacySceneBackgroundVideoLoopInfinite = $true
        legacyBackgroundDirectory = $null
        testProcessWindowTitlePattern = $null
        testSceneAllowlist = $sceneAllowlist
        trustedLiveOutputEnabled = $true
        queueCapacity = 64
        connectTimeoutMilliseconds = 5000
        operationTimeoutMilliseconds = 5000
        disconnectTimeoutMilliseconds = 3000
        processPollIntervalMilliseconds = 1000
        reconnectDelayMilliseconds = 1000
        maximumReconnectAttempts = 0
        reconnectEnabled = $false
        maximumAutomaticRefreshesPerTakeIn = 0
    }
    $authorization = [ordered]@{
        schemaVersion = 1
        mode = 'Live'
        authorization = 'I_AUTHORIZE_LIVE_PROGRAM_OUTPUT_FOR_THIS_LAUNCH'
        nativeSha256 = $ApprovedNativeHash.ToUpperInvariant()
        interopSha256 = $ApprovedInteropHash.ToUpperInvariant()
    }
    $playoutJson = Get-CanonicalJsonText -Value $playout -Depth 6
    $authorizationJson = Get-CanonicalJsonText -Value $authorization -Depth 3

    $playoutPath = Join-Path $ConfigurationRoot 'playout.local.json'
    $authorizationPath = Join-Path $ConfigurationRoot (
        'playout.development-live.local.json')
    Assert-NoReparsePointInExistingAncestry `
        -Path $ConfigurationRoot `
        -Description 'Development Live configuration directory'
    $playoutExists = Test-Path -LiteralPath $playoutPath
    $authorizationExists = Test-Path -LiteralPath $authorizationPath
    if ($playoutExists) {
        [void](Assert-RegularFile `
            -Path $playoutPath `
            -Description 'Development Live base configuration')
    }
    if ($authorizationExists) {
        [void](Assert-RegularFile `
            -Path $authorizationPath `
            -Description 'Development Live authorization')
    }
    $playoutMatches = -not $playoutExists -or
        (Test-ExistingJsonContract `
            -Path $playoutPath `
            -ExpectedJson $playoutJson)
    $authorizationMatches = -not $authorizationExists -or
        (Test-ExistingJsonContract `
            -Path $authorizationPath `
            -ExpectedJson $authorizationJson)
    if ((-not $playoutMatches -or -not $authorizationMatches) -and
        -not $AllowReplacement) {
        # A mismatched file is never left usable merely because replacement was
        # refused. The caller asked to configure Live, so failure is fail-closed.
        Remove-DevelopmentLiveAuthorizationFailClosed -Path $authorizationPath
        throw (
            'Existing Development Live files differ from the exact requested ' +
            'endpoint/hash contract. Re-run with -ReplaceLiveConfig.')
    }

    return [pscustomobject]@{
        # Every requested setup invalidates any old one-launch authorization and
        # issues a new file only after runtime/DB/build/K3D verification succeeds.
        NeedsWrite = $true
        NeedsForce = [bool] ($playoutExists -or $authorizationExists)
        PlayoutPath = $playoutPath
        AuthorizationPath = $authorizationPath
        PlayoutJson = $playoutJson
        AuthorizationJson = $authorizationJson
    }
}

function Get-K3DPinPreflight {
    param(
        [Parameter(Mandatory = $true)][string] $ConfigurationRoot,
        [Parameter(Mandatory = $true)][string] $ApprovedNativeHash,
        [Parameter(Mandatory = $true)][string] $ApprovedInteropHash,
        [switch] $AllowReplacement
    )

    $pin = [ordered]@{
        schemaVersion = 1
        nativeSha256 = $ApprovedNativeHash.ToUpperInvariant()
        interopSha256 = $ApprovedInteropHash.ToUpperInvariant()
    }
    $pinJson = Get-CanonicalJsonText -Value $pin -Depth 3
    $pinPath = Join-Path $ConfigurationRoot 'k3d-pins.local.json'
    Assert-NoReparsePointInExistingAncestry `
        -Path $ConfigurationRoot `
        -Description 'K3D pin configuration directory'
    $pinExists = Test-Path -LiteralPath $pinPath
    if ($pinExists) {
        [void](Assert-RegularFile `
            -Path $pinPath `
            -Description 'K3D persistent pin')
    }

    $pinMatches = $pinExists -and
        (Test-ExistingJsonContract -Path $pinPath -ExpectedJson $pinJson)
    if ($pinExists -and -not $pinMatches -and -not $AllowReplacement) {
        throw (
            'The installed K3D files differ from the persistent local pin. ' +
            'Automatic pinning cannot approve a changed DLL. Independently ' +
            'verify the vendor files before using -ReplaceK3DPin with supplied hashes.')
    }

    return [pscustomobject]@{
        NeedsWrite = [bool] (-not $pinExists -or -not $pinMatches)
        NeedsReplace = [bool] $pinExists
        Path = $pinPath
        Json = $pinJson
    }
}

function Write-ProtectedUtf8TextAtomically {
    param(
        [Parameter(Mandatory = $true)][string] $Path,
        [Parameter(Mandatory = $true)][string] $ExpectedText,
        [Parameter(Mandatory = $true)][string] $Description,
        [switch] $Replace
    )

    $destination = Get-NormalizedFullPath -Path $Path
    $directory = [IO.Path]::GetDirectoryName($destination)
    [IO.Directory]::CreateDirectory($directory) | Out-Null
    Assert-NoReparsePointInExistingAncestry `
        -Path $directory `
        -Description "$Description directory"
    Set-UserOnlyDirectoryAcl -Path $directory

    $temporaryPath = Join-Path $directory (
        '.' + [IO.Path]::GetFileName($destination) + '.' +
        [Guid]::NewGuid().ToString('N') + '.tmp')
    $rollbackSnapshot = New-FileRollbackSnapshot `
        -Path $destination `
        -BackupDirectory $directory `
        -Description $Description
    $mutationStarted = $false
    $preserveRollbackBackup = $false
    try {
        [IO.File]::WriteAllText(
            $temporaryPath,
            $ExpectedText,
            (New-Object Text.UTF8Encoding($false)))
        [IO.File]::SetAccessControl($temporaryPath, (Get-UserOnlyAcl))
        if (-not (Test-ExactUtf8Text `
                -Path $temporaryPath `
                -ExpectedText $ExpectedText) -or
            -not (Test-UserOnlyFileAcl -Path $temporaryPath)) {
            throw "$Description temporary file failed exact JSON/ACL verification."
        }

        if (Test-Path -LiteralPath $destination) {
            if (-not $Replace) {
                throw "Refusing to replace the existing $Description."
            }
            [void](Assert-RegularFile -Path $destination -Description $Description)
            $mutationStarted = $true
            [IO.File]::Replace($temporaryPath, $destination, $null)
            $temporaryPath = $null
        }
        else {
            $mutationStarted = $true
            [IO.File]::Move($temporaryPath, $destination)
            $temporaryPath = $null
        }

        [IO.File]::SetAccessControl($destination, (Get-UserOnlyAcl))
        if (-not (Test-ExactUtf8Text `
                -Path $destination `
                -ExpectedText $ExpectedText) -or
            -not (Test-UserOnlyFileAcl -Path $destination)) {
            throw "The atomically installed $Description failed exact JSON/ACL verification."
        }
    }
    catch {
        $writeError = $_
        if ($mutationStarted) {
            try {
                Restore-FileRollbackSnapshot `
                    -Snapshot $rollbackSnapshot `
                    -Description $Description
            }
            catch {
                $preserveRollbackBackup = $true
                throw [InvalidOperationException]::new(
                    (
                        "$Description write failed and its previous trust " +
                        'anchor could not be restored: ' + $_.Exception.Message
                    ),
                    $writeError.Exception)
            }
        }
        throw $writeError
    }
    finally {
        if (-not [string]::IsNullOrWhiteSpace($temporaryPath) -and
            (Test-Path -LiteralPath $temporaryPath)) {
            [IO.File]::Delete($temporaryPath)
        }
        if (-not $preserveRollbackBackup -and
            $null -ne $rollbackSnapshot -and
            -not [string]::IsNullOrWhiteSpace(
                [string] $rollbackSnapshot.BackupPath) -and
            (Test-Path -LiteralPath ([string] $rollbackSnapshot.BackupPath))) {
            [IO.File]::Delete([string] $rollbackSnapshot.BackupPath)
        }
    }
}

function Copy-DatabaseProfileAtomically {
    param(
        [Parameter(Mandatory = $true)][string] $Source,
        [Parameter(Mandatory = $true)][string] $Destination,
        [Parameter(Mandatory = $true)][string] $ExpectedHash,
        [switch] $Replace
    )

    $directory = [IO.Path]::GetDirectoryName($Destination)
    [IO.Directory]::CreateDirectory($directory) | Out-Null
    Assert-NoReparsePointInExistingAncestry `
        -Path $directory `
        -Description 'Local database profile directory'
    Set-UserOnlyDirectoryAcl -Path $directory

    $temporaryPath = Join-Path $directory (
        '.MmoneyCoder.' + [Guid]::NewGuid().ToString('N') + '.tmp')
    $backupPath = Join-Path $directory (
        '.MmoneyCoder.' + [Guid]::NewGuid().ToString('N') + '.bak')
    $preserveBackup = $false
    $previousDestinationLength = [long] 0
    $previousDestinationHash = $null
    try {
        [IO.File]::Copy($Source, $temporaryPath, $false)
        $temporaryItem = Assert-RegularFile `
            -Path $temporaryPath `
            -Description 'Temporary database profile'
        if ($temporaryItem.Length -le 0 -or
            $temporaryItem.Length -gt $maximumDatabaseIniLength -or
            -not (Get-FileHash -LiteralPath $temporaryPath -Algorithm SHA256).Hash.Equals(
                $ExpectedHash,
                [StringComparison]::Ordinal)) {
            throw 'Database profile changed while it was copied.'
        }
        [IO.File]::SetAccessControl($temporaryPath, (Get-UserOnlyAcl))

        if (Test-Path -LiteralPath $Destination) {
            if (-not $Replace) {
                throw 'Refusing to replace the existing database profile.'
            }
            $previousDestinationItem = Assert-RegularFile `
                -Path $Destination `
                -Description 'Previous installed database profile'
            $previousDestinationLength = [long] $previousDestinationItem.Length
            $previousDestinationHash = (
                Get-FileHash -LiteralPath $Destination -Algorithm SHA256
            ).Hash.ToUpperInvariant()
            [IO.File]::Replace($temporaryPath, $Destination, $backupPath)
            $temporaryPath = $null
        }
        else {
            [IO.File]::Move($temporaryPath, $Destination)
            $temporaryPath = $null
        }

        try {
            if (Test-Path -LiteralPath $backupPath) {
                # File.Replace creates a second copy of the old credential file.
                # Protect and hash-check that copy in the immediately following
                # statements before continuing with the new destination.
                [IO.File]::SetAccessControl($backupPath, (Get-UserOnlyAcl))
                $backupItem = Assert-RegularFile `
                    -Path $backupPath `
                    -Description 'Previous database profile rollback backup'
                $backupHash = (
                    Get-FileHash -LiteralPath $backupPath -Algorithm SHA256
                ).Hash.ToUpperInvariant()
                if ($backupItem.Length -ne $previousDestinationLength -or
                    -not $backupHash.Equals(
                        $previousDestinationHash,
                        [StringComparison]::Ordinal) -or
                    -not (Test-UserOnlyFileAcl -Path $backupPath)) {
                    throw 'Previous database profile backup failed immediate ACL/hash verification.'
                }
            }
            [IO.File]::SetAccessControl($Destination, (Get-UserOnlyAcl))
            $installedItem = Assert-RegularFile `
                -Path $Destination `
                -Description 'Installed database profile'
            if ($installedItem.Length -le 0 -or
                $installedItem.Length -gt $maximumDatabaseIniLength -or
                -not (Get-FileHash `
                    -LiteralPath $Destination `
                    -Algorithm SHA256).Hash.Equals(
                        $ExpectedHash,
                        [StringComparison]::Ordinal) -or
                -not (Test-UserOnlyFileAcl -Path $Destination)) {
                throw 'The atomically installed database profile failed verification.'
            }
        }
        catch {
            if (Test-Path -LiteralPath $backupPath) {
                try {
                    [IO.File]::Replace($backupPath, $Destination, $null)
                    $backupPath = $null
                }
                catch {
                    $preserveBackup = $true
                    throw
                }
            }
            elseif (Test-Path -LiteralPath $Destination) {
                [IO.File]::Delete($Destination)
            }
            throw
        }
    }
    finally {
        if (-not [string]::IsNullOrWhiteSpace($temporaryPath) -and
            (Test-Path -LiteralPath $temporaryPath)) {
            [IO.File]::Delete($temporaryPath)
        }
        if (-not $preserveBackup -and
            -not [string]::IsNullOrWhiteSpace($backupPath) -and
            (Test-Path -LiteralPath $backupPath)) {
            [IO.File]::Delete($backupPath)
        }
    }
}

function Assert-DebugRuntimePayloadMatchesManifest {
    param(
        [Parameter(Mandatory = $true)][string] $TargetDirectory,
        [Parameter(Mandatory = $true)][string] $ManifestPath,
        [Parameter(Mandatory = $true)][string] $ExpectedManifestSha256
    )

    $targetRoot = Get-NormalizedFullPath -Path $TargetDirectory
    Assert-NoReparsePointInTree `
        -Root $targetRoot `
        -Description 'Debug x64 output'
    $manifest = Get-VerifiedRuntimeManifest -ManifestPath $ManifestPath
    if (-not ([string] $manifest.Sha256).Equals(
            $ExpectedManifestSha256,
            [StringComparison]::Ordinal)) {
        throw 'Debug x64 verification received an unexpected runtime manifest.'
    }

    $expectedPaths = New-Object 'System.Collections.Generic.HashSet[string]' (
        [StringComparer]::OrdinalIgnoreCase)
    foreach ($record in @($manifest.Records)) {
        $relativePath = [string] $record.path
        if (-not $expectedPaths.Add($relativePath)) {
            throw "Debug x64 manifest contains a duplicate path: $relativePath"
        }
        $outputPath = Get-NormalizedFullPath -Path (
            Join-Path $targetRoot $relativePath.Replace('/', '\'))
        if (-not (Test-PathIsWithin -Root $targetRoot -Candidate $outputPath)) {
            throw "Debug x64 manifest path escaped the output: $relativePath"
        }
        $outputItem = Assert-RegularFile `
            -Path $outputPath `
            -Description 'Debug x64 runtime payload'
        $outputHash = (
            Get-FileHash -LiteralPath $outputPath -Algorithm SHA256
        ).Hash.ToUpperInvariant()
        if ($outputItem.Length -ne [long] $record.length -or
            -not $outputHash.Equals(
                [string] $record.sha256,
                [StringComparison]::Ordinal)) {
            throw "Debug x64 runtime payload differs from the manifest: $relativePath"
        }
    }

    $actualPaths = New-Object 'System.Collections.Generic.HashSet[string]' (
        [StringComparer]::OrdinalIgnoreCase)
    $targetPrefix = $targetRoot.TrimEnd('\', '/') + '\'
    foreach ($assetRootName in @('Cuts', 'Res')) {
        $assetRoot = Join-Path $targetRoot $assetRootName
        if (-not (Test-Path -LiteralPath $assetRoot -PathType Container) -or
            -not (Test-SamePath `
                -Left ([IO.Path]::GetDirectoryName($assetRoot)) `
                -Right $targetRoot)) {
            throw "Debug x64 $assetRootName output directory is missing."
        }
        Assert-NoReparsePointInTree `
            -Root $assetRoot `
            -Description "Debug x64 $assetRootName output"
        foreach ($file in @(Get-ChildItem `
                -LiteralPath $assetRoot `
                -File `
                -Recurse `
                -Force)) {
            $relativePath = $file.FullName.Substring(
                $targetPrefix.Length).Replace('\', '/')
            if (-not $actualPaths.Add($relativePath) -or
                -not $expectedPaths.Contains($relativePath)) {
                throw "Debug x64 runtime output contains an unexpected file: $relativePath"
            }
        }
    }
    if ($actualPaths.Count -ne $expectedPaths.Count) {
        throw 'Debug x64 runtime output does not match the exact manifest file set.'
    }

    return [pscustomobject]@{
        CutsFileCount = [int] $manifest.CutsFileCount
        ResFileCount = [int] $manifest.ResFileCount
    }
}

function Invoke-DebugBuildAndVerify {
    param(
        [Parameter(Mandatory = $true)][string] $DotNetPath,
        [Parameter(Mandatory = $true)][string] $ProjectPath,
        [Parameter(Mandatory = $true)][string] $ExpectedRuntimeRoot
    )

    $runtimeRootProperty = "-p:LegacyRuntimeSourceRoot=$ExpectedRuntimeRoot"
    & $DotNetPath build $ProjectPath `
        -c Debug `
        '--no-restore' `
        '-p:Platform=x64' `
        '-p:LegacyRuntimeAssetsMode=Required' `
        $runtimeRootProperty |
        ForEach-Object { Write-Host $_ }
    if ($LASTEXITCODE -ne 0) {
        throw "Debug x64 build failed with exit code $LASTEXITCODE."
    }

    $propertyText = (& $DotNetPath msbuild $ProjectPath `
        '-p:Configuration=Debug' `
        '-p:Platform=x64' `
        '-p:LegacyRuntimeAssetsMode=Required' `
        $runtimeRootProperty `
        '-getProperty:TargetDir' `
        '-getProperty:LegacyRuntimeAssetsEnabled' `
        '-getProperty:LegacyRuntimeAssetsMode' `
        '-getProperty:LegacyRuntimeSourceRoot' | Out-String)
    if ($LASTEXITCODE -ne 0) {
        throw "Could not query Debug x64 build properties (exit code $LASTEXITCODE)."
    }
    $propertyEnvelope = $propertyText | ConvertFrom-Json
    Assert-ExactObjectProperties `
        -Value $propertyEnvelope `
        -Names @('Properties') `
        -Description 'Debug x64 MSBuild property envelope'
    $properties = $propertyEnvelope.Properties
    Assert-ExactObjectProperties `
        -Value $properties `
        -Names @(
            'TargetDir',
            'LegacyRuntimeAssetsEnabled',
            'LegacyRuntimeAssetsMode',
            'LegacyRuntimeSourceRoot') `
        -Description 'Debug x64 MSBuild properties'
    foreach ($propertyName in @(
            'TargetDir',
            'LegacyRuntimeAssetsEnabled',
            'LegacyRuntimeAssetsMode',
            'LegacyRuntimeSourceRoot')) {
        if ($properties.$propertyName -isnot [string]) {
            throw "Debug x64 MSBuild property '$propertyName' must be a string."
        }
    }
    if ([string] $properties.LegacyRuntimeAssetsEnabled -cne 'true' -or
        [string] $properties.LegacyRuntimeAssetsMode -cne 'Required' -or
        -not (Test-SamePath `
            -Left ([string] $properties.LegacyRuntimeSourceRoot) `
            -Right $ExpectedRuntimeRoot)) {
        throw 'Debug x64 unexpectedly resolved as SourceOnly or used another runtime root.'
    }

    $targetDir = Get-NormalizedFullPath -Path ([string] $properties.TargetDir
    )
    $projectDirectory = Get-NormalizedFullPath -Path (
        [IO.Path]::GetDirectoryName($ProjectPath))
    $expectedBinRoot = Join-Path $projectDirectory 'bin'
    if ($targetDir.IndexOf(
            ([IO.Path]::DirectorySeparatorChar + 'SourceOnly' +
                [IO.Path]::DirectorySeparatorChar),
            [StringComparison]::OrdinalIgnoreCase) -ge 0 -or
        -not (Test-Path -LiteralPath $targetDir -PathType Container) -or
        -not (Test-PathIsWithin -Root $expectedBinRoot -Candidate $targetDir)) {
        throw 'Debug x64 target directory is missing or is a SourceOnly output.'
    }

    if (Test-Path -LiteralPath (Join-Path $targetDir 'Cuts')) {
        throw 'Debug x64 copied Cuts instead of using the selected external path.'
    }
    if (@(Get-ChildItem -LiteralPath $targetDir -File -Recurse -Force |
            Where-Object {
                $_.Name.Equals('MmoneyCoder.ini', [StringComparison]::OrdinalIgnoreCase)
            }).Count -ne 0) {
        throw 'Credential-bearing MmoneyCoder.ini entered the Debug build output.'
    }

    return [pscustomobject]@{
        TargetDirectory = $targetDir
        SourceOnly = $false
    }
}

$setupMutex = [Threading.Mutex]::new(
    $false,
    'Local\MBN_STOCK_WEBVIEW.ExistingDevelopmentPcInitialization.v1')
$setupMutexOwned = $false
try {
try {
    $setupMutexOwned = $setupMutex.WaitOne(0)
}
catch [Threading.AbandonedMutexException] {
    # The prior process ended unexpectedly. Atomic local files are re-preflighted
    # below so this run can safely complete the setup forward from that state.
    $setupMutexOwned = $true
}
if (-not $setupMutexOwned) {
    throw 'Another existing-development-PC initialization is already running.'
}

$repositoryRoot = Get-NormalizedFullPath -Path (Join-Path $PSScriptRoot '..')
$projectPath = Join-Path $repositoryRoot (
    'src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyParityApp.csproj')
$registrationInspectorPath = Join-Path $PSScriptRoot 'Inspect-K3DRegistration.ps1'
$liveInitializerPath = Join-Path $PSScriptRoot 'Initialize-DevelopmentLiveConfig.ps1'

foreach ($requiredScript in @(
        $registrationInspectorPath,
        $liveInitializerPath)) {
    [void](Assert-RegularFile -Path $requiredScript -Description 'Required setup script')
}

if ($ReplaceLiveConfig -and -not $ConfigureDevelopmentLive) {
    throw '-ReplaceLiveConfig requires -ConfigureDevelopmentLive.'
}
if ($ReplaceK3DPin -and -not $ConfigureDevelopmentLive) {
    throw '-ReplaceK3DPin requires -ConfigureDevelopmentLive.'
}
if ($ReplaceK3DPin -and $PinRegisteredK3D) {
    throw (
        '-ReplaceK3DPin cannot be combined with -PinRegisteredK3D. ' +
        'A replacement requires independently supplied native and interop hashes.')
}
if ($ConfigureDevelopmentLive -and $SkipBuild) {
    throw '-ConfigureDevelopmentLive requires the default Debug x64 build verification.'
}
if (-not $ConfigureDevelopmentLive -and
    (-not [string]::IsNullOrWhiteSpace($PlayoutHost) -or
     $PlayoutPort -ne 0 -or
     -not [string]::IsNullOrWhiteSpace($NativeSha256) -or
     -not [string]::IsNullOrWhiteSpace($InteropSha256) -or
     $PinRegisteredK3D -or
     $ReplaceK3DPin -or
     $null -ne $OutputChannel)) {
    throw 'Development Live endpoint/hash options require -ConfigureDevelopmentLive.'
}

$dotnetPath = $null
if (-not $SkipBuild) {
    $dotnetCommand = Get-Command -Name 'dotnet.exe' -CommandType Application `
        -ErrorAction Stop | Select-Object -First 1
    if ($null -eq $dotnetCommand -or
        [string]::IsNullOrWhiteSpace([string] $dotnetCommand.Source)) {
        throw 'A .NET SDK dotnet.exe could not be resolved for build verification.'
    }
    $dotnetPath = Get-NormalizedFullPath -Path ([string] $dotnetCommand.Source)
    [void](Assert-RegularFile -Path $dotnetPath -Description '.NET SDK host')
    Assert-FixedLocalPath -Path $dotnetPath -Description '.NET SDK host'
}

$runtimeSourceRoot = Select-LegacyRuntimeRoot `
    -RepositoryRoot $repositoryRoot `
    -RequestedRoot $LegacyRuntimeSourceRoot `
    -DisablePicker:$NoFolderPicker
Assert-LegacyRuntimeRoot -Root $runtimeSourceRoot -RepositoryRoot $repositoryRoot
$cutsSourceRoot = Get-NormalizedFullPath -Path (
    Join-Path $runtimeSourceRoot 'Cuts')
$resourceSourceRoot = Get-NormalizedFullPath -Path (
    Join-Path $runtimeSourceRoot 'Res')
$existingRuntime = Get-ExistingRuntimeBinding -RepositoryRoot $repositoryRoot
$runtimeBindingReused = $null -ne $existingRuntime -and
    (Test-SamePath `
        -Left ([string] $existingRuntime.BoundRoot) `
        -Right $runtimeSourceRoot)
if ($null -ne $existingRuntime -and
    -not $runtimeBindingReused -and
    -not $ReplaceRuntimeBinding) {
    throw (
        'The selected Cuts/Res path differs from the existing runtime binding. ' +
        'Re-run with -ReplaceRuntimeBinding.')
}

# This is a read-only registry/file inspection. It never activates a COM class.
$registrationReports = @(& $registrationInspectorPath)
$registration = Get-VerifiedK3DRegistration -Reports $registrationReports
$registeredClasses = @($registration.Classes)
$nativePath = Get-NormalizedFullPath -Path ([string] $registration.TypeLibPath)
[void](Assert-RegularFile -Path $nativePath -Description 'Registered K3D native DLL')
Assert-FixedLocalPath -Path $nativePath -Description 'Registered K3D native DLL'
foreach ($classReport in $registeredClasses) {
    if (-not (Test-SamePath `
            -Left ([string] $classReport.ServerPath) `
            -Right $nativePath)) {
        throw 'The K3D classes and type library do not resolve to one native DLL.'
    }
}

$releaseDirectory = [IO.Directory]::GetParent($nativePath)
$x64Directory = if ($null -ne $releaseDirectory) { $releaseDirectory.Parent } else { $null }
$dllDirectory = if ($null -ne $x64Directory) { $x64Directory.Parent } else { $null }
$vendorRoot = if ($null -ne $dllDirectory) { $dllDirectory.Parent } else { $null }
if ($null -eq $vendorRoot -or
    -not ([IO.Path]::GetFileName($nativePath)).Equals(
        'K3DAsyncEngine.dll',
        [StringComparison]::OrdinalIgnoreCase) -or
    -not $releaseDirectory.Name.Equals('Release', [StringComparison]::OrdinalIgnoreCase) -or
    -not $x64Directory.Name.Equals('x64', [StringComparison]::OrdinalIgnoreCase) -or
    -not $dllDirectory.Name.Equals('DLL', [StringComparison]::OrdinalIgnoreCase)) {
    throw 'The registered K3D native DLL is outside the supported vendor layout.'
}
$interopPath = Get-NormalizedFullPath -Path (
    Join-Path $vendorRoot.FullName 'Bin\x64\C#\Interop.K3DAsyncEngineLib.dll')
[void](Assert-RegularFile -Path $interopPath -Description 'Derived K3D interop assembly')
Assert-FixedLocalPath -Path $interopPath -Description 'Derived K3D interop assembly'

$localAppData = [Environment]::GetFolderPath(
    [Environment+SpecialFolder]::LocalApplicationData)
if ([string]::IsNullOrWhiteSpace($localAppData)) {
    throw 'The Windows LocalApplicationData known folder is unavailable.'
}
$localAppDataRoot = Get-NormalizedFullPath -Path $localAppData
Assert-FixedLocalPath -Path $localAppDataRoot -Description 'LocalAppData'
Assert-NoReparsePointInExistingAncestry `
    -Path $localAppDataRoot `
    -Description 'LocalAppData'
$runtimeFoldersPreflight = Get-RuntimeFoldersPreflight `
    -LocalAppDataRoot $localAppDataRoot `
    -CutsDirectory $cutsSourceRoot `
    -ResourceDirectory $resourceSourceRoot `
    -AllowReplacement:$ReplaceRuntimeBinding

# Preflight a requested Live replacement before any setup write. The caller
# either supplies both independent approval hashes or explicitly requests
# first-use pinning of the already verified HKLM x64 K3D registration. Both
# paths keep the exact file hashes in the one-launch authorization and the app
# verifies them again before loading either vendor binary.
$livePreflight = $null
$k3dPinPreflight = $null
$k3dPinWritten = $false
$liveAuthorizationInvalidated = $false
$approvedNativeSha256 = $null
$approvedInteropSha256 = $null
if ($ConfigureDevelopmentLive) {
    $configurationRoot = Join-Path $localAppDataRoot 'MBN_STOCK_WEBVIEW\Config'
    $prospectiveAuthorizationPath = Join-Path $configurationRoot (
        'playout.development-live.local.json')
    try {
        if ($PlayoutHost -cne '127.0.0.1' -and $PlayoutHost -cne '::1') {
            throw 'Development Live playout host must be exactly 127.0.0.1 or ::1.'
        }
        if ($PlayoutPort -lt 1 -or $PlayoutPort -gt 65535) {
            throw 'Development Live playout port must be between 1 and 65535.'
        }

        $actualNativeHash = (
            Get-FileHash -LiteralPath $nativePath -Algorithm SHA256
        ).Hash.ToUpperInvariant()
        $actualInteropHash = (
            Get-FileHash -LiteralPath $interopPath -Algorithm SHA256
        ).Hash.ToUpperInvariant()
        if ($PinRegisteredK3D) {
            if (-not [string]::IsNullOrWhiteSpace($NativeSha256) -or
                -not [string]::IsNullOrWhiteSpace($InteropSha256)) {
                throw (
                    '-PinRegisteredK3D cannot be combined with independently ' +
                    'supplied K3D hashes.')
            }

            $approvedNativeSha256 = $actualNativeHash
            $approvedInteropSha256 = $actualInteropHash
        }
        else {
            if ([string]::IsNullOrWhiteSpace($NativeSha256) -or
                $NativeSha256 -notmatch '^[0-9A-Fa-f]{64}$' -or
                [string]::IsNullOrWhiteSpace($InteropSha256) -or
                $InteropSha256 -notmatch '^[0-9A-Fa-f]{64}$') {
                throw 'Independent native and interop SHA-256 approvals are both required.'
            }

            $approvedNativeSha256 = $NativeSha256.ToUpperInvariant()
            $approvedInteropSha256 = $InteropSha256.ToUpperInvariant()
            if (-not $actualNativeHash.Equals(
                    $approvedNativeSha256,
                    [StringComparison]::Ordinal) -or
                -not $actualInteropHash.Equals(
                    $approvedInteropSha256,
                    [StringComparison]::Ordinal)) {
                throw 'An independently supplied K3D SHA-256 does not match the registered files.'
            }
        }

        $k3dPinPreflight = Get-K3DPinPreflight `
            -ConfigurationRoot $configurationRoot `
            -ApprovedNativeHash $approvedNativeSha256 `
            -ApprovedInteropHash $approvedInteropSha256 `
            -AllowReplacement:$ReplaceK3DPin
        if ($k3dPinPreflight.NeedsWrite) {
            # This is a durable trust anchor, not transactional runtime state.
            # Preserve the first-seen/independently replaced pin even when a
            # later DB, build, Live-config, or cleanup step fails. Otherwise a
            # changed DLL could be automatically accepted on the next attempt.
            Write-ProtectedUtf8TextAtomically `
                -Path ([string] $k3dPinPreflight.Path) `
                -ExpectedText ([string] $k3dPinPreflight.Json) `
                -Description 'K3D persistent pin' `
                -Replace:$k3dPinPreflight.NeedsReplace
            $k3dPinWritten = $true
        }
        $livePreflight = Get-DevelopmentLivePreflight `
            -ConfigurationRoot $configurationRoot `
            -HostName $PlayoutHost `
            -Port $PlayoutPort `
            -ApprovedNativeHash $approvedNativeSha256 `
            -ApprovedInteropHash $approvedInteropSha256 `
            -Channel $OutputChannel `
            -AllowReplacement:$ReplaceLiveConfig
    }
    catch {
        $livePreflightError = $_
        try {
            Remove-DevelopmentLiveAuthorizationFailClosed `
                -Path $prospectiveAuthorizationPath
        }
        catch {
            throw [InvalidOperationException]::new(
                (
                    'Development Live preflight failed and its existing ' +
                    'authorization could not be invalidated: ' + $_.Exception.Message
                ),
                $livePreflightError.Exception)
        }
        throw $livePreflightError
    }

    # Invalidate the previous one-launch authorization before database/runtime
    # preflight and before any setup write. Every subsequent failure remains
    # fail-closed at the exact path consumed by the app.
    Remove-DevelopmentLiveAuthorizationFailClosed `
        -Path ([string] $livePreflight.AuthorizationPath)
    if (Test-Path -LiteralPath ([string] $livePreflight.AuthorizationPath)) {
        throw 'Development Live authorization still exists before setup preflight.'
    }
    $liveAuthorizationInvalidated = $true

}

$databaseSource = $null
$databaseDestination = Join-Path $localAppDataRoot (
    'MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini')
$databaseSourceHash = $null
$databaseNeedsWrite = $false
$databaseNeedsReplace = $false
if (-not $SkipDatabaseProfile) {
    $databaseSource = if ([string]::IsNullOrWhiteSpace($DatabaseIniPath)) {
        Join-Path $runtimeSourceRoot 'Res\MmoneyCoder.ini'
    }
    else {
        if (-not [IO.Path]::IsPathRooted($DatabaseIniPath)) {
            throw '-DatabaseIniPath must be an absolute path.'
        }
        Get-NormalizedFullPath -Path $DatabaseIniPath
    }
    Assert-FixedLocalPath -Path $databaseSource -Description 'Legacy database INI'
    if ((Test-SamePath -Left $databaseSource -Right $repositoryRoot) -or
        (Test-PathIsWithin -Root $repositoryRoot -Candidate $databaseSource)) {
        throw 'Database credentials must not be sourced from inside this repository.'
    }
    $databaseItem = Assert-RegularFile `
        -Path $databaseSource `
        -Description 'Legacy database INI'
    if ($databaseItem.Length -le 0 -or
        $databaseItem.Length -gt $maximumDatabaseIniLength) {
        throw 'Legacy database INI is empty or exceeds the 1 MiB safety limit.'
    }
    $databaseSourceHash = (
        Get-FileHash -LiteralPath $databaseSource -Algorithm SHA256
    ).Hash.ToUpperInvariant()

    if (Test-Path -LiteralPath $databaseDestination) {
        $databaseDestinationItem = Assert-RegularFile `
            -Path $databaseDestination `
            -Description 'Installed database profile'
        if ($databaseDestinationItem.Length -le 0 -or
            $databaseDestinationItem.Length -gt $maximumDatabaseIniLength) {
            throw 'Installed database profile is empty or exceeds the 1 MiB safety limit.'
        }
        $sameDatabase = $databaseDestinationItem.Length -eq $databaseItem.Length -and
            (Get-FileHash `
                -LiteralPath $databaseDestination `
                -Algorithm SHA256).Hash.Equals(
                    $databaseSourceHash,
                    [StringComparison]::Ordinal) -and
            (Test-UserOnlyFileAcl -Path $databaseDestination)
        if (-not $sameDatabase) {
            if (-not $ReplaceDatabaseProfile) {
                throw (
                    'The installed database profile differs or is not user-only. ' +
                    'Re-run with -ReplaceDatabaseProfile.')
            }
            $databaseNeedsWrite = $true
            $databaseNeedsReplace = $true
        }
    }
    else {
        $databaseNeedsWrite = $true
    }
}
elseif (-not [string]::IsNullOrWhiteSpace($DatabaseIniPath) -or
    $ReplaceDatabaseProfile) {
    throw '-DatabaseIniPath/-ReplaceDatabaseProfile cannot be used with -SkipDatabaseProfile.'
}

$setupParent = Join-Path $localAppDataRoot 'MBN_STOCK_WEBVIEW\SetupTemp'
$rollbackParent = Join-Path $localAppDataRoot 'MBN_STOCK_WEBVIEW\SetupRollback'
Assert-NoReparsePointInExistingAncestry `
    -Path $setupParent `
    -Description 'Setup temporary directory'
Assert-NoReparsePointInExistingAncestry `
    -Path $rollbackParent `
    -Description 'Setup rollback directory'
$setupRoot = Join-Path $setupParent ([Guid]::NewGuid().ToString('N'))
$rollbackRoot = Join-Path $rollbackParent ([Guid]::NewGuid().ToString('N'))

$pendingRuntimeBinding = $null
$buildResult = $null
$databaseInstalled = $false
$runtimeFoldersWritten = $false
$liveConfigured = $false
$runtimeBindingMutationStarted = $false
$runtimeFoldersMutationStarted = $false
$databaseMutationStarted = $false
$propsSnapshot = $null
$runtimeFoldersSnapshot = $null
$databaseSnapshot = $null
$playoutSnapshot = $null
$playoutMutationStarted = $false
$transactionCommitted = $false
$transactionError = $null
try {
    [IO.Directory]::CreateDirectory($setupParent) | Out-Null
    [IO.Directory]::CreateDirectory($rollbackParent) | Out-Null
    Assert-NoReparsePointInExistingAncestry `
        -Path $setupParent `
        -Description 'Setup temporary directory'
    Assert-NoReparsePointInExistingAncestry `
        -Path $rollbackParent `
        -Description 'Setup rollback directory'
    [IO.Directory]::CreateDirectory($setupRoot) | Out-Null
    [IO.Directory]::CreateDirectory($rollbackRoot) | Out-Null
    Set-UserOnlyDirectoryAcl -Path $setupRoot
    Set-UserOnlyDirectoryAcl -Path $rollbackRoot
    Assert-NoReparsePointInExistingAncestry `
        -Path $setupRoot `
        -Description 'Owned setup temporary directory'
    Assert-NoReparsePointInExistingAncestry `
        -Path $rollbackRoot `
        -Description 'Owned setup rollback directory'

    $propsSnapshot = New-FileRollbackSnapshot `
        -Path (Join-Path $repositoryRoot 'Directory.Build.local.props') `
        -BackupDirectory $rollbackRoot `
        -Description 'Local build props'
    $runtimeFoldersSnapshot = New-FileRollbackSnapshot `
        -Path ([string] $runtimeFoldersPreflight.Path) `
        -BackupDirectory $rollbackRoot `
        -Description 'Runtime folder configuration'
    if (-not $SkipDatabaseProfile) {
        $databaseSnapshot = New-FileRollbackSnapshot `
            -Path $databaseDestination `
            -BackupDirectory $rollbackRoot `
            -Description 'Installed database profile'
    }
    if ($ConfigureDevelopmentLive) {
        $playoutSnapshot = New-FileRollbackSnapshot `
            -Path ([string] $livePreflight.PlayoutPath) `
            -BackupDirectory $rollbackRoot `
            -Description 'Development Live base configuration'
    }

    if (-not $runtimeBindingReused) {
        $generatedPropsPath = New-DirectRuntimeBindingFile `
            -RuntimeRoot $runtimeSourceRoot `
            -OutputDirectory $setupRoot
        $pendingRuntimeBinding = [pscustomobject]@{
            GeneratedPropsPath = $generatedPropsPath
            Replace = [bool] ($null -ne $existingRuntime)
        }
    }

    if (-not $SkipBuild) {
        $buildResult = Invoke-DebugBuildAndVerify `
            -DotNetPath $dotnetPath `
            -ProjectPath $projectPath `
            -ExpectedRuntimeRoot $runtimeSourceRoot
    }

    if ($databaseNeedsWrite) {
        $databaseMutationStarted = $true
        Copy-DatabaseProfileAtomically `
            -Source $databaseSource `
            -Destination $databaseDestination `
            -ExpectedHash $databaseSourceHash `
            -Replace:$databaseNeedsReplace
        $databaseInstalled = $true
    }

    if ($runtimeFoldersPreflight.NeedsWrite) {
        $runtimeFoldersMutationStarted = $true
        Write-ProtectedUtf8TextAtomically `
            -Path ([string] $runtimeFoldersPreflight.Path) `
            -ExpectedText ([string] $runtimeFoldersPreflight.Json) `
            -Description 'runtime folder configuration' `
            -Replace:$runtimeFoldersPreflight.NeedsReplace
        Assert-ExistingJsonContract `
            -Path ([string] $runtimeFoldersPreflight.Path) `
            -ExpectedJson ([string] $runtimeFoldersPreflight.Json) `
            -Description 'Runtime folder configuration'
        $runtimeFoldersWritten = $true
    }

    if ($ConfigureDevelopmentLive) {
        [void](Assert-RegularFile `
            -Path $nativePath `
            -Description 'Registered K3D native DLL before authorization')
        [void](Assert-RegularFile `
            -Path $interopPath `
            -Description 'K3D interop assembly before authorization')
        $finalNativeHash = (
            Get-FileHash -LiteralPath $nativePath -Algorithm SHA256
        ).Hash.ToUpperInvariant()
        $finalInteropHash = (
            Get-FileHash -LiteralPath $interopPath -Algorithm SHA256
        ).Hash.ToUpperInvariant()
        if (-not $finalNativeHash.Equals(
                $approvedNativeSha256,
                [StringComparison]::Ordinal) -or
            -not $finalInteropHash.Equals(
                $approvedInteropSha256,
                [StringComparison]::Ordinal)) {
            throw (
                'A K3D DLL changed during setup. The durable pin remains at ' +
                'the originally approved hash, and no Live authorization was issued.')
        }
    }

    # Live authorization is the final functional mutation. No COM activation,
    # PGM command, or database connection is performed by this setup script.
    if ($ConfigureDevelopmentLive -and $livePreflight.NeedsWrite) {
        $liveArguments = @{
            PlayoutHost = $PlayoutHost
            PlayoutPort = $PlayoutPort
            NativeSha256 = $approvedNativeSha256
            InteropSha256 = $approvedInteropSha256
            ConfigurationDirectory = $configurationRoot
        }
        if ($null -ne $OutputChannel) {
            $liveArguments.OutputChannel = $OutputChannel
        }
        if ($livePreflight.NeedsForce) {
            $liveArguments.Force = $true
        }

        $playoutMutationStarted = $true
        $liveResults = @(& $liveInitializerPath @liveArguments)
        if ($liveResults.Count -ne 0) {
            throw 'Development Live initializer returned unexpected success-stream output.'
        }
        Assert-ExistingJsonContract `
            -Path ([string] $livePreflight.PlayoutPath) `
            -ExpectedJson ([string] $livePreflight.PlayoutJson) `
            -Description 'Development Live base configuration'
        Assert-ExistingJsonContract `
            -Path ([string] $livePreflight.AuthorizationPath) `
            -ExpectedJson ([string] $livePreflight.AuthorizationJson) `
            -Description 'Development Live authorization'
        $liveConfigured = $true
    }

    # Directory.Build.local.props is the durable success marker for a fresh
    # clone. Keep it source-only until runtime/build/DB/K3D/Live are complete so
    # a stopped setup returns to this first-run UI on the next F5.
    if ($null -ne $pendingRuntimeBinding) {
        $runtimeBindingMutationStarted = $true
        Set-RuntimeBindingAtomically `
            -GeneratedPropsPath ([string] $pendingRuntimeBinding.GeneratedPropsPath) `
            -RepositoryRoot $repositoryRoot `
            -Replace:([bool] $pendingRuntimeBinding.Replace)
        $verifiedBinding = Get-ExistingRuntimeBinding `
            -RepositoryRoot $repositoryRoot
        if ($null -eq $verifiedBinding -or
            -not (Test-SamePath `
                -Left ([string] $verifiedBinding.BoundRoot) `
                -Right $runtimeSourceRoot)) {
            throw 'The atomically installed runtime binding failed final verification.'
        }
    }

    # A cleanup failure is still a transaction failure. Rollback snapshots are
    # also held in memory so rollback remains possible if cleanup only partly ran.
    Remove-OwnedTemporaryDirectory `
        -Parent $setupParent `
        -Path $setupRoot `
        -Description 'setup temporary directory'
    Remove-OwnedTemporaryDirectory `
        -Parent $rollbackParent `
        -Path $rollbackRoot `
        -Description 'setup rollback directory'
    $transactionCommitted = $true
}
catch {
    $transactionError = $_
}

if (-not $transactionCommitted) {
    $secondaryFailures = New-Object 'System.Collections.Generic.List[string]'

    # Always fail closed before restoring any build/runtime state.
    if ($ConfigureDevelopmentLive -and $liveAuthorizationInvalidated) {
        try {
            Remove-DevelopmentLiveAuthorizationFailClosed `
                -Path ([string] $livePreflight.AuthorizationPath)
        }
        catch {
            $secondaryFailures.Add(
                'Development Live fail-closed cleanup failed: ' +
                $_.Exception.Message)
        }
    }
    if ($playoutMutationStarted -and $null -ne $playoutSnapshot) {
        try {
            Restore-FileRollbackSnapshot `
                -Snapshot $playoutSnapshot `
                -Description 'Development Live base configuration'
        }
        catch {
            $secondaryFailures.Add(
                'Development Live base configuration rollback failed: ' +
                $_.Exception.Message)
        }
    }
    if ($runtimeFoldersMutationStarted -and
        $null -ne $runtimeFoldersSnapshot) {
        try {
            Restore-FileRollbackSnapshot `
                -Snapshot $runtimeFoldersSnapshot `
                -Description 'Runtime folder configuration'
        }
        catch {
            $secondaryFailures.Add(
                'Runtime folder configuration rollback failed: ' +
                $_.Exception.Message)
        }
    }
    if ($databaseMutationStarted -and $null -ne $databaseSnapshot) {
        try {
            Restore-FileRollbackSnapshot `
                -Snapshot $databaseSnapshot `
                -Description 'Installed database profile'
        }
        catch {
            $secondaryFailures.Add(
                'Database rollback failed: ' + $_.Exception.Message)
        }
    }
    if ($runtimeBindingMutationStarted -and $null -ne $propsSnapshot) {
        try {
            Restore-FileRollbackSnapshot `
                -Snapshot $propsSnapshot `
                -Description 'Local build props'
        }
        catch {
            $secondaryFailures.Add(
                'Runtime binding rollback failed: ' + $_.Exception.Message)
        }
    }

    foreach ($cleanupTarget in @(
            [pscustomobject]@{
                Parent = $setupParent
                Path = $setupRoot
                Description = 'setup temporary directory'
            },
            [pscustomobject]@{
                Parent = $rollbackParent
                Path = $rollbackRoot
                Description = 'setup rollback directory'
            })) {
        try {
            Remove-OwnedTemporaryDirectory `
                -Parent $cleanupTarget.Parent `
                -Path $cleanupTarget.Path `
                -Description $cleanupTarget.Description
        }
        catch {
            $secondaryFailures.Add(
                $cleanupTarget.Description + ' cleanup failed: ' +
                $_.Exception.Message)
        }
    }

    if ($null -eq $transactionError) {
        $transactionError = [System.Management.Automation.ErrorRecord]::new(
            [InvalidOperationException]::new(
                'Existing development PC initialization did not commit.'),
            'InitializationDidNotCommit',
            [System.Management.Automation.ErrorCategory]::InvalidResult,
            $null)
    }
    if ($secondaryFailures.Count -gt 0) {
        throw [InvalidOperationException]::new(
            (
                'Existing development PC initialization failed. Original failure: ' +
                $transactionError.Exception.Message + ' Secondary recovery failure(s): ' +
                ($secondaryFailures -join ' | ')
            ),
            $transactionError.Exception)
    }
    throw $transactionError
}

[pscustomobject]@{
    RuntimeSourceRoot = $runtimeSourceRoot
    BoundRuntimeRoot = $runtimeSourceRoot
    RuntimeBindingReused = [bool] $runtimeBindingReused
    RuntimeFolderConfigurationPath = [string] $runtimeFoldersPreflight.Path
    RuntimeFolderConfigurationWritten = $runtimeFoldersWritten
    DatabaseProfilePath = if ($SkipDatabaseProfile) { $null } else {
        $databaseDestination
    }
    DatabaseProfileWritten = $databaseInstalled
    K3DStatus = [string] $registration.Status
    K3DComActivated = [bool] $registration.ComActivated
    K3DPinPath = if ($ConfigureDevelopmentLive) {
        [string] $k3dPinPreflight.Path
    } else {
        $null
    }
    K3DPinWritten = $k3dPinWritten
    K3DPinReused = [bool] (
        $ConfigureDevelopmentLive -and -not $k3dPinPreflight.NeedsWrite)
    BuildVerified = $null -ne $buildResult
    BuildOutputPath = if ($null -eq $buildResult) { $null } else {
        $buildResult.TargetDirectory
    }
    DevelopmentLiveRequested = [bool] $ConfigureDevelopmentLive
    DevelopmentLiveWritten = $liveConfigured
    DevelopmentLiveReused = $false
}
}
finally {
    try {
        if ($setupMutexOwned) {
            $setupMutex.ReleaseMutex()
        }
    }
    finally {
        $setupMutex.Dispose()
    }
}
