#include "Runner.h"
#include <juce_audio_utils/juce_audio_utils.h>
#include <algorithm>
#include <chrono>
#include <iostream>
#include <limits>
#include <set>

namespace deeppluck::benchmark
{
namespace
{
juce::File absoluteFile (const std::string& text)
{
    require(juce::File::isAbsolutePath(juce::String(text)),"Expected absolute path");
    return juce::File(juce::String(text));
}
int integer (const std::string& text)
{
    size_t end=0; const auto value=std::stoi(text,&end);
    require(end==text.size(),"Invalid integer argument"); return value;
}
void expectFailure (const std::function<void()>& call, const char* message)
{
    bool failed=false;
    try { call(); } catch(const std::exception&) { failed=true; }
    require(failed,message);
}
}
Options parseOptions (int argc, char** argv)
{
    Options options; std::set<std::string> seen;
    for(int i=1;i<argc;++i)
    {
        const std::string key=argv[i]; require(seen.insert(key).second,"Duplicate CLI option: "+key);
        if(key=="--help") {options.help=true; continue;}
        if(key=="--list") {options.list=true; continue;}
        require(i+1<argc,"Missing CLI value: "+key); const std::string value=argv[++i];
        if(key=="--suite") options.suite=value;
        else if(key=="--mode") options.mode=value;
        else if(key=="--case") options.caseId=value;
        else if(key=="--repeat") options.repeats=integer(value);
        else if(key=="--shard-index") options.shardIndex=integer(value);
        else if(key=="--shard-count") options.shardCount=integer(value);
        else if(key=="--output") options.output=absoluteFile(value);
        else if(key=="--reference") options.reference=absoluteFile(value);
        else if(key=="--metadata") options.metadata=absoluteFile(value);
        else if(key=="--pair-statistics") options.pairStatistics=absoluteFile(value);
        else if(key=="--self-test") options.selfTest=value;
        else if(key=="--profile-seconds") options.profileSeconds=integer(value);
        else throw std::runtime_error("Unknown CLI option: "+key);
    }
    require(options.repeats>0 && options.repeats<=1000,"Invalid repeat count");
    require(options.mode=="audio" || options.mode=="timing" || options.mode=="profile","Unknown mode");
    require(options.profileSeconds>=30 && options.profileSeconds<=3600,"Profile duration must be 30..3600 seconds");
    require(options.suite=="all" || options.suite=="matrix" || options.suite=="reference" || options.suite=="transitions" || options.suite=="quick" || options.suite=="representative","Unknown suite");
    require(options.shardCount>0 && options.shardIndex>=0 && options.shardIndex<options.shardCount,"Invalid shard");
    if(options.reference!=juce::File{}) require(options.reference.isDirectory(),"Missing reference directory");
    return options;
}
void printHelp()
{
    std::cout << "DeepPluckBenchmark - " << manifestVersion << "\n"
        "--suite representative|matrix|reference|transitions|quick|all (default all)\n"
        "representative: 39 fixed one-factor/reference/stress cases, 2 s windows, 0.5 s disposable warmup\n"
        "--list (print selected stable IDs; with --output export validated manifests)\n"
        "--mode timing|audio|profile (default audio)\n"
        "--case ID --repeat N --shard-index I --shard-count N\n"
        "--output NEW_ABSOLUTE_DIRECTORY --metadata ABSOLUTE_ENVIRONMENT_JSON\n"
        "--reference ABSOLUTE_AUDIO_DIRECTORY (strict external reference comparison)\n"
        "--profile-seconds 30..3600 (profile workload only; timing is not acceptance)\n"
        "--self-test manifest|determinism|audio-equivalence|quick\n"
        "--pair-statistics ABSOLUTE_JSON (nine pairs, fixed-seed bootstrap)\n"
        "Comparison/merge/paired timing orchestration: Tests/RunCPUBenchmark.ps1.\n"
        "Audio files are little-endian interleaved L/R float32, no normalization.\n"
        "Exit 0 = requested run/tests completed, NOT complete milestone approval.\n"
        "Exit 1 = invalid input, missing reference, nonfinite audio, or failed gate.\n"
        "Quick, profile and individual shards never claim full acceptance.\n";
}
void selfTest (const Options& original)
{
    const auto all=makeCases();
    const auto& test=original.selfTest;
    require(test=="manifest" || test=="determinism" || test=="audio-equivalence" || test=="quick","Unknown self-test");
    auto options=original;
    if(test=="manifest")
    {
        require(selectCases(all,"matrix",{},0,1).size()==6912,"Matrix coverage");
        require(selectCases(all,"reference",{},0,1).size()==588,"Reference coverage");
        require(selectCases(all,"transitions",{},0,1).size()==420,"Transition coverage");
        const auto reduced=selectCases(all,"representative",{},0,1);
        require(reduced.size()==representativeCaseCount,"Representative coverage");
        for(const auto& c:reduced)
        {
            (void) validatedManifest(c);
            require(!c.originId.empty() && c.samples>=2LL*c.rate && c.samples<2LL*c.rate+c.block,"Short measurement window");
            const auto warmup=prefixWindow(c,0.5);
            require(warmup.samples>=c.rate/2 && warmup.samples<c.rate/2+c.block,"Short fresh warmup");
            for(const auto& e:c.events) require(e.sample<c.samples,"Event beyond short window");
            const auto found=std::find_if(all.begin(),all.end(),[&](const Case& old){return old.id==c.originId;});
            require(found!=all.end() && found->requested==c.requested && found->polyphony==c.polyphony,"Changed representative input");
        }
        for(const auto& c:all) (void) validatedManifest(c);
        juce::Array<juce::var> pairs;
        for(int i=0;i<9;++i) {auto pair=object();put(pair,"baseline_rtf",1.0);put(pair,"candidate_rtf",.9);pairs.add(pair);}
        auto input=object();put(input,"pairs",pairs); const auto statistics=evaluatePairs(input);
        require(static_cast<bool>(statistics["efficiency_threshold_pass"]),"Fixed bootstrap positive control");
        expectFailure([&]{ (void) selectCases(all,"all","bad-id",0,1); },"Unknown ID passed");
        expectFailure([&]{ (void) selectCases(all,"invalid",{},0,1); },"Unknown suite passed");
        auto invalid=all.front(); invalid.requested[0]=std::numeric_limits<float>::quiet_NaN();
        expectFailure([&]{ (void) validatedManifest(invalid); },"NaN request passed");
        invalid=all.front(); invalid.requested[29]=2.5f;
        expectFailure([&]{ (void) validatedManifest(invalid); },"Clamped choice passed");
        return;
    }
    require(options.output!=juce::File{},"Self-test requires new output directory");
    options.mode="audio";
    const auto selected=selectCases(all,"quick",{},0,1);
    if(test=="audio-equivalence")
    {
        require(options.reference!=juce::File{},"Audio equivalence requires external reference");
        for(const auto& c:selected) (void) execute(c,options,0,false);
        std::vector<float> values(2050,0.0f);
        auto bad=values; bad.back()=std::numeric_limits<float>::quiet_NaN();
        expectFailure([&]{(void)compareAudio(values,bad);},"NaN reference gate passed");
        bad=values; bad.pop_back();
        expectFailure([&]{(void)compareAudio(values,bad);},"Short reference passed");
        bad=values; bad.back()=1e-4f;
        expectFailure([&]{(void)compareAudio(values,bad);},"Last partial window gate passed");
        return;
    }
    for(const auto& c:selected)
    {
        const auto first=execute(c,options,0,false);
        if(test=="determinism")
        {
            auto compare=options; compare.reference=options.output;
            const auto second=execute(c,compare,1,false);
            require(first["audio_sha256"].toString()==second["audio_sha256"].toString(),"Fresh-instance nondeterminism");
        }
    }
}
}
int main (int argc,char** argv)
{
    using namespace deeppluck::benchmark;
    try
    {
        const auto options=parseOptions(argc,argv);
        if(options.help) { printHelp(); return 0; }
        if(options.pairStatistics!=juce::File{})
        {
            std::cout<<json(evaluatePairs(readJson(options.pairStatistics)))<<'\n'; return 0;
        }
        juce::ScopedJuceInitialiser_GUI juceInitialisation; // Message manager only, no window/device.
        if(options.output!=juce::File{})
        {
            require(!options.output.exists(),"Output already exists; use a new evidence directory");
            require(options.output.createDirectory().wasOk(),"Cannot create output directory");
        }
        if(!options.selfTest.empty())
        {
            selfTest(options); std::cout << "PASS: " << options.selfTest << " (not full performance acceptance)\n"; return 0;
        }
        const auto all=makeCases();
        const auto selected=selectCases(all,options.suite,options.caseId,options.shardIndex,options.shardCount);
        if(options.list && options.output==juce::File{})
        {
            for(const auto& c:selected) std::cout<<c.id<<'\n';
            std::cout<<"# cases="<<selected.size()<<'\n'; return 0;
        }
        require(options.output!=juce::File{},"Missing output directory");
        if(!options.list) require(options.metadata.existsAsFile(),"Missing environment/build metadata");
        auto run=object(); put(run,"manifest_version",manifestVersion); put(run,"target_version",DEEPPLUCK_BENCHMARK_VERSION);
        put(run,"configuration",DEEPPLUCK_BENCHMARK_CONFIG); put(run,"suite",options.suite); put(run,"mode",options.mode);
        if(options.suite=="representative") put(run,"measurement_protocol",representativeProtocol);
        put(run,"repeat_count",options.repeats); put(run,"shard_index",options.shardIndex); put(run,"shard_count",options.shardCount);
        put(run,"started_utc",juce::Time::getCurrentTime().toISO8601(true));
        const auto executable=juce::File::getSpecialLocation(juce::File::currentExecutableFile);
        put(run,"executable",executable.getFullPathName()); put(run,"executable_sha256",fileSha256(executable));
        if(options.metadata.existsAsFile())
        {
            const auto metadata=readJson(options.metadata);
            require(metadata.hasProperty("source_sha256") && metadata.hasProperty("environment_sha256"),"Invalid provenance metadata");
            for(const auto* key:{"source_sha256","environment_sha256"})
            {
                const auto hash=metadata[key].toString();
                require(hash.length()==64 && hash.containsOnly("0123456789abcdefABCDEF"),"Malformed provenance hash");
            }
            require(metadata["schema"].toString()=="DeepPluck-CPU-Provenance-Rev1"
                    && metadata["configuration"].toString()==DEEPPLUCK_BENCHMARK_CONFIG
                    && metadata["requestedManifest"].toString()==manifestVersion,"Wrong provenance schema/configuration/manifest");
            put(run,"provenance",metadata); put(run,"metadata_sha256",fileSha256(options.metadata));
        }
        juce::Array<juce::var> inventory;
        for(const auto& c:selected) inventory.add(validatedManifest(c));
        put(run,"cases",inventory); put(run,"case_count",static_cast<int>(selected.size()));
        put(run,"selected_manifest_sha256",jsonSha256(juce::var(inventory)));
        put(run,"full_milestone_pass",false);
        saveJson(options.output.getChildFile("manifest.json"),run);
        if(options.list) { std::cout<<"Exported "<<selected.size()<<" validated cases\n"; return 0; }
        saveJson(options.output.getChildFile("timer.json"),timerCalibration());
        juce::Array<juce::var> results;
        if(options.mode=="profile")
        {
            require(selected.size()==1,"Profile one predeclared workload per collection");
            const auto start=std::chrono::steady_clock::now(); int repetitions=0;
            do { (void)execute(selected.front(),options,repetitions++,true); }
            while(std::chrono::duration<double>(std::chrono::steady_clock::now()-start).count()<options.profileSeconds);
            put(run,"profile_wall_seconds",std::chrono::duration<double>(std::chrono::steady_clock::now()-start).count());
            put(run,"profile_repetitions",repetitions);
        }
        else
            for(size_t index=0;index<selected.size();++index)
            {
                const auto& c=selected[index];
                if(options.mode=="timing")
                    (void) execute(options.suite=="representative" ? prefixWindow(c,0.5) : c,options,-1,true);
                for(int repeat=0;repeat<options.repeats;++repeat)
                {
                    auto current=options;
                    // Retain the complete first raw render; subsequent fresh
                    // renders compare every sample without duplicate disk use.
                    if(options.mode=="audio" && repeat>0 && options.reference==juce::File{}) current.reference=options.output;
                    results.add(execute(c,current,repeat,false));
                }
                std::cout<<"Completed "<<index+1<<'/'<<selected.size()<<" "<<c.id<<std::endl;
            }
        put(run,"completed_utc",juce::Time::getCurrentTime().toISO8601(true)); put(run,"results",results);
        put(run,"completed_requested_run",true); saveJson(options.output.getChildFile("results.json"),run);
        std::cout<<"Completed requested run. Full milestone/performance acceptance is separate.\n"; return 0;
    }
    catch(const std::exception& error)
    {
        std::cerr<<"FAIL: "<<error.what()<<'\n'; return 1;
    }
}
