param(
    [string]$PackageRoot = 'C:\HyperTwist_worktrees\phase10validate_packaged_alpha_release19_shipping_20260723',
    [string]$OutputDirectory = 'C:\HyperTwist_worktrees\installer-release19-20260726',
    [string]$InstallerFileName = 'HyperTwist-Alpha-Test-20260723-R19-Setup.exe',
    [string]$ProductVersion = '0.19.0.0',
    [string]$DisplayVersion = 'Alpha R19',
    [string]$ReleaseId = '20260723-R19',
    [string]$MakensisPath = 'C:\Program Files (x86)\NSIS\makensis.exe',
    [string]$VcRedistPath = '',
    [string]$ReportPath = '',
    [switch]$KeepBuildContext
)

$ErrorActionPreference = 'Stop'

$ExpectedSolverTableSizeBytes = 676207080
$ExpectedSolverTableSha256 = 'dc6de3d909a37afe2ba7f1f978543a13eae215bd20d954d66813524a8ca20034'

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 Resolve-WindowsPayloadRoot {
    param(
        [Parameter(Mandatory = $true)]
        [string]$RootPath
    )

    $ResolvedRootPath = (Resolve-Path -LiteralPath $RootPath).Path
    $WindowsCandidate = Join-Path $ResolvedRootPath 'Windows'
    if (Test-Path -LiteralPath (Join-Path $WindowsCandidate 'UnrealHyperTwist.exe') -PathType Leaf)
    {
        return $WindowsCandidate
    }

    if (Test-Path -LiteralPath (Join-Path $ResolvedRootPath 'UnrealHyperTwist.exe') -PathType Leaf)
    {
        return $ResolvedRootPath
    }

    throw "No packaged Windows payload was found beneath '$RootPath'."
}

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

    $RootPrefix = $RootPath.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
    $ResolvedFilePath = [System.IO.Path]::GetFullPath($FilePath)
    if (-not $ResolvedFilePath.StartsWith(
        $RootPrefix,
        [System.StringComparison]::OrdinalIgnoreCase))
    {
        throw "File '$ResolvedFilePath' is outside payload root '$RootPath'."
    }

    return $ResolvedFilePath.Substring($RootPrefix.Length).Replace('\', '/')
}

