#pragma once
#include <epictuner/sdk/tune.hpp>
#include <epictuner/sdk/ui.hpp>
#include <epictuner/sdk/jobs.hpp>
#include <algorithm>
#include <cstring>
#include <sstream>
#include <memory>
using namespace et::sdk;
class Calibration : public Plugin {
    Ui ownedUi;Ui& ui;Tune tune;Reads project,logs,channels;Jobs jobs;
    bool floating=false;
    std::unique_ptr<Calibration> popout;
    et_handle panel{},proposal{},job{};uint64_t generation=0,readOperation=0;
    uint32_t uiRevision=0,tuneRevision=0;std::string definition,status="Refresh to read the current tune",preview;
    double requested=ET_FUEL_DEMO?1:10,target=1,old=0,limit=.1;bool demoAllowed=false,hasAnalysis=false;uint32_t settingsRevision=0,analysisRevision=0;std::vector<TuneChange> changes;std::string logId;uint32_t offset=0,lambdaColumn=0;
    std::vector<double> lambda;bool cancelled=false,patching=false,dirty=false,busy=false;
    std::string channelOptions;std::vector<double> points;
    static et_result ET_CALL ignore(void*,const et_read_reply*){return ET_OK;}
    ReadCallback discarded{ignore,this};
    ReadCallback projectCb{projectResult,this},catalogCb{catalogResult,this},snapshotCb{snapshotResult,this},proposalCb{proposalResult,this},applyCb{applyResult,this},statusCb{statusResult,this},logCb{logResult,this},batchCb{batchResult,this},channelsCb{channelResult,this};
    et_async_options completion{sizeof(et_async_options),ET_OPERATION_TIMEOUT_MS,0,0,this,completed};
    static std::string copy(et_string s){return std::string(s.data?s.data:"",s.size);}
    static void ET_CALL completed(void* p,const et_completion* r) noexcept {try{auto& s=*static_cast<Calibration*>(p);s.patching=false;if(r->code==ET_OK&&r->value.kind==ET_VALUE_U64)s.uiRevision=std::max(s.uiRevision,uint32_t(r->value.data.u64));if(s.dirty)s.render();}catch(...) {}}
    void render(){if(!panel.slot)return;if(patching){dirty=true;return;}dirty=false;
        patching=ui.patch(panel,{Ui::text("status",ET_UI_TITLE,status),Ui::text("results",ET_UI_VALUE,status),Ui::text("results",ET_UI_DATA,preview),
            Ui::number("results",ET_UI_MINIMUM,busy?50:100),boolPatch("results",ET_UI_TWO_WAY,proposal.slot!=0),Ui::text("picker",ET_UI_OPTIONS,channelOptions)},uiRevision,0,&completion)==ET_OK;
    }
    static et_ui_patch boolPatch(std::string_view node,uint32_t prop,bool value){et_value v{};v.kind=ET_VALUE_BOOL;v.data.boolean=value;return {string(node),{prop,0,v}};}
    void discard(){if(proposal.slot){auto q=Reads::make(ET_TUNE_DISCARD,generation);q.handle=proposal;tune.request(q,discarded);proposal={};}}
    void refresh(){if(busy)return;discard();hasAnalysis=false;status="Reading current calibration";project.request(Reads::make(ET_READ_PROJECT),projectCb);render();}
    static et_result ET_CALL projectResult(void* p,const et_read_reply* r){auto& s=*static_cast<Calibration*>(p);if(r->code!=ET_OK)return ET_OK;s.generation=r->project_generation;s.tune.request(Reads::make(ET_TUNE_CATALOG,s.generation),s.catalogCb);auto channels=Reads::make(ET_READ_CHANNELS,s.generation);s.channels.request(channels,s.channelsCb);return ET_OK;}
    static et_result ET_CALL channelResult(void* p,const et_read_reply* r){auto& s=*static_cast<Calibration*>(p);s.channelOptions.clear();for(uint32_t i=0;i<r->item_count;++i){if(i)s.channelOptions+='\n';s.channelOptions+=copy(r->items[i].id)+" · "+copy(r->items[i].units);}s.render();return ET_OK;}
    static et_result ET_CALL catalogResult(void* p,const et_read_reply* r){auto& s=*static_cast<Calibration*>(p);if(r->code!=ET_OK){s.status="Catalog unavailable";s.render();return ET_OK;}s.demoAllowed=r->flags&(ET_TUNE_OFFLINE|ET_TUNE_MOCK);s.definition=copy(r->text);s.tuneRevision=r->schema;auto q=Reads::make(ET_TUNE_SNAPSHOT,s.generation);q.id=string(ET_FUEL_DEMO?"fuel":"setting");s.tune.request(q,s.snapshotCb);return ET_OK;}
    static et_result ET_CALL snapshotResult(void* p,const et_read_reply* r){auto& s=*static_cast<Calibration*>(p);s.changes.clear();s.tuneRevision=r->schema;if(r->code!=ET_OK){s.status="Open the supplied synthetic calibration project";s.render();return ET_OK;}for(uint32_t i=0;i<r->item_count;++i)s.changes.push_back({copy(r->items[i].id),uint32_t(r->items[i].sequence),r->items[i].value,r->items[i].value});s.old=s.changes.empty()?0:s.changes[0].expected;s.status="Current tune read; changes require Preview then Apply";s.render();return ET_OK;}
    void propose(){if(changes.empty()||busy)return;discard();if(ET_FUEL_DEMO&&!hasAnalysis){status="Analyze before creating a fuel proposal";render();return;}if(ET_FUEL_DEMO&&!demoAllowed){status="Fuel demo requires offline mode or Mock ECU";render();return;}for(auto& e:changes)e.value=ET_FUEL_DEMO?e.expected*std::clamp(requested,1-limit,1+limit):requested;
        std::string body=Tune::proposal(ET_FUEL_DEMO?"Fuel analysis correction":"Calibration workbench",changes);auto q=Reads::make(ET_TUNE_PROPOSE,generation);q.id=string(definition);q.schema=tuneRevision;q.text=string(body);tune.request(q,proposalCb);}
    static et_result ET_CALL proposalResult(void* p,const et_read_reply* r){auto& s=*static_cast<Calibration*>(p);s.proposal=r->code==ET_OK?r->handle:et_handle{};s.status=r->code==ET_OK?"Normalized preview ready. Apply is explicit.":"Proposal rejected ("+std::to_string(r->code)+"): refresh and recompute";s.preview="Field\tIndex\tOld\tStored\n";for(uint32_t i=0;i<r->item_count;++i){const auto& e=r->items[i];s.preview+=copy(e.id)+'\t'+std::to_string(e.sequence)+'\t'+std::to_string(e.timestamp)+'\t'+std::to_string(e.value)+'\n';}s.render();return ET_OK;}
    void action(uint32_t op){auto q=Reads::make(op,generation);q.id=string(definition);q.schema=tuneRevision;q.handle=op==ET_TUNE_APPLY?proposal:et_handle{};tune.request(q,applyCb);}
    static et_result ET_CALL applyResult(void* p,const et_read_reply* r){auto& s=*static_cast<Calibration*>(p);s.proposal={};s.tuneRevision=r->schema;s.status=r->code==ET_OK?"Local acceptance; use Verify for ECU RAM. Flash burn is separate.":"Edit rejected ("+std::to_string(r->code)+"); refresh and recompute";s.render();return ET_OK;}
    static et_result ET_CALL statusResult(void* p,const et_read_reply* r){auto& s=*static_cast<Calibration*>(p);s.status=(r->flags&ET_TUNE_RAM_VERIFIED)?"ECU RAM readback verified; flash is separate":copy(r->text);if(s.status.empty())s.status="ECU RAM has not been verified";s.render();return ET_OK;}
    struct Work { std::vector<double> values; double target=1,limit=.1; } workData;
    static et_result ET_CALL calculate(void* owner,const et_job_control* control,et_job_output* out){
        const auto& w=*static_cast<Work*>(owner);double sum=0;
        for(double v:w.values){if(control->is_cancelled(control->context))return ET_ERROR_CANCELLED;sum+=v;}
        if(w.values.empty()||out->capacity<sizeof(double))return ET_ERROR_ARGUMENT;
        const double factor=std::clamp(sum/w.values.size()/w.target,1-w.limit,1+w.limit);std::memcpy(out->data,&factor,sizeof(factor));out->size=sizeof(factor);return ET_OK;
    }
    static void ET_CALL calculated(void* owner,const et_completion* r){auto& s=*static_cast<Calibration*>(owner);s.job={};s.busy=false;
        if(s.cancelled||s.settingsRevision!=s.analysisRevision||r->code!=ET_OK||r->value.kind!=ET_VALUE_BYTES||r->value.data.bytes.size!=sizeof(double)){s.status="Cancelled, settings changed or analysis failed";s.render();return;}
        std::memcpy(&s.requested,r->value.data.bytes.data,sizeof(double));s.hasAnalysis=true;s.propose();
    }
    void analyze(){if(busy||changes.empty())return;busy=true;cancelled=false;hasAnalysis=false;analysisRevision=settingsRevision;lambda.clear();offset=0;status="Reading host-approved lambda log";logs.request(Reads::make(ET_READ_LOGS,generation),logCb,&readOperation);render();}
    static et_result ET_CALL logResult(void* p,const et_read_reply* r){auto& s=*static_cast<Calibration*>(p);if(s.cancelled){s.busy=false;s.readOperation=0;s.status="Cancelled";s.render();return ET_OK;}
        if(r->code!=ET_OK||!r->item_count){s.busy=false;s.status="Open a log containing a lambda channel";s.render();return ET_OK;}
        if(s.offset==0){s.logId=copy(r->items[0].id);s.offset=1;auto q=Reads::make(ET_READ_LOG_CHANNELS,s.generation);q.id=string(s.logId);s.logs.request(q,s.logCb,&s.readOperation);return ET_OK;}
        bool found=false;for(uint32_t i=0;i<r->item_count;++i)if(copy(r->items[i].label)=="lambda"){s.lambdaColumn=uint32_t(std::stoul(copy(r->items[i].id)));found=true;break;}
        if(!found){s.busy=false;s.status="No lambda channel in selected log";s.render();return ET_OK;}s.offset=0;s.batch();return ET_OK;
    }
    void batch(){auto q=Reads::make(ET_READ_LOG_BATCH,generation);q.id=string(logId);q.offset=offset;std::string col=std::to_string(lambdaColumn);auto c=string(col);q.channels=&c;q.channel_count=1;logs.request(q,batchCb,&readOperation);}
    static et_result ET_CALL batchResult(void* p,const et_read_reply* r){auto& s=*static_cast<Calibration*>(p);if(s.cancelled){s.busy=false;s.readOperation=0;s.status="Cancelled";s.render();return ET_OK;}if(r->code!=ET_OK){s.busy=false;s.status="Log replaced or read cancelled";s.render();return ET_OK;}
        for(uint32_t i=0;i<r->item_count;++i)if((r->items[i].flags&ET_READ_VALID)&&r->items[i].value>.5&&r->items[i].value<2)s.lambda.push_back(r->items[i].value);
        s.offset+=r->item_count;if(s.offset<r->total&&r->item_count&&s.offset<65536){s.batch();return ET_OK;}
        if(s.lambda.empty()){s.busy=false;s.status="No valid lambda samples";s.render();return ET_OK;}
        s.workData={s.lambda,s.target,s.limit};const et_async_options opts{sizeof(et_async_options),ET_OPERATION_TIMEOUT_MS,0,0,&s,calculated};
        if(s.jobs.submit(calculate,&s.workData,opts,s.job)!=ET_OK){s.busy=false;s.status="Analysis job unavailable";s.render();}return ET_OK;
    }
    static et_result ET_CALL event(void* p,const et_ui_event* e){auto& s=*static_cast<Calibration*>(p);
        if(s.popout&&copy(e->definition)==std::string(id)+".popout")return event(s.popout.get(),e);
        s.uiRevision=e->revision;
        if(e->type==ET_UI_OPENED){s.panel=e->panel;s.refresh();return ET_OK;}
        if(e->type==ET_UI_CLOSED){s.cancelled=true;if(s.job.slot)s.jobs.cancel(s.job);else if(!s.readOperation||s.logs.cancel(s.readOperation)!=ET_OK)s.busy=false;s.discard();s.panel={};return ET_OK;}
        const auto id=copy(e->node);if(id=="requested"&&e->type==ET_UI_COMMIT)s.requested=e->value.data.f64;
        if(e->type==ET_UI_CLICK&&id=="popout")return s.ui.open(std::string(Calibration::id)+".popout",{},ET_PLACEMENT_FLOATING);
        if(e->type==ET_UI_CLICK&&id=="dock")return s.ui.open(std::string(Calibration::id)+".main",{},ET_PLACEMENT_WORKSPACE);
        if((id=="target"||id=="limit"||id=="requested")&&e->type==ET_UI_COMMIT){++s.settingsRevision;s.hasAnalysis=false;s.discard();s.status="Settings changed; recompute preview";s.render();}
        if(id=="target"&&e->type==ET_UI_COMMIT)s.target=e->value.data.f64;
        if(id=="limit"&&e->type==ET_UI_COMMIT)s.limit=e->value.data.f64/100;
        if(e->type==ET_UI_CLICK){if(id=="refresh")s.refresh();if(id=="preview")s.propose();if(id=="undo")s.action(ET_TUNE_UNDO);if(id=="redo")s.action(ET_TUNE_REDO);if(id=="burn"&&!ET_FUEL_DEMO)s.action(ET_TUNE_BURN);if(id=="verify")s.tune.request(Reads::make(ET_TUNE_STATUS,s.generation),s.statusCb);}
        if(id=="results"&&e->type==ET_UI_COMMIT){auto value=copy(e->value.data.string);if(value=="apply"&&s.proposal.slot)s.action(ET_TUNE_APPLY);if(value=="analyze"){if(ET_FUEL_DEMO)s.analyze();else s.propose();}if(value=="cancel"){s.cancelled=true;if(s.job.slot)s.jobs.cancel(s.job);else if(!s.readOperation||s.logs.cancel(s.readOperation)!=ET_OK)s.busy=false;s.discard();s.status="Cancelled";s.render();}}
        return ET_OK;
    }
public:
    Calibration():ui(ownedUi){}
    explicit Calibration(Ui& shared):ui(shared),floating(true){}
    static constexpr std::string_view id=ET_FUEL_DEMO?"org.epictuner.examples.fuel":"org.epictuner.examples.calibration";
    static constexpr uint64_t capabilities=ET_CAP_LOG|ET_CAP_EVENTS|ET_CAP_UI|ET_CAP_JOBS|ET_CAP_PROJECT_READ|ET_CAP_CHANNEL_READ|ET_CAP_DATALOG_READ|ET_CAP_TUNE_READ|ET_CAP_TUNE_PROPOSE|ET_CAP_TUNE_APPLY|(ET_FUEL_DEMO?0:ET_CAP_TUNE_BURN);
    et_result start(Context& c){
        if(jobs.connect(c)!=ET_OK||(!floating&&ui.connect(c,event,this)!=ET_OK)||tune.connect(c)!=ET_OK||project.connect(c,ET_SERVICE_PROJECT)!=ET_OK||logs.connect(c,ET_SERVICE_DATALOG)!=ET_OK||channels.connect(c,ET_SERVICE_CHANNELS)!=ET_OK)return ET_ERROR_UNSUPPORTED;
        PanelBuilder b(std::string(id)+(floating?".popout":".main"),ET_FUEL_DEMO?"Fuel analysis demo":floating?"Calibration workbench (popout)":"Calibration workbench");b.definition.flags|=ET_PANEL_PROJECT;b.definition.width=1100;b.definition.height=780;
        if(!ET_FUEL_DEMO)b.definition.placements=floating?ET_PLACEMENT_FLOATING:ET_PLACEMENT_WORKSPACE;
        b.add(ET_UI_COLUMN,"root");b.add(ET_UI_BANNER,"status","root").text(ET_UI_TITLE,status);
        b.add(ET_UI_ROW,"actions","root");for(auto [id,title]:{std::pair{"refresh","Refresh"},{"preview","Preview"},{"undo","Undo"},{"redo","Redo"},{"verify","Verify RAM"},{"burn","Request Burn"}})if(!ET_FUEL_DEMO||std::string_view(id)!="burn")b.add(ET_UI_BUTTON,id,"actions").text(ET_UI_TITLE,title);
        if(!ET_FUEL_DEMO)b.add(ET_UI_BUTTON,floating?"dock":"popout","actions").text(ET_UI_TITLE,floating?"Show docked":"Open popout");
        b.add(ET_UI_NUMBER,ET_FUEL_DEMO?"target":"requested","root").text(ET_UI_TITLE,ET_FUEL_DEMO?"Target lambda":"Proposed setting").number(ET_UI_MINIMUM,ET_FUEL_DEMO?.65:0).number(ET_UI_MAXIMUM,ET_FUEL_DEMO?1.2:255).number(ET_UI_VALUE,ET_FUEL_DEMO?1:10).number(ET_UI_DECIMALS,2);
        if(ET_FUEL_DEMO)b.add(ET_UI_NUMBER,"limit","root").text(ET_UI_TITLE,"Maximum correction (%)").number(ET_UI_MINIMUM,1).number(ET_UI_MAXIMUM,10).number(ET_UI_VALUE,10);
        b.add(ET_UI_TABS,"tabs","root").number(ET_UI_VALUE,0).boolean(ET_UI_PERSIST,true);
        b.add(ET_UI_SECTION,"analysis","tabs").text(ET_UI_TITLE,"Preview");b.prefab(ET_UI_ANALYSIS_PANEL,"results",{},"analysis").text(ET_UI_VALUE,status);
        b.add(ET_UI_SECTION,"calibration","tabs").text(ET_UI_TITLE,"Calibration");b.prefab(ET_UI_FIELD_EDITOR,"field","setting","calibration");b.prefab(ET_UI_CALIBRATION_TABLE,"table","fuelTable","calibration");
        b.add(ET_UI_SECTION,"curveTab","tabs").text(ET_UI_TITLE,"Curve");b.prefab(ET_UI_CURVE_EDITOR,"curve","warmupCurve","curveTab");
        b.add(ET_UI_SECTION,"surfaceTab","tabs").text(ET_UI_TITLE,"Surface");b.prefab(ET_UI_TABLE_SURFACE,"surface","fuelTable","surfaceTab");
        b.add(ET_UI_SECTION,"dataTab","tabs").text(ET_UI_TITLE,"Channels / data");b.prefab(ET_UI_CHANNEL_PICKER,"picker",{},"dataTab").boolean(ET_UI_TWO_WAY,true).boolean(ET_UI_PERSIST,true);b.prefab(ET_UI_LIVE_PANEL,"live","rpm","dataTab").number(ET_UI_STEP,10).text(ET_UI_VALUE,"Live telemetry").text(ET_UI_DATA,"1\n2\n3\n2\n4");
        b.add(ET_UI_SECTION,"logTab","tabs").text(ET_UI_TITLE,"Datalog");b.prefab(ET_UI_DATALOG_PANEL,"log",{},"logTab");
        b.add(ET_UI_SECTION,"datasetTab","tabs").text(ET_UI_TITLE,"Plugin dataset");
        b.prefab(ET_UI_CALIBRATION_TABLE,"dataset",{},"datasetTab").number(ET_UI_COLUMNS,4).number(ET_UI_MAXIMUM,200).text(ET_UI_DATA,"1\t2\t3\t4\n5\t6\t7\t8").boolean(ET_UI_PERSIST,true);
        const auto result=ui.define(b,&completion);
        if(result==ET_OK&&!ET_FUEL_DEMO&&!floating){popout=std::make_unique<Calibration>(ui);return popout->start(c);}
        return result;
    }
    et_result onEvent(const et_event&){return ET_OK;}
};
ET_PLUGIN(Calibration)
