param(
    [Parameter(Mandatory=$true)][string]$TimingRoot,
    [Parameter(Mandatory=$true)][string]$AudioRoot,
    [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 analysis output directory'}
New-Item -ItemType Directory -Path $Output | Out-Null
function Read-Json([string]$Path){
    if(-not(Test-Path -LiteralPath $Path -PathType Leaf)){throw "Missing evidence: $Path"}
    Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
}
$timing=Read-Json (Join-Path $TimingRoot 'results.json')
$audio=Read-Json (Join-Path $AudioRoot 'manifest.json')
if(-not$timing.completed_requested_run -or $timing.mode -ne 'timing' -or $timing.configuration -ne 'Release' -or
    $audio.mode -ne 'audio' -or $audio.configuration -ne $timing.configuration -or
    $audio.manifest_version -ne $timing.manifest_version -or
    $timing.case_count -le 0 -or $timing.case_count -ne $timing.cases.Count -or
    $timing.repeat_count -le 0 -or $timing.results.Count -ne $timing.case_count*$timing.repeat_count){throw 'Incompatible audio/timing evidence'}
if(-not('DeepPluckTimingAnalysis' -as [type])){Add-Type -TypeDefinition @'
using System;
using System.Collections.Generic;
public sealed class DeepPluckPhaseTimes {
    public string name;
    public int blocks, deadlineMisses;
    public long frames;
    public double callbackSeconds, audioSeconds, rtf, maxBlockSeconds, maxBlockRatio;
    public double p50, p95, p99, p999;
}
public static class DeepPluckTimingAnalysis {
    public static void FillPlan(int[] counts, int[] rates, int count, int rate) {
        for(int i=0;i<counts.Length;i++) {counts[i]=count; rates[i]=rate;}
    }
    public static void FillCategory(byte[] categories, int first, int count, byte category) {
        if(first<0 || count<=0 || first+count>categories.Length || category>5)
            throw new InvalidOperationException("Invalid lifecycle run");
        for(int i=first;i<first+count;i++) categories[i]=category;
    }
    public static DeepPluckPhaseTimes[] Analyze(string path, byte[] categories, int[] counts, int[] rates) {
        var bytes=System.IO.File.ReadAllBytes(path);
        if(bytes.Length!=categories.Length*8 || counts.Length!=categories.Length || rates.Length!=categories.Length)
            throw new InvalidOperationException("Timing/block/diagnostic lengths differ");
        var times=new double[categories.Length]; Buffer.BlockCopy(bytes,0,times,0,bytes.Length);
        string[] names={"held-voices","releasing-voices","steal-residual","fx-only","idle","mixed-lifecycle-boundary"};
        var output=new DeepPluckPhaseTimes[names.Length];
        for(int category=0;category<names.Length;category++) {
            var result=new DeepPluckPhaseTimes {name=names[category]}; var samples=new List<double>();
            for(int i=0;i<times.Length;i++) {
                if(double.IsNaN(times[i]) || double.IsInfinity(times[i]) || times[i]<0)
                    throw new InvalidOperationException("Invalid block duration");
                if(categories[i]>5 || counts[i]<0 || rates[i]<=0)
                    throw new InvalidOperationException("Invalid diagnostic block plan");
                if(categories[i]!=category || counts[i]==0) continue;
                double deadline=(double)counts[i]/rates[i];
                result.blocks++; result.frames+=counts[i]; result.callbackSeconds+=times[i]; result.audioSeconds+=deadline;
                result.maxBlockSeconds=Math.Max(result.maxBlockSeconds,times[i]);
                result.maxBlockRatio=Math.Max(result.maxBlockRatio,times[i]/deadline);
                if(times[i]>=deadline) result.deadlineMisses++;
                samples.Add(times[i]);
            }
            if(result.blocks>0) {
                samples.Sort(); result.rtf=result.callbackSeconds/result.audioSeconds;
                result.p50=samples[(int)Math.Ceiling(.5*(samples.Count-1))];
                result.p95=samples[(int)Math.Ceiling(.95*(samples.Count-1))];
                result.p99=samples[(int)Math.Ceiling(.99*(samples.Count-1))];
                result.p999=samples[(int)Math.Ceiling(.999*(samples.Count-1))];
            }
            output[category]=result;
        }
        return output;
    }
}
'@
}
function Category($State){
    if($State.residual_voices -gt 0){return 2}
    if($State.active_voices -gt 0){if($State.releasing_voices -eq $State.active_voices){return 1}else{return 0}}
    if(-not$State.fx_idle){return 3}
    return 4
}
$cases=@{};$rows=@{};$audioCases=@{}
foreach($case in $audio.cases){
    if($audioCases.ContainsKey($case.id)){throw 'Duplicate audio manifest case'}
    $audioCases[$case.id]=$case
}
foreach($case in $timing.cases){
    if($cases.ContainsKey($case.id)){throw 'Duplicate case manifest'}
    if(-not$audioCases.ContainsKey($case.id) -or
        ($case | ConvertTo-Json -Depth 30 -Compress) -cne ($audioCases[$case.id] | ConvertTo-Json -Depth 30 -Compress)){throw 'Audio/timing case inputs differ'}
    $cases[$case.id]=$case
}
foreach($result in $timing.results){
    if(-not$cases.ContainsKey($result.id)){throw 'Unknown timing result'}
    if(-not$rows.ContainsKey($result.id)){$rows[$result.id]=[Collections.Generic.List[object]]::new()}
    $rows[$result.id].Add($result)
}
$phaseRows=[Collections.Generic.List[object]]::new()
$summaries=[Collections.Generic.List[object]]::new()
foreach($id in $cases.Keys | Sort-Object){
    $case=$cases[$id];$diagnostic=Read-Json (Join-Path $AudioRoot "$id-rep0.json")
    $caseRows=@($rows[$id] | Sort-Object repeat)
    if($caseRows.Count -ne $timing.repeat_count){throw "Missing timing repetition for $id"}
    for($r=0;$r -lt $caseRows.Count;$r++){
        if($caseRows[$r].repeat -ne $r -or $diagnostic.manifest_sha256 -cne $caseRows[$r].manifest_sha256){throw "Duplicate repetition or different diagnostic inputs for $id"}
    }
    $blocks=if($case.lifecycle_plan){$case.lifecycle_block_plan.Count}else{[int]($case.frames/$case.nominal_block_size)}
    $categories=New-Object byte[] $blocks;$counts=New-Object int[] $blocks;$rates=New-Object int[] $blocks
    if($case.lifecycle_plan){
        for($i=0;$i -lt $blocks;$i++){$counts[$i]=$case.lifecycle_block_plan[$i].samples;$rates[$i]=$case.lifecycle_block_plan[$i].sample_rate}
    }else{[DeepPluckTimingAnalysis]::FillPlan($counts,$rates,$case.nominal_block_size,$case.sample_rate)}
    $covered=0
    foreach($run in $diagnostic.lifecycle_runs){
        if($run.first_block -ne $covered -or $run.blocks -le 0 -or $covered+$run.blocks -gt $blocks){throw 'Incomplete lifecycle run coverage'}
        $before=Category $run.before;$after=Category $run.after
        $category=if($before -eq $after){$before}else{5}
        [DeepPluckTimingAnalysis]::FillCategory($categories,$run.first_block,$run.blocks,[byte]$category)
        $covered+=$run.blocks
    }
    if($covered -ne $blocks){throw 'Lifecycle/timing block count mismatch'}
    if($case.lifecycle_plan){
        for($i=0;$i -lt $blocks;$i++){
            if(@($case.lifecycle_block_plan[$i].midi | Where-Object {$_.offset -gt 0}).Count -gt 0){$categories[$i]=5}
        }
    }else{
        foreach($event in $case.events){
            if($event.kind -le 2 -and $event.block_offset -gt 0){$categories[[int]$event.block_index]=5}
        }
    }
    $rtf=@($caseRows.total.rtf | Sort-Object)
    $median=$rtf[[int][math]::Floor($rtf.Count/2)]
    $deviations=@($rtf | ForEach-Object {[math]::Abs($_-$median)} | Sort-Object)
    $slowest=$caseRows | Sort-Object {$_.total.rtf} -Descending | Select-Object -First 1
    $summaries.Add([pscustomobject]@{id=$id;medianRtf=$median;minimumRtf=$rtf[0];maximumRtf=$rtf[-1];
        madRtf=$deviations[[int][math]::Floor($deviations.Count/2)];slowestRepeat=$slowest.repeat;
        maximumBlockSeconds=($caseRows.total.max_block_seconds | Measure-Object -Maximum).Maximum;
        maximumBlockRatio=($caseRows.total.max_block_ratio | Measure-Object -Maximum).Maximum;
        deadlineMisses=($caseRows.total.deadline_misses | Measure-Object -Sum).Sum})
    foreach($row in $caseRows){
        $phases=[DeepPluckTimingAnalysis]::Analyze((Join-Path $TimingRoot "$id-rep$($row.repeat).timings.f64"),$categories,$counts,$rates)
        $callbackSum=($phases.callbackSeconds | Measure-Object -Sum).Sum
        $audioSum=($phases.audioSeconds | Measure-Object -Sum).Sum
        if([math]::Abs($callbackSum-$row.total.callback_seconds) -gt 1e-10*[math]::Max(1,$callbackSum) -or
           [math]::Abs($audioSum-$row.total.audio_seconds) -gt 1e-10*[math]::Max(1,$audioSum) -or
           # Windows PowerShell 5 can parse a round-trip JSON number as Decimal.
           # Compare the actual float64 timer domain, not Decimal coercion of a
           # binary double. This is still exact equality, not an extra tolerance.
           ($phases.maxBlockSeconds | Measure-Object -Maximum).Maximum -ne [double]$row.total.max_block_seconds -or
           ($phases.deadlineMisses | Measure-Object -Sum).Sum -ne $row.total.deadline_misses){throw "Block data/summary mismatch: $id"}
        foreach($phase in $phases){
            $phaseRows.Add([pscustomobject]@{id=$id;repeat=$row.repeat;phase=$phase.name;blocks=$phase.blocks;frames=$phase.frames;
                callbackSeconds=$phase.callbackSeconds;audioSeconds=$phase.audioSeconds;rtf=$phase.rtf;
                averageBlockSeconds=$(if($phase.blocks -gt 0){$phase.callbackSeconds/$phase.blocks}else{$null});
                maxBlockSeconds=$phase.maxBlockSeconds;maxBlockRatio=$phase.maxBlockRatio;deadlineMisses=$phase.deadlineMisses;
                p50=$phase.p50;p95=$phase.p95;p99=$phase.p99;p999=$phase.p999})
        }
    }
}
Export-CPUCsv $summaries (Join-Path $Output 'case-summary.csv')
Export-CPUCsv $phaseRows (Join-Path $Output 'diagnostic-phases.csv')
$worstD=$summaries | Where-Object {$_.id -like 'D-*'} | Sort-Object medianRtf -Descending | Select-Object -First 1
$violations=@($phaseRows | Where-Object {$_.blocks -gt 0 -and ($_.rtf -ge 1 -or $_.maxBlockRatio -ge 1 -or $_.deadlineMisses -gt 0 -or ($_.id -like 'D-*' -and $_.rtf -gt .9))})
$softViolations=[Collections.Generic.List[object]]::new()
foreach($row in $timing.results){
    $target=if($row.id -like 'B-*'){.20}elseif($row.id -like 'D-*'){.50}else{$null}
    if($null -eq $target){continue}
    $firstPhase=$true
    foreach($phase in @($row.total)+@($row.phases)){
        if($phase.valid -and $phase.rtf -gt $target){
            $softViolations.Add([pscustomobject]@{id=$row.id;repeat=$row.repeat;kind='static';
                phase=$(if($firstPhase){'total'}else{$phase.name});rtf=$phase.rtf;softTargetRtf=$target})
        }
        $firstPhase=$false
    }
}
foreach($phase in $phaseRows){
    $target=if($phase.id -like 'B-*'){.20}elseif($phase.id -like 'D-*'){.50}else{$null}
    if($null -ne $target -and $phase.blocks -gt 0 -and $phase.rtf -gt $target){
        $softViolations.Add([pscustomobject]@{id=$phase.id;repeat=$phase.repeat;kind='diagnostic';
            phase=$phase.phase;rtf=$phase.rtf;softTargetRtf=$target})
    }
}
[ordered]@{caseCount=$cases.Count;repeatCount=$timing.repeat_count;worstD=$worstD;dynamicPhaseViolations=$violations;
    worstOverallMedian=($summaries | Sort-Object medianRtf -Descending | Select-Object -First 1);
    worstOverallBlockRatio=($summaries | Sort-Object maximumBlockRatio -Descending | Select-Object -First 1);
    worstDBlockRatio=($summaries | Where-Object {$_.id -like 'D-*'} | Sort-Object maximumBlockRatio -Descending | Select-Object -First 1);
    softTargets=@{B=.20;D=.50;meaning='Reported per repetition, whole/static and diagnostic phases; not hard acceptance thresholds';violations=$softViolations};
    timingRoot=[IO.Path]::GetFullPath($TimingRoot);audioRoot=[IO.Path]::GetFullPath($AudioRoot);
    executableSha256=$timing.executable_sha256;sourceSha256=$timing.provenance.source_sha256;
    environmentSha256=$timing.provenance.environment_sha256;
    comparisonEnvironmentSha256=(Get-CPUComparisonEnvironmentHash $timing.provenance ([bool]$timing.syntheticUnitFixture));
    phaseMeaning='Matched pre/post callback diagnostic categories. Residual-steal state takes precedence over other active voices. Held-voices means at least one held voice and may include concurrently releasing voices; releasing-voices means all active voices are releasing. Mixed MIDI/lifecycle boundary blocks remain included separately, never dropped from totals.';
    fullMilestonePass=$false} | ConvertTo-Json -Depth 15 | Set-Content -LiteralPath (Join-Path $Output 'analysis.json') -Encoding utf8
Write-Host "Analyzed $($cases.Count) cases; $($violations.Count) dynamic phase hard-gate violations. No milestone approval."
