param(
    [string]$PackageRoot = 'C:\HyperTwist\packaged\xr',
    [string]$MapAssetPath = '/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining',
    [double]$ObservationSeconds = 8.0,
    [double]$TimeoutSeconds = 30.0,
    [ValidateSet('nullrhi', 'windowed')]
    [string]$RuntimeLaunchMode = 'nullrhi',
    [int]$ResX = 1600,
    [int]$ResY = 900,
    [string]$OpenXrRuntimePathOverride = '',
    [string]$ReportPath = '',
    [switch]$KeepRunning
)

$ErrorActionPreference = 'Stop'

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 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 Get-CurrentSessionId {
    $CurrentProcess = Get-Process -Id $PID -ErrorAction Stop
    return [int]$CurrentProcess.SessionId
}

function Get-ExplorerSessionIds {
    return @(
        Get-Process explorer -ErrorAction SilentlyContinue |
            Select-Object -ExpandProperty SessionId -Unique
    )
}

function Get-OpenXrActiveRuntimePath {
    $RegistryPaths = @(
        'HKLM:\SOFTWARE\Khronos\OpenXR\1',
        'HKLM:\SOFTWARE\WOW6432Node\Khronos\OpenXR\1'
    )

    foreach ($RegistryPath in $RegistryPaths)
    {
        if (-not (Test-Path -LiteralPath $RegistryPath))
        {
            continue
        }

        try
        {
            $RegistryItem = Get-ItemProperty -LiteralPath $RegistryPath
            if (-not [string]::IsNullOrWhiteSpace($RegistryItem.ActiveRuntime))
            {
                return [string]$RegistryItem.ActiveRuntime
            }
        }
        catch
        {
            continue
        }
    }

    return $null
}

