param(
    [string]$ProjectRoot = 'C:\HyperTwist',
    [string]$ArchiveDirectory = 'C:\HyperTwist\packaged\classic-cube',
    [ValidateSet('Development', 'Shipping')]
    [string]$Configuration = 'Development',
    [string]$CookMap = '/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining',
    [string[]]$AdditionalCookMaps = @('/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining'),
    [string[]]$SmokeMaps = @(),
    [string[]]$AdditionalCookerOptions = @(
        '-DisablePlugins=MovieRenderPipeline',
        '-SkipCookingEditorContent'
    ),
    [bool]$HeadlessSmoke = $true,
    [string]$ValidationReportPath = '',
    [string]$SolverTablePath = '',
    [Int64]$ExpectedSolverTableSizeBytes = 676207080,
    [string]$ExpectedSolverTableSha256 = 'dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034',
    [string]$BrowserShellSourceDirectory = '',
    [string[]]$BootstrapLaunchArguments = @(
        '-notraceserver',
        '-traceautostart=0'
    ),
    [switch]$CleanArchive,
    [switch]$SkipBuild,
    [switch]$SkipBrowserShellBuild,
    [switch]$SkipLaunch
)

$ErrorActionPreference = 'Stop'

$RunUatPath = 'C:\Program Files\Epic Games\UE_5.7\Engine\Build\BatchFiles\RunUAT.bat'
$UProjectPath = Join-Path $ProjectRoot 'UnrealHyperTwist\UnrealHyperTwist.uproject'
$LaunchScriptPath = Join-Path $ProjectRoot 'scripts\Launch-HyperTwistClassicCubePackage.ps1'
$BootstrapLaunchArgumentsScriptPath = Join-Path $ProjectRoot 'scripts\Set-HyperTwistBootstrapLaunchArguments.ps1'
$GameTargetReceiptPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Binaries\Win64\UnrealHyperTwist.target'
$UnrealBuildToolSavedPath = Join-Path $ProjectRoot 'UnrealHyperTwist\Saved\UnrealBuildTool'
$CookMaps = @($CookMap) + $AdditionalCookMaps | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
$ResolvedSmokeMaps = @(
    if ($SmokeMaps.Count -gt 0)
    {
        $SmokeMaps
    }
    else
    {
        $CookMaps
    }
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
$ResolvedAdditionalCookerOptions = @($AdditionalCookerOptions) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
$ExpectedClassicCubeMaterialPaths = @(
    (Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\M_HT_ClassicCubeFaceMaster.uasset'),
    (Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Up.uasset'),
    (Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Down.uasset'),
    (Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Front.uasset'),
    (Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Back.uasset'),
    (Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Left.uasset'),
    (Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Right.uasset'),
    (Join-Path $ProjectRoot 'UnrealHyperTwist\Content\HyperTwistTraining\Materials\MI_HT_ClassicCube_Internal.uasset')
)
$ValidationRootPath = Join-Path $ArchiveDirectory 'validation'
$SmokeReportDirectory = Join-Path $ValidationRootPath 'smoke'
$BootstrapLaunchArgumentsReportPath = Join-Path $ValidationRootPath 'bootstrap-launch-arguments-report.json'

if ([string]::IsNullOrWhiteSpace($SolverTablePath))
{
    $SolverTablePath = Join-Path $ProjectRoot 'UnrealHyperTwist\Saved\twophase-ht.tbl'
}

if ([string]::IsNullOrWhiteSpace($BrowserShellSourceDirectory))
{
    $BrowserShellSourceDirectory = Join-Path $ProjectRoot 'Content\Browser'
}

function Write-Utf8JsonFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,
        [Parameter(Mandatory = $true)]
        [object]$Value
    )

    $ParentPath = Split-Path -Parent $Path
    if (-not [string]::IsNullOrWhiteSpace($ParentPath))
    {
        New-Item -ItemType Directory -Force -Path $ParentPath | Out-Null
    }

    $Json = $Value | ConvertTo-Json -Depth 10
    $Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::WriteAllText($Path, $Json, $Utf8NoBom)
}

function Convert-PathToken {
    param(
        [string]$Value
    )

    if ([string]::IsNullOrWhiteSpace($Value))
    {
        return 'unknown'
    }

    return ($Value -replace '[\\/:*?"<>| ]', '_')
}

function Read-JsonFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
}

function Get-BrowserShellEvidence {
    param(
        [Parameter(Mandatory = $true)]
        [string]$RootPath
    )

    $RequiredRelativePaths = @(
        'index.html',
        'src\browser-runtime-bootstrap.js',
        'src\browser-spatial-runtime-fallback.js',
        'dist\browser-spatial-runtime.js'
    )
    $RequiredFiles = @()
    foreach ($RelativePath in $RequiredRelativePaths)
    {
        $AbsolutePath = Join-Path $RootPath $RelativePath
        if (-not (Test-Path -LiteralPath $AbsolutePath -PathType Leaf))
        {
            throw "Required browser-shell artifact '$RelativePath' was not found beneath '$RootPath'."
        }

        $Item = Get-Item -LiteralPath $AbsolutePath
        $RequiredFiles += [pscustomobject][ordered]@{
            relativePath = $RelativePath.Replace('\', '/')
            sizeBytes = [Int64]$Item.Length
            sha256 = (Get-FileHash -LiteralPath $AbsolutePath -Algorithm SHA256).Hash.ToLowerInvariant()
        }
    }

    $AllFiles = @(
        Get-ChildItem -LiteralPath $RootPath -Recurse -File |
            Where-Object {
                $_.FullName -notmatch '[\\/]node_modules[\\/]' `
                    -and $_.Extension -ne '.map'
            }
    )

    return [pscustomobject][ordered]@{
        rootPath = (Get-Item -LiteralPath $RootPath).FullName
        deployableFileCount = $AllFiles.Count
        deployableSizeBytes = [Int64](($AllFiles | Measure-Object -Property Length -Sum).Sum)
        requiredFiles = @($RequiredFiles)
    }
}

function Invoke-BrowserShellBuild {
    param(
        [Parameter(Mandatory = $true)]
        [string]$SourceDirectory,
        [Parameter(Mandatory = $true)]
        [bool]$SkipBuild
    )

    $PackageManifestPath = Join-Path $SourceDirectory 'package.json'
    if (-not (Test-Path -LiteralPath $PackageManifestPath -PathType Leaf))
    {
        throw "Browser-shell package manifest was not found at '$PackageManifestPath'."
    }

    if (-not $SkipBuild)
    {
        $NpmCommand = Get-Command 'npm.cmd' -ErrorAction SilentlyContinue
        if ($null -eq $NpmCommand)
        {
            throw 'npm.cmd is required to verify and build the packaged HyperTwist browser shell.'
        }

        Write-Host "Verifying HyperTwist browser shell beneath '$SourceDirectory'..."
        & $NpmCommand.Source --prefix $SourceDirectory run verify:shell
        if ($LASTEXITCODE -ne 0)
        {
            throw "Browser-shell verification failed with exit code $LASTEXITCODE."
        }

        Write-Host "Restoring the locked HyperTwist browser-shell toolchain beneath '$SourceDirectory'..."
        & $NpmCommand.Source --prefix $SourceDirectory ci --no-audit --no-fund
        if ($LASTEXITCODE -ne 0)
        {
            throw "Browser-shell locked dependency restore failed with exit code $LASTEXITCODE."
        }

        Write-Host "Building HyperTwist browser shell beneath '$SourceDirectory'..."
        & $NpmCommand.Source --prefix $SourceDirectory run build
        if ($LASTEXITCODE -ne 0)
        {
            throw "Browser-shell production build failed with exit code $LASTEXITCODE."
        }
    }

    return Get-BrowserShellEvidence -RootPath $SourceDirectory
}

function Copy-BrowserShellToPackage {
    param(
        [Parameter(Mandatory = $true)]
        [string]$SourceDirectory,
        [Parameter(Mandatory = $true)]
        [string]$PackagedExecutablePath
    )

    $ExecutableDirectory = Split-Path -Parent $PackagedExecutablePath
    $DestinationDirectory = Join-Path $ExecutableDirectory 'Content\Browser'
    if (Test-Path -LiteralPath $DestinationDirectory)
    {
        Remove-Item -LiteralPath $DestinationDirectory -Recurse -Force
    }

    New-Item -ItemType Directory -Force -Path $DestinationDirectory | Out-Null
    New-Item -ItemType Directory -Force -Path (Join-Path $DestinationDirectory 'src') | Out-Null

    Copy-Item `
        -LiteralPath (Join-Path $SourceDirectory 'index.html') `
        -Destination (Join-Path $DestinationDirectory 'index.html') `
        -Force
    Copy-Item `
        -LiteralPath (Join-Path $SourceDirectory 'src\browser-runtime-bootstrap.js') `
        -Destination (Join-Path $DestinationDirectory 'src\browser-runtime-bootstrap.js') `
        -Force
    Copy-Item `
        -LiteralPath (Join-Path $SourceDirectory 'src\browser-spatial-runtime-fallback.js') `
        -Destination (Join-Path $DestinationDirectory 'src\browser-spatial-runtime-fallback.js') `
        -Force

    $DistSourceDirectory = Join-Path $SourceDirectory 'dist'
    Get-ChildItem -LiteralPath $DistSourceDirectory -Recurse -File |
        Where-Object { $_.Extension -ne '.map' } |
        ForEach-Object {
            $RelativePath = $_.FullName.Substring($DistSourceDirectory.Length).TrimStart('\', '/')
            $DestinationPath = Join-Path (Join-Path $DestinationDirectory 'dist') $RelativePath
            New-Item -ItemType Directory -Force -Path (Split-Path -Parent $DestinationPath) | Out-Null
            Copy-Item -LiteralPath $_.FullName -Destination $DestinationPath -Force
        }

    return Get-BrowserShellEvidence -RootPath $DestinationDirectory
}

function Get-BootstrapRuntimeArgumentEvidence {
    param(
        [string]$RuntimeLogPath,
        [Parameter(Mandatory = $true)]
        [string]$RuntimeDiagnosticsPath,
        [string[]]$ObservedCommandLines = @(),
        [object[]]$TraceControlListeningEndpoints = @(),
        [Parameter(Mandatory = $true)]
        [string[]]$ExpectedArguments
    )

    $RuntimeLogExists = -not [string]::IsNullOrWhiteSpace($RuntimeLogPath) `
        -and (Test-Path -LiteralPath $RuntimeLogPath -PathType Leaf)
    $RuntimeDiagnosticsExists = -not [string]::IsNullOrWhiteSpace($RuntimeDiagnosticsPath) `
        -and (Test-Path -LiteralPath $RuntimeDiagnosticsPath -PathType Leaf)
    if (-not $RuntimeDiagnosticsExists)
    {
        return [pscustomobject][ordered]@{
            runtimeLogPath = $RuntimeLogPath
            runtimeLogExists = $false
            runtimeDiagnosticsPath = $RuntimeDiagnosticsPath
            runtimeDiagnosticsExists = $false
            expectedArguments = @($ExpectedArguments)
            matchedArguments = @()
            missingArguments = @($ExpectedArguments)
            traceControlListenerLines = @()
            traceControlListeningEndpoints = @($TraceControlListeningEndpoints)
            result = 'failed'
            error = 'The direct HyperTwist diagnostics file was not found.'
        }
    }

    $RuntimeLogLines = [string[]]@(
        if ($RuntimeLogExists)
        {
            Get-Content -LiteralPath $RuntimeLogPath |
                ForEach-Object { $_.ToString() }
        }
    )
    $RuntimeDiagnosticsLines = [string[]]@(
        if ($RuntimeDiagnosticsExists)
        {
            Get-Content -LiteralPath $RuntimeDiagnosticsPath |
                ForEach-Object { $_.ToString() }
        }
    )
    $RuntimeEvidenceLines = [string[]]@(
        @($RuntimeLogLines) +
        @($RuntimeDiagnosticsLines) +
        @($ObservedCommandLines)
    )
    $RuntimeEvidenceText = [string]::Join([Environment]::NewLine, $RuntimeEvidenceLines)
    $MatchedArguments = @(
        $ExpectedArguments |
            Where-Object {
                $RuntimeEvidenceText.IndexOf(
                    $_,
                    [System.StringComparison]::OrdinalIgnoreCase
                ) -ge 0
            }
    )
    $MissingArguments = @(
        $ExpectedArguments |
            Where-Object { $MatchedArguments -notcontains $_ }
    )
    $TraceControlListenerLines = [string[]]@(
        @($RuntimeLogLines + $RuntimeDiagnosticsLines) |
            Where-Object {
                $_.IndexOf(
                    'Control listening on port',
                    [System.StringComparison]::OrdinalIgnoreCase
                ) -ge 0
            }
    )
    $Passed = $MissingArguments.Count -eq 0 `
        -and $TraceControlListenerLines.Count -eq 0 `
        -and $TraceControlListeningEndpoints.Count -eq 0

    return [pscustomobject][ordered]@{
        runtimeLogPath = $RuntimeLogPath
        runtimeLogExists = $RuntimeLogExists
        runtimeDiagnosticsPath = $RuntimeDiagnosticsPath
        runtimeDiagnosticsExists = $RuntimeDiagnosticsExists
        observedCommandLines = @($ObservedCommandLines)
        expectedArguments = @($ExpectedArguments)
        matchedArguments = @($MatchedArguments)
        missingArguments = @($MissingArguments)
        traceControlListenerLines = @($TraceControlListenerLines)
        traceControlListeningEndpoints = @($TraceControlListeningEndpoints)
        result = if ($Passed) { 'passed' } else { 'failed' }
        error = if ($Passed)
        {
            $null
        }
        elseif ($MissingArguments.Count -gt 0)
        {
            "Runtime evidence omitted bootstrap arguments: $($MissingArguments -join ', ')"
        }
        else
        {
            'Runtime opened an Unreal trace-control listener.'
        }
    }
}

function Get-ValidatedSolverTableEvidence {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,
        [Parameter(Mandatory = $true)]
        [Int64]$ExpectedSizeBytes,
        [Parameter(Mandatory = $true)]
        [string]$ExpectedSha256
    )

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf))
    {
        throw "Required two-phase solver table was not found at '$Path'."
    }

    $Item = Get-Item -LiteralPath $Path
    if ($Item.Length -ne $ExpectedSizeBytes)
    {
        throw "Two-phase solver table '$Path' was $($Item.Length) bytes; expected exactly $ExpectedSizeBytes bytes."
    }

    $ActualSha256 = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
    $NormalizedExpectedSha256 = $ExpectedSha256.Trim().ToLowerInvariant()
    if ($ActualSha256 -ne $NormalizedExpectedSha256)
    {
        throw "Two-phase solver table '$Path' had SHA-256 '$ActualSha256'; expected '$NormalizedExpectedSha256'."
    }

    return [pscustomobject][ordered]@{
        path = $Item.FullName
        sizeBytes = [Int64]$Item.Length
        sha256 = $ActualSha256
    }
}

function Copy-ValidatedSolverTableToPackage {
    param(
        [Parameter(Mandatory = $true)]
        [string]$SourcePath,
        [Parameter(Mandatory = $true)]
        [string]$PackagedExecutablePath,
        [Parameter(Mandatory = $true)]
        [Int64]$ExpectedSizeBytes,
        [Parameter(Mandatory = $true)]
        [string]$ExpectedSha256
    )

    $ExecutableDirectory = Split-Path -Parent $PackagedExecutablePath
    $DestinationPath = Join-Path $ExecutableDirectory 'UnrealHyperTwist\Saved\twophase-ht.tbl'
    New-Item -ItemType Directory -Force -Path (Split-Path -Parent $DestinationPath) | Out-Null
    Copy-Item -LiteralPath $SourcePath -Destination $DestinationPath -Force

    return Get-ValidatedSolverTableEvidence `
        -Path $DestinationPath `
        -ExpectedSizeBytes $ExpectedSizeBytes `
        -ExpectedSha256 $ExpectedSha256
}

function Resolve-PackagedExecutablePath {
    param(
        [string]$PackageRoot
    )

    $CandidateExecutablePaths = @(
        (Join-Path $PackageRoot 'Windows\UnrealHyperTwist.exe'),
        (Join-Path $PackageRoot 'WindowsNoEditor\UnrealHyperTwist.exe'),
        (Join-Path $PackageRoot 'UnrealHyperTwist.exe')
    )

    return $CandidateExecutablePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
}

function Convert-GameMapPathToContentPath {
    param(
        [string]$RootPath,
        [string]$GameMapPath
    )

    if (-not $GameMapPath.StartsWith('/Game/'))
    {
        throw "Cook map '$GameMapPath' is not a supported /Game asset path."
    }

    $RelativeMapPath = $GameMapPath.Substring('/Game/'.Length).Replace('/', '\')
    return Join-Path $RootPath ("UnrealHyperTwist\Content\{0}.umap" -f $RelativeMapPath)
}

function Assert-CookMapExists {
    param(
        [string]$RootPath,
        [string]$GameMapPath
    )

    $ExpectedMapPath = Convert-GameMapPathToContentPath -RootPath $RootPath -GameMapPath $GameMapPath
    if (-not (Test-Path $ExpectedMapPath))
    {
        throw "Expected cook map '$GameMapPath' was not found at '$ExpectedMapPath'. Run 'scripts\Invoke-HyperTwistClassicCubeMapAuthoring.ps1' first."
    }
}

function Resolve-ExistingPath {
    param(
        [string[]]$CandidatePaths
    )

    return $CandidatePaths | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
}

function Get-MostRecentFileItem {
    param(
        [string[]]$Paths
    )

    $MostRecentItem = $null
    foreach ($Path in $Paths)
    {
        if (-not (Test-Path -LiteralPath $Path))
        {
            continue
        }

        $PathItem = Get-Item -LiteralPath $Path
        $CandidateItem = $null
        if ($PathItem.PSIsContainer)
        {
            $CandidateItem = Get-ChildItem -LiteralPath $Path -Recurse -File -ErrorAction SilentlyContinue |
                Sort-Object LastWriteTimeUtc -Descending |
                Select-Object -First 1
        }
        else
        {
            $CandidateItem = $PathItem
        }

        if ($null -eq $CandidateItem)
        {
            continue
        }

        if ($null -eq $MostRecentItem -or $CandidateItem.LastWriteTimeUtc -gt $MostRecentItem.LastWriteTimeUtc)
        {
            $MostRecentItem = $CandidateItem
        }
    }

    return $MostRecentItem
}

function Assert-SkipBuildInputFreshness {
    param(
        [string]$RootPath,
        [string]$Configuration,
        [string]$GameTargetReceiptPath
    )

    if (-not (Test-Path -LiteralPath $GameTargetReceiptPath))
    {
        throw "SkipBuild was requested, but the packaged-game target receipt was not found at '$GameTargetReceiptPath'. Re-run without -SkipBuild so RunUAT can build the Win64 game target."
    }

    $GameBinaryPath = Resolve-ExistingPath -CandidatePaths @(
        (Join-Path $RootPath 'UnrealHyperTwist\Binaries\Win64\UnrealHyperTwist.exe'),
        (Join-Path $RootPath ("UnrealHyperTwist\Binaries\Win64\UnrealHyperTwist-Win64-{0}.exe" -f $Configuration))
    )
    if ($null -eq $GameBinaryPath)
    {
        throw "SkipBuild was requested, but no local packaged-game binary was found beneath '$(Join-Path $RootPath 'UnrealHyperTwist\Binaries\Win64')'. Re-run without -SkipBuild so RunUAT can build the Win64 game target."
    }

    $NewestBuildInputItem = Get-MostRecentFileItem -Paths @(
        (Join-Path $RootPath 'UnrealHyperTwist\UnrealHyperTwist.uproject'),
        (Join-Path $RootPath 'UnrealHyperTwist\Source'),
        (Join-Path $RootPath 'UnrealHyperTwist\Config'),
        (Join-Path $RootPath 'UnrealHyperTwist\Plugins')
    )
    if ($null -eq $NewestBuildInputItem)
    {
        return
    }

    $ReceiptItem = Get-Item -LiteralPath $GameTargetReceiptPath
    $GameBinaryItem = Get-Item -LiteralPath $GameBinaryPath
    if ($NewestBuildInputItem.LastWriteTimeUtc -gt $ReceiptItem.LastWriteTimeUtc `
        -or $NewestBuildInputItem.LastWriteTimeUtc -gt $GameBinaryItem.LastWriteTimeUtc)
    {
        throw (
            "SkipBuild was requested, but the packaged-game build artifacts are stale. " +
            "Newest build input '{0}' ({1:o}) is newer than receipt '{2}' ({3:o}) or game binary '{4}' ({5:o}). " +
            "Re-run without -SkipBuild so RunUAT can rebuild the Win64 game target; a passed editor build alone is not sufficient packaged-runtime proof."
        ) -f `
            $NewestBuildInputItem.FullName,
            $NewestBuildInputItem.LastWriteTimeUtc,
            $ReceiptItem.FullName,
            $ReceiptItem.LastWriteTimeUtc,
            $GameBinaryItem.FullName,
            $GameBinaryItem.LastWriteTimeUtc
    }
}

if (-not (Test-Path $RunUatPath))
{
    throw "RunUAT was not found at '$RunUatPath'."
}

if (-not (Test-Path $UProjectPath))
{
    throw "UnrealHyperTwist project file was not found at '$UProjectPath'."
}

if (-not (Test-Path -LiteralPath $BootstrapLaunchArgumentsScriptPath -PathType Leaf))
{
    throw "Bootstrap launch-argument helper was not found at '$BootstrapLaunchArgumentsScriptPath'."
}

foreach ($TargetCookMap in $CookMaps)
{
    Assert-CookMapExists -RootPath $ProjectRoot -GameMapPath $TargetCookMap
}

foreach ($TargetSmokeMap in $ResolvedSmokeMaps)
{
    Assert-CookMapExists -RootPath $ProjectRoot -GameMapPath $TargetSmokeMap
}

foreach ($ExpectedMaterialPath in $ExpectedClassicCubeMaterialPaths)
{
    if (-not (Test-Path $ExpectedMaterialPath))
    {
        throw "Expected classic-cube material asset was not found at '$ExpectedMaterialPath'. Run 'scripts\\Invoke-HyperTwistClassicCubeMapAuthoring.ps1' first."
    }
}

if ($SkipBuild)
{
    Assert-SkipBuildInputFreshness `
        -RootPath $ProjectRoot `
        -Configuration $Configuration `
        -GameTargetReceiptPath $GameTargetReceiptPath
}

if ($CleanArchive -and (Test-Path $ArchiveDirectory))
{
    Remove-Item -LiteralPath $ArchiveDirectory -Recurse -Force
}

New-Item -ItemType Directory -Force -Path $UnrealBuildToolSavedPath | Out-Null
New-Item -ItemType Directory -Force -Path $ArchiveDirectory | Out-Null
New-Item -ItemType Directory -Force -Path $ValidationRootPath | Out-Null
New-Item -ItemType Directory -Force -Path $SmokeReportDirectory | Out-Null

if ([string]::IsNullOrWhiteSpace($ValidationReportPath))
{
    $ValidationReportPath = Join-Path $ValidationRootPath 'classic-cube-package-validation-report.json'
}

$RunUatArguments = @(
    'BuildCookRun',
    '-WaitForUATMutex',
    "-project=$UProjectPath",
    '-noP4',
    '-platform=Win64',
    "-clientconfig=$Configuration",
    '-cook',
    '-stage',
    '-package',
    '-pak',
    '-archive',
    "-archivedirectory=$ArchiveDirectory",
    "-map=$($CookMaps -join '+')",
    '-unattended',
    '-utf8output'
)

if (-not $SkipBuild)
{
    $RunUatArguments += '-build'
}

if ($ResolvedAdditionalCookerOptions.Count -gt 0)
{
    # Keep the runtime package lane insulated from editor-only plugin/content bleed.
    $RunUatArguments += "-AdditionalCookerOptions=$($ResolvedAdditionalCookerOptions -join ' ')"
}

$ValidationReport = [ordered]@{
    reportVersion = 'ht-classic-cube-package-validation/v2'
    generatedAtUtc = [DateTime]::UtcNow.ToString('o')
    projectRoot = $ProjectRoot
    archiveDirectory = $ArchiveDirectory
    configuration = $Configuration
    cookMaps = @($CookMaps)
    smokeMaps = @($ResolvedSmokeMaps)
    additionalCookerOptions = @($ResolvedAdditionalCookerOptions)
    headlessSmoke = $HeadlessSmoke
    cleanArchive = [bool]$CleanArchive
    skipBuild = [bool]$SkipBuild
    skipLaunch = [bool]$SkipLaunch
    result = 'failed'
    packagedExecutablePath = $null
    bootstrapLaunchArguments = [ordered]@{
        result = 'pending'
        traceControlProtection = if ($Configuration -eq 'Shipping')
        {
            'shipping-compile-time-disabled'
        }
        else
        {
            'runtime-listener-rejection-only'
        }
        expectedArguments = @($BootstrapLaunchArguments)
        patchReportPath = $BootstrapLaunchArgumentsReportPath
        patchReport = $null
        runtimeProofs = @()
    }
    solverTable = [ordered]@{
        result = 'pending'
        sourcePath = $SolverTablePath
        expectedSizeBytes = $ExpectedSolverTableSizeBytes
        expectedSha256 = $ExpectedSolverTableSha256.ToLowerInvariant()
        sourceEvidence = $null
        packagedEvidence = $null
    }
    browserShell = [ordered]@{
        result = 'pending'
        sourceDirectory = $BrowserShellSourceDirectory
        skipBuild = [bool]$SkipBrowserShellBuild
        sourceEvidence = $null
        packagedEvidence = $null
    }
    smokeReports = @()
    error = $null
}

try
{
    $ValidationReport.solverTable.sourceEvidence = Get-ValidatedSolverTableEvidence `
        -Path $SolverTablePath `
        -ExpectedSizeBytes $ExpectedSolverTableSizeBytes `
        -ExpectedSha256 $ExpectedSolverTableSha256

    $ValidationReport.browserShell.sourceEvidence = Invoke-BrowserShellBuild `
        -SourceDirectory $BrowserShellSourceDirectory `
        -SkipBuild ([bool]$SkipBrowserShellBuild)

    Write-Host "Packaging HyperTwist classic-cube validation lane to '$ArchiveDirectory'..."
    & $RunUatPath @RunUatArguments

    if ($LASTEXITCODE -ne 0)
    {
        throw "RunUAT packaging failed with exit code $LASTEXITCODE."
    }

    $PackagedExecutablePath = Resolve-PackagedExecutablePath -PackageRoot $ArchiveDirectory
    if ($null -eq $PackagedExecutablePath)
    {
        throw "No packaged UnrealHyperTwist executable was found beneath '$ArchiveDirectory' after packaging."
    }

    $ValidationReport.packagedExecutablePath = $PackagedExecutablePath
    & $BootstrapLaunchArgumentsScriptPath `
        -ExecutablePath $PackagedExecutablePath `
        -AdditionalArguments $BootstrapLaunchArguments `
        -ReportPath $BootstrapLaunchArgumentsReportPath | Out-Null
    $BootstrapPatchReport = Read-JsonFile -Path $BootstrapLaunchArgumentsReportPath
    if ($null -eq $BootstrapPatchReport -or $BootstrapPatchReport.result -ne 'passed')
    {
        throw "Packaged bootstrap launch-argument patch did not record a passed result."
    }
    $ValidationReport.bootstrapLaunchArguments.patchReport = $BootstrapPatchReport

    $ValidationReport.solverTable.packagedEvidence = Copy-ValidatedSolverTableToPackage `
        -SourcePath $SolverTablePath `
        -PackagedExecutablePath $PackagedExecutablePath `
        -ExpectedSizeBytes $ExpectedSolverTableSizeBytes `
        -ExpectedSha256 $ExpectedSolverTableSha256
    $ValidationReport.solverTable.result = 'passed'

    $ValidationReport.browserShell.packagedEvidence = Copy-BrowserShellToPackage `
        -SourceDirectory $BrowserShellSourceDirectory `
        -PackagedExecutablePath $PackagedExecutablePath
    $ValidationReport.browserShell.result = 'passed'

    if (-not $SkipLaunch)
    {
        if (-not (Test-Path $LaunchScriptPath))
        {
            throw "Launch script was not found at '$LaunchScriptPath'."
        }

        foreach ($SmokeMap in $ResolvedSmokeMaps)
        {
            $SmokeReportPath = Join-Path $SmokeReportDirectory (
                '{0}.json' -f (Convert-PathToken -Value $SmokeMap)
            )
            Write-Host "Smoke validating packaged classic-cube map '$SmokeMap'..."
            & $LaunchScriptPath `
                -PackageRoot $ArchiveDirectory `
                -MapUrl $SmokeMap `
                -ReportPath $SmokeReportPath `
                -UseNullRHI:$HeadlessSmoke `
                -NoSound:$HeadlessSmoke
            if ($LASTEXITCODE -ne 0)
            {
                throw "Packaged classic-cube smoke launch failed for '$SmokeMap' with exit code $LASTEXITCODE."
            }

            if (-not (Test-Path $SmokeReportPath))
            {
                throw "Packaged classic-cube smoke launch for '$SmokeMap' completed without writing the expected report '$SmokeReportPath'."
            }

            $SmokeReport = Read-JsonFile -Path $SmokeReportPath
            if ($null -eq $SmokeReport)
            {
                throw "Packaged classic-cube smoke report '$SmokeReportPath' could not be parsed."
            }

            if ($SmokeReport.result -ne 'passed')
            {
                throw "Packaged classic-cube smoke report '$SmokeReportPath' did not record a passed result."
            }

            if ($SmokeReport.mapUrl -ne $SmokeMap)
            {
                throw "Packaged classic-cube smoke report '$SmokeReportPath' targeted '$($SmokeReport.mapUrl)' instead of '$SmokeMap'."
            }

            $RuntimeArgumentEvidence = Get-BootstrapRuntimeArgumentEvidence `
                -RuntimeLogPath $SmokeReport.runtimeLogPath `
                -RuntimeDiagnosticsPath $SmokeReport.runtimeDiagnosticsPath `
                -ObservedCommandLines @($SmokeReport.processCommandLines) `
                -TraceControlListeningEndpoints @($SmokeReport.traceControlListeningEndpoints) `
                -ExpectedArguments $BootstrapLaunchArguments
            $ValidationReport.bootstrapLaunchArguments.runtimeProofs += @(
                $RuntimeArgumentEvidence
            )
            if ($RuntimeArgumentEvidence.result -ne 'passed')
            {
                throw $RuntimeArgumentEvidence.error
            }

            $ValidationReport.smokeReports += @($SmokeReport)
        }

        $ValidationReport.bootstrapLaunchArguments.result = 'passed'
    }
    else
    {
        $ValidationReport.bootstrapLaunchArguments.result = 'patch-passed-runtime-not-run'
    }

    $ValidationReport.result = 'passed'
}
catch
{
    $ValidationReport.error = $_.Exception.Message
    Write-Utf8JsonFile -Path $ValidationReportPath -Value $ValidationReport
    throw
}

Write-Utf8JsonFile -Path $ValidationReportPath -Value $ValidationReport
