# package-release.ps1

[CmdletBinding()]
param(
    [Parameter(Mandatory)][string]$ArtifactDirectory,
    [Parameter(Mandatory)][string]$SignedFfmpegPath,
    [Parameter(Mandatory)][string]$HardwareQualificationReport,
    [string]$WorkRoot = 'C:\nnffb',
    [string]$DependencyLockPath,
    [string]$ReleaseDirectory
)

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
Import-Module (Join-Path $PSScriptRoot 'BuildCommon.psm1') -Force

function Assert-Sha256 {
    param([string]$Path, [string]$Expected, [string]$Description)
    $actual = Get-FileSha256 $Path
    if ($actual -ne $Expected.ToLowerInvariant()) {
        throw "$Description SHA-256 mismatch: expected $Expected, got $actual."
    }
    $actual
}

function Get-LocalSevenZip {
    param($Lock, $Paths)
    $component = Get-LockComponent $Lock '7zip'
    $installer = Join-Path $Paths.downloads $component.filename
    Get-SingleResult -Context '7-Zip download' -Results @(
        Invoke-PinnedDownload -Url $component.url -OutFile $installer -Sha256 $component.sha256 -AllowedHosts $component.allowedHosts) | Out-Null
    Assert-Sha256 -Path $installer -Expected $component.sha256 -Description 'Pinned 7-Zip installer' | Out-Null

    $toolRoot = Join-Path $Paths.workRoot 'tools\7zip'
    $sevenZip = Join-Path $toolRoot '7z.exe'
    if (-not (Test-Path $sevenZip)) {
        New-Item -ItemType Directory -Force -Path $toolRoot | Out-Null
        $process = Start-Process -FilePath $installer -ArgumentList @('/S', "/D=$toolRoot") -PassThru -Wait
        if ($process.ExitCode -ne 0) { throw "Pinned 7-Zip installer failed with exit code $($process.ExitCode)." }
    }
    if (-not (Test-Path $sevenZip)) { throw "7z.exe was not installed to $sevenZip." }
    $sevenZip
}

function Assert-SingleFfmpegArchive {
    param([string]$SevenZipPath, [string]$ArchivePath)
    $listing = @(& $SevenZipPath l -slt $ArchivePath)
    if ($LASTEXITCODE -ne 0) { throw "7-Zip could not list $ArchivePath (exit $LASTEXITCODE)." }
    $separator = [Array]::IndexOf($listing, '----------')
    if ($separator -lt 0) { throw "7-Zip listing format was unexpected for $ArchivePath." }
    $entryPaths = @($listing[($separator + 1)..($listing.Count - 1)] |
        Where-Object { $_ -match '^Path = ' } |
        ForEach-Object { $_.Substring(7) })
    if ($entryPaths.Count -ne 1 -or $entryPaths[0] -ne 'ffmpeg.exe') {
        throw "Archive must contain exactly one file named ffmpeg.exe; found [$($entryPaths -join ', ')]."
    }
    & $SevenZipPath t -bd $ArchivePath | Out-Null
    if ($LASTEXITCODE -ne 0) { throw "7-Zip integrity test failed for $ArchivePath (exit $LASTEXITCODE)." }
}

function Write-TextFile {
    param([string]$Path, [string[]]$Lines)
    [System.IO.File]::WriteAllText($Path, ($Lines -join [Environment]::NewLine) + [Environment]::NewLine, (New-Object System.Text.UTF8Encoding($false)))
}

Assert-WorkRoot $WorkRoot
$paths = Get-BuildPaths $WorkRoot
$repoRoot = Get-RepoRoot
$dependencyLockPath = Get-DependencyLockPath $DependencyLockPath
$lock = Read-DependencyLock -Path $dependencyLockPath

$artifactDirectory = (Resolve-Path $ArtifactDirectory).Path
$signedFfmpegPath = (Resolve-Path $SignedFfmpegPath).Path
$hardwareQualificationReport = (Resolve-Path $HardwareQualificationReport).Path
$buildManifestPath = Join-Path $artifactDirectory 'build-manifest.json'
$unsignedFfmpegPath = Join-Path $artifactDirectory 'ffmpeg.exe'
$testReportPath = Join-Path $artifactDirectory 'test-report.json'
$verifyResultPath = Join-Path $artifactDirectory 'verify-binary-result.json'

foreach ($requiredFile in $buildManifestPath, $unsignedFfmpegPath, $testReportPath, $verifyResultPath) {
    if (-not (Test-Path $requiredFile)) { throw "Required verified build record is missing: $requiredFile" }
}

