param(
    [Parameter(Mandatory=$true)][string]$FactoryRoot,
    [Parameter(Mandatory=$true)][string]$OutputRoot,
    [Parameter(Mandatory=$true)][string]$Executable,
    [Parameter(Mandatory=$true)][ValidateSet('Debug','Release')][string]$Configuration,
    [switch]$FinalizeExisting,
    [switch]$PathValidationOnly
)

$ErrorActionPreference = 'Stop'
$repo = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$factory = [IO.Path]::GetFullPath($FactoryRoot)
$output = [IO.Path]::GetFullPath($OutputRoot)
function Test-AllowedEvidencePath([string]$Path) {
    foreach ($version in @('V0.12.0','V0.12.5','V0.12.75','V0.13.0')) {
        $root = [IO.Path]::GetFullPath((Join-Path $repo (Join-Path 'Artifacts' $version))) + '\'
        if ($Path.StartsWith($root, [StringComparison]::OrdinalIgnoreCase)) { return $true }
    }
    return $false
}
if (-not (Test-AllowedEvidencePath $output)) {
    throw 'Final audio evidence must remain below repository Artifacts/V0.12.0, Artifacts/V0.12.5, Artifacts/V0.12.75, or Artifacts/V0.13.0.'
}
if ($PathValidationOnly) { Write-Host 'PASS: bounded final-audio path accepted.'; exit 0 }
if ((Test-Path -LiteralPath $output) -and -not $FinalizeExisting) {
    throw 'Preserve previous evidence; use a new output root.'
}
if ($FinalizeExisting -and -not (Test-Path -LiteralPath $output)) {
    throw 'Existing audit output is required for finalization.'
}

$catalogPath = Join-Path $factory 'catalog.json'
$catalog = Get-Content -LiteralPath $catalogPath -Raw | ConvertFrom-Json
if ($catalog.format -ne 'DeepPluckFactoryLibrary' -or $catalog.status -ne 'approved' -or
    $catalog.expectedActiveCount -ne 116 -or $catalog.entries.Count -ne 116) {
    throw 'Expected the approved 116-entry Factory catalogue.'
}
$exe = (Resolve-Path -LiteralPath $Executable).Path

function Write-Json($Value, [string]$Path) {
    [IO.File]::WriteAllText($Path, ($Value | ConvertTo-Json -Depth 20), [Text.UTF8Encoding]::new($false))
}

$plan = [ordered]@{
    format='DeepPluckFactoryFinalAudioPlan'; schemaVersion=1; configuration=$Configuration
    executable=$exe; executableSha256=(Get-FileHash -LiteralPath $exe -Algorithm SHA256).Hash.ToLowerInvariant()
    catalogSha256=(Get-FileHash -LiteralPath $catalogPath -Algorithm SHA256).Hash.ToLowerInvariant()
    presetCount=116; sampleRates=@(44100,48000,96000); buffer=128; scene='full'; fxModes=@('dry','wet')
    expectedRenders=696; rawAudio='Hash recorded then disposable raw float32 removed; result receipts retained.'
}
if (-not $FinalizeExisting) {
    [void](New-Item -ItemType Directory -Path $output)
}
if ($FinalizeExisting) {
    $existingPlan = Get-Content -LiteralPath (Join-Path $output 'plan.json') -Raw | ConvertFrom-Json
    if ($existingPlan.catalogSha256 -cne $plan.catalogSha256 -or
        $existingPlan.executableSha256 -cne $plan.executableSha256 -or
        $existingPlan.configuration -cne $Configuration) {
        throw 'Existing audit plan does not match the requested finalization inputs.'
    }
} else {
    Write-Json $plan (Join-Path $output 'plan.json')
}

if ($FinalizeExisting) {
    $rows = [Collections.Generic.List[object]]::new()
    $failures = [Collections.Generic.List[object]]::new()
    foreach ($entry in $catalog.entries) {
        foreach ($rate in @(44100,48000,96000)) {
            foreach ($fx in @('dry','wet')) {
                $target = Join-Path $output (Join-Path ([string]$entry.name) ("${rate}-${fx}"))
                $resultPath = Join-Path $target 'result.json'
                if (-not (Test-Path -LiteralPath $resultPath)) { throw "Missing result: $($entry.name)/$rate/$fx" }
                $data = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json
                $pass = [bool]$data.contentPass -and [bool]$data.tailQuiet -and [double]$data.peak -le 0.95
                $row = [pscustomobject][ordered]@{
                    id=[string]$entry.id; name=[string]$entry.name; category=[string]$entry.category
                    presetSha256=[string]$entry.sha256; rate=$rate; buffer=128; fx=$fx
                    nativeExit=$(if ($pass) { 0 } else { 2 }); pass=$pass; peak=[double]$data.peak
                    rms=[double]$data.rms; activeRms=[double]$data.activeRms; monoRms=[double]$data.monoRms
                    frames=[int64]$data.frames; tailQuiet=[bool]$data.tailQuiet
                    rawAudioSha256='disposed-after-successful-native-render'
                }
                $rows.Add($row)
                if (-not $pass) { $failures.Add($row) }
            }
        }
    }
    $peakMaximum = ($rows | ForEach-Object { [double]$_.peak } | Measure-Object -Maximum).Maximum
    Write-Json ([ordered]@{
        format='DeepPluckFactoryFinalAudioSummary'; schemaVersion=1; configuration=$Configuration
        presetCount=116; renderCount=$rows.Count; passCount=@($rows | Where-Object { $_.pass }).Count
        failureCount=$failures.Count; peakMaximum=$peakMaximum; catalogSha256=$plan.catalogSha256
        executableSha256=$plan.executableSha256; completedUtc=[DateTime]::UtcNow.ToString('o')
        reconstructedFromRetainedNativeReceipts=$true; failures=@($failures); results=@($rows)
    }) (Join-Path $output 'summary.json')
    if ($failures.Count -ne 0) { throw "$($failures.Count) retained final Factory results failed." }
    Write-Host "PASS: finalized $($rows.Count) retained $Configuration native render receipts without rerendering."
    exit 0
}

