#include "Runner.h"
#include "PluginProcessor.h"
#include "Synth/DeepPluckVoice.h"
#include <juce_audio_utils/juce_audio_utils.h>
#include <algorithm>
#include <bit>
#include <chrono>
#include <cstring>
#include <iostream>
#include <numeric>

namespace deeppluck::benchmark
{
using Clock = std::chrono::steady_clock;
static_assert (Clock::is_steady);
using Buffer = juce::AudioBuffer<float>;

// Compiled exclusively into this test executable. This friend reads existing
// state outside processBlock, and is never invoked by a timing/profile run.
struct Access
{
    struct State
    {
        int active = 0, releasing = 0, residual = 0, unison = 0, fxIdle = 0, order = 0, fxFlags = 0;
        std::array<std::uint32_t, 16> voiceFlags {};
        bool operator== (const State&) const = default;
    };
    static State inspect (const DeepPluckAudioProcessor& p)
    {
        State result;
        for (int i=0;i<p.synthesiser.getNumVoices();++i)
        {
            const auto* v=dynamic_cast<const DeepPluckVoice*>(p.synthesiser.getVoice(i));
            require(v!=nullptr,"Unexpected voice type");
            const auto active=v->isVoiceActive();
            result.active += active ? 1 : 0;
            result.releasing += active && v->isReleasing() ? 1 : 0;
            result.residual += v->residualTailSamplesRemaining>0 ? 1 : 0;
            result.unison += active ? v->getLatchedUnisonVoiceCount() : 0;
            result.voiceFlags[static_cast<size_t>(i)] = (active ? 1u : 0u) | (v->isReleasing() ? 2u : 0u)
                | (v->isFilterEnvelopeReleasing() ? 4u : 0u) | (v->isFilterEnvelopeActive() ? 8u : 0u)
                | (v->isTransientActive() ? 16u : 0u) | (v->isNoiseActive() ? 32u : 0u)
                | (static_cast<std::uint32_t>(v->getCurrentlyPlayingNote()+1)<<8u)
                | (static_cast<std::uint32_t>(v->getLatchedUnisonVoiceCount())<<16u)
                | (static_cast<std::uint32_t>(v->residualTailSamplesRemaining)<<20u);
        }
        result.fxIdle=p.effectsRack.isIdle() ? 1 : 0;
        result.order=p.effectsRack.getCurrentOrder();
        result.fxFlags=(p.effectsRack.chorus.isIdle() ? 1 : 0)
            | (p.effectsRack.delay.isIdle() ? 2 : 0) | (p.effectsRack.reverb.isIdle() ? 4 : 0)
            | (static_cast<int>(p.effectsRack.bridgePhase)<<3) | (p.effectsRack.pendingOrder<<5)
            | (p.effectsRack.orderBridge.isSmoothing() ? 256 : 0) | (p.outputGain.isSmoothing() ? 512 : 0);
        return result;
    }
    static std::array<double, 129> oscillatorAndLfoPhases (const DeepPluckAudioProcessor& p)
    {
        std::array<double,129> result {};
        result[0]=p.synthesiser.getLfoPhase();
        for(int i=0;i<16;++i)
        {
            const auto* voice=static_cast<const DeepPluckVoice*>(p.synthesiser.getVoice(i));
            for(int slot=0;slot<4;++slot)
            {
                result[1+static_cast<size_t>(i)*8+static_cast<size_t>(slot)*2]=voice->getUnisonPrimaryPhase(slot);
                result[2+static_cast<size_t>(i)*8+static_cast<size_t>(slot)*2]=voice->getUnisonSecondPhase(slot);
            }
        }
        return result;
    }
};
namespace
{
double seconds (Clock::duration duration) { return std::chrono::duration<double>(duration).count(); }
struct PlayHead final : juce::AudioPlayHead
{
    std::int64_t sample=0;
    double rate=48000, bpm=120, ppq=0;
    bool missing=false;
    juce::Optional<PositionInfo> getPosition() const override
    {
        PositionInfo info;
        info.setTimeInSamples(sample); info.setTimeInSeconds(static_cast<double>(sample)/rate);
        info.setPpqPosition(ppq); info.setTimeSignature(TimeSignature {4,4}); info.setIsPlaying(true);
        if (!missing) info.setBpm(bpm);
        return info;
    }
};
struct Applied
{
    Values raw{}, normalized{};
    juce::var jsonValue() const
    {
        auto out=object(); put(out,"raw_applied",array(raw)); put(out,"normalized_applied",array(normalized));
        return out;
    }
};
float applyOne (DeepPluckAudioProcessor& p, int index, float requested)
{
    require(index>=0 && index<62 && std::isfinite(requested),"Invalid requested parameter");
    const auto* id=allParameterIds[static_cast<size_t>(index)];
    auto& state=p.getValueTreeState();
    auto* parameter=state.getParameter(id);
    require(parameter!=nullptr,"Missing frozen parameter");
    const auto& range=parameter->getNormalisableRange();
    require(requested>=range.start && requested<=range.end,"Out-of-range request: "+std::string(id));
    if (range.interval>0)
    {
        const auto snapped=range.snapToLegalValue(requested);
        const auto rounding=8*std::numeric_limits<float>::epsilon()*std::max(1.0f,std::abs(requested));
        require(std::abs(snapped-requested)<=rounding,"Off-grid request: "+std::string(id));
    }
    const auto norm=parameter->convertTo0to1(requested);
    require(std::isfinite(norm) && norm>=0 && norm<=1,"Invalid normalized request");
    parameter->setValueNotifyingHost(norm);
    const auto applied=state.getRawParameterValue(id)->load();
    // The APVTS adapter denormalises parameter.getValue(), which itself performs
    // a second raw->normalised conversion, and retains approximatelyEqual raw
    // values. Validate that actual JUCE path, not a hypothetical single pass.
    const auto expected=parameter->convertFrom0to1(parameter->getValue());
    require(std::isfinite(applied) && juce::approximatelyEqual(applied,expected),"APVTS request was not applied exactly: "+std::string(id)
        +" requested="+std::to_string(requested)+" expected="+std::to_string(expected)+" applied="+std::to_string(applied));
    return applied;
}
Applied applySnapshot (DeepPluckAudioProcessor& p, const Values& values)
{
    Applied applied;
    require(p.getParameters().size()==62,"Frozen parameter count changed");
    for (int i=0;i<62;++i)
    {
        const auto n=static_cast<size_t>(i);
        auto* parameter=dynamic_cast<juce::RangedAudioParameter*>(p.getParameters()[i]);
        require(parameter!=nullptr && parameter==p.getValueTreeState().getParameter(allParameterIds[n]),"Frozen order changed");
        applied.raw[n]=applyOne(p,i,values[n]); applied.normalized[n]=parameter->getValue();
    }
    return applied;
}
struct Block
{
    std::int64_t sample=0;
    int count=0, rate=0;
    bool reset=false, bypass=false;
    juce::MidiBuffer midi;
    std::vector<Event> events;
};
std::vector<Block> plan (const Case& c)
{
    std::vector<Block> blocks;
    if (c.lifecycle)
    {
        // Each epoch has a new prepare and Preroll, with releaseResources before
        // the next. Empty and host-bypassed callbacks are explicit, untimed for
        // zero samples. Musical sample 0 is restarted in every epoch.
        constexpr std::array sizes {1,63,64,65,127,128,129,256,512};
        for (int rate : {44100,48000,96000,44100})
        {
            std::int64_t pos=0;
            int index=0;
            while (pos<static_cast<std::int64_t>(rate)*3/2)
            {
                const auto size=sizes[static_cast<size_t>(index)%sizes.size()];
                Block b; b.sample=pos; b.count=size; b.rate=rate; b.reset=index==0;
                b.bypass=index%31==30; b.midi.ensureSize(256);
                if (index==0) b.midi.addEvent(juce::MidiMessage::noteOn(1,60,static_cast<juce::uint8>(96)),0);
                if (pos<=rate && pos+size>rate) b.midi.addEvent(juce::MidiMessage::noteOff(1,60,static_cast<juce::uint8>(0)),static_cast<int>(rate-pos));
                blocks.push_back(std::move(b)); pos+=size; ++index;
                if (index==4)
                { Block empty; empty.sample=pos; empty.rate=rate; blocks.push_back(std::move(empty)); }
            }
        }
        return blocks;
    }
    blocks.reserve(static_cast<size_t>(c.samples/c.block));
    size_t next=0;
    for (std::int64_t pos=0;pos<c.samples;pos+=c.block)
    {
        Block b; b.sample=pos; b.rate=c.rate; b.count=c.block; b.midi.ensureSize(2048);
        while (next<c.events.size() && c.events[next].sample<pos+c.block)
        {
            const auto& event=c.events[next++];
            const auto offset=static_cast<int>(event.sample-pos);
            require(offset>=0,"Late event in block plan");
            switch(event.kind)
            {
                case Kind::noteOn: b.midi.addEvent(juce::MidiMessage::noteOn(1,event.index,static_cast<juce::uint8>(event.value)),offset); break;
                case Kind::noteOff: b.midi.addEvent(juce::MidiMessage::noteOff(1,event.index,static_cast<juce::uint8>(0)),offset); break;
                case Kind::controller: b.midi.addEvent(juce::MidiMessage::controllerEvent(1,event.index,static_cast<int>(event.value)),offset); break;
                default: require(offset==0,"Host change not aligned to block start"); b.events.push_back(event); break;
            }
        }
        blocks.push_back(std::move(b));
    }
    require(next==c.events.size(),"Unapplied event");
    return blocks;
}
juce::File isolatedRoot()
{
    // Never create or scan this directory; IO is disabled on the processor.
    return juce::File::getSpecialLocation(juce::File::currentExecutableFile).getSiblingFile("NoPresetIO");
}
double quantile (std::vector<double> values, double fraction)
{
    require(!values.empty(),"Empty timing quantile"); std::sort(values.begin(),values.end());
    return values[static_cast<size_t>(std::ceil(fraction*static_cast<double>(values.size()-1)))];
}
juce::var timingStats (const std::vector<double>& times, const std::vector<double>& deadlines)
{
    require(times.size()==deadlines.size(),"Invalid timing vectors");
    auto out=object(); put(out,"blocks",static_cast<int>(times.size()));
    if(times.empty()) { put(out,"valid",false); return out; }
    const auto sum=std::accumulate(times.begin(),times.end(),0.0);
    const auto duration=std::accumulate(deadlines.begin(),deadlines.end(),0.0);
    require(duration>0 && sum>0,"Invalid timing duration");
    double maxRatio=0; int missed=0;
    for(size_t i=0;i<times.size();++i)
    {
        require(std::isfinite(times[i]) && times[i]>=0 && deadlines[i]>0,"Nonfinite/invalid block time");
        maxRatio=std::max(maxRatio,times[i]/deadlines[i]); if(times[i]>=deadlines[i]) ++missed;
    }
    put(out,"valid",true); put(out,"callback_seconds",sum); put(out,"audio_seconds",duration);
    put(out,"rtf",sum/duration); put(out,"average_block_seconds",sum/static_cast<double>(times.size()));
    put(out,"max_block_seconds",*std::max_element(times.begin(),times.end())); put(out,"max_block_ratio",maxRatio);
    put(out,"deadline_misses",missed); put(out,"p50_seconds",quantile(times,.50)); put(out,"p95_seconds",quantile(times,.95));
    put(out,"p99_seconds",quantile(times,.99)); put(out,"p999_seconds",quantile(times,.999));
    return out;
}
juce::var stateJson (const Access::State& s)
{
    auto v=object(); put(v,"active_voices",s.active); put(v,"releasing_voices",s.releasing);
    put(v,"residual_voices",s.residual); put(v,"active_unison_slots",s.unison);
    put(v,"fx_idle",s.fxIdle); put(v,"fx_order",s.order);
    put(v,"fx_lifecycle_flags",s.fxFlags);
    juce::Array<juce::var> flags;
    for(const auto f:s.voiceFlags) flags.add(static_cast<juce::int64>(f));
    put(v,"voice_flags",flags); return v;
}
void writeRaw (const juce::File& file, const std::vector<float>& audio)
{
    require(!file.exists(),"Refusing to overwrite raw reference");
    juce::FileOutputStream stream(file);
    require(stream.openedOk() && stream.write(audio.data(),audio.size()*sizeof(float)),"Cannot write raw audio");
    stream.flush(); require(stream.getStatus().wasOk(),"Raw audio flush failed");
}
std::vector<float> readRaw (const juce::File& file)
{
    require(file.existsAsFile() && file.getSize()>0 && file.getSize()%8==0,"Missing/invalid stereo float reference");
    const auto bytes=file.getSize(); require(bytes<=std::numeric_limits<int>::max(),"Oversized reference");
    std::vector<float> audio(static_cast<size_t>(bytes)/sizeof(float));
    juce::FileInputStream stream(file);
    require(stream.openedOk() && stream.read(audio.data(),static_cast<int>(bytes))==bytes,"Short reference read");
    return audio;
}
void writeWave (const juce::File& file, const std::vector<float>& audio, int rate)
{
    require(!file.exists(),"Refusing to overwrite WAV");
    const auto frames=static_cast<int>(audio.size()/2); Buffer buffer(2,frames);
    for(int i=0;i<frames;++i) { buffer.setSample(0,i,audio[2*static_cast<size_t>(i)]); buffer.setSample(1,i,audio[2*static_cast<size_t>(i)+1]); }
    std::unique_ptr<juce::OutputStream> stream=file.createOutputStream();
    juce::WavAudioFormat format;
    auto writer=format.createWriterFor(stream,juce::AudioFormatWriterOptions{}.withSampleRate(rate).withNumChannels(2)
        .withBitsPerSample(32).withSampleFormat(juce::AudioFormatWriterOptions::SampleFormat::floatingPoint));
    require(writer!=nullptr && writer->writeFromAudioSampleBuffer(buffer,0,frames),"Cannot write float WAV");
}
}
juce::var validatedManifest (const Case& c)
{
    DeepPluckAudioProcessor p(isolatedRoot(),false);
    auto out=describe(c); const auto applied=applySnapshot(p,c.requested);
    put(out,"raw_applied",array(applied.raw)); put(out,"normalized_applied",array(applied.normalized));
    juce::Array<juce::var> changes;
    for(const auto& e:c.events)
    {
        if(e.kind!=Kind::parameter) continue;
        auto v=object(); put(v,"sample",static_cast<juce::int64>(e.sample)); put(v,"index",e.index);
        put(v,"requested",e.value); put(v,"applied",applyOne(p,e.index,static_cast<float>(e.value)));
        put(v,"normalized",p.getValueTreeState().getParameter(allParameterIds[static_cast<size_t>(e.index)])->getValue());
        changes.add(v);
    }
    put(out,"parameter_changes_applied",changes);
    if(c.lifecycle)
    {
        juce::Array<juce::var> blockList;
        std::int64_t actualFrames=0;
        for(const auto& block:plan(c))
        {
            auto item=object(); put(item,"sample_in_epoch",static_cast<juce::int64>(block.sample));
            put(item,"sample_rate",block.rate); put(item,"samples",block.count);
            put(item,"release_reprepare_preroll",block.reset); put(item,"host_bypass",block.bypass);
            juce::Array<juce::var> midi;
            for(const auto metadata:block.midi)
            {
                auto event=object(); put(event,"offset",metadata.samplePosition);
                const auto message=metadata.getMessage();
                put(event,"midi_hex",juce::String::toHexString(message.getRawData(),message.getRawDataSize(),0)); midi.add(event);
            }
            put(item,"midi",midi); blockList.add(item); actualFrames+=block.count;
        }
        put(out,"nominal_fixture_frames",out["frames"]); put(out,"frames",static_cast<juce::int64>(actualFrames));
        put(out,"lifecycle_block_plan",blockList);
    }
    return out;
}
juce::var timerCalibration()
{
    std::vector<double> times(10000);
    for(auto& t:times) { const auto begin=Clock::now(); const auto end=Clock::now(); t=seconds(end-begin); }
    auto out=object(); put(out,"clock","std::chrono::steady_clock (MSVC QueryPerformanceCounter)");
    put(out,"empty_pairs",static_cast<int>(times.size())); put(out,"median_seconds",quantile(times,.5));
    put(out,"p99_seconds",quantile(times,.99)); put(out,"subtracted",false); return out;
}
juce::var execute (const Case& c, const Options& options, int repeat, bool warmup)
{
    const auto audioMode=options.mode=="audio" && !warmup;
    const auto timingMode=options.mode=="timing" && !warmup;
    const auto manifest=validatedManifest(c);
    auto blocks=plan(c); require(!blocks.empty(),"Empty block plan");
    size_t frameCount=0;
    for(const auto& b:blocks) frameCount+=static_cast<size_t>(b.count);
    std::vector<float> audio(audioMode ? frameCount*2 : 0);
    std::vector<double> times(timingMode ? blocks.size() : 0);
    std::vector<Access::State> before(audioMode ? blocks.size() : 0),after(before.size());
    std::vector<std::array<double,129>> oscillatorPhases(audioMode ? blocks.size() : 0);
    Buffer storage(2,512); std::array<float*,2> pointers{storage.getWritePointer(0),storage.getWritePointer(1)};
    Buffer buffer(pointers.data(),2,c.block);
    PlayHead playHead; playHead.rate=c.rate;
    const auto constructStart=Clock::now();
    DeepPluckAudioProcessor p(isolatedRoot(),false);
    const auto constructTime=seconds(Clock::now()-constructStart);
    applySnapshot(p,c.requested); p.setPlayHead(&playHead); p.setRateAndBufferSizeDetails(c.rate,c.lifecycle ? 512 : c.block);
    // Prepared state XML is intentionally created outside every callback and
    // before timing, even for runs that never consume a state transition.
    std::array<juce::MemoryBlock,2> savedStates;
    if(c.family=="T8")
    {
        DeepPluckAudioProcessor stateSource(isolatedRoot(),false);
        applySnapshot(stateSource,foundation()); stateSource.getStateInformation(savedStates[0]);
        applySnapshot(stateSource,c.requested); stateSource.getStateInformation(savedStates[1]);
    }
    double prepareTime=0, prerollTime=0, firstCallbackTime=0;
    const auto prepare=[&](int rate,int nominal)
    {
        const auto begin=Clock::now();
        const auto maximum=c.lifecycle ? 512 : nominal;
        p.setRateAndBufferSizeDetails(rate,maximum); p.prepareToPlay(rate,maximum);
        prepareTime+=seconds(Clock::now()-begin);
        playHead.rate=rate; playHead.sample=0; playHead.ppq=0; playHead.bpm=120; playHead.missing=false;
        buffer.setDataToReferTo(pointers.data(),2,nominal);
        juce::MidiBuffer empty;
        const auto count=static_cast<int>(std::ceil(.25*rate/nominal));
        const auto start=Clock::now();
        for(int i=0;i<count;++i)
        {
            if(i==0) { const auto first=Clock::now(); p.processBlock(buffer,empty); firstCallbackTime+=seconds(Clock::now()-first); }
            else p.processBlock(buffer,empty);
            playHead.sample+=nominal; playHead.ppq+=static_cast<double>(nominal)/rate*2;
        }
        prerollTime+=seconds(Clock::now()-start);
        playHead.sample=0; playHead.ppq=0;
    };
    prepare(c.rate,c.block);
    size_t destination=0;
    double lastValidBpm=120;
#if defined(DEEPPLUCK_PROFILE_MARKERS)
    // Diagnostic build only, outside processBlock. Absolute QPC timestamps let
    // ETL sample stacks be sliced into known E release/FX-only/idle windows.
    struct ProfileBoundary { std::int64_t sample; std::string name; };
    struct ProfileMarker { size_t block; Clock::time_point time; std::string name; };
    std::vector<ProfileBoundary> profileBoundaries;
    for(const auto& phase:c.phases) profileBoundaries.push_back({phase.begin,phase.name});
    if(options.mode=="profile" && c.family=="E")
    {
        require(options.reference!=juce::File{},"E profiling requires the independently measured voice-end reference");
        const auto diagnostic=readJson(options.reference.getChildFile(c.id+"-rep0.json"));
        require(diagnostic["manifest_sha256"].toString()==jsonSha256(manifest),"Profile diagnostic inputs differ");
        const auto voiceEnd=static_cast<juce::int64>(diagnostic["first_no_voices_after_off_sample"]);
        require(voiceEnd>=4LL*c.rate && voiceEnd<c.samples,"Missing measured E voice-end position");
        profileBoundaries.push_back({voiceEnd,"diagnostic-voice-end"});
        std::stable_sort(profileBoundaries.begin(),profileBoundaries.end(),[](const auto& a,const auto& b){return a.sample<b.sample;});
    }
    std::vector<ProfileMarker> profileMarkers;
    profileMarkers.reserve(profileBoundaries.size()+2);
    size_t nextProfilePhase=0;
#endif
    const auto loopStart=Clock::now();
    for(size_t i=0;i<blocks.size();++i)
    {
        auto& block=blocks[i];
        if(block.reset)
        {
            p.releaseResources(); applySnapshot(p,c.requested); prepare(block.rate,c.block); lastValidBpm=120;
        }
        buffer.setDataToReferTo(pointers.data(),2,block.count);
        playHead.sample=block.sample;
        for(const auto& e:block.events)
        {
            switch(e.kind)
            {
                case Kind::parameter: applyOne(p,e.index,static_cast<float>(e.value)); break;
                case Kind::tempo:
                    playHead.bpm=e.value; playHead.missing=e.index==1;
                    if(!playHead.missing && e.value>=20 && e.value<=400) lastValidBpm=e.value;
                    break;
                case Kind::state: p.setStateInformation(savedStates[static_cast<size_t>(e.index)].getData(),static_cast<int>(savedStates[static_cast<size_t>(e.index)].getSize())); break;
                case Kind::factory: require(p.loadFactoryPreset(e.index),"Factory load failed"); break;
                default: throw std::runtime_error("MIDI event in host change list");
            }
        }
        if(audioMode) before[i]=Access::inspect(p);
#if defined(DEEPPLUCK_PROFILE_MARKERS)
        while(options.mode=="profile" && nextProfilePhase<profileBoundaries.size()
              && block.sample>=profileBoundaries[nextProfilePhase].sample)
        {
            profileMarkers.push_back({i,Clock::now(),profileBoundaries[nextProfilePhase].name}); ++nextProfilePhase;
        }
#endif
        // No analysis, event construction, allocation, serialization or logging
        // lies between these two timer reads. This is the real plugin callback.
        if(timingMode)
        {
            const auto begin=Clock::now();
            if(block.bypass) p.processBlockBypassed(buffer,block.midi); else p.processBlock(buffer,block.midi);
            const auto end=Clock::now(); times[i]=seconds(end-begin);
        }
        else
        {
            if(block.bypass) p.processBlockBypassed(buffer,block.midi); else p.processBlock(buffer,block.midi);
        }
        if(audioMode)
        {
            after[i]=Access::inspect(p);
            oscillatorPhases[i]=Access::oscillatorAndLfoPhases(p);
            for(int n=0;n<block.count;++n)
            {
                audio[2*(destination+static_cast<size_t>(n))]=buffer.getSample(0,n);
                audio[2*(destination+static_cast<size_t>(n))+1]=buffer.getSample(1,n);
            }
        }
        destination+=static_cast<size_t>(block.count);
        playHead.ppq+=static_cast<double>(block.count)/block.rate*lastValidBpm/60;
    }
    const auto loopTime=seconds(Clock::now()-loopStart);
    p.releaseResources(); p.setPlayHead(nullptr);
    auto out=object(); put(out,"id",c.id); put(out,"repeat",repeat); put(out,"manifest_sha256",jsonSha256(manifest));
    put(out,"frames",static_cast<juce::int64>(frameCount)); put(out,"construct_seconds",constructTime);
    put(out,"prepare_seconds",prepareTime); put(out,"preroll_seconds",prerollTime);
    put(out,"cold_first_callback_seconds",firstCallbackTime); put(out,"loop_wall_seconds",loopTime);
#if defined(DEEPPLUCK_PROFILE_MARKERS)
    if(warmup && options.mode!="profile") return out;
#else
    // Keep the normal measurement path identical to the frozen S adapter.
    if(warmup) return out;
#endif
    const auto prefix=c.id+"-rep"+std::to_string(repeat);
#if defined(DEEPPLUCK_PROFILE_MARKERS)
    if(options.mode=="profile")
    {
        juce::Array<juce::var> markers;
        for(size_t i=0;i<profileMarkers.size();++i)
        {
            auto marker=object(); put(marker,"phase",profileMarkers[i].name);
            put(marker,"first_block",static_cast<int>(profileMarkers[i].block));
            put(marker,"sample",static_cast<juce::int64>(blocks[profileMarkers[i].block].sample));
            put(marker,"steady_epoch_seconds",seconds(profileMarkers[i].time.time_since_epoch()));
            markers.add(marker);
        }
        put(out,"profile_phase_markers",markers);
        put(out,"profile_loop_end_steady_epoch_seconds",seconds(loopStart.time_since_epoch())+loopTime);
        put(out,"profile_only_not_timing_acceptance",true);
        saveJson(options.output.getChildFile(prefix+"-profile-markers.json"),out);
    }
#endif
    if(audioMode)
    {
        auto peaks=std::array<double,4>{}; std::array<double,4> energy{};
        std::int64_t overZero=0;
        for(size_t n=0;n<frameCount;++n)
        {
            const double l=audio[n*2],r=audio[n*2+1]; require(std::isfinite(l)&&std::isfinite(r),"Nonfinite output");
            overZero+=(std::abs(l)>1 ? 1 : 0)+(std::abs(r)>1 ? 1 : 0);
            const std::array v{l,r,(l+r)*.5,(l-r)*.5};
            for(size_t ch=0;ch<4;++ch) { peaks[ch]=std::max(peaks[ch],std::abs(v[ch])); energy[ch]+=v[ch]*v[ch]; }
        }
        for(auto& e:energy) e=std::sqrt(e/static_cast<double>(frameCount));
        put(out,"peak_L_R_M_S",array(peaks)); put(out,"rms_L_R_M_S",array(energy));
        put(out,"samples_over_0dbfs",static_cast<juce::int64>(overZero));
        const auto hash=sha256(audio.data(),audio.size()*sizeof(float)); put(out,"audio_sha256",hash);
        juce::Array<juce::var> runs;
        size_t start=0;
        while(start<blocks.size())
        {
            size_t end=start+1;
            while(end<blocks.size() && before[end]==before[start] && after[end]==after[start] && !blocks[end].reset) ++end;
            auto run=object(); put(run,"first_block",static_cast<int>(start)); put(run,"blocks",static_cast<int>(end-start));
            put(run,"before",stateJson(before[start])); put(run,"after",stateJson(after[start])); runs.add(run); start=end;
        }
        put(out,"lifecycle_runs",runs); put(out,"lifecycle_sha256",jsonSha256(juce::var(runs)));
        put(out,"oscillator_and_lfo_phase_sha256",sha256(oscillatorPhases.data(),oscillatorPhases.size()*sizeof(oscillatorPhases[0])));
        int maxActive=0; std::int64_t firstNoVoices=-1;
        for(size_t i=0;i<blocks.size();++i)
        {
            maxActive=std::max(maxActive,after[i].active);
            if(c.sustainLoad && blocks[i].sample>=c.rate && blocks[i].sample+blocks[i].count<=3LL*c.rate)
                require(before[i].active==c.polyphony && after[i].active==c.polyphony && after[i].releasing==0,
                        "Load fixture lost requested active polyphony");
            if(c.family=="T3" && (blocks[i].sample==512*static_cast<std::int64_t>(std::ceil(c.rate/512.0))
                                || blocks[i].sample==512*static_cast<std::int64_t>(std::ceil(1.5*c.rate/512.0))))
            {
                require(before[i].releasing>0,"Envelope live test did not hit an active Amp release");
                require(std::any_of(before[i].voiceFlags.begin(),before[i].voiceFlags.end(),[](auto f){return (f&4u)!=0;}),
                        "Envelope live test did not hit an active Filter release");
            }
            if(c.family=="E" && blocks[i].sample>=4LL*c.rate && after[i].active==0 && after[i].residual==0 && firstNoVoices<0)
                firstNoVoices=blocks[i].sample+blocks[i].count;
        }
        put(out,"max_active_voices",maxActive); put(out,"first_no_voices_after_off_sample",static_cast<juce::int64>(firstNoVoices));
        if(options.reference!=juce::File{})
        {
            const auto meta=readJson(options.reference.getChildFile(c.id+"-rep0.json"));
            require(meta["manifest_sha256"].toString()==out["manifest_sha256"].toString(),"Reference manifest differs");
            require(meta["lifecycle_sha256"].toString()==out["lifecycle_sha256"].toString(),"Lifecycle changed");
            require(meta["oscillator_and_lfo_phase_sha256"].toString()==out["oscillator_and_lfo_phase_sha256"].toString(),"Oscillator/LFO phase trajectory changed");
            const auto ref=readRaw(options.reference.getChildFile(c.id+"-rep0.f32"));
            require(sha256(ref.data(),ref.size()*sizeof(float))==meta["audio_sha256"].toString(),"Reference audio hash mismatch");
            put(out,"equivalence",compareAudio(ref,audio));
            put(out,"reference_audio_sha256",meta["audio_sha256"]);
        }
        else writeRaw(options.output.getChildFile(prefix+".f32"),audio);
        if(!c.lifecycle && c.rate==48000 && c.block==128 && c.family!="M" && c.family!="W" && c.family!="X")
            writeWave(options.output.getChildFile(prefix+".wav"),audio,c.rate);
    }
    if(timingMode)
    {
        std::vector<double> nonempty,deadlines; juce::Array<juce::var> phaseResults;
        size_t mixedSamples=0; int mixedBlocks=0,emptyBlocks=0;
        for(size_t i=0;i<blocks.size();++i)
        {
            if(blocks[i].count==0) { ++emptyBlocks; continue; }
            nonempty.push_back(times[i]); deadlines.push_back(static_cast<double>(blocks[i].count)/blocks[i].rate);
        }
        put(out,"total",timingStats(nonempty,deadlines)); put(out,"empty_callbacks",emptyBlocks);
        if(!c.lifecycle)
        {
            for(const auto& phase:c.phases)
            {
                std::vector<double> phaseTimes,phaseDeadlines;
                for(size_t i=0;i<blocks.size();++i)
                    if(blocks[i].sample>=phase.begin && blocks[i].sample+blocks[i].count<=phase.end)
                    { phaseTimes.push_back(times[i]); phaseDeadlines.push_back(static_cast<double>(blocks[i].count)/blocks[i].rate); }
                auto stats=timingStats(phaseTimes,phaseDeadlines); put(stats,"name",phase.name); phaseResults.add(stats);
            }
            for(const auto& b:blocks)
                if(std::none_of(c.phases.begin(),c.phases.end(),[&](const Phase& phase){return b.sample>=phase.begin && b.sample+b.count<=phase.end;}))
                { ++mixedBlocks; mixedSamples+=static_cast<size_t>(b.count); }
        }
        put(out,"phases",phaseResults); put(out,"mixed_boundary_blocks",mixedBlocks);
        put(out,"mixed_boundary_samples",static_cast<juce::int64>(mixedSamples));
        require(!options.output.getChildFile(prefix+".timings.f64").exists(),"Timing evidence exists");
        juce::FileOutputStream stream(options.output.getChildFile(prefix+".timings.f64"));
        require(stream.openedOk() && stream.write(times.data(),times.size()*sizeof(double)),"Timing write failed");
        stream.flush(); require(stream.getStatus().wasOk(),"Timing flush failed");
    }
    if(options.mode!="profile") saveJson(options.output.getChildFile(prefix+".json"),out);
    return out;
}
}