$buildManifest = Get-Content $buildManifestPath -Raw | ConvertFrom-Json
if (-not (Test-ObjectProperty $buildManifest 'status') -or $buildManifest.status -ne 'automated-tests-passed') {
    throw 'The build artifact is not marked automated-tests-passed. Rebuild without -SkipTests before packaging.'
}
Assert-Sha256 -Path $unsignedFfmpegPath -Expected $buildManifest.unsignedSha256 -Description 'Unsigned artifact ffmpeg.exe' | Out-Null

$testReport = Get-Content $testReportPath -Raw | ConvertFrom-Json
if ($testReport.overallResult -ne 'passed') { throw 'The copied automated test report is not passed.' }
$verifyResult = Get-Content $verifyResultPath -Raw | ConvertFrom-Json
if (-not $verifyResult.passed) { throw 'The copied configure verification result is not passed.' }

$hardware = Get-Content $hardwareQualificationReport -Raw | ConvertFrom-Json
$expectedOrder = 'h264_nvenc', 'h264_amf', 'h264_qsv'
if ($hardware.overallResult -notin 'passed', 'passed-with-manual-waiver') {
    throw 'Hardware qualification report is neither passed nor passed-with-manual-waiver.'
}
if ((@($hardware.encoderPreferenceOrder) -join ',') -ne ($expectedOrder -join ',')) {
    throw 'Hardware qualification report does not confirm the required NVENC -> AMF -> QSV preference order.'
}

foreach ($machineClass in 'NVIDIA', 'Intel') {
    $machineResults = @($hardware.machines | Where-Object { $_.machineClass -eq $machineClass })
    if ($machineResults.Count -eq 0 -or @($machineResults | Where-Object { $_.result -ne 'passed' }).Count -ne 0) {
        throw "Hardware qualification report has no passing $machineClass result."
    }
}

$amdResults = @($hardware.machines | Where-Object { $_.machineClass -eq 'AMD' })
if ($amdResults.Count -eq 0) {
    throw 'Hardware qualification report has no AMD result.'
}

$amdPassed = @($amdResults | Where-Object { $_.result -eq 'passed' }).Count -gt 0
$amdWaived = @($amdResults | Where-Object { $_.result -eq 'waived' }).Count -eq 1
if (-not $amdPassed) {
    if (-not $amdWaived -or $hardware.overallResult -ne 'passed-with-manual-waiver') {
        throw 'Hardware qualification report has no passing AMD result or valid manual AMD waiver.'
    }

    $approval = $hardware.manualApproval
    if ($null -eq $approval -or
        $approval.approved -ne $true -or
        [string]::IsNullOrWhiteSpace([string]$approval.approvedBy) -or
        [string]::IsNullOrWhiteSpace([string]$approval.approvedUtc) -or
        [string]::IsNullOrWhiteSpace([string]$approval.reason) -or
        $approval.scope -ne 'AMD hardware encoder qualification (h264_amf)') {
        throw 'The AMD waiver is missing its explicit manual approval record.'
    }

    $approvedUtc = [DateTimeOffset]::MinValue
    if (-not [DateTimeOffset]::TryParse([string]$approval.approvedUtc, [ref]$approvedUtc)) {
        throw 'The AMD waiver approval timestamp is invalid.'
    }

    Write-Warning "AMD hardware qualification was manually waived by $($approval.approvedBy) at $($approvedUtc.ToUniversalTime().ToString('O')). Reason: $($approval.reason)"
}

$signature = Get-AuthenticodeSignature -FilePath $signedFfmpegPath
if ($signature.Status -ne 'Valid') { throw "Signed ffmpeg.exe does not have a valid Authenticode signature: $($signature.Status)." }
$signedHash = Get-FileSha256 $signedFfmpegPath

# Revalidate the signed executable itself. Signing must never let a substituted
# GPL/non-free binary pass on the strength of the unsigned candidate's record.
$signedLicensePath = Join-Path $artifactDirectory 'signed-license-check.txt'
$signedLicenseResult = Invoke-FfmpegProcess -FfmpegPath $signedFfmpegPath -ArgumentLine '-hide_banner -L' -StdoutFile $signedLicensePath
if ($signedLicenseResult.exitCode -ne 0) { throw "Signed ffmpeg.exe -L failed: $($signedLicenseResult.stderrTail)" }
$signedLicenseText = Get-Content $signedLicensePath -Raw
$normalisedSignedLicenseText = [regex]::Replace([string]$signedLicenseText, '\s+', ' ').Trim()
if ($normalisedSignedLicenseText.IndexOf('GNU Lesser General Public License', [StringComparison]::OrdinalIgnoreCase) -lt 0) {
    throw 'Signed ffmpeg.exe does not identify an LGPL build.'
}

