param(
    [string]$ProjectRoot = 'C:\HyperTwist',
    [string]$ArchiveDirectory = 'C:\HyperTwist\packaged\xr',
    [ValidateSet('Development', 'Shipping')]
    [string]$Configuration = 'Development',
    [string]$CookMap = '/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining',
    [string[]]$AdditionalCookMaps = @('/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining'),
    [string[]]$SmokeMaps = @(),
    [double]$ObservationSeconds = 8.0,
    [double]$TimeoutSeconds = 30.0,
    [ValidateSet('nullrhi', 'windowed')]
    [string]$RuntimeLaunchMode = 'nullrhi',
    [string[]]$AdditionalCookerOptions = @(
        '-DisablePlugins=MovieRenderPipeline',
        '-SkipCookingEditorContent'
    ),
    [string]$ValidationReportPath = '',
    [switch]$CleanArchive,
    [switch]$SkipBuild,
    [switch]$PrePackageEditorBuildPerformed,
    [ValidateSet('not-run', 'passed')]
    [string]$PrePackageEditorBuildResult = 'not-run',
    [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-HyperTwistXrPackage.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
$ValidationRootPath = Join-Path $ArchiveDirectory 'validation'
$SmokeReportDirectory = Join-Path $ValidationRootPath 'smoke'

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 12
    $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-JsonPropertyValue {
    param(
        [Parameter(Mandatory = $true)]
        [object]$Object,
        [Parameter(Mandatory = $true)]
        [string[]]$Names,
        [object]$Default = $null
    )

    foreach ($Name in $Names)
    {
        $Property = $Object.PSObject.Properties[$Name]
        if ($null -ne $Property)
        {
            return $Property.Value
        }
    }

    return $Default
}

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 XR packaged-validation cook map '$GameMapPath' was not found at '$ExpectedMapPath'."
    }
}

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'."
}

foreach ($TargetMap in ($CookMaps + $ResolvedSmokeMaps | Select-Object -Unique))
{
    Assert-CookMapExists -RootPath $ProjectRoot -GameMapPath $TargetMap
}

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

if ($PrePackageEditorBuildPerformed -and $PrePackageEditorBuildResult -ne 'passed')
{
    throw "PrePackageEditorBuildPerformed was set, but PrePackageEditorBuildResult was '$PrePackageEditorBuildResult' instead of 'passed'."
}

if (-not $PrePackageEditorBuildPerformed -and $PrePackageEditorBuildResult -eq 'passed')
{
    throw "PrePackageEditorBuildResult was 'passed', but PrePackageEditorBuildPerformed was not set."
}

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 'xr-package-validation-report.json'
}

$RunUatArguments = @(
    'BuildCookRun',
    "-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)
{
    $RunUatArguments += "-AdditionalCookerOptions=$($ResolvedAdditionalCookerOptions -join ' ')"
}

$ValidationReport = [ordered]@{
    reportVersion = 'ht-xr-package-validation/v1'
    generatedAtUtc = [DateTime]::UtcNow.ToString('o')
    projectRoot = $ProjectRoot
    archiveDirectory = $ArchiveDirectory
    configuration = $Configuration
    cookMaps = @($CookMaps)
    smokeMaps = @($ResolvedSmokeMaps)
    observationSeconds = $ObservationSeconds
    timeoutSeconds = $TimeoutSeconds
    runtimeLaunchMode = $RuntimeLaunchMode
    validationLaunchSurfaceId = 'xr/openxr-desktop-training-packaged-validation-launch-surface'
    validationGameModeClassPath = '/Script/UnrealHyperTwist.HyperTwistXrTrainingGameMode'
    additionalCookerOptions = @($ResolvedAdditionalCookerOptions)
    cleanArchive = [bool]$CleanArchive
    skipBuild = [bool]$SkipBuild
    prePackageEditorBuild = [ordered]@{
        lane = 'Windows Unreal editor build'
        performed = [bool]$PrePackageEditorBuildPerformed
        result = $PrePackageEditorBuildResult
    }
    skipLaunch = [bool]$SkipLaunch
    result = 'failed'
    packagedExecutablePath = $null
    smokeReports = @()
    packagedHeadMountedDisplaySessionObserved = $false
    packagedControllerInputObserved = $false
    packagedProofStatusSet = @()
    remainingGateIds = @()
    error = $null
}

try
{
    Write-Host "Packaging HyperTwist XR 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

    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 XR map '$SmokeMap'..."
            & $LaunchScriptPath `
                -PackageRoot $ArchiveDirectory `
                -MapAssetPath $SmokeMap `
                -ObservationSeconds $ObservationSeconds `
                -TimeoutSeconds $TimeoutSeconds `
                -RuntimeLaunchMode $RuntimeLaunchMode `
                -ReportPath $SmokeReportPath
            if ($LASTEXITCODE -ne 0)
            {
                throw "Packaged XR smoke launch failed for '$SmokeMap' with exit code $LASTEXITCODE."
            }

            if (-not (Test-Path $SmokeReportPath))
            {
                throw "Packaged XR smoke report '$SmokeReportPath' was not written for '$SmokeMap'."
            }

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

            if ($SmokeReport.result -ne 'passed')
            {
                throw "Packaged XR smoke report '$SmokeReportPath' recorded result '$($SmokeReport.result)' for '$SmokeMap'."
            }

            if ($SmokeReport.mapAssetPath -ne $SmokeMap)
            {
                throw "Packaged XR smoke report '$SmokeReportPath' surfaced map '$($SmokeReport.mapAssetPath)' instead of '$SmokeMap'."
            }

            if ($null -eq $SmokeReport.runtimeReport)
            {
                throw "Packaged XR smoke report '$SmokeReportPath' did not retain the nested runtime report."
            }

            $RuntimeReport = $SmokeReport.runtimeReport
            $ValidationReport.packagedHeadMountedDisplaySessionObserved =
                $ValidationReport.packagedHeadMountedDisplaySessionObserved -or [bool](Get-JsonPropertyValue `
                    -Object $RuntimeReport `
                    -Names @('bObservedHeadMountedDisplaySession', 'ObservedHeadMountedDisplaySession') `
                    -Default $false)
            $ValidationReport.packagedControllerInputObserved =
                $ValidationReport.packagedControllerInputObserved -or [bool](Get-JsonPropertyValue `
                    -Object $RuntimeReport `
                    -Names @('bObservedControllerInputActivity', 'ObservedControllerInputActivity') `
                    -Default $false)
            $ValidationReport.smokeReports += $SmokeReport
            $ValidationReport.packagedProofStatusSet += [string]$RuntimeReport.PackagedProofStatus
            foreach ($RemainingGateId in @((Get-JsonPropertyValue `
                -Object $RuntimeReport `
                -Names @('RemainingGateIds', 'remainingGateIds') `
                -Default @())))
            {
                if (-not [string]::IsNullOrWhiteSpace($RemainingGateId))
                {
                    $ValidationReport.remainingGateIds += $RemainingGateId
                }
            }
        }
    }

    $ValidationReport.packagedProofStatusSet =
        @($ValidationReport.packagedProofStatusSet | Select-Object -Unique)
    $ValidationReport.remainingGateIds =
        @($ValidationReport.remainingGateIds | Select-Object -Unique)
    $ValidationReport.result = 'passed'
}
catch
{
    $ValidationReport.error = $_.Exception.Message
    Write-Utf8JsonFile -Path $ValidationReportPath -Value $ValidationReport
    throw
}

Write-Utf8JsonFile -Path $ValidationReportPath -Value $ValidationReport
