param(
    [string]$PackageRoot = 'C:\HyperTwist\packaged\desktop',
    [string]$MapUrl = '',
    [int]$SmokeSeconds = 30,
    [int]$ResX = 1600,
    [int]$ResY = 900,
    [string]$ReportPath = '',
    [switch]$KeepRunning,
    [switch]$UseNullRHI,
    [switch]$NoSound,
    [switch]$RequireStartupDiagnostics,
    [string]$ExpectedStartupDiagnosticsResult = ''
)

$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 8
    $Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::WriteAllText($Path, $Json, $Utf8NoBom)
}

function Resolve-StartupDiagnosticsLogPath {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ResolvedPackageRoot,
        [Parameter(Mandatory = $true)]
        [DateTime]$NotBeforeUtc,
        [int]$TimeoutSeconds = 8,
        [int]$PollMilliseconds = 250
    )

    $CandidateDirectories = @(
        (Join-Path $ResolvedPackageRoot 'Saved\Logs'),
        (Join-Path $ResolvedPackageRoot 'Windows\Saved\Logs'),
        (Join-Path $ResolvedPackageRoot 'Windows\UnrealHyperTwist\Saved\Logs'),
        (Join-Path $env:LOCALAPPDATA 'UnrealHyperTwist\Saved\Logs')
    ) | Select-Object -Unique

    $CandidatePaths = @(
        (Join-Path $ResolvedPackageRoot 'Saved\Logs\HyperTwistFirstRunLaunch-latest.log'),
        (Join-Path $ResolvedPackageRoot 'Windows\Saved\Logs\HyperTwistFirstRunLaunch-latest.log'),
        (Join-Path $ResolvedPackageRoot 'Windows\UnrealHyperTwist\Saved\Logs\HyperTwistFirstRunLaunch-latest.log'),
        (Join-Path $env:LOCALAPPDATA 'UnrealHyperTwist\Saved\Logs\HyperTwistFirstRunLaunch-latest.log')
    ) | Select-Object -Unique

    $DeadlineUtc = [DateTime]::UtcNow.AddSeconds([Math]::Max(0, $TimeoutSeconds))
    do
    {
        foreach ($CandidatePath in $CandidatePaths)
        {
            if ((Test-Path -LiteralPath $CandidatePath) `
                -and (Get-Item -LiteralPath $CandidatePath).LastWriteTimeUtc -ge $NotBeforeUtc.AddSeconds(-2))
            {
                return $CandidatePath
            }
        }

        foreach ($CandidateDirectory in $CandidateDirectories)
        {
            if (-not (Test-Path -LiteralPath $CandidateDirectory))
            {
                continue
            }

            $NewestDiagnosticsLog = Get-ChildItem -LiteralPath $CandidateDirectory -Filter 'HyperTwistFirstRunLaunch*.log' -File -ErrorAction SilentlyContinue |
                Where-Object { $_.LastWriteTimeUtc -ge $NotBeforeUtc.AddSeconds(-2) } |
                Sort-Object LastWriteTimeUtc -Descending |
                Select-Object -First 1
            if ($null -ne $NewestDiagnosticsLog)
            {
                return $NewestDiagnosticsLog.FullName
            }
        }

        if ([DateTime]::UtcNow -ge $DeadlineUtc)
        {
            break
        }

        Start-Sleep -Milliseconds ([Math]::Max(50, $PollMilliseconds))
    }
    while ($true)

    return $null
}

function Get-OwnedPackageProcessIds {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ResolvedPackageRoot,
        [int[]]$ExcludedProcessIds = @()
    )

    return @(
        Get-OwnedPackageProcessRecords `
            -ResolvedPackageRoot $ResolvedPackageRoot `
            -ExcludedProcessIds $ExcludedProcessIds |
            Select-Object -ExpandProperty ProcessId
    )
}

function Get-OwnedPackageProcessRecords {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ResolvedPackageRoot,
        [int[]]$ExcludedProcessIds = @()
    )

    $RootPrefix = $ResolvedPackageRoot.TrimEnd(
        [System.IO.Path]::DirectorySeparatorChar,
        [System.IO.Path]::AltDirectorySeparatorChar
    ) + [System.IO.Path]::DirectorySeparatorChar

    return @(
        Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
            Where-Object {
                ([string]$_.Name) -like 'UnrealHyperTwist*.exe' `
                    -and -not [string]::IsNullOrWhiteSpace([string]$_.ExecutablePath) `
                    -and ([string]$_.ExecutablePath).StartsWith(
                        $RootPrefix,
                        [System.StringComparison]::OrdinalIgnoreCase
                    ) `
                    -and $ExcludedProcessIds -notcontains [int]$_.ProcessId
            }
    )
}