$signedBuildconfCheckPath = Join-Path $artifactDirectory 'signed-buildconf-check.txt'
$signedBuildconfCheck = Invoke-FfmpegProcess -FfmpegPath $signedFfmpegPath -ArgumentLine '-hide_banner -buildconf' -StdoutFile $signedBuildconfCheckPath
if ($signedBuildconfCheck.exitCode -ne 0) { throw "Signed ffmpeg.exe -buildconf failed: $($signedBuildconfCheck.stderrTail)" }
$signedBuildconfText = Get-Content $signedBuildconfCheckPath -Raw
foreach ($forbiddenFlag in '--enable-gpl', '--enable-version3', '--enable-libx264', '--enable-nonfree') {
    if ($signedBuildconfText.IndexOf($forbiddenFlag, [StringComparison]::OrdinalIgnoreCase) -ge 0) {
        throw "Signed ffmpeg.exe contains forbidden configure flag $forbiddenFlag."
    }
}

$signedEncodersCheckPath = Join-Path $artifactDirectory 'signed-encoders-check.txt'
$signedEncodersCheck = Invoke-FfmpegProcess -FfmpegPath $signedFfmpegPath -ArgumentLine '-hide_banner -encoders' -StdoutFile $signedEncodersCheckPath
if ($signedEncodersCheck.exitCode -ne 0) { throw "Signed ffmpeg.exe -encoders failed: $($signedEncodersCheck.stderrTail)" }
$signedEncodersText = Get-Content $signedEncodersCheckPath -Raw
if ($signedEncodersText -match '(?m)^\s*V\S*\s+libx264(?:\s|$)') {
    throw 'Signed ffmpeg.exe unexpectedly contains the GPL libx264 encoder.'
}

if (-not $ReleaseDirectory) {
    $ReleaseDirectory = Join-Path $repoRoot ("artifacts\{0}-{1}-release" -f $buildManifest.ffmpegVersion, $buildManifest.buildId)
}
if (Test-Path $ReleaseDirectory) { throw "Release directory already exists: $ReleaseDirectory" }
New-Item -ItemType Directory -Force -Path $ReleaseDirectory | Out-Null

$sevenZipPath = Get-LocalSevenZip -Lock $lock -Paths $paths
$packageWork = Join-Path $paths.workRoot ("package-{0}" -f [Guid]::NewGuid().ToString('N'))
$stageDirectory = Join-Path $packageWork 'archive'