function Assert-RequiredPayloadFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$PayloadRoot,
        [Parameter(Mandatory = $true)]
        [string]$RelativePath,
        [Int64]$ExpectedSizeBytes = -1,
        [string]$ExpectedSha256 = ''
    )

    $RequiredPath = Join-Path $PayloadRoot $RelativePath
    if (-not (Test-Path -LiteralPath $RequiredPath -PathType Leaf))
    {
        throw "Required payload file '$RelativePath' was not found."
    }

    $RequiredItem = Get-Item -LiteralPath $RequiredPath
    if ($RequiredItem.Length -le 0)
    {
        throw "Required payload file '$RelativePath' is empty."
    }
    if ($ExpectedSizeBytes -ge 0 -and $RequiredItem.Length -ne $ExpectedSizeBytes)
    {
        throw "Required payload file '$RelativePath' has an unexpected size."
    }

    $ActualSha256 = (Get-FileHash -LiteralPath $RequiredPath -Algorithm SHA256).Hash.ToLowerInvariant()
    if (-not [string]::IsNullOrWhiteSpace($ExpectedSha256) `
        -and $ActualSha256 -ne $ExpectedSha256.ToLowerInvariant())
    {
        throw "Required payload file '$RelativePath' has an unexpected SHA-256."
    }
}

function Resolve-VcRedistPath {
    param(
        [string]$RequestedPath
    )

    if (-not [string]::IsNullOrWhiteSpace($RequestedPath))
    {
        if (-not (Test-Path -LiteralPath $RequestedPath -PathType Leaf))
        {
            throw "Requested VC++ redistributable '$RequestedPath' was not found."
        }

        return (Resolve-Path -LiteralPath $RequestedPath).Path
    }

    $CandidateRoots = @(
        'C:\Program Files\Epic Games\UE_5.7\Engine\Extras\Redist',
        'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Redist',
        'C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Redist'
    )
    $Candidates = @(
        foreach ($CandidateRoot in $CandidateRoots)
        {
            if (Test-Path -LiteralPath $CandidateRoot)
            {
                Get-ChildItem -LiteralPath $CandidateRoot `
                    -Filter 'vc_redist.x64.exe' `
                    -File `
                    -Recurse `
                    -ErrorAction SilentlyContinue
            }
        }
    )
    $Candidate = $Candidates |
        Sort-Object { [version]$_.VersionInfo.FileVersion } -Descending |
        Select-Object -First 1
    if ($null -eq $Candidate)
    {
        throw 'No Microsoft Visual C++ 2015-2022 x64 redistributable was found.'
    }

    return $Candidate.FullName
}

if (-not (Test-Path -LiteralPath $PackageRoot -PathType Container))
{
    throw "Package root '$PackageRoot' was not found."
}
if (-not (Test-Path -LiteralPath $MakensisPath -PathType Leaf))
{
    throw "NSIS compiler '$MakensisPath' was not found."
}
try
{
    $ParsedProductVersion = [version]$ProductVersion
}
catch
{
    throw "ProductVersion '$ProductVersion' is not a valid numeric Windows file version."
}
if ($ParsedProductVersion.Build -lt 0 -or $ParsedProductVersion.Revision -lt 0)
{
    throw 'ProductVersion must contain exactly four numeric components.'
}
if ($InstallerFileName -ne [System.IO.Path]::GetFileName($InstallerFileName) `
    -or [System.IO.Path]::GetExtension($InstallerFileName) -ne '.exe')
{
    throw "InstallerFileName '$InstallerFileName' must be a leaf .exe filename."
}
foreach ($MetadataValue in @($DisplayVersion, $ReleaseId))
{
    if ([string]::IsNullOrWhiteSpace($MetadataValue) `
        -or $MetadataValue -notmatch '^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$')
    {
        throw "Installer metadata value '$MetadataValue' contains unsupported characters or length."
    }
}

$ResolvedPackageRoot = (Resolve-Path -LiteralPath $PackageRoot).Path
$ResolvedPayloadRoot = Resolve-WindowsPayloadRoot -RootPath $ResolvedPackageRoot
$ResolvedOutputDirectory = [System.IO.Path]::GetFullPath($OutputDirectory)
$ResolvedInstallerPath = Join-Path $ResolvedOutputDirectory $InstallerFileName
$PayloadRootPrefix = $ResolvedPayloadRoot.TrimEnd('\') + '\'
if ($ResolvedInstallerPath.StartsWith(
    $PayloadRootPrefix,
    [System.StringComparison]::OrdinalIgnoreCase))
{
    throw 'Installer output must not be written inside its source payload.'
}
$RepositoryRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
$NsiScriptPath = Join-Path $RepositoryRoot 'packaging\windows\HyperTwistInstaller.nsi'
$ReadinessScriptPath = Join-Path $RepositoryRoot 'scripts\Test-HyperTwistWindowsReadiness.ps1'
if (-not (Test-Path -LiteralPath $NsiScriptPath -PathType Leaf))
{
    throw "Installer definition '$NsiScriptPath' was not found."
}
if (-not (Test-Path -LiteralPath $ReadinessScriptPath -PathType Leaf))
{
    throw "Readiness script '$ReadinessScriptPath' was not found."
}

$ResolvedVcRedistPath = Resolve-VcRedistPath -RequestedPath $VcRedistPath
$VcRedistSignature = Get-AuthenticodeSignature -LiteralPath $ResolvedVcRedistPath
if ($VcRedistSignature.Status -ne [System.Management.Automation.SignatureStatus]::Valid)
{
    throw "VC++ redistributable '$ResolvedVcRedistPath' does not have a valid Authenticode signature."
}

$RequiredRelativePaths = @(
    'UnrealHyperTwist.exe',
    'UnrealHyperTwist\Binaries\Win64\UnrealHyperTwist-Win64-Shipping.exe',
    'UnrealHyperTwist\Binaries\Win64\tbb12.dll',
    'UnrealHyperTwist\Binaries\Win64\tbbmalloc.dll',
    'UnrealHyperTwist\Content\Paks\UnrealHyperTwist-Windows.pak',
    'UnrealHyperTwist\Content\Paks\UnrealHyperTwist-Windows.utoc',
    'UnrealHyperTwist\Content\Paks\UnrealHyperTwist-Windows.ucas',
    'Content\Browser\index.html',
    'Content\Browser\src\browser-runtime-bootstrap.js',
    'Content\Browser\src\browser-spatial-runtime-fallback.js',
    'Content\Browser\dist\browser-spatial-runtime.js'
)
foreach ($RequiredRelativePath in $RequiredRelativePaths)
{
    Assert-RequiredPayloadFile `
        -PayloadRoot $ResolvedPayloadRoot `
        -RelativePath $RequiredRelativePath
}
Assert-RequiredPayloadFile `
    -PayloadRoot $ResolvedPayloadRoot `
    -RelativePath 'UnrealHyperTwist\Saved\twophase-ht.tbl' `
    -ExpectedSizeBytes $ExpectedSolverTableSizeBytes `
    -ExpectedSha256 $ExpectedSolverTableSha256

$ReparsePoints = @(
    Get-ChildItem -LiteralPath $ResolvedPayloadRoot -Force -Recurse |
        Where-Object {
            $_.Attributes -band [System.IO.FileAttributes]::ReparsePoint
        }
)
if ($ReparsePoints.Count -gt 0)
{
    throw (
        "The payload contains reparse points, which are not permitted in an installer source: " +
        (($ReparsePoints | Select-Object -ExpandProperty FullName) -join ', ')
    )
}

New-Item -ItemType Directory -Force -Path $ResolvedOutputDirectory | Out-Null
$ValidationDirectory = Join-Path $ResolvedOutputDirectory 'validation'
$BuildContextDirectory = Join-Path $ResolvedOutputDirectory 'build-context'
New-Item -ItemType Directory -Force -Path $ValidationDirectory | Out-Null
if (Test-Path -LiteralPath $BuildContextDirectory)
{
    Remove-Item -LiteralPath $BuildContextDirectory -Recurse -Force
}
New-Item -ItemType Directory -Force -Path $BuildContextDirectory | Out-Null

if ([string]::IsNullOrWhiteSpace($ReportPath))
{
    $ReportPath = Join-Path $ValidationDirectory 'windows-installer-build-report.json'
}
$PayloadManifestPath = Join-Path $ValidationDirectory 'windows-installer-payload-manifest.json'
$SourceReadinessReportPath = Join-Path $ValidationDirectory 'windows-installer-source-readiness-report.json'
$MakensisLogPath = Join-Path $ValidationDirectory 'makensis.log'
$InstallerIconPath = Join-Path $BuildContextDirectory 'HyperTwist.ico'

$BuildReport = [ordered]@{
    reportVersion = 'ht-windows-installer-build/v1'
    generatedAtUtc = [DateTime]::UtcNow.ToString('o')
    result = 'failed'
    releaseId = $ReleaseId
    displayVersion = $DisplayVersion
    productVersion = $ProductVersion
    packageRoot = $ResolvedPackageRoot
    payloadRoot = $ResolvedPayloadRoot
    installerPath = $ResolvedInstallerPath
    payloadManifestPath = $PayloadManifestPath
    sourceReadinessReportPath = $SourceReadinessReportPath
    makensisPath = (Resolve-Path -LiteralPath $MakensisPath).Path
    makensisLogPath = $MakensisLogPath
    vcRedist = $null
    excludedPatterns = @('*.pdb', 'Manifest_DebugFiles_Win64.txt')
    payloadFileCount = 0
    payloadSizeBytes = 0
    installerSizeBytes = 0
    installerSha256 = $null
    installerSignatureStatus = $null
    incompleteInstallerRemoved = $false
    error = $null
}

try
{
    $VcRedistItem = Get-Item -LiteralPath $ResolvedVcRedistPath
    $BuildReport.vcRedist = [pscustomobject][ordered]@{
        path = $ResolvedVcRedistPath
        version = [string]$VcRedistItem.VersionInfo.FileVersion
        sizeBytes = [Int64]$VcRedistItem.Length
        sha256 = (Get-FileHash -LiteralPath $ResolvedVcRedistPath -Algorithm SHA256).Hash.ToLowerInvariant()
        signatureStatus = $VcRedistSignature.Status.ToString()
        signer = [string]$VcRedistSignature.SignerCertificate.Subject
    }

    $PayloadFiles = @(
        Get-ChildItem -LiteralPath $ResolvedPayloadRoot -Force -File -Recurse |
            Where-Object {
                $_.Extension -ne '.pdb' `
                    -and $_.Name -ne 'Manifest_DebugFiles_Win64.txt'
            } |
            Sort-Object FullName
    )
    $PayloadManifestEntries = @(
        foreach ($PayloadFile in $PayloadFiles)
        {
            [pscustomobject][ordered]@{
                relativePath = Get-NormalizedRelativePath `
                    -RootPath $ResolvedPayloadRoot `
                    -FilePath $PayloadFile.FullName
                sizeBytes = [Int64]$PayloadFile.Length
                sha256 = (Get-FileHash -LiteralPath $PayloadFile.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
            }
        }
    )
    $PayloadSizeBytes = [Int64](($PayloadFiles | Measure-Object Length -Sum).Sum)
    $PayloadManifest = [pscustomobject][ordered]@{
        manifestVersion = 'ht-windows-installer-payload/v1'
        generatedAtUtc = [DateTime]::UtcNow.ToString('o')
        releaseId = $ReleaseId
        sourcePayloadRoot = $ResolvedPayloadRoot
        excludedPatterns = @('*.pdb', 'Manifest_DebugFiles_Win64.txt')
        fileCount = $PayloadManifestEntries.Count
        totalSizeBytes = $PayloadSizeBytes
        files = $PayloadManifestEntries
    }
    Write-Utf8JsonFile -Path $PayloadManifestPath -Value $PayloadManifest
    $BuildReport.payloadFileCount = $PayloadManifestEntries.Count
    $BuildReport.payloadSizeBytes = $PayloadSizeBytes

    $ReadinessReport = & $ReadinessScriptPath `
        -PayloadRoot $ResolvedPayloadRoot `
        -TargetInstallRoot (Join-Path ([Environment]::GetFolderPath('ProgramFiles')) 'HyperTwist') `
        -ExpectedPayloadManifestPath $PayloadManifestPath `
        -ReportPath $SourceReadinessReportPath `
        -VerifyPayloadHashes `
        -PassThru
    if ($ReadinessReport.result -eq 'blocked')
    {
        throw 'Source payload or Windows host readiness is blocked.'
    }

    Add-Type -AssemblyName System.Drawing
    $PayloadExecutablePath = Join-Path $ResolvedPayloadRoot 'UnrealHyperTwist.exe'
    $InstallerIcon = [System.Drawing.Icon]::ExtractAssociatedIcon($PayloadExecutablePath)
    if ($null -eq $InstallerIcon)
    {
        throw "No application icon could be extracted from '$PayloadExecutablePath'."
    }
    $IconStream = [System.IO.File]::Open(
        $InstallerIconPath,
        [System.IO.FileMode]::Create,
        [System.IO.FileAccess]::Write,
        [System.IO.FileShare]::None
    )
    try
    {
        $InstallerIcon.Save($IconStream)
    }
    finally
    {
        $IconStream.Dispose()
        $InstallerIcon.Dispose()
    }

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

    $PayloadSizeKilobytes = [Math]::Ceiling($PayloadSizeBytes / 1KB)
    $MakensisArguments = @(
        '/V4',
        "/DPRODUCT_VERSION=$ProductVersion",
        "/DDISPLAY_VERSION=$DisplayVersion",
        "/DRELEASE_ID=$ReleaseId",
        "/DOUTPUT_FILE=$ResolvedInstallerPath",
        "/DPAYLOAD_ROOT=$ResolvedPayloadRoot",
        "/DPAYLOAD_MANIFEST=$PayloadManifestPath",
        "/DREADINESS_SCRIPT=$ReadinessScriptPath",
        "/DVC_REDIST_PATH=$ResolvedVcRedistPath",
        "/DINSTALLER_ICON=$InstallerIconPath",
        "/DPAYLOAD_SIZE_KB=$PayloadSizeKilobytes",
        $NsiScriptPath
    )

    $MakensisOutput = @(
        & $MakensisPath @MakensisArguments 2>&1 |
            ForEach-Object {
                $Line = $_.ToString()
                [Console]::Out.WriteLine($Line)
                $Line
            }
    )
    $MakensisExitCode = $LASTEXITCODE
    [System.IO.File]::WriteAllLines(
        $MakensisLogPath,
        $MakensisOutput,
        (New-Object System.Text.UTF8Encoding($false))
    )
    if ($MakensisExitCode -ne 0)
    {
        throw "NSIS compilation failed with exit code $MakensisExitCode."
    }
    if (-not (Test-Path -LiteralPath $ResolvedInstallerPath -PathType Leaf))
    {
        throw "NSIS did not create '$ResolvedInstallerPath'."
    }

    $InstallerItem = Get-Item -LiteralPath $ResolvedInstallerPath
    if ($InstallerItem.Length -le 0)
    {
        throw 'The generated installer is empty.'
    }
    $InstallerSignature = Get-AuthenticodeSignature -LiteralPath $ResolvedInstallerPath
    $BuildReport.installerSizeBytes = [Int64]$InstallerItem.Length
    $BuildReport.installerSha256 = (Get-FileHash -LiteralPath $ResolvedInstallerPath -Algorithm SHA256).Hash.ToLowerInvariant()
    $BuildReport.installerSignatureStatus = $InstallerSignature.Status.ToString()
    $BuildReport.result = 'passed'
}
catch
{
    $BuildReport.error = $_.Exception.Message
    throw
}
finally
{
    if ($BuildReport.result -ne 'passed' `
        -and (Test-Path -LiteralPath $ResolvedInstallerPath -PathType Leaf))
    {
        Remove-Item -LiteralPath $ResolvedInstallerPath -Force
        $BuildReport.incompleteInstallerRemoved = $true
    }
    Write-Utf8JsonFile -Path $ReportPath -Value $BuildReport
    if (-not $KeepBuildContext -and (Test-Path -LiteralPath $BuildContextDirectory))
    {
        Remove-Item -LiteralPath $BuildContextDirectory -Recurse -Force
    }
}

[Console]::Out.WriteLine('HyperTwist Windows installer built successfully.')
[Console]::Out.WriteLine("Installer: $ResolvedInstallerPath")
[Console]::Out.WriteLine("SHA-256: $($BuildReport.installerSha256)")
[Console]::Out.WriteLine("Report: $([System.IO.Path]::GetFullPath($ReportPath))")