function Get-OwnedListeningTcpEndpoints {
    param(
        [int[]]$ProcessIds
    )

    if ($ProcessIds.Count -eq 0)
    {
        return @()
    }

    return @(
        Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
            Where-Object { $ProcessIds -contains [int]$_.OwningProcess } |
            Sort-Object OwningProcess, LocalPort |
            ForEach-Object {
                [pscustomobject][ordered]@{
                    processId = [int]$_.OwningProcess
                    localAddress = [string]$_.LocalAddress
                    localPort = [int]$_.LocalPort
                }
            }
    )
}

function Stop-OwnedPackageProcesses {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ResolvedPackageRoot,
        [int[]]$ExcludedProcessIds = @()
    )

    $StoppedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'
    for ($Attempt = 0; $Attempt -lt 5; $Attempt += 1)
    {
        $OwnedProcessIds = @(
            Get-OwnedPackageProcessIds `
                -ResolvedPackageRoot $ResolvedPackageRoot `
                -ExcludedProcessIds $ExcludedProcessIds
        )
        if ($OwnedProcessIds.Count -eq 0)
        {
            break
        }

        foreach ($OwnedProcessId in $OwnedProcessIds)
        {
            Stop-Process -Id $OwnedProcessId -Force -ErrorAction SilentlyContinue
            [void]$StoppedProcessIds.Add([int]$OwnedProcessId)
        }
        Start-Sleep -Milliseconds 200
    }

    return @($StoppedProcessIds)
}

function Get-FatalRuntimeLogMatches {
    param(
        [Parameter(Mandatory = $true)]
        [string]$RuntimeLogPath
    )

    if (-not (Test-Path -LiteralPath $RuntimeLogPath))
    {
        return @()
    }

    $FatalPatterns = @(
        'Fatal error:',
        'LowLevelFatalError',
        'Assertion failed:',
        'Critical error:',
        'appError called:',
        'DXGI_ERROR_NOT_CURRENTLY_AVAILABLE',
        'CreateSwapChainResult failed',
        'Unhandled Exception:',
        'StaticShutdownAfterError'
    )

    return @(Get-Content -LiteralPath $RuntimeLogPath | Where-Object {
        $Line = $_
        foreach ($Pattern in $FatalPatterns)
        {
            if ($Line -like "*$Pattern*")
            {
                return $true
            }
        }

        return $false
    })
}

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

$LogDirectory = Join-Path $PackageRoot 'validation\logs'
New-Item -ItemType Directory -Force -Path $LogDirectory | Out-Null
$RuntimeLogPath = Join-Path $LogDirectory 'desktop-runtime.log'
$RuntimeDiagnosticsPath = Join-Path $LogDirectory 'desktop-runtime.hyperdiagnostics.log'
Remove-Item -LiteralPath $RuntimeLogPath -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $RuntimeDiagnosticsPath -Force -ErrorAction SilentlyContinue