$rows = [Collections.Generic.List[object]]::new()
$failures = [Collections.Generic.List[object]]::new()
$caseIndex = 0
foreach ($entry in $catalog.entries) {
    $preset = [IO.Path]::GetFullPath((Join-Path $factory ([string]$entry.path -replace '/', '\')))
    if (-not $preset.StartsWith($factory + '\', [StringComparison]::OrdinalIgnoreCase)) { throw 'Factory path escaped.' }
    if ((Get-FileHash -LiteralPath $preset -Algorithm SHA256).Hash -ine [string]$entry.sha256) {
        throw "Factory preset changed: $($entry.name)"
    }
    foreach ($rate in @(44100,48000,96000)) {
        foreach ($fx in @('dry','wet')) {
            ++$caseIndex
            $target = Join-Path $output (Join-Path ([string]$entry.name) ("${rate}-${fx}"))
            $arguments = @('--preset',$preset,'--output',$target,'--rate',[string]$rate,
                           '--buffer','128','--mode','audio','--scene','full','--fx',$fx)
            $started = [DateTime]::UtcNow.ToString('o')
            $nativeText = @(& $exe @arguments)
            $nativeExit = $LASTEXITCODE
            if ($nativeExit -notin @(0,2)) { throw "Audit process failed for $($entry.name)/$rate/$fx (exit $nativeExit)." }
            $resultPath = Join-Path $target 'result.json'
            if (-not (Test-Path -LiteralPath $resultPath)) { throw 'Missing native result receipt.' }
            $data = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json
            $rawPath = Join-Path $target 'audio.f32'
            $rawHash = if (Test-Path -LiteralPath $rawPath) {
                (Get-FileHash -LiteralPath $rawPath -Algorithm SHA256).Hash.ToLowerInvariant()
            } else { '' }
            if ($rawHash) { Remove-Item -LiteralPath $rawPath }
            $pass = $nativeExit -eq 0 -and [bool]$data.contentPass -and [bool]$data.tailQuiet -and
                    [double]$data.peak -le 0.95 -and -not [string]::IsNullOrWhiteSpace($rawHash)
            $row = [pscustomobject][ordered]@{
                id=[string]$entry.id; name=[string]$entry.name; category=[string]$entry.category
                presetSha256=[string]$entry.sha256; rate=$rate; buffer=128; fx=$fx
                nativeExit=$nativeExit; pass=$pass; peak=[double]$data.peak; rms=[double]$data.rms
                activeRms=[double]$data.activeRms; monoRms=[double]$data.monoRms
                frames=[int64]$data.frames; tailQuiet=[bool]$data.tailQuiet; rawAudioSha256=$rawHash
                startedUtc=$started; completedUtc=[DateTime]::UtcNow.ToString('o')
            }
            $rows.Add($row)
            if (-not $pass) { $failures.Add($row) }
            if ($caseIndex % 24 -eq 0 -or $caseIndex -eq 696) {
                Write-Host ("{0}: {1}/696 renders, failures={2}" -f $Configuration,$caseIndex,$failures.Count)
            }
        }
    }
}

$peakMaximum = ($rows | ForEach-Object { [double]$_.peak } | Measure-Object -Maximum).Maximum
$summary = [ordered]@{
    format='DeepPluckFactoryFinalAudioSummary'; schemaVersion=1; configuration=$Configuration
    presetCount=116; renderCount=$rows.Count; passCount=@($rows | Where-Object { $_.pass }).Count
    failureCount=$failures.Count; peakMaximum=$peakMaximum
    catalogSha256=$plan.catalogSha256; executableSha256=$plan.executableSha256
    completedUtc=[DateTime]::UtcNow.ToString('o'); failures=@($failures); results=@($rows)
}
Write-Json $summary (Join-Path $output 'summary.json')
if ($failures.Count -ne 0) { throw "$($failures.Count) final Factory audio renders failed; see $output." }
Write-Host "PASS: $($rows.Count) $Configuration final Factory renders across 44.1/48/96 kHz, dry and wet."