try {
    New-Item -ItemType Directory -Force -Path $stageDirectory | Out-Null
    $stageFfmpeg = Join-Path $stageDirectory 'ffmpeg.exe'
    Copy-FileAtomically -Source $signedFfmpegPath -Destination $stageFfmpeg
    [System.IO.File]::SetLastWriteTimeUtc($stageFfmpeg, [DateTime]::Parse('2000-01-01T00:00:00Z').ToUniversalTime())

    $archivePath = Join-Path $ReleaseDirectory 'ffmpeg.7z'
    Push-Location $stageDirectory
    try {
        & $sevenZipPath a -t7z -mx=9 -mmt=off -mtc=off -mta=off -mtm=off -bd -bso0 $archivePath 'ffmpeg.exe' | Out-Null
        if ($LASTEXITCODE -ne 0) { throw "7-Zip archive creation failed with exit code $LASTEXITCODE." }
    } finally {
        Pop-Location
    }
    Assert-SingleFfmpegArchive -SevenZipPath $sevenZipPath -ArchivePath $archivePath
    $archiveHash = Get-FileSha256 $archivePath

    Copy-FileAtomically -Source $signedFfmpegPath -Destination (Join-Path $ReleaseDirectory 'ffmpeg.exe')

    $ffmpegManifest = [ordered]@{
        schemaVersion = 1
        buildId = $buildManifest.buildId
        ffmpegVersion = $buildManifest.ffmpegVersion
        license = 'LGPL-2.1-or-later'
        executableSha256 = $signedHash
        archiveSha256 = $archiveHash
    }
    Write-JsonFile -Object $ffmpegManifest -Path (Join-Path $ReleaseDirectory 'ffmpeg.manifest.json')

    $versionPath = Join-Path $ReleaseDirectory 'ffmpeg-version.txt'
    $versionResult = Invoke-FfmpegProcess -FfmpegPath $signedFfmpegPath -ArgumentLine '-hide_banner -version' -StdoutFile $versionPath
    if ($versionResult.exitCode -ne 0) { throw "Signed ffmpeg.exe -version failed: $($versionResult.stderrTail)" }

    $buildconfPath = Join-Path $ReleaseDirectory 'ffmpeg-buildconf.txt'
    $buildconfResult = Invoke-FfmpegProcess -FfmpegPath $signedFfmpegPath -ArgumentLine '-hide_banner -buildconf' -StdoutFile $buildconfPath
    if ($buildconfResult.exitCode -ne 0) { throw "Signed ffmpeg.exe -buildconf failed: $($buildconfResult.stderrTail)" }

    $capabilityLines = @()
    foreach ($capabilityArgument in '-devices', '-formats', '-filters', '-encoders', '-decoders', '-hwaccels', '-bsfs', '-protocols') {
        $capturePath = Join-Path $packageWork ("capability-{0}.txt" -f $capabilityArgument.TrimStart('-'))
        $capture = Invoke-FfmpegProcess -FfmpegPath $signedFfmpegPath -ArgumentLine "-hide_banner $capabilityArgument" -StdoutFile $capturePath
        if ($capture.exitCode -ne 0) { throw "Signed ffmpeg.exe $capabilityArgument failed: $($capture.stderrTail)" }
        $capabilityLines += "=== ffmpeg $capabilityArgument ==="
        $capabilityLines += Get-Content $capturePath
        $capabilityLines += ''
    }
    Write-TextFile -Path (Join-Path $ReleaseDirectory 'ffmpeg-capabilities.txt') -Lines $capabilityLines

    $sourceRoot = Join-Path $ReleaseDirectory 'sources'
    $upstreamRoot = Join-Path $sourceRoot 'upstream'
    New-Item -ItemType Directory -Force -Path $upstreamRoot | Out-Null
    foreach ($componentName in 'ffmpeg', 'amf-headers', 'nv-codec-headers', 'libvpl') {
        $component = Get-LockComponent $lock $componentName
        $downloadPath = Join-Path $paths.downloads $component.filename
        if (-not (Test-Path $downloadPath)) { throw "Pinned source input is missing: $downloadPath" }
        Assert-Sha256 -Path $downloadPath -Expected $component.sha256 -Description "$componentName source input" | Out-Null
        Copy-FileAtomically -Source $downloadPath -Destination (Join-Path $upstreamRoot $component.filename)
    }
    Copy-Item (Join-Path $repoRoot 'config') (Join-Path $sourceRoot 'config') -Recurse -Force
    Copy-FileAtomically -Source $dependencyLockPath -Destination (Join-Path $sourceRoot 'config\dependencies.lock.json')
    Copy-Item (Join-Path $repoRoot 'scripts') (Join-Path $sourceRoot 'scripts') -Recurse -Force
    Copy-Item (Join-Path $repoRoot 'test.ps1') (Join-Path $sourceRoot 'test.ps1') -Force
    Copy-Item (Join-Path $repoRoot 'tests') (Join-Path $sourceRoot 'tests') -Recurse -Force
    Copy-Item (Join-Path $repoRoot 'README.md') (Join-Path $sourceRoot 'README.md') -Force
    Copy-Item (Join-Path $repoRoot 'PROJECT_PLAN.md') (Join-Path $sourceRoot 'PROJECT_PLAN.md') -Force

    $ffmpegSource = Join-Path $paths.sources ((Get-LockComponent $lock 'ffmpeg').extractedRoot)
    $lgplSource = Join-Path $ffmpegSource 'COPYING.LGPLv2.1'
    if (-not (Test-Path $lgplSource)) { throw "FFmpeg source tree is missing $lgplSource; package only from the verified build work root." }
    Copy-FileAtomically -Source $lgplSource -Destination (Join-Path $ReleaseDirectory 'COPYING.LGPLv2.1')

    $noticeLines = @('NextNote FFmpeg release third-party notices', '')
    foreach ($component in $lock.components) {
        $noticeLines += ("{0} {1}" -f $component.name, $component.version)
        $noticeLines += ("License: {0}" -f $component.license)
        $noticeLines += ("Purpose: {0}" -f $component.reason)
        $noticeLines += ''
    }
    Write-TextFile -Path (Join-Path $ReleaseDirectory 'THIRD_PARTY_NOTICES.txt') -Lines $noticeLines
    Write-TextFile -Path (Join-Path $ReleaseDirectory 'SOURCE_README.txt') -Lines @(
        'This directory contains the exact upstream source archives and the build inputs used for this NextNote FFmpeg release.',
        'The package was built by scripts/build.ps1 from the pinned inputs in sources/config/dependencies.lock.json.',
        'Use sources/README.md and PROJECT_PLAN.md in the build repository for the Windows MSYS2 prerequisite and full rebuild procedure.',
        'Local build scripts, tests, configuration, and the dependency lock are included under this sources directory.'
    )

    # Produce one uploadable source bundle alongside the binary. This contains
    # the exact pinned upstream inputs and the corresponding NextNote build
    # scripts/configuration, so the hosted LGPL source offer is independently
    # useful rather than relying on web-directory browsing.
    $sourceArchiveName = "nextnote-ffmpeg-$($buildManifest.ffmpegVersion)-sources.7z"
    $sourceArchivePath = Join-Path $ReleaseDirectory $sourceArchiveName
    Push-Location $ReleaseDirectory
    try {
        & $sevenZipPath a -t7z -mx=9 -mmt=off -mtc=off -mta=off -mtm=off -bd -bso0 $sourceArchivePath 'sources' 'COPYING.LGPLv2.1' 'THIRD_PARTY_NOTICES.txt' 'SOURCE_README.txt' | Out-Null
        if ($LASTEXITCODE -ne 0) { throw "Source archive creation failed with exit code $LASTEXITCODE." }
    } finally {
        Pop-Location
    }
    & $sevenZipPath t -bd $sourceArchivePath | Out-Null
    if ($LASTEXITCODE -ne 0) { throw "Source archive integrity test failed with exit code $LASTEXITCODE." }
    $sourceArchiveHash = Get-FileSha256 $sourceArchivePath

    foreach ($recordName in 'build-manifest.json', 'test-report.json', 'verify-binary-result.json', 'verification-manifest.json', 'bootstrap-manifest.json', 'msys2-package-inventory.txt') {
        $recordPath = Join-Path $artifactDirectory $recordName
        if (Test-Path $recordPath) { Copy-FileAtomically -Source $recordPath -Destination (Join-Path $ReleaseDirectory $recordName) }
    }
    Copy-FileAtomically -Source $hardwareQualificationReport -Destination (Join-Path $ReleaseDirectory 'hardware-qualification.json')

    $releaseManifest = [ordered]@{
        schemaVersion = 1
        status = 'release-ready'
        packagedUtc = (Get-Date).ToUniversalTime().ToString('o')
        buildId = $buildManifest.buildId
        ffmpegVersion = $buildManifest.ffmpegVersion
        license = 'LGPL-2.1-or-later'
        unsignedSha256 = $buildManifest.unsignedSha256
        signedSha256 = $signedHash
        archiveSha256 = $archiveHash
        sourceArchiveName = $sourceArchiveName
        sourceArchiveSha256 = $sourceArchiveHash
        buildManifestSha256 = Get-FileSha256 $buildManifestPath
        hardwareQualificationSha256 = Get-FileSha256 $hardwareQualificationReport
        ffmpegManifestSha256 = Get-FileSha256 (Join-Path $ReleaseDirectory 'ffmpeg.manifest.json')
    }
    Write-JsonFile -Object $releaseManifest -Path (Join-Path $ReleaseDirectory 'release-manifest.json')

    $sumLines = @()
    Get-ChildItem $ReleaseDirectory -Recurse -File |
        Where-Object { $_.Name -ne 'SHA256SUMS.txt' } |
        Sort-Object FullName |
        ForEach-Object {
            $relativePath = $_.FullName.Substring($ReleaseDirectory.Length).TrimStart('\\')
            $sumLines += ("{0} *{1}" -f (Get-FileSha256 $_.FullName), $relativePath)
        }
    [System.IO.File]::WriteAllText((Join-Path $ReleaseDirectory 'SHA256SUMS.txt'), ($sumLines -join [Environment]::NewLine) + [Environment]::NewLine, (New-Object System.Text.UTF8Encoding($false)))
} finally {
    if (Test-Path $packageWork) { Remove-Item -Recurse -Force $packageWork }
}

Write-Host "Release package: $ReleaseDirectory" -ForegroundColor Green
