param(
    [Parameter(Mandatory=$true)][string]$Reference,
    [Parameter(Mandatory=$true)][string]$Candidate,
    [Parameter(Mandatory=$true)][string]$Output
)
$ErrorActionPreference = 'Stop'
$referencePath = (Resolve-Path -LiteralPath $Reference).Path
$candidatePath = (Resolve-Path -LiteralPath $Candidate).Path
$outputPath = [IO.Path]::GetFullPath($Output)
if (Test-Path -LiteralPath $outputPath) { throw 'Comparison output exists; preserve the previous evidence and choose another directory.' }
New-Item -ItemType Directory -Path $outputPath | Out-Null
$failures = [Collections.Generic.List[string]]::new()
function Assert-Audit([bool]$condition, [string]$message) {
    if (-not $condition) { $script:failures.Add($message); Write-Warning $message }
}
function Get-Hash([string]$path) { (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash }
function Get-TextHash([string]$value) {
    $algorithm = [Security.Cryptography.SHA256]::Create()
    try { [Convert]::ToHexString($algorithm.ComputeHash([Text.Encoding]::UTF8.GetBytes($value))) }
    finally { $algorithm.Dispose() }
}
function Get-Db([double]$a,[double]$b) {
    if ($a -le 1e-15 -or $b -le 1e-15) { return $null }
    20*[Math]::Log10($b/$a)
}
$candidateRun = Get-Content -LiteralPath (Join-Path $candidatePath 'run.json') -Raw | ConvertFrom-Json
Assert-Audit ($null -ne $candidateRun.failures -and $candidateRun.failures -eq 0) 'Candidate audit must finish without failures'
foreach ($name in @('contract.json','oscillators.json','filters.json')) {
    Assert-Audit ((Get-Hash (Join-Path $referencePath $name)) -eq (Get-Hash (Join-Path $candidatePath $name))) "Unchanged contract/component differs: $name"
}
$aEnvelopes = @(Get-Content -LiteralPath (Join-Path $referencePath 'envelopes.json') -Raw | ConvertFrom-Json)
$bEnvelopes = @(Get-Content -LiteralPath (Join-Path $candidatePath 'envelopes.json') -Raw | ConvertFrom-Json)
Assert-Audit ($aEnvelopes.Count -eq $bEnvelopes.Count) 'Envelope case count differs'
Assert-Audit (@($aEnvelopes | Where-Object { $_.maximumDecayRise -gt 1e-6 }).Count -eq 2) 'Reference must reproduce the two documented pre-existing rounding rebounds'
for ($i=0; $i -lt $aEnvelopes.Count; ++$i) {
    $a=$aEnvelopes[$i]; $b=$bEnvelopes[$i]
    Assert-Audit ($a.attackSum -eq $b.attackSum) "Attack changed at envelope case $i"
    Assert-Audit ($a.attackSamples -eq $b.attackSamples -and $a.decaySamples -eq $b.decaySamples -and $a.releaseSamples -eq $b.releaseSamples) "Stage time changed at envelope case $i"
    Assert-Audit ($b.maximumDecayRise -le 1e-6) "Candidate decay is not monotonic at case $i"
}

# Read/write floating-point WAV without quantization or normalizing raw evidence.
# Preserve all original RIFF chunks; mono listening copies are dual-mono L=R=(L+R)/2.
Add-Type -TypeDefinition @'
using System;
using System.IO;
using System.Text;
public static class ToneAuditWave {
    public static double[] Attenuate(string input, string output, double gain, bool mono) {
        byte[] bytes=File.ReadAllBytes(input);
        int data=-1, length=0, channels=0, bits=0;
        for(int p=12;p+8<=bytes.Length;) {
            string id=Encoding.ASCII.GetString(bytes,p,4);
            int n=BitConverter.ToInt32(bytes,p+4);
            if(n<0 || p+8L+n>bytes.Length) throw new Exception("Invalid WAV chunk");
            if(id=="fmt ") { channels=BitConverter.ToInt16(bytes,p+10); bits=BitConverter.ToInt16(bytes,p+22); }
            if(id=="data") { data=p+8; length=n; break; }
            p+=8+n+(n&1);
        }
        if(data<0 || channels!=2 || bits!=32 || length%8!=0) throw new Exception("Expected stereo float32 WAV");
        double energy=0,peak=0;
        for(int p=data;p<data+length;p+=8) {
            double left=BitConverter.ToSingle(bytes,p), right=BitConverter.ToSingle(bytes,p+4);
            if(mono) { left=(left+right)*0.5; right=left; }
            float a=(float)(left*gain), b=(float)(right*gain);
            if(float.IsNaN(a)||float.IsInfinity(a)||float.IsNaN(b)||float.IsInfinity(b)) throw new Exception("Nonfinite WAV");
            Array.Copy(BitConverter.GetBytes(a),0,bytes,p,4); Array.Copy(BitConverter.GetBytes(b),0,bytes,p+4,4);
            energy+=(double)a*a+(double)b*b; peak=Math.Max(peak,Math.Max(Math.Abs(a),Math.Abs(b)));
        }
        File.WriteAllBytes(output,bytes);
        return new double[]{Math.Sqrt(energy/(length/4)),peak};
    }
}
'@
$aCases = @(Get-Content -LiteralPath (Join-Path $referencePath 'cases.json') -Raw | ConvertFrom-Json)
$bCases = @(Get-Content -LiteralPath (Join-Path $candidatePath 'cases.json') -Raw | ConvertFrom-Json)
Assert-Audit ($aCases.Count -eq $bCases.Count) 'Corpus case count differs'
$rows = [Collections.Generic.List[object]]::new()
$listening = [Collections.Generic.List[object]]::new()
foreach ($i in 0..($aCases.Count-1)) {
    $a=$aCases[$i]; $b=$bCases[$i]
    Assert-Audit ($a.id -eq $b.id) "Case ordering differs at $i"
    if (-not $a.metrics) { Assert-Audit ($b.delta -le 1e-6) "Partition regression: $($b.id)"; continue }
    $values = $a.values | ConvertTo-Json -Compress
    $events = $a.events | ConvertTo-Json -Compress -Depth 5
    Assert-Audit ($values -ceq ($b.values | ConvertTo-Json -Compress)) "Input parameters changed: $($a.id)"
    Assert-Audit ($events -ceq ($b.events | ConvertTo-Json -Compress -Depth 5)) "MIDI changed: $($a.id)"
    $aPeak=[Math]::Max($a.metrics.peakL,$a.metrics.peakR)
    $bPeak=[Math]::Max($b.metrics.peakL,$b.metrics.peakR)
    $peakDb=Get-Db $aPeak $bPeak
    $rmsDb=Get-Db $a.metrics.rms $b.metrics.rms
    $attackDb=Get-Db ([Math]::Max($a.attack10ms.peakL,$a.attack10ms.peakR)) ([Math]::Max($b.attack10ms.peakL,$b.attack10ms.peakR))
    $monoRatioDb=Get-Db ($a.metrics.rmsMid/$a.metrics.rms) ($b.metrics.rmsMid/$b.metrics.rms)
    if ($a.id.StartsWith('stress-')) { Assert-Audit ($null -eq $peakDb -or $peakDb -le .5) "Stress peak increase exceeds 0.5 dB: $($a.id) ($peakDb)" }
    else {
        Assert-Audit ($bPeak -le .95) "Musical peak exceeds 0.95: $($a.id)"
        Assert-Audit ($null -eq $attackDb -or $attackDb -le .5) "Attack increase exceeds 0.5 dB: $($a.id) ($attackDb)"
        Assert-Audit ($null -eq $rmsDb -or [Math]::Abs($rmsDb) -le 2) "RMS delta exceeds 2 dB: $($a.id) ($rmsDb)"
        Assert-Audit ($null -eq $monoRatioDb -or $monoRatioDb -ge -.5) "Mono ratio worsens: $($a.id) ($monoRatioDb)"
    }
    $row=[ordered]@{id=$a.id;parameterSHA256=(Get-TextHash $values);midiSHA256=(Get-TextHash $events);peakA=$aPeak;peakB=$bPeak;peakDeltaDb=$peakDb;rmsA=$a.metrics.rms;rmsB=$b.metrics.rms;rmsDeltaDb=$rmsDb;attackPeakDeltaDb=$attackDb;monoRatioDeltaDb=$monoRatioDb;correlationA=$a.metrics.correlation;correlationB=$b.metrics.correlation}
    $rows.Add([pscustomobject]$row)
    if ($a.rawFile) {
        $pairDirectory=Join-Path $outputPath $a.id
        New-Item -ItemType Directory -Path $pairDirectory | Out-Null
        $target=[Math]::Min($a.metrics.rms,$b.metrics.rms)
        $gainA=$target/$a.metrics.rms; $gainB=$target/$b.metrics.rms
        $audioA=Join-Path $referencePath $a.rawFile; $audioB=Join-Path $candidatePath $b.rawFile
        $stereoA=[ToneAuditWave]::Attenuate($audioA,(Join-Path $pairDirectory 'A-stereo.wav'),$gainA,$false)
        $stereoB=[ToneAuditWave]::Attenuate($audioB,(Join-Path $pairDirectory 'B-stereo.wav'),$gainB,$false)
        $monoA=[ToneAuditWave]::Attenuate($audioA,(Join-Path $pairDirectory 'A-mono.wav'),$gainA,$true)
        $monoB=[ToneAuditWave]::Attenuate($audioB,(Join-Path $pairDirectory 'B-mono.wav'),$gainB,$true)
        $difference=Get-Db $stereoA[0] $stereoB[0]
        Assert-Audit ([Math]::Abs($difference) -le .2) "Matched stereo loudness outside 0.2 dB: $($a.id)"
        $listening.Add([ordered]@{id=$a.id;rawA=$audioA;rawB=$audioB;rawSHA256A=(Get-Hash $audioA);rawSHA256B=(Get-Hash $audioB);gainA=$gainA;gainB=$gainB;matchedRmsDeltaDb=$difference;monoRmsDeltaDb=(Get-Db $monoA[0] $monoB[0]);mono='dual mono arithmetic (L+R)/2, same stereo-match gains';manualStatus='listening pending';parameters=$a.values;events=$a.events;parameterSHA256=$row.parameterSHA256;midiSHA256=$row.midiSHA256;rate=$a.rate;block=$a.block;tempo=$a.tempo;duration=$a.seconds;reset=$a.reset})
    }
}
$rows | Export-Csv -LiteralPath (Join-Path $outputPath 'comparison.csv') -NoTypeInformation -Encoding utf8
$rows | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $outputPath 'comparison.json') -Encoding utf8
$listening | ConvertTo-Json -Depth 9 | Set-Content -LiteralPath (Join-Path $outputPath 'listening-manifest.json') -Encoding utf8
[ordered]@{reference=$referencePath;candidate=$candidatePath;referenceRun=(Get-Content (Join-Path $referencePath 'run.json') -Raw | ConvertFrom-Json);candidateRun=(Get-Content (Join-Path $candidatePath 'run.json') -Raw | ConvertFrom-Json);caseCount=$aCases.Count;measuredCases=$rows.Count;listeningPairs=$listening.Count;failures=@($failures);manualStatus='listening pending'} | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $outputPath 'summary.json') -Encoding utf8
if ($failures.Count -gt 0) { throw "$($failures.Count) comparison gates failed; candidate not accepted." }
Write-Host "PASS: $($rows.Count) measured comparisons and $($listening.Count) constant-gain listening pairs. Listening pending."
