# fetch-and-verify.ps1
#
# Downloads every source input pinned in config/dependencies.lock.json and
# verifies it, failing closed on any problem:
#   - downloads ONLY the URLs in the lock, HTTPS-only, redirect hosts allowlisted
#   - SHA-256 verified for every file before it is accepted
#   - FFmpeg release tarball verified against its detached PGP signature using
#     the pinned FFmpeg release key (config/ffmpeg-release-key.asc), via MSYS2's
#     gpg -- run scripts\bootstrap-msys2.ps1 first
#   - never substitutes a newer release when a source is unavailable
#
#   powershell -ExecutionPolicy Bypass -File scripts\fetch-and-verify.ps1 [-TamperTest]
#
# Options:
#   -WorkRoot <path>   work root (default C:\nnffb)
#   -TamperTest        additionally prove the fail-closed behaviour: corrupt a
#                      cached file (must be detected + replaced), verify a wrong
#                      pin and a disallowed host both hard-fail
#
# Produces <WorkRoot>\manifests\verification-manifest.json -- consumed by
# build.ps1 into the release build manifest.

[CmdletBinding()]
param(
    [string]$WorkRoot = 'C:\nnffb',
    [string]$DependencyLockPath,
    [switch]$TamperTest
)

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

Write-Host ("BuildCommon revision: {0}" -f (Get-BuildCommonRevision))
Assert-WorkRoot $WorkRoot
$paths = Get-BuildPaths $WorkRoot
New-Item -ItemType Directory -Force -Path $paths.downloads, $paths.manifests | Out-Null

$repoRoot = Get-RepoRoot
$lockPath = Get-DependencyLockPath $DependencyLockPath
$lock     = Read-DependencyLock -Path $lockPath

$manifest = [ordered]@{
    schemaVersion = 1
    runUtc        = (Get-Date).ToUniversalTime().ToString('o')
    machine       = $env:COMPUTERNAME
    workRoot      = $WorkRoot
    lockFile      = $lockPath
    lockSha256    = Get-FileSha256 $lockPath
    components    = @()
    pgp           = $null
    tamperTest    = $null
    overallResult = 'incomplete'
}

# ---------------------------------------------------------------------------
Write-Host "`n=== 1. Download and hash-verify every pinned input ===" -ForegroundColor Cyan

foreach ($comp in $lock.components) {
    $rec = Get-SingleResult -Context "$($comp.name) download" -Results @(
        Invoke-PinnedDownload -Url $comp.url -OutFile (Join-Path $paths.downloads $comp.filename) `
            -Sha256 $comp.sha256 -AllowedHosts $comp.allowedHosts)
    $entry = [ordered]@{
        name = $comp.name; version = $comp.version; result = 'sha256-verified'
        download = $rec
    }
    Write-Host ("{0,-20} {1,-46} sha256 ok{2}" -f $comp.name, $comp.filename, $(if ($rec.cached) { ' (cached)' } else { '' }))

    if (Test-ObjectProperty $comp 'signature') {
        $sig = $comp.signature
        $sigRec = Get-SingleResult -Context "$($comp.name) signature download" -Results @(
            Invoke-PinnedDownload -Url $sig.url -OutFile (Join-Path $paths.downloads $sig.filename) `
                -Sha256 $sig.sha256 -AllowedHosts $comp.allowedHosts)
        $entry.signatureDownload = $sigRec
        Write-Host ("{0,-20} {1,-46} sha256 ok{2}" -f '', $sig.filename, $(if ($sigRec.cached) { ' (cached)' } else { '' }))
    }
    $manifest.components += $entry
}

# ---------------------------------------------------------------------------
Write-Host "`n=== 2. PGP-verify the FFmpeg release ===" -ForegroundColor Cyan

$ffmpeg = Get-LockComponent $lock 'ffmpeg'
$sig    = $ffmpeg.signature