function Get-ResolvedOpenXrRuntime {
    param(
        [string]$RuntimePathOverride = ''
    )

    $RegistryRuntimePath = Get-OpenXrActiveRuntimePath
    $ResolvedOverridePath = if ([string]::IsNullOrWhiteSpace($RuntimePathOverride))
    {
        $null
    }
    else
    {
        [string]$RuntimePathOverride
    }

    if (-not [string]::IsNullOrWhiteSpace($ResolvedOverridePath))
    {
        return [ordered]@{
            activeRuntimePath = $ResolvedOverridePath
            runtimeSource = 'environment-override'
            runtimePresent = [bool](Test-Path -LiteralPath $ResolvedOverridePath)
            registryRuntimePath = $RegistryRuntimePath
            environmentOverridePath = $ResolvedOverridePath
        }
    }

    return [ordered]@{
        activeRuntimePath = $RegistryRuntimePath
        runtimeSource = 'registry'
        runtimePresent =
            -not [string]::IsNullOrWhiteSpace($RegistryRuntimePath) `
            -and [bool](Test-Path -LiteralPath $RegistryRuntimePath)
        registryRuntimePath = $RegistryRuntimePath
        environmentOverridePath = $null
    }
}

function Get-XrWindowedPreflight {
    param(
        [string]$RuntimePathOverride = ''
    )

    $CurrentSessionId = Get-CurrentSessionId
    $ExplorerSessionIds = @(Get-ExplorerSessionIds)
    $ResolvedOpenXrRuntime = Get-ResolvedOpenXrRuntime -RuntimePathOverride $RuntimePathOverride

    return [ordered]@{
        currentSessionId = $CurrentSessionId
        explorerSessionIds = @($ExplorerSessionIds)
        isInteractiveDesktopSession = [bool]($ExplorerSessionIds -contains $CurrentSessionId)
        openXrActiveRuntimePath = $ResolvedOpenXrRuntime.activeRuntimePath
        openXrActiveRuntimePresent = [bool]$ResolvedOpenXrRuntime.runtimePresent
        openXrRuntimeSource = $ResolvedOpenXrRuntime.runtimeSource
        openXrRegistryRuntimePath = $ResolvedOpenXrRuntime.registryRuntimePath
        openXrEnvironmentOverridePath = $ResolvedOpenXrRuntime.environmentOverridePath
    }
}

function Assert-XrWindowedPreflight {
    param(
        [Parameter(Mandatory = $true)]
        [object]$Preflight
    )

    if (-not [bool]$Preflight.isInteractiveDesktopSession)
    {
        $ExplorerSessionDisplay = if ($null -ne $Preflight.explorerSessionIds `
            -and $Preflight.explorerSessionIds.Count -gt 0)
        {
            ($Preflight.explorerSessionIds -join ', ')
        }
        else
        {
            'none'
        }

        throw (
            "Windowed XR validation must be launched from the interactive Windows desktop session. " +
            "The current PowerShell session id '{0}' did not match Explorer desktop session ids '{1}'. " +
            "Re-run on the logged-in Windows desktop instead of the reverse-SSH/OpenSSH session."
        ) -f $Preflight.currentSessionId, $ExplorerSessionDisplay
    }

    if (-not [bool]$Preflight.openXrActiveRuntimePresent)
    {
        $RuntimeSource = [string](Get-JsonPropertyValue `
            -Object $Preflight `
            -Names @('openXrRuntimeSource', 'OpenXrRuntimeSource') `
            -Default 'registry')

        if ([string]::IsNullOrWhiteSpace([string]$Preflight.openXrActiveRuntimePath))
        {
            if ($RuntimeSource -eq 'environment-override')
            {
                throw (
                    "Windowed XR validation received an OpenXR runtime override request, but the override path was empty. " +
                    "Provide a full runtime manifest path or remove the override and activate a local OpenXR runtime."
                )
            }

            throw (
                "Windowed XR validation requires an installed active OpenXR runtime, but no OpenXR ActiveRuntime registry entry was found. " +
                "Activate SteamVR, Oculus, Windows Mixed Reality, or another local OpenXR runtime on the interactive desktop and rerun."
            )
        }

        if ($RuntimeSource -eq 'environment-override')
        {
            throw (
                "Windowed XR validation received OpenXR runtime override path '{0}', but that manifest was not present on disk. " +
                "Repair the override path or remove it and reactivate a local OpenXR runtime."
            ) -f $Preflight.openXrActiveRuntimePath
        }

        throw (
            "Windowed XR validation requires an installed active OpenXR runtime, but the configured ActiveRuntime path '{0}' was not present on disk. " +
            "Repair or reactivate the local OpenXR runtime on the interactive desktop and rerun."
        ) -f $Preflight.openXrActiveRuntimePath
    }
}

function Format-ProcessArgument {
    param(
        [AllowNull()]
        [string]$Value
    )

    if ($null -eq $Value)
    {
        return '""'
    }

    if ($Value -notmatch '[\s"]')
    {
        return $Value
    }

    $EscapedValue = $Value -replace '(\\*)"', '$1$1\"'
    $EscapedValue = $EscapedValue -replace '(\\+)$', '$1$1'
    return '"' + $EscapedValue + '"'
}

function Start-ObservedProcess {
    param(
        [Parameter(Mandatory = $true)]
        [string]$FilePath,
        [Parameter(Mandatory = $true)]
        [string[]]$ArgumentList,
        [Parameter(Mandatory = $true)]
        [string]$WorkingDirectory,
        [string]$OpenXrRuntimePathOverride = ''
    )

    if ([string]::IsNullOrWhiteSpace($OpenXrRuntimePathOverride))
    {
        return Start-Process `
            -FilePath $FilePath `
            -ArgumentList $ArgumentList `
            -WorkingDirectory $WorkingDirectory `
            -PassThru
    }

    $ProcessStartInfo = New-Object System.Diagnostics.ProcessStartInfo
    $ProcessStartInfo.FileName = $FilePath
    $ProcessStartInfo.Arguments = (
        @($ArgumentList) |
        ForEach-Object { Format-ProcessArgument -Value ([string]$_) }
    ) -join ' '
    $ProcessStartInfo.WorkingDirectory = $WorkingDirectory
    $ProcessStartInfo.UseShellExecute = $false
    $ProcessStartInfo.EnvironmentVariables['XR_RUNTIME_JSON'] = $OpenXrRuntimePathOverride

    return [System.Diagnostics.Process]::Start($ProcessStartInfo)
}

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

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

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

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

$ExecutablePath = $CandidateExecutablePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($null -eq $ExecutablePath)
{
    throw "No packaged UnrealHyperTwist executable was found beneath '$PackageRoot'."
}

$ValidationGameModeClassPath = '/Script/UnrealHyperTwist.HyperTwistXrTrainingGameMode'
$ResolvedMapUrl = '{0}?game={1}' -f $MapAssetPath, $ValidationGameModeClassPath
$ResolvedPackageRoot = (Resolve-Path -LiteralPath $PackageRoot).Path
$GeneratedAtUtc = [DateTime]::UtcNow.ToString('o')
$SanitizedMapToken = Convert-PathToken -Value $MapAssetPath
$ResolvedRuntimeLaunchMode = if ([string]::IsNullOrWhiteSpace($RuntimeLaunchMode))
{
    'nullrhi'
}
else
{
    $RuntimeLaunchMode
}
$ReportLeafBase = [System.IO.Path]::GetFileNameWithoutExtension($ReportPath)
$RuntimeReportPath = if (-not [string]::IsNullOrWhiteSpace($ReportPath))
{
    Join-Path (Split-Path -Parent $ReportPath) ('{0}.runtime.json' -f $ReportLeafBase)
}
else
{
    Join-Path ([System.IO.Path]::GetTempPath()) ('hypertwist-xr-runtime-{0}.json' -f $SanitizedMapToken)
}

if (Test-Path -LiteralPath $RuntimeReportPath)
{
    Remove-Item -LiteralPath $RuntimeReportPath -Force
}

$WindowedPreflight = if ($ResolvedRuntimeLaunchMode -eq 'windowed')
{
    Get-XrWindowedPreflight -RuntimePathOverride $OpenXrRuntimePathOverride
}
else
{
    $null
}

$ArgumentList = @(
    $ResolvedMapUrl,
    '-log',
    "-HyperTwistXrValidationReportPath=$RuntimeReportPath",
    "-HyperTwistXrValidationObservationSeconds=$ObservationSeconds"
)

switch ($ResolvedRuntimeLaunchMode)
{
    'nullrhi'
    {
        $ArgumentList += @(
            '-nullrhi',
            '-unattended',
            '-nosound'
        )
        break
    }
    'windowed'
    {
        $ArgumentList += @(
            "-ResX=$ResX",
            "-ResY=$ResY",
            '-windowed'
        )
        break
    }
}

$Process = $null
$Report = [ordered]@{
    reportVersion = 'ht-xr-package-smoke/v1'
    generatedAtUtc = $GeneratedAtUtc
    packageRoot = $ResolvedPackageRoot
    executablePath = $ExecutablePath
    mapAssetPath = $MapAssetPath
    mapUrl = $ResolvedMapUrl
    runtimeLaunchMode = $ResolvedRuntimeLaunchMode
    observationSeconds = $ObservationSeconds
    timeoutSeconds = $TimeoutSeconds
    resolution = if ($ResolvedRuntimeLaunchMode -eq 'windowed')
    {
        [ordered]@{
            width = $ResX
            height = $ResY
        }
    }
    else
    {
        $null
    }
    keepRunning = [bool]$KeepRunning
    runtimeReportPath = $RuntimeReportPath
    preflight = $WindowedPreflight
    runtimeReportGraceApplied = $false
    runtimeReportGraceSeconds = if ($ResolvedRuntimeLaunchMode -eq 'windowed')
    {
        [double][Math]::Max(60.0, [Math]::Ceiling($ObservationSeconds * 0.5))
    }
    else
    {
        0.0
    }
    result = 'failed'
    processId = $null
    processStopped = $false
    exitCode = $null
    runtimeReport = $null
    error = $null
}

try
{
    if ($ResolvedRuntimeLaunchMode -eq 'windowed')
    {
        Assert-XrWindowedPreflight -Preflight $WindowedPreflight
    }

    Write-Host "Launching packaged XR validation lane from '$ExecutablePath'..."
    $Process = Start-ObservedProcess `
        -FilePath $ExecutablePath `
        -ArgumentList $ArgumentList `
        -WorkingDirectory (Split-Path -Parent $ExecutablePath) `
        -OpenXrRuntimePathOverride $OpenXrRuntimePathOverride
    $Report.processId = $Process.Id
    $Deadline = [DateTime]::UtcNow.AddSeconds([Math]::Max($TimeoutSeconds, $ObservationSeconds + 5.0))

    while ([DateTime]::UtcNow -lt $Deadline)
    {
        if (Test-Path -LiteralPath $RuntimeReportPath)
        {
            break
        }

        Start-Sleep -Milliseconds 500
        $Process.Refresh()
        if ($Process.HasExited)
        {
            $Report.exitCode = $Process.ExitCode
        }
    }

    if (-not (Test-Path -LiteralPath $RuntimeReportPath) `
        -and $ResolvedRuntimeLaunchMode -eq 'windowed' `
        -and $null -ne $Process)
    {
        $Process.Refresh()
        if (-not $Process.HasExited -and $Report.runtimeReportGraceSeconds -gt 0.0)
        {
            $Report.runtimeReportGraceApplied = $true
            $GraceDeadline = [DateTime]::UtcNow.AddSeconds($Report.runtimeReportGraceSeconds)
            while ([DateTime]::UtcNow -lt $GraceDeadline)
            {
                if (Test-Path -LiteralPath $RuntimeReportPath)
                {
                    break
                }

                Start-Sleep -Milliseconds 500
                $Process.Refresh()
                if ($Process.HasExited)
                {
                    $Report.exitCode = $Process.ExitCode
                    break
                }
            }
        }
    }

    if (-not (Test-Path -LiteralPath $RuntimeReportPath))
    {
        throw "The packaged XR runtime report was not written to '$RuntimeReportPath' before timeout."
    }

    $RuntimeReport = Read-JsonFile -Path $RuntimeReportPath
    if ($null -eq $RuntimeReport)
    {
        throw "The packaged XR runtime report '$RuntimeReportPath' could not be parsed."
    }

    $Report.runtimeReport = $RuntimeReport
    $Process.Refresh()
    if (-not $Process.HasExited)
    {
        $RemainingWaitMs = [Math]::Max(0, [int]($Deadline - [DateTime]::UtcNow).TotalMilliseconds)
        [void]$Process.WaitForExit($RemainingWaitMs)
        $Process.Refresh()
    }

    if ($Process.HasExited)
    {
        $Report.exitCode = $Process.ExitCode
    }
    elseif (-not $KeepRunning)
    {
        Stop-Process -Id $Process.Id -Force
        $Report.processStopped = $true
    }

    if ($RuntimeReport.ReportVersion -ne 'ht-xr-packaged-validation/v1')
    {
        throw "The packaged XR runtime report version '$($RuntimeReport.ReportVersion)' was unexpected."
    }

    if ($RuntimeReport.Result -ne 'passed')
    {
        throw "The packaged XR runtime report recorded Result='$($RuntimeReport.Result)' instead of 'passed'."
    }

    if ($RuntimeReport.CookMapAssetPath -ne $MapAssetPath)
    {
        throw "The packaged XR runtime report cooked '$($RuntimeReport.CookMapAssetPath)' instead of '$MapAssetPath'."
    }

    if ($RuntimeReport.LaunchMapUrl -ne $ResolvedMapUrl)
    {
        throw "The packaged XR runtime report launched '$($RuntimeReport.LaunchMapUrl)' instead of '$ResolvedMapUrl'."
    }

    if ($RuntimeReport.GameModeClassPath -ne $ValidationGameModeClassPath)
    {
        throw "The packaged XR runtime report used game mode '$($RuntimeReport.GameModeClassPath)' instead of '$ValidationGameModeClassPath'."
    }

    if ($RuntimeReport.RuntimeOwnerId -ne 'xr/openxr-desktop-training-runtime-owner')
    {
        throw "The packaged XR runtime report surfaced runtime owner '$($RuntimeReport.RuntimeOwnerId)' instead of 'xr/openxr-desktop-training-runtime-owner'."
    }

    if ($RuntimeReport.ControllerSettingsOwnerId -ne 'xr-openxr-controller-settings-owner/v1')
    {
        throw "The packaged XR runtime report surfaced controller settings owner '$($RuntimeReport.ControllerSettingsOwnerId)' instead of 'xr-openxr-controller-settings-owner/v1'."
    }

    $RuntimeOwnerStructurallyValid = [bool](Get-JsonPropertyValue `
        -Object $RuntimeReport `
        -Names @('bRuntimeOwnerStructurallyValid', 'RuntimeOwnerStructurallyValid') `
        -Default $false)
    if (-not $RuntimeOwnerStructurallyValid)
    {
        throw "The packaged XR runtime report did not keep the runtime-owner surface structurally valid."
    }

    $ControllerSettingsOwnerStructurallyValid = [bool](Get-JsonPropertyValue `
        -Object $RuntimeReport `
        -Names @('bControllerSettingsOwnerStructurallyValid', 'ControllerSettingsOwnerStructurallyValid') `
        -Default $false)
    if (-not $ControllerSettingsOwnerStructurallyValid)
    {
        throw "The packaged XR runtime report did not keep the controller-settings surface structurally valid."
    }

    if ($null -ne $Report.exitCode -and $Report.exitCode -ne 0)
    {
        throw "The packaged XR executable exited with code $($Report.exitCode)."
    }

    $Report.result = 'passed'
}
catch
{
    $Report.error = $_.Exception.Message
    if ($null -ne $Process)
    {
        $Process.Refresh()
        if ($Process.HasExited)
        {
            $Report.exitCode = $Process.ExitCode
        }
        elseif (-not $KeepRunning)
        {
            Stop-Process -Id $Process.Id -Force
            $Report.processStopped = $true
        }
    }

    if (-not [string]::IsNullOrWhiteSpace($ReportPath))
    {
        Write-Utf8JsonFile -Path $ReportPath -Value $Report
    }

    throw
}

if (-not [string]::IsNullOrWhiteSpace($ReportPath))
{
    Write-Utf8JsonFile -Path $ReportPath -Value $Report
}
