param(
    [Parameter(Mandatory = $true)]
    [string]$InlineCommand,
    [int]$TimeoutSeconds = 120,
    [string]$TaskNamePrefix = 'HyperTwist-InteractiveDesktop',
    [string]$ResultPath = '',
    [switch]$KeepArtifacts
)

$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 Convert-TaskToken {
    param(
        [string]$Value
    )

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

    return ($Value -replace '[^A-Za-z0-9_-]', '_')
}

function Get-TaskOwnedProcessIds {
    param(
        [Parameter(Mandatory = $true)]
        [string]$RunnerPath
    )

    $ProcessSnapshots = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue)
    $OwnedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'
    foreach ($ProcessSnapshot in $ProcessSnapshots)
    {
        if (-not [string]::IsNullOrWhiteSpace([string]$ProcessSnapshot.CommandLine) `
            -and ([string]$ProcessSnapshot.CommandLine).IndexOf(
                $RunnerPath,
                [System.StringComparison]::OrdinalIgnoreCase
            ) -ge 0)
        {
            [void]$OwnedProcessIds.Add([int]$ProcessSnapshot.ProcessId)
        }
    }

    $AddedProcess = $true
    while ($AddedProcess)
    {
        $AddedProcess = $false
        foreach ($ProcessSnapshot in $ProcessSnapshots)
        {
            if ($OwnedProcessIds.Contains([int]$ProcessSnapshot.ParentProcessId) `
                -and $OwnedProcessIds.Add([int]$ProcessSnapshot.ProcessId))
            {
                $AddedProcess = $true
            }
        }
    }

    return @($OwnedProcessIds)
}

if ([string]::IsNullOrWhiteSpace($InlineCommand))
{
    throw 'Provide -InlineCommand.'
}

$TaskToken = Convert-TaskToken -Value (
    '{0}-{1}' -f $TaskNamePrefix, (Get-Date).ToString('yyyyMMdd-HHmmss')
)
$TaskName = $TaskToken
$TaskRunnerPath = 'C:\Windows\Temp\{0}.runner.ps1' -f $TaskToken
$TaskResultPath = 'C:\Windows\Temp\{0}.result.json' -f $TaskToken
$EncodedInlineCommand = [Convert]::ToBase64String(
    [System.Text.Encoding]::Unicode.GetBytes($InlineCommand)
)

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