$keyFile = Join-Path $repoRoot ($sig.keyFile -replace '/', '\')
if (-not (Test-Path $keyFile)) { throw "Pinned FFmpeg release key missing: $keyFile" }
$keyHash = Get-FileSha256 $keyFile
if ($keyHash -ne $sig.keyFileSha256.ToLowerInvariant()) {
    throw "FFmpeg release key file hash mismatch: expected $($sig.keyFileSha256), got $keyHash. The key in the repo has been altered -- refusing to verify with it."
}

$gpgExe = Join-Path $paths.msysRoot 'usr\bin\gpg.exe'
if (-not (Test-Path $gpgExe)) {
    throw "MSYS2 gpg not found at $gpgExe. Run scripts\bootstrap-msys2.ps1 first -- PGP verification is mandatory and will not be skipped."
}

$keyPosix     = ConvertTo-MsysPath $keyFile
$tarballPosix = ConvertTo-MsysPath (Join-Path $paths.downloads $ffmpeg.filename)
$ascPosix     = ConvertTo-MsysPath (Join-Path $paths.downloads $sig.filename)
$pinnedFpr    = $sig.keyFingerprint.ToUpperInvariant()

$gpgScript = "set -e; export GNUPGHOME=`$(mktemp -d); " +
             "gpg --batch --quiet --import '$keyPosix'; " +
             "gpg --batch --with-colons --list-keys; " +
             "gpg --batch --status-fd 1 --verify '$ascPosix' '$tarballPosix' 2>/dev/null"
$gpgOut = Invoke-MsysCommand -MsysRoot $paths.msysRoot -Command $gpgScript
$gpgText = @($gpgOut) -join "`n"

# The imported keyring must contain exactly the pinned key...
if ($gpgText -notmatch [regex]::Escape($pinnedFpr)) {
    throw "Imported key does not carry the pinned fingerprint $pinnedFpr -- refusing."
}
# ...and the signature must be VALIDSIG under that exact fingerprint.
# VALIDSIG status line carries the signing (sub)key fingerprint first and the
# primary key fingerprint as its last field; the pinned primary fingerprint
# must appear in that line for the signature to be attributable to our key.
$validSig = @($gpgOut) | Where-Object { "$_" -match '^\[GNUPG:\]\s+VALIDSIG\s' }
if (-not $validSig) {
    throw "PGP verification FAILED for $($ffmpeg.filename): no VALIDSIG status from gpg.`n$gpgText"
}
$validSigLine = "$(@($validSig)[0])".ToUpperInvariant()
if ($validSigLine -notmatch [regex]::Escape($pinnedFpr)) {
    throw "PGP signature was made by an UNEXPECTED key (VALIDSIG: $validSigLine); pinned key is $pinnedFpr. Refusing."
}
$manifest.pgp = [ordered]@{
    file = $ffmpeg.filename; signature = $sig.filename
    keyFingerprint = $pinnedFpr; keyFileSha256 = $keyHash; result = 'VALIDSIG'
}
Write-Host "PGP signature valid: $($ffmpeg.filename) signed by $pinnedFpr" -ForegroundColor Green

# ---------------------------------------------------------------------------
if ($TamperTest) {
    Write-Host "`n=== 3. Tamper tests (fail-closed proof) ===" -ForegroundColor Cyan
    $results = [ordered]@{}
    $sevenZip = Get-LockComponent $lock '7zip'
    $target = Join-Path $paths.downloads $sevenZip.filename

    # A. Corrupt an already-verified cached file: the checker must detect the
    #    bad hash, discard the file, and re-download a good copy.
    Add-Content -Path $target -Value 'TAMPER' -NoNewline
    $rec = Get-SingleResult -Context 'tamper test A re-download' -Results @(
        Invoke-PinnedDownload -Url $sevenZip.url -OutFile $target `
            -Sha256 $sevenZip.sha256 -AllowedHosts $sevenZip.allowedHosts)
    if ($rec.cached -or -not $rec.staleCacheRemoved) { throw "Tamper test A FAILED: corrupted cached file was not detected." }
    $results.corruptCacheDetected = $true
    Write-Host "A. corrupted cached file detected, discarded, and re-verified  PASS"

    # B. A wrong pin (simulating a tampered lock or altered upstream file)
    #    must hard-fail and leave nothing behind.
    $wrongPin = ($sevenZip.sha256.Substring(0, 63)) + $(if ($sevenZip.sha256.EndsWith('0')) { '1' } else { '0' })
    $tmpOut = Join-Path $paths.downloads 'tamper-test-b.bin'
    $threw = $false
    try {
        Invoke-PinnedDownload -Url $sevenZip.url -OutFile $tmpOut -Sha256 $wrongPin -AllowedHosts $sevenZip.allowedHosts | Out-Null
    } catch { $threw = $true }
    if (-not $threw) { throw "Tamper test B FAILED: wrong SHA-256 pin was accepted." }
    if (Test-Path $tmpOut) { throw "Tamper test B FAILED: rejected download left a file behind." }
    $results.wrongPinRejected = $true
    Write-Host "B. wrong SHA-256 pin rejected, no file left behind             PASS"

    # C. A host outside the allowlist must be refused before any transfer.
    $threw = $false
    try {
        Invoke-PinnedDownload -Url $sevenZip.url -OutFile $tmpOut -Sha256 $sevenZip.sha256 -AllowedHosts @('example.invalid') | Out-Null
    } catch { $threw = $true }
    if (-not $threw) { throw "Tamper test C FAILED: disallowed host was accepted." }
    $results.disallowedHostRejected = $true
    Write-Host "C. host outside allowlist refused                              PASS"

    $manifest.tamperTest = $results
}

# ---------------------------------------------------------------------------
Write-Host "`n=== Result ===" -ForegroundColor Cyan
$manifest.overallResult = 'verified'
Write-JsonFile -Object $manifest -Path (Join-Path $paths.manifests 'verification-manifest.json')

Write-Host "All pinned inputs downloaded and verified (SHA-256 all, PGP for FFmpeg)." -ForegroundColor Green
Write-Host "Manifest: $(Join-Path $paths.manifests 'verification-manifest.json')"
