param(
    [Parameter(Mandatory=$true)][string]$Root,
    [Parameter(Mandatory=$true)][string]$BaselineExecutable,
    [Parameter(Mandatory=$true)][string]$CandidateExecutable,
    [Parameter(Mandatory=$true)][string]$Output
)
$ErrorActionPreference='Stop'
. (Join-Path $PSScriptRoot 'CPUReportIO.ps1')
if(-not[IO.Path]::IsPathRooted($Output) -or (Test-Path -LiteralPath $Output)){throw 'Use a new absolute paired-analysis directory'}
New-Item -ItemType Directory -Path $Output | Out-Null
function Read([string]$Path){Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json}
function Save($Value,[string]$Path){$Value | ConvertTo-Json -Depth 40 | Set-Content -LiteralPath $Path -Encoding utf8}
function Assert([bool]$Condition,[string]$Message){if(-not$Condition){throw $Message}}
function Median($Values){
    $sorted=@($Values | Sort-Object);Assert ($sorted.Count -gt 0) 'Empty statistic'
    if($sorted.Count%2 -eq 1){return $sorted[[int][math]::Floor($sorted.Count/2)]}
    return ($sorted[$sorted.Count/2-1]+$sorted[$sorted.Count/2])/2
}
function Summary($Values){
    $median=Median $Values
    [ordered]@{median=$median;minimum=($Values | Measure-Object -Minimum).Minimum;maximum=($Values | Measure-Object -Maximum).Maximum;
        mad=(Median @($Values | ForEach-Object {[math]::Abs($_-$median)}))}
}
function Hard($Row,[string]$Side,[int]$Pair){
    Assert ($Row.total.valid -and $Row.total.blocks -gt 0) 'Missing whole-case timing'
    foreach($phase in @($Row.total)+@($Row.phases)){
        if(-not$phase.valid){continue}
        Assert ($phase.blocks -gt 0 -and $phase.deadline_misses -ge 0 -and [int]$phase.deadline_misses -eq $phase.deadline_misses) 'Invalid block counters'
        foreach($name in @('rtf','callback_seconds','audio_seconds','max_block_seconds','max_block_ratio','p99_seconds','p999_seconds')){
            Assert ($name -in $phase.PSObject.Properties.Name) "Missing timing value: $name"
            $value=[double]$phase.$name
            Assert (-not[double]::IsNaN($value) -and -not[double]::IsInfinity($value) -and $value -gt 0) "Invalid timing value: $name"
        }
        if($phase.rtf -ge 1 -or $phase.max_block_ratio -ge 1 -or $phase.deadline_misses -gt 0 -or
            ($Row.id -like 'D-*' -and $phase.rtf -gt .90)){
            $hardFailures.Add([pscustomobject]@{side=$Side;id=$Row.id;pair=$Pair;phase=$phase.name;
                rtf=$phase.rtf;maxBlockRatio=$phase.max_block_ratio;deadlineMisses=$phase.deadline_misses})
        }
    }
}
$schedule=Read (Join-Path $Root 'paired.json')
Assert ($schedule.complete -and $schedule.pairs -in @(5,9) -and $schedule.schedule.Count -gt 0) 'Incomplete paired run'
$hashes=@{S=(Get-FileHash -LiteralPath $BaselineExecutable).Hash;C=(Get-FileHash -LiteralPath $CandidateExecutable).Hash}
$first=@{};$byCase=@{};$seen=@{};$caseOrder=[Collections.Generic.List[string]]::new()
$hardFailures=[Collections.Generic.List[object]]::new();$measurements=[Collections.Generic.List[object]]::new()
$allRows=@{S=[Collections.Generic.List[object]]::new();C=[Collections.Generic.List[object]]::new()}
$lastEnd=[DateTimeOffset]::MinValue
foreach($pair in $schedule.schedule){
    Assert ($pair.id -cmatch '^[A-Za-z0-9.\-]+$' -and $pair.pair -ge 0 -and $pair.pair -lt $schedule.pairs) 'Invalid paired case/index'
    $key="$($pair.id)/$($pair.pair)";Assert (-not$seen.ContainsKey($key)) 'Duplicate pair';$seen[$key]=$true
    $expectedOrder=if($pair.pair%2 -eq 0){'S,C'}else{'C,S'}
    Assert (($pair.order -join ',') -ceq $expectedOrder) 'Order must alternate S,C / C,S'
    if(-not$byCase.ContainsKey($pair.id)){
        $byCase[$pair.id]=@{S=[Collections.Generic.List[object]]::new();C=[Collections.Generic.List[object]]::new()}
        $caseOrder.Add($pair.id)
    }
    foreach($side in $pair.order){
        $directory=Join-Path $Root "$($pair.id)-pair$($pair.pair)-$side"
        $run=Read (Join-Path $directory 'results.json')
        Assert ($run.completed_requested_run -and $run.configuration -eq 'Release' -and $run.mode -eq 'timing' -and
            $run.case_count -eq 1 -and $run.cases.Count -eq 1 -and $run.results.Count -eq 1 -and $run.repeat_count -eq 1 -and
            $run.executable_sha256 -ieq $hashes[$side] -and -not$run.provenance.environment.profiling -and
            -not$run.provenance.environment.debugger -and $run.results[0].id -ceq $pair.id -and $run.cases[0].id -ceq $pair.id -and
            $run.results[0].repeat -eq 0) 'Invalid one-side paired measurement'
        $start=[DateTimeOffset]$run.started_utc;$end=[DateTimeOffset]$run.completed_utc
        Assert ($start -ge $lastEnd -and $end -gt $start) 'Overlapping, reversed or missing paired execution timestamps'
        $lastEnd=$end
        if(-not$first.ContainsKey($side)){$first[$side]=$run}
        Assert ($run.provenance.source_sha256 -ceq $first[$side].provenance.source_sha256 -and
            $run.provenance.environment_sha256 -ceq $first[$side].provenance.environment_sha256 -and
            $run.manifest_version -ceq $first[$side].manifest_version -and $run.target_version -ceq $first[$side].target_version) 'Paired build/environment changed within a side'
        $row=$run.results[0];Hard $row $side $pair.pair
        $byCase[$pair.id][$side].Add([pscustomobject]@{case=$run.cases[0];row=$row;pair=$pair.pair;root=$directory})
        $row.repeat=[int]$pair.pair
        $row | Add-Member -NotePropertyName paired_source_directory -NotePropertyValue $directory
        $allRows[$side].Add($row)
        $measurements.Add([pscustomobject]@{id=$pair.id;pair=$pair.pair;side=$side;startUtc=$start.ToUniversalTime().ToString('o');
            endUtc=$end.ToUniversalTime().ToString('o');executableSha256=$run.executable_sha256;metadataSha256=$run.metadata_sha256})
    }
}
$comparisonEnvironment=Get-CPUComparisonEnvironmentHash $first.S.provenance ([bool]$first.S.syntheticUnitFixture)
Assert ($comparisonEnvironment -ceq (Get-CPUComparisonEnvironmentHash $first.C.provenance ([bool]$first.C.syntheticUnitFixture))) 'S/C environments differ'
$comparisons=[Collections.Generic.List[object]]::new()
foreach($id in $caseOrder){
    $s=@($byCase[$id].S | Sort-Object pair);$c=@($byCase[$id].C | Sort-Object pair)
    Assert ($s.Count -eq $schedule.pairs -and $c.Count -eq $schedule.pairs) 'Incomplete case pairing'
    for($r=0;$r -lt $schedule.pairs;$r++){
        Assert ($s[$r].pair -eq $r -and $c[$r].pair -eq $r -and
            $s[$r].row.manifest_sha256 -ceq $c[$r].row.manifest_sha256 -and
            $s[$r].row.manifest_sha256 -ceq $s[0].row.manifest_sha256) "Different inputs or missing pair: $id expected $r; S=$($s[$r].pair), C=$($c[$r].pair)"
    }
    $phaseNames=@('total')+@($s[0].row.phases | Where-Object {$_.valid} | ForEach-Object {$_.name})
    foreach($phaseName in $phaseNames){
        $pairs=@();$baseline=@();$candidate=@();$percent=@();$sMax=@();$cMax=@();$sP99=@();$cP99=@()
        for($r=0;$r -lt $schedule.pairs;$r++){
            $a=if($phaseName -eq 'total'){$s[$r].row.total}else{@($s[$r].row.phases | Where-Object {$_.name -ceq $phaseName})}
            $b=if($phaseName -eq 'total'){$c[$r].row.total}else{@($c[$r].row.phases | Where-Object {$_.name -ceq $phaseName})}
            Assert (@($a).Count -eq 1 -and @($b).Count -eq 1 -and $a.valid -and $b.valid) 'Missing/mismatched pure phase'
            $pairs+=@{baseline_rtf=$a.rtf;candidate_rtf=$b.rtf}
            $baseline+=$a.rtf;$candidate+=$b.rtf;$percent+=100*($a.rtf-$b.rtf)/$a.rtf
            $sMax+=$a.max_block_seconds;$cMax+=$b.max_block_seconds;$sP99+=$a.p99_seconds;$cP99+=$b.p99_seconds
        }
        $statistic=$null
        if($schedule.pairs -eq 9){
            $path=Join-Path $Output "$id-$phaseName-pairs.json";Save @{pairs=$pairs} $path
            $text= & $BaselineExecutable --pair-statistics $path
            Assert ($LASTEXITCODE -eq 0) 'Fixed-seed paired statistics failed'
            $statistic=($text | Out-String) | ConvertFrom-Json
        }
        $aSummary=Summary $baseline;$bSummary=Summary $candidate;$change=Summary $percent
        $ratio=100*($aSummary.median-$bSummary.median)/$aSummary.median
        $comparisons.Add([pscustomobject]@{id=$id;phase=$phaseName;baselineRtf=$aSummary;candidateRtf=$bSummary;
            pairedImprovementPercent=$change;ratioOfMediansImprovementPercent=$ratio;statistics=$statistic;
            suspectedRegression=($change.median -lt -3 -or $ratio -lt -3);
            baselineMax=($sMax | Measure-Object -Maximum).Maximum;candidateMax=($cMax | Measure-Object -Maximum).Maximum;
            baselineP99=(Summary $sP99);candidateP99=(Summary $cP99);
            investigateP99OrMax=(($cMax | Measure-Object -Maximum).Maximum -gt ($sMax | Measure-Object -Maximum).Maximum -or (Median $cP99) -gt (Median $sP99))})
    }
}
foreach($side in @('S','C')){
    $directory=Join-Path $Output $side;New-Item -ItemType Directory -Path $directory | Out-Null
    $run=$first[$side];$run.cases=@($caseOrder | ForEach-Object {$byCase[$_][$side][0].case})
    $run.results=@($allRows[$side]);$run.case_count=$caseOrder.Count;$run.repeat_count=$schedule.pairs
    $sideExecutions=@($measurements | Where-Object {$_.side -ceq $side})
    $run.started_utc=$sideExecutions[0].startUtc;$run.completed_utc=$sideExecutions[-1].endUtc
    $run | Add-Member -NotePropertyName paired_source_root -NotePropertyValue ([IO.Path]::GetFullPath($Root))
    # Each individual case retains its validated manifest digest. This is an
    # explicit paired subset, not a fabricated full selected-manifest digest.
    $run.selected_manifest_sha256=$null
    foreach($row in $allRows[$side]){
        $source=Join-Path $row.paired_source_directory "$($row.id)-rep0.timings.f64"
        New-Item -ItemType HardLink -Path (Join-Path $directory "$($row.id)-rep$($row.repeat).timings.f64") -Target $source | Out-Null
    }
    Save $run (Join-Path $directory 'results.json')
}
Export-CPUCsv $measurements (Join-Path $Output 'execution-order.csv')
[ordered]@{pairedRoot=[IO.Path]::GetFullPath($Root);caseCount=$caseOrder.Count;pairCount=$schedule.pairs;
    syntheticUnitFixture=[bool]$schedule.syntheticUnitFixture;
    caseIds=$caseOrder;allOriginalPairsRetained=$true;alternatingSequentialOrderVerified=$true;
    baselineVersion=$first.S.target_version;candidateVersion=$first.C.target_version;
    baselineExecutableSha256=$hashes.S;candidateExecutableSha256=$hashes.C;
    baselineSourceSha256=$first.S.provenance.source_sha256;candidateSourceSha256=$first.C.provenance.source_sha256;
    environmentSha256=$first.S.provenance.environment_sha256;candidateEnvironmentSha256=$first.C.provenance.environment_sha256;
    comparisonEnvironmentSha256=$comparisonEnvironment;comparisonIdentityRule='Ignore only CPU-set Parked observation bit; preserve raw metadata hashes/flags';comparisons=$comparisons;hardFailures=$hardFailures;
    candidateHardGatesPass=(@($hardFailures | Where-Object {$_.side -eq 'C'}).Count -eq 0);
    completeFullInventoryStillRequiresVerification=$true;profileAndTwoRunEvidenceStillRequired=$true;
    fullMilestonePass=$false} | ConvertTo-Json -Depth 40 | Set-Content -LiteralPath (Join-Path $Output 'paired-analysis.json') -Encoding utf8
Write-Host "Analyzed $($caseOrder.Count) cases x $($schedule.pairs) original pairs; $($hardFailures.Count) hard violations. Not complete milestone approval."