$RunnerScript = @"
`$ErrorActionPreference = 'Stop'
`$Result = [ordered]@{
    succeeded = `$false
    user = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
    processId = `$PID
    sessionId = (Get-Process -Id `$PID).SessionId
    explorerSessionIds = @((Get-Process explorer -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SessionId -Unique))
    generatedAtUtc = [DateTime]::UtcNow.ToString('o')
    outputLines = @()
    error = `$null
}
try {
    `$CommandSource = [System.Text.Encoding]::Unicode.GetString(
        [Convert]::FromBase64String('$EncodedInlineCommand')
    )
    `$CapturedOutput = @(& ([scriptblock]::Create(`$CommandSource)) 2>&1 | ForEach-Object { `$_.ToString() })
    `$Result.outputLines = @(`$CapturedOutput)
    `$Result.succeeded = `$true
} catch {
    if (`$null -ne `$CapturedOutput) {
        `$Result.outputLines = @(`$CapturedOutput)
    }
    `$Result.error = `$_.Exception.Message
}
[System.IO.File]::WriteAllText(
    '$TaskResultPath',
    (`$Result | ConvertTo-Json -Depth 12),
    (New-Object System.Text.UTF8Encoding(`$false))
)
if (-not `$Result.succeeded) {
    exit 1
}
"@

[System.IO.File]::WriteAllText(
    $TaskRunnerPath,
    $RunnerScript,
    (New-Object System.Text.UTF8Encoding($false))
)

$Result = [ordered]@{
    reportVersion = 'ht-interactive-desktop-task/v1'
    generatedAtUtc = [DateTime]::UtcNow.ToString('o')
    taskName = $TaskName
    taskRunnerPath = $TaskRunnerPath
    taskResultPath = $TaskResultPath
    timeoutSeconds = $TimeoutSeconds
    currentSessionId = (Get-Process -Id $PID).SessionId
    explorerSessionIds = @(
        Get-Process explorer -ErrorAction SilentlyContinue |
            Select-Object -ExpandProperty SessionId -Unique
    )
    createExitCode = $null
    runExitCode = $null
    taskOutput = $null
    terminatedOwnedProcessIds = @()
    result = 'failed'
    error = $null
}
$TaskCreated = $false
$FailureResultPath = $null

try
{
    $CreateArgs = @(
        '/Create', '/F',
        '/TN', $TaskName,
        '/SC', 'ONCE',
        '/ST', '23:59',
        '/TR', ('C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File {0}' -f $TaskRunnerPath),
        '/RU', 'INTERACTIVE'
    )
    & schtasks.exe @CreateArgs | Out-Null
    $Result.createExitCode = $LASTEXITCODE
    if ($Result.createExitCode -ne 0)
    {
        throw "Creating the interactive desktop task failed with exit code $($Result.createExitCode)."
    }
    $TaskCreated = $true

    & schtasks.exe /Run /TN ('\' + $TaskName) | Out-Null
    $Result.runExitCode = $LASTEXITCODE
    if ($Result.runExitCode -ne 0)
    {
        throw "Running the interactive desktop task failed with exit code $($Result.runExitCode)."
    }

    $Deadline = [DateTime]::UtcNow.AddSeconds([Math]::Max($TimeoutSeconds, 1))
    while ([DateTime]::UtcNow -lt $Deadline)
    {
        if (Test-Path -LiteralPath $TaskResultPath)
        {
            break
        }

        Start-Sleep -Milliseconds 500
    }

    if (-not (Test-Path -LiteralPath $TaskResultPath))
    {
        throw "The interactive desktop task did not write '$TaskResultPath' before timeout."
    }

    $TaskOutput = Read-JsonFile -Path $TaskResultPath
    if ($null -eq $TaskOutput)
    {
        throw "The interactive desktop task result '$TaskResultPath' could not be parsed."
    }

    $Result.taskOutput = $TaskOutput
    if (-not [bool]$TaskOutput.succeeded)
    {
        $TaskError = if ([string]::IsNullOrWhiteSpace([string]$TaskOutput.error))
        {
            'The interactive desktop task reported failure without an explicit error message.'
        }
        else
        {
            [string]$TaskOutput.error
        }

        throw $TaskError
    }

    $Result.result = 'passed'
}
catch
{
    $Result.error = $_.Exception.Message
    if ($null -eq $Result.taskOutput -and (Test-Path -LiteralPath $TaskResultPath))
    {
        $Result.taskOutput = Read-JsonFile -Path $TaskResultPath
    }
    $FailureResultPath = if ([string]::IsNullOrWhiteSpace($ResultPath))
    {
        $TaskResultPath
    }
    else
    {
        $ResultPath
    }
    Write-Utf8JsonFile -Path $FailureResultPath -Value $Result
    throw
}
finally
{
    if (-not $KeepArtifacts)
    {
        $TaskOwnedProcessIds = if ($TaskCreated)
        {
            @(Get-TaskOwnedProcessIds -RunnerPath $TaskRunnerPath)
        }
        else
        {
            @()
        }
        if ($TaskCreated)
        {
            # A timed-out interactive child must not survive as a hidden,
            # memory-consuming desktop task.
            & schtasks.exe /End /TN ('\' + $TaskName) *> $null
            if ($LASTEXITCODE -eq 0)
            {
                Start-Sleep -Milliseconds 250
            }
            & schtasks.exe /Delete /TN $TaskName /F | Out-Null
        }
        foreach ($TaskOwnedProcessId in @($TaskOwnedProcessIds | Sort-Object -Descending))
        {
            if ($TaskOwnedProcessId -ne $PID `
                -and $null -ne (Get-Process -Id $TaskOwnedProcessId -ErrorAction SilentlyContinue))
            {
                Stop-Process -Id $TaskOwnedProcessId -Force -ErrorAction SilentlyContinue
                $Result.terminatedOwnedProcessIds += [int]$TaskOwnedProcessId
            }
        }
        if (Test-Path -LiteralPath $TaskRunnerPath)
        {
            Remove-Item -LiteralPath $TaskRunnerPath -Force
        }
        if (-not [string]::IsNullOrWhiteSpace($FailureResultPath))
        {
            Write-Utf8JsonFile -Path $FailureResultPath -Value $Result
        }
    }
}

if ([string]::IsNullOrWhiteSpace($ResultPath))
{
    $ResultPath = $TaskResultPath
}

Write-Utf8JsonFile -Path $ResultPath -Value $Result
$Result | ConvertTo-Json -Depth 12