$ArgumentList = @(
    "-ResX=$ResX",
    "-ResY=$ResY",
    '-windowed',
    '-log',
    '-FORCELOGFLUSH',
    "-abslog=$RuntimeLogPath",
    "-HyperTwistDiagnosticsLog=`"$RuntimeDiagnosticsPath`""
)

if ($UseNullRHI)
{
    $ArgumentList += '-NullRHI'
}

if ($NoSound)
{
    $ArgumentList += '-nosound'
}

if (-not [string]::IsNullOrWhiteSpace($MapUrl))
{
    $ArgumentList = @($MapUrl) + $ArgumentList
}

Write-Host "Launching packaged HyperTwist desktop experience from '$ExecutablePath'..."
$ResolvedPackageRoot = (Resolve-Path -LiteralPath $PackageRoot).Path
$ExistingPackageProcessIds = @(
    Get-OwnedPackageProcessIds -ResolvedPackageRoot $ResolvedPackageRoot
)
$GeneratedAtUtc = [DateTime]::UtcNow.ToString('o')
$Process = $null
$Report = [ordered]@{
    reportVersion = 'ht-desktop-package-launch-surface/v2'
    generatedAtUtc = $GeneratedAtUtc
    packageRoot = $ResolvedPackageRoot
    executablePath = $ExecutablePath
    mapUrl = $MapUrl
    runtimeLogPath = $RuntimeLogPath
    runtimeLogExists = $false
    runtimeDiagnosticsPath = $RuntimeDiagnosticsPath
    runtimeDiagnosticsExists = $false
    runtimeDiagnosticsInitialized = $false
    detectedFatalLogLines = @()
    smokeSeconds = $SmokeSeconds
    resolution = [ordered]@{
        width = $ResX
        height = $ResY
    }
    keepRunning = [bool]$KeepRunning
    useNullRhi = [bool]$UseNullRHI
    noSound = [bool]$NoSound
    requireStartupDiagnostics = [bool]$RequireStartupDiagnostics
    expectedStartupDiagnosticsResult = $ExpectedStartupDiagnosticsResult
    startupDiagnosticsLogPath = $null
    startupDiagnosticsLogExists = $false
    startupDiagnosticsTail = @()
    startupDiagnosticsResult = $null
    result = 'failed'
    processId = $null
    processIds = @()
    processCommandLines = @()
    ownedListeningTcpEndpoints = @()
    traceControlListenerLines = @()
    traceControlListeningEndpoints = @()
    processStopped = $false
    stoppedProcessIds = @()
    exitCode = $null
    error = $null
}

try
{
    $LaunchStartedAtUtc = [DateTime]::UtcNow
    $Process = Start-Process -FilePath $ExecutablePath -ArgumentList $ArgumentList -PassThru
    Start-Sleep -Seconds $SmokeSeconds

    $Process.Refresh()
    $Report.processId = $Process.Id
    $OwnedProcessRecords = @(
        Get-OwnedPackageProcessRecords `
            -ResolvedPackageRoot $ResolvedPackageRoot `
            -ExcludedProcessIds $ExistingPackageProcessIds
    )
    $Report.processIds = @($OwnedProcessRecords | Select-Object -ExpandProperty ProcessId)
    $Report.processCommandLines = @(
        $OwnedProcessRecords |
            ForEach-Object { [string]$_.CommandLine } |
            Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
    )
    $Report.ownedListeningTcpEndpoints = @(
        Get-OwnedListeningTcpEndpoints -ProcessIds $Report.processIds
    )
    $Report.traceControlListeningEndpoints = @(
        $Report.ownedListeningTcpEndpoints |
            Where-Object { [int]$_.localPort -eq 1985 }
    )

    Start-Sleep -Milliseconds 500
    $Report.runtimeLogExists = Test-Path -LiteralPath $RuntimeLogPath
    $Report.runtimeDiagnosticsExists = Test-Path -LiteralPath $RuntimeDiagnosticsPath
    $Report.detectedFatalLogLines = @(
        @(Get-FatalRuntimeLogMatches -RuntimeLogPath $RuntimeLogPath) +
        @(Get-FatalRuntimeLogMatches -RuntimeLogPath $RuntimeDiagnosticsPath)
    )
    $CurrentRuntimeLogLines = @(
        if ($Report.runtimeLogExists)
        {
            Get-Content -LiteralPath $RuntimeLogPath
        }
    )
    $CurrentRuntimeDiagnosticsLines = @(
        if ($Report.runtimeDiagnosticsExists)
        {
            Get-Content -LiteralPath $RuntimeDiagnosticsPath
        }
    )
    $CurrentRuntimeEvidenceLines = @(
        $CurrentRuntimeLogLines + $CurrentRuntimeDiagnosticsLines
    )
    $Report.runtimeDiagnosticsInitialized = @(
        $CurrentRuntimeDiagnosticsLines |
            Where-Object {
                $_.ToString().IndexOf(
                    '[Process] HyperTwist runtime module initialized.',
                    [System.StringComparison]::OrdinalIgnoreCase
                ) -ge 0
            }
    ).Count -gt 0
    $Report.traceControlListenerLines = @(
        $CurrentRuntimeEvidenceLines |
            Where-Object {
                $_.ToString().IndexOf(
                    'Control listening on port',
                    [System.StringComparison]::OrdinalIgnoreCase
                ) -ge 0
            } |
            ForEach-Object { $_.ToString() }
    )
    $StartupDiagnosticsLogPath = Resolve-StartupDiagnosticsLogPath `
        -ResolvedPackageRoot $ResolvedPackageRoot `
        -NotBeforeUtc $LaunchStartedAtUtc
    $Report.startupDiagnosticsLogPath = $StartupDiagnosticsLogPath
    $Report.startupDiagnosticsLogExists = $null -ne $StartupDiagnosticsLogPath
    if ($Report.startupDiagnosticsLogExists)
    {
        $Report.startupDiagnosticsTail = @(
            Get-Content -LiteralPath $StartupDiagnosticsLogPath -Tail 40 |
                ForEach-Object { $_.ToString() }
        )
        $Report.startupDiagnosticsResult = [string](
            $Report.startupDiagnosticsTail |
                Where-Object { $_ -like 'result=*' } |
                Select-Object -Last 1
        )
    }

    if (-not $Report.runtimeDiagnosticsExists)
    {
        throw 'Packaged desktop launch did not write its direct HyperTwist diagnostics log.'
    }
    if (-not $Report.runtimeDiagnosticsInitialized)
    {
        throw 'Packaged desktop launch did not record direct HyperTwist runtime initialization.'
    }
    if ($Report.detectedFatalLogLines.Count -gt 0)
    {
        throw (
            "Packaged desktop runtime diagnostics recorded fatal startup evidence: " +
            ($Report.detectedFatalLogLines -join ' | ')
        )
    }
    if ($Report.traceControlListenerLines.Count -gt 0 `
        -or $Report.traceControlListeningEndpoints.Count -gt 0)
    {
        throw 'Packaged desktop runtime opened an Unreal trace-control listener.'
    }

    if ($RequireStartupDiagnostics -and -not $Report.startupDiagnosticsLogExists)
    {
        throw "Packaged desktop launch did not write HyperTwistFirstRunLaunch-latest.log."
    }

    if (-not [string]::IsNullOrWhiteSpace($ExpectedStartupDiagnosticsResult) `
        -and $Report.startupDiagnosticsResult -ne "result=$ExpectedStartupDiagnosticsResult")
    {
        throw (
            "Expected startup diagnostics result '$ExpectedStartupDiagnosticsResult' but observed " +
            "'$($Report.startupDiagnosticsResult)'."
        )
    }

    if ($Report.processIds.Count -eq 0)
    {
        if ($Process.HasExited)
        {
            $Report.exitCode = $Process.ExitCode
        }
        throw 'Packaged desktop executable exited before the smoke interval completed.'
    }

    $Report.result = 'passed'
}
catch
{
    $Report.error = $_.Exception.Message
    if ($null -ne $Process -and $Process.HasExited)
    {
        $Report.exitCode = $Process.ExitCode
    }
    if (-not $KeepRunning)
    {
        $Report.stoppedProcessIds = @(
            Stop-OwnedPackageProcesses `
                -ResolvedPackageRoot $ResolvedPackageRoot `
                -ExcludedProcessIds $ExistingPackageProcessIds
        )
        $Report.processStopped = $Report.stoppedProcessIds.Count -gt 0
    }

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

    throw
}

Write-Host "Packaged desktop smoke launch succeeded (PID $($Process.Id))."
if (-not $KeepRunning)
{
    $Report.stoppedProcessIds = @(
        Stop-OwnedPackageProcesses `
            -ResolvedPackageRoot $ResolvedPackageRoot `
            -ExcludedProcessIds $ExistingPackageProcessIds
    )
    $Report.processStopped = $Report.stoppedProcessIds.Count -gt 0
    Write-Host 'Stopped the complete packaged desktop smoke process tree after successful launch validation.'
}

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