package com.android.commands.monkey.ape.jev;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import android.graphics.Bitmap;
import android.graphics.Rect;
import org.json.*;
import com.android.commands.monkey.MonkeySourceApe;
import com.android.commands.monkey.MonkeyEvent;
import com.android.commands.monkey.ape.model.*;
import com.android.commands.monkey.ape.tree.*;

/** Local, best-effort audit only. Never returns advice or changes decision health.
 * GUI objects are read/frozen on the APE thread; workers receive copied JSON/pixels.
 * No local file size limit is inherited from the Provider's 32 KiB wire budget. */
final class JevEvidenceAudit implements AutoCloseable {
    private final File root;
    private final String runId;
    private final JevTiming timing;
    private final ThreadPoolExecutor writer;
    private final Semaphore pixels;
    private final AtomicLong sequence=new AtomicLong(),failures=new AtomicLong(),rejected=new AtomicLong(),bytes=new AtomicLong();
    private final Map<GUITree,JSONObject> trees=new WeakHashMap<GUITree,JSONObject>();
    private final Map<GUITree,JSONObject> shots=Collections.synchronizedMap(new WeakHashMap<GUITree,JSONObject>());
    private final IdentityHashMap<GUITreeAction,JSONObject> inputs=new IdentityHashMap<GUITreeAction,JSONObject>();
    private final IdentityHashMap<GUITreeAction,List<GUITreeTransition>> facts=new IdentityHashMap<GUITreeAction,List<GUITreeTransition>>();
    private final IdentityHashMap<GUITreeTransition,JSONObject> executionEvidence=new IdentityHashMap<GUITreeTransition,JSONObject>();
    private final IdentityHashMap<GUITreeTransition,String> factIds=new IdentityHashMap<GUITreeTransition,String>();
    private final Set<String> sampled=new HashSet<String>();
    private final ArrayDeque<JSONObject> recent=new ArrayDeque<JSONObject>();
    private volatile JSONObject lastOperation=new JSONObject(),lastInjectedOperation=new JSONObject();
    private final IdentityHashMap<MonkeyEvent,AuditInput> eventInputs=new IdentityHashMap<MonkeyEvent,AuditInput>();
    private static final class AuditInput {
        final JSONObject input;int remaining,success,failed,discarded;
        AuditInput(JSONObject value){input=value;}
    }
    private static final class NativeSnapshot {
        final GUITree source,target;final JSONObject value;
        NativeSnapshot(GUITree source,GUITree target,JSONObject value){this.source=source;this.target=target;this.value=value;}
    }
    private volatile NativeSnapshot lastNative,lastConfirmedNative,lastInputSource;
    private volatile boolean closed;
    JevEvidenceAudit(File directory,String id,JevTiming timing,Semaphore pixels) {
        root=directory;runId=id;this.timing=timing;this.pixels=pixels;
        writer=new ThreadPoolExecutor(1,1,0,TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(64),new ThreadFactory(){
            public Thread newThread(Runnable r){Thread t=new Thread(r,"ape-local-audit");t.setDaemon(true);return t;}});
    }
    private void recordTiming(String segment,long ns){try{timing.record(segment,ns);}catch(RuntimeException skipped){ /* No deadline/lock failure may escape audit. */ }}
    private JSONObject stamp(String kind,long step,long version) {
        return JevRegistry.fields("schema","guitaboo-local-audit-v1","kind",kind,"run_id",runId,"step",step,
            "abstraction_version",version,"wall_time_ms",System.currentTimeMillis(),"monotonic_ns",System.nanoTime());
    }
    private boolean submit(final Runnable job) {
        if(closed){rejected.incrementAndGet();return false;}
        try {writer.execute(new Runnable(){public void run(){try{job.run();}catch(Throwable error){failures.incrementAndGet();}}});return true;}
        catch(RejectedExecutionException error){rejected.incrementAndGet();return false;}
    }
    private void write(String path,Object frozen) {
        try {
            File file=new File(root,path);File parent=file.getParentFile();
            if(!parent.isDirectory() && !parent.mkdirs())throw new IOException("audit directory unavailable");
            if(!file.createNewFile())throw new IOException("audit file already exists");
            byte[] data=(JevOutput.json(frozen).toString()+"\n").getBytes("UTF-8");
            FileOutputStream stream=new FileOutputStream(file);try{stream.write(data);}finally{stream.close();}bytes.addAndGet(data.length);
        }catch(Exception error){throw new RuntimeException(error);}
    }
    JSONObject save(final String path,JSONObject value) {
        try {final Object frozen=JevOutput.freeze(value);boolean queued=submit(new Runnable(){public void run(){write(path,frozen);}});
            return JevRegistry.fields("status",queued?"queued":"queue_rejected","path",queued?path:JSONObject.NULL);
        }catch(Exception error){failures.incrementAndGet();return JevRegistry.fields("status","freeze_failed");}
    }
    private static ConcreteActionIdentity auditIdentity(final ModelAction action,final GUITreeNode node) {
        return com.android.commands.monkey.ape.naming.NameManager.withoutInterning(new java.util.function.Supplier<ConcreteActionIdentity>() {
            public ConcreteActionIdentity get(){return ConcreteActionIdentity.of(action.getType().name(),node);}
        });
    }
    static JSONObject concrete(ModelAction action,GUITreeNode node) {
        JSONObject result=JevRegistry.fields("action_type",action.getType().name(),"model_action_id",action.getGraphId());
        if(node==null)return result;
        ConcreteActionIdentity identity=auditIdentity(action,node);
        return JevRegistry.merge(result,JevRegistry.fields("concrete_identity",identity.toString(),"identity_digest",JevEvidence.digest(identity.toString()),
            "node_path",node.getIndexPath(),"class",node.getClassName(),"resource_id",node.getResourceID(),"text",node.getText(),
            "content_desc",node.getContentDesc(),"bounds",rect(node.getBoundsInScreen()),"enabled",node.isEnabled(),
            "clickable",node.isClickable(),"long_clickable",node.isLongClickable()));
    }
    private static JSONArray rect(Rect b){return new JSONArray().put(b.left).put(b.top).put(b.right).put(b.bottom);}
    /** Complete retained Native tree, including ordered parent/child links. No depth/node/label truncation. */
    static JSONArray nodes(GUITree tree) {
        JSONArray rows=new JSONArray();ArrayDeque<GUITreeNode> pending=new ArrayDeque<GUITreeNode>();
        pending.add(tree.getRootNode());IdentityHashMap<GUITreeNode,Integer> ids=new IdentityHashMap<GUITreeNode,Integer>();
        ids.put(tree.getRootNode(),0);
        while(!pending.isEmpty()) {
            GUITreeNode node=pending.removeFirst();JSONArray children=new JSONArray();Iterator<GUITreeNode> it=node.getChildren();
            while(it.hasNext()){GUITreeNode child=it.next();int id=ids.size();ids.put(child,id);children.put(id);pending.addLast(child);}
            rows.put(JevRegistry.fields("id",ids.get(node),"parent",node.getParent()==null?JSONObject.NULL:ids.get(node.getParent()),
                "children",children,"index",node.getIndex(),"class",node.getClassName(),"package",node.getPackageName(),
                "resource_id",node.getResourceID(),"native_text",node.getText(),"native_content_desc",node.getContentDesc(),
                "raw_observation",node.getAuditObservation(),"bounds",rect(node.getBoundsInScreen()),
                "bounds_in_parent",rect(node.getBoundsInParent()),"enabled",node.isEnabled(),"clickable",node.isClickable(),
                "long_clickable",node.isLongClickable(),"checkable",node.isCheckable(),"checked",node.isChecked(),
                "scrollable",node.isScrollable(),"focused",node.isFocused(),"focusable",node.isFocusable(),
                "selected",node.getJevSelected(),"visible_to_user",node.getJevVisibleToUser(),"drawing_order",node.getJevDrawingOrder(),
                "generated_image_text",node.isGeneratedImageText()));
        }
        return rows;
    }
    synchronized JSONObject tree(GUITree tree,long step,long version,String phase) {
        if(tree==null)return JevRegistry.fields("status","tree_unavailable");
        JSONObject old=trees.get(tree);if(old!=null)return old;
        long start=System.nanoTime();
        try {
            String path="jev-audit/gui/gui-"+tree.getId()+"-t"+tree.getTimestamp()+"-"+sequence.incrementAndGet()+".json";
            JSONObject value=stamp("native_gui_tree",step,version);
            JevRegistry.merge(value,JevRegistry.fields("gui_id",tree.getId(),"gui_timestamp",tree.getTimestamp(),"activity",tree.getActivityClassName(),
                "state_id",tree.getCurrentState()==null?JSONObject.NULL:tree.getCurrentState().getGraphId(),"phase",phase,
                "observation_identity",tree.getJevObservationIdentity(),"display_bounds",rect(tree.getDisplayBounds()),"rotation",tree.getDisplayRotation(),
                "tree_observation_started_ns",tree.getAuditObservationTimes()[0],"tree_observation_finished_ns",tree.getAuditObservationTimes()[1],
                "tree_observation_wall_time_ms",tree.getAuditObservationTimes()[2],
                "coverage","complete retained native GUITree; Native preprocessing/pruning is not reconstructed","nodes",nodes(tree)));
            JSONObject ref=save(path,value);JevRegistry.merge(ref,JevRegistry.fields("gui_id",tree.getId(),"gui_timestamp",tree.getTimestamp(),"snapshot_phase",phase));
            if("queued".equals(ref.optString("status")))trees.put(tree,ref);return ref;
        }catch(Exception error){failures.incrementAndGet();return JevRegistry.fields("status","tree_snapshot_failed");}
        finally{recordTiming("audit_tree_freeze",System.nanoTime()-start);}
    }
    /** Live pixels are acquired now, never by the IO worker. A tree identity check
     * on both sides exposes GUI drift; Android offers no atomic tree+bitmap API. */
    JSONObject capture(final MonkeySourceApe ape,GUITree tree,final long step,final long version,String phase) {
        JSONObject existing=tree==null?null:shots.get(tree);
        if(existing!=null&&existing.optLong("step",-1)==step&&phase.equals(existing.optString("capture_phase")))return existing;
        final JSONObject value=stamp("screenshot",step,version);long start=System.nanoTime();Bitmap bitmap=null;boolean slot=false;
        try {
            JevRegistry.merge(value,JevRegistry.fields("gui_id",tree==null?JSONObject.NULL:tree.getId(),"gui_timestamp",tree==null?JSONObject.NULL:tree.getTimestamp(),
                "capture_phase",phase,"capture_started_ns",start,"activity",tree==null?JSONObject.NULL:tree.getActivityClassName()));
            if(ape==null)return JevRegistry.merge(value,JevRegistry.fields("status","capture_unavailable"));
            if(!(slot=pixels.tryAcquire()))return JevRegistry.merge(value,JevRegistry.fields("status","queue_full"));
            String before=liveIdentity(ape);bitmap=ape.captureBitmap();String after=liveIdentity(ape);
            value.put("capture_finished_ns",System.nanoTime()).put("capture_wall_time_ms",System.currentTimeMillis());
            String expected=tree==null?null:tree.getJevObservationIdentity();
            value.put("alignment",expected==null||before==null||after==null?"unverified":expected.equals(before)&&expected.equals(after)?"identity_verified":"gui_drift")
                .put("expected_observation_identity",expected==null?JSONObject.NULL:expected).put("before_identity",before==null?JSONObject.NULL:before).put("after_identity",after==null?JSONObject.NULL:after);
            if(bitmap==null)return value.put("status","capture_null");
            final Bitmap captured=bitmap;
            final String path="jev-audit/screenshots/step-"+step+"-gui-"+(tree==null?"unknown":tree.getId())+"-"+sequence.incrementAndGet()+".png";
            value.put("width",bitmap.getWidth()).put("height",bitmap.getHeight()).put("path",path).put("completion_path",path+".json");
            final Object completion=JevOutput.freeze(value);
            if(!submit(new Runnable(){public void run(){String status="saved";try {
                File file=new File(root,path);File parent=file.getParentFile();if(!parent.isDirectory()&&!parent.mkdirs())throw new IOException("screenshot directory unavailable");
                if(!file.createNewFile())throw new IOException("screenshot already exists");
                FileOutputStream stream=new FileOutputStream(file);try{if(!captured.compress(Bitmap.CompressFormat.PNG,100,stream))throw new IOException("PNG compression failed");}finally{stream.close();}
                bytes.addAndGet(file.length());
            }catch(Throwable error){status="write_failed";new File(root,path).delete();failures.incrementAndGet();}
            finally{captured.recycle();pixels.release();try{JSONObject done=(JSONObject)JevOutput.json(completion);done.put("status",status);write(path+".json",JevOutput.freeze(done));}catch(Exception error){failures.incrementAndGet();}}}}))return value.put("status","output_rejected");
            bitmap=null;slot=false;value.put("status","capture_queued");if(tree!=null)shots.put(tree,value);return value;
        }catch(Exception error){failures.incrementAndGet();return JevRegistry.merge(value,JevRegistry.fields("status","capture_exception"));}
        finally{if(tree!=null)shots.put(tree,value);if(bitmap!=null)bitmap.recycle();if(slot)pixels.release();recordTiming("audit_live_capture",System.nanoTime()-start);}
    }
    private String liveIdentity(MonkeySourceApe ape) {
        android.view.accessibility.AccessibilityNodeInfo root=null;
        try{root=ape.getRootInActiveWindow();return JevEvidence.accessibilityIdentity(root);}
        catch(LinkageError unavailable){return null;}catch(Exception unavailable){return null;}
        finally{if(root!=null)root.recycle();}
    }
    private JSONObject screenshot(GUITree tree){JSONObject shot=shots.get(tree);return shot==null?JevRegistry.fields("status","not_captured_at_source_time"):shot;}
    void generated(Action action,List<? extends MonkeyEvent> events,int first,long step,long version) {
        try {
            JSONObject operation=action instanceof ModelAction?inputs.get(((ModelAction)action).getResolvedGUITreeAction()):lastOperation;
            if(operation==null)operation=JevRegistry.fields("status","source_not_recorded","probe",false);
            AuditInput trace=new AuditInput(operation);JSONArray kinds=new JSONArray();int index=0;
            for(MonkeyEvent event:events){if(index++<first)continue;eventInputs.put(event,trace);trace.remaining++;
                kinds.put(JevRegistry.fields("type",event.getEventType(),"class",event.getClass().getSimpleName()));}
            save("jev-audit/inputs/generated-"+sequence.incrementAndGet()+".json",JevRegistry.merge(stamp("input_event_inventory",step,version),
                JevRegistry.fields("input",operation,"events",kinds)));
        }catch(Exception error){failures.incrementAndGet();}
    }
    void injected(MonkeyEvent event,int code,boolean discarded,long step,long version) {
        try {
            AuditInput trace=eventInputs.remove(event);if(trace==null)return;
            if(discarded)trace.discarded++;else {if(code==MonkeyEvent.INJECT_SUCCESS)trace.success++;else trace.failed++;
                lastInjectedOperation=JevRegistry.fields("input",trace.input,"probe",trace.input.optBoolean("probe"),"event_type",event.getEventType(),
                    "event_class",event.getClass().getSimpleName(),"inject_code",code,"step",step,"monotonic_ns",System.nanoTime());}
            if(--trace.remaining==0)save("jev-audit/inputs/injection-"+sequence.incrementAndGet()+".json",JevRegistry.merge(stamp("input_injection_summary",step,version),
                JevRegistry.fields("input",trace.input,"success",trace.success,"failed",trace.failed,"discarded",trace.discarded,"business_success",JSONObject.NULL)));
        }catch(Exception error){failures.incrementAndGet();}
    }
    void beforeOtherInput(Action action,long step,long version) {
        try {
            JSONObject value=stamp("native_non_model_input",step,version);
            value.put("action",new JSONObject(action.toJSONObject().toString())).put("probe",false).put("input_id","other-"+sequence.incrementAndGet());
            lastOperation=value;synchronized(recent){recent.addLast(value);while(recent.size()>32)recent.removeFirst();}
            save("jev-audit/inputs/other-"+sequence.incrementAndGet()+".json",value);
        }catch(Exception error){failures.incrementAndGet();}
    }
    void directText(GUITreeAction action,String text,boolean sent,long step,long version) {
        try {
            JSONObject input=inputs.get(action);
            lastInjectedOperation=JevRegistry.fields("input",input,"probe",input!=null&&input.optBoolean("probe"),"phase","direct_text_attempt","step",step,"monotonic_ns",System.nanoTime());
            save("jev-audit/inputs/text-"+sequence.incrementAndGet()+".json",JevRegistry.merge(stamp("direct_text_attempt",step,version),
                JevRegistry.fields("input_id",input==null?JSONObject.NULL:input.opt("input_id"),"actual_text",text,"shell_send_text_return",sent,
                    "fallback_to_key_events",!sent,"business_success",JSONObject.NULL)));
        }catch(Exception error){failures.incrementAndGet();}
    }
    void beforeInput(MonkeySourceApe ape,ModelAction action,long step,long version,boolean probe,JSONObject probeShot,String entry,String request,String candidate) {
        try {
            GUITreeAction concrete=action.getResolvedGUITreeAction();GUITree source=action.getResolvedGUITree();if(concrete==null||source==null)return;
            JSONObject value=stamp("native_input",step,version);String id="input-"+sequence.incrementAndGet();
            String sampleKey=action.getState().getActivity()+"|"+action.getState().getGraphId()+"|"+(action.getResolvedNode()==null?action.getType().name():auditIdentity(action,action.getResolvedNode()).toString());
            boolean history=action.getResolvedNodes()!=null&&action.getResolvedNodes().length>=JevSemanticReview.MIN_CONCRETE_ACTIONS&&sampled.add(sampleKey);
            JevRegistry.merge(value,JevRegistry.fields("input_id",id,"kind",probe?"probe_input":"native_input","entry_id",entry,"request_id",request,"candidate_id",candidate,
                "source_gui_id",source.getId(),"source_gui_timestamp",source.getTimestamp(),"concrete",concrete(action,action.getResolvedNode()),
                "planned_input_text",action.getResolvedNode()==null?JSONObject.NULL:action.getResolvedNode().getInputText(),"probe",probe,"sampled",probe||history));
            if(probe||history)value.put("source_gui",tree(source,step,version,"before_input")).put("source_screenshot",probeShot==null?capture(ape,source,step,version,"history_before_input"):probeShot);
            inputs.put(concrete,value);lastOperation=value;lastInputSource=new NativeSnapshot(source,null,value);
            synchronized(recent){recent.addLast(value);while(recent.size()>32)recent.removeFirst();}
        }catch(Exception error){failures.incrementAndGet();}
    }
    void input(GUITreeAction action,long step,long version,String phase,int total,int success,int failed,int discarded) {
        try {
            JSONObject source=inputs.get(action);JSONObject value=stamp("input_lifecycle",step,version);
            JevRegistry.merge(value,JevRegistry.fields("input_id",source==null?JSONObject.NULL:source.opt("input_id"),"phase",phase,"event_count",total,
                "injected_success",success,"injected_failed",failed,"discarded",discarded,"business_success",JSONObject.NULL,
                "direct_text_injection_limitation","shell sendText/IME business effects are not proven by queued-event completion"));
            if(source!=null)value.put("input",source);save("jev-audit/inputs/lifecycle-"+sequence.incrementAndGet()+".json",value);
        }catch(Exception error){failures.incrementAndGet();}
    }
    void transition(MonkeySourceApe ape,GUITreeTransition fact,String id,long step,long version,boolean confirmed,boolean probe,JSONObject targetShot) {
        try {
            JSONObject transition=JevRegistry.fields("transition_id",id,"input_confirmed",confirmed,"probe",probe,
                "source_gui_id",fact.getSource().getId(),"target_gui_id",fact.getTarget().getId(),"native_receipt","jev-receipts/transition-"+id.replace(':','-')+".json");
            NativeSnapshot observation=new NativeSnapshot(fact.getSource(),fact.getTarget(),transition);
            lastNative=observation;if(confirmed)lastConfirmedNative=observation;
            factIds.put(fact,id);List<GUITreeTransition> list=facts.get(fact.getAction());if(list==null){list=new ArrayList<GUITreeTransition>();facts.put(fact.getAction(),list);}list.add(fact);
            JSONObject source=inputs.get(fact.getAction());if(source==null||!source.optBoolean("sampled"))return;
            JSONObject value=stamp("execution_transition",step,version);
            JevRegistry.merge(value,JevRegistry.fields("transition_id",id,"transition_receipt","jev-receipts/transition-"+id.replace(':','-')+".json",
                "input_confirmed",confirmed,"probe",probe,"input",source,"business_success",JSONObject.NULL,
                "target_gui",tree(fact.getTarget(),step,version,"first_native_target"),
                "target_screenshot",targetShot==null?capture(ape,fact.getTarget(),step,version,"history_first_native_target"):targetShot));
            executionEvidence.put(fact,value);
            save("jev-audit/executions/transition-"+id.replace(':','-')+".json",value);
        }catch(Exception error){failures.incrementAndGet();}
    }
    private JSONObject history(GUITreeAction action,long step,long version) {
        JSONArray transitions=new JSONArray();List<GUITreeTransition> list=facts.get(action);
        if(list!=null)for(GUITreeTransition fact:list) {
            JSONObject execution=executionEvidence.get(fact);
            transitions.put(JevRegistry.fields("transition_id",factIds.get(fact),
                "receipt","jev-receipts/transition-"+factIds.get(fact).replace(':','-')+".json",
                "target_gui",tree(fact.getTarget(),step,version,"retained_history_at_review"),
                "target_screenshot",execution==null?JevRegistry.fields("status","not_captured_at_execution_time"):execution.opt("target_screenshot")));
        }
        JSONObject input=inputs.get(action);
        return JevRegistry.fields("concrete",concrete(action.getModelAction(),action.getGUITreeNode()),
            "source_gui",tree(action.getGUITree(),step,version,"retained_history_at_review"),
            "source_screenshot",input!=null&&input.has("source_screenshot")?input.opt("source_screenshot"):JevRegistry.fields("status","not_captured_at_execution_time"),
            "input",input==null?JevRegistry.fields("status","execution_source_not_recorded"):input,"transitions",transitions,
            "transition_count",transitions.length(),"transition_status",transitions.length()==1?"unique_native_transition":transitions.length()==0?"no_native_transition":"multiple_native_transitions");
    }
    JSONObject review(MonkeySourceApe ape,JevHistoryPairwiseProtocol.Request request,Map<String,JevSemanticReview.Candidate> candidates,long step,long version) {
        try {
            JSONObject value=stamp("pairwise_review_source",step,version),current=new JSONObject(),histories=new JSONObject();
            JSONObject used=new JSONObject(request.bodyJson).getJSONObject("state").getJSONObject("executed_actions");
            for(JevSemanticReview.Candidate c:candidates.values()) {
                GUITree source=c.auditSource;
                current.put(c.candidateId,JevRegistry.fields("entry_id",request.requestId+"-"+c.candidateId,
                    "concrete",concrete(c.action,c.node),"gui",tree(source,step,version,"review_source_before_submit"),
                    "screenshot",capture(ape,source,step,version,"review_source_before_submit")));
                for(Map.Entry<String,GUITreeAction> h:c.auditHistory.entrySet())if(used.has(h.getKey())&&!histories.has(h.getKey()))histories.put(h.getKey(),history(h.getValue(),step,version));
            }
            JevRegistry.merge(value,JevRegistry.fields("request_id",request.requestId,"input_fingerprint",request.inputFingerprint,
                "request_receipt","jev-receipts/semantic-request-"+request.requestId+".json","response_receipt","jev-receipts/semantic-response-"+request.requestId+".json",
                "candidates",current,"history",histories,"pairs",request.plan.getJSONArray("pairs"),
                "provider_body_bytes",request.requestBytes,"model_input_expanded",false));
            return save("jev-audit/reviews/"+request.requestId+".json",value);
        }catch(Exception error){failures.incrementAndGet();return JevRegistry.fields("status","review_audit_failed");}
    }
    JSONObject navigation(MonkeySourceApe ape,GUITree tree,long step,long version,JSONObject details,boolean live) {
        try {
            JSONObject value=stamp("probe_navigation",step,version);JevRegistry.merge(value,details);
            value.put("gui",tree(tree,step,version,live?"navigation_arrival":"retained_promote_source"))
                .put("screenshot",live?capture(ape,tree,step,version,"navigation_arrival"):screenshot(tree));
            return save("jev-audit/navigation/event-"+sequence.incrementAndGet()+".json",value);
        }catch(Exception error){failures.incrementAndGet();return JevRegistry.fields("status","navigation_audit_failed");}
    }
    void crash(MonkeySourceApe ape,String process,int pid,String shortMsg,String longMsg,long timestamp,String stack,long step,long version) {
        try {
            NativeSnapshot observed=lastNative,confirmed=lastConfirmedNative,inputSource=lastInputSource;
            JSONObject value=stamp("application_crash",step,version);JSONArray ops=new JSONArray();synchronized(recent){for(JSONObject op:recent)ops.put(op);}
            JevRegistry.merge(value,JevRegistry.fields("process_name",process,"pid",pid,"application_timestamp_ms",timestamp,"short_message",shortMsg,
                "long_message",longMsg,"stack_trace",stack,"last_generated_operation",lastOperation,"last_injected_operation",lastInjectedOperation,"recent_generated_operations",ops,
                "probe_association",lastInjectedOperation.length()==0?"no_injection_observed":lastInjectedOperation.optBoolean("probe")?"recent_probe_temporal_only":"recent_native_temporal_only",
                "causal_probe_attribution","not_established","operation_history","executor.log and Native transition receipts",
                "device_identity_reference","runner launch.json device serial and executor.log Build Label; joined by run directory",
                "last_observed_transition",observed==null?JSONObject.NULL:observed.value,
                "last_confirmed_transition",confirmed==null?JSONObject.NULL:confirmed.value,
                "last_input_source_gui",inputSource==null?JSONObject.NULL:tree(inputSource.source,step,version,"crash_retained_input_source"),
                "last_input_source_screenshot",inputSource!=null&&inputSource.value.has("source_screenshot")?inputSource.value.opt("source_screenshot"):JevRegistry.fields("status","not_captured_at_execution_time"),
                "last_transition_source_gui",observed==null?JSONObject.NULL:tree(observed.source,step,version,"crash_retained_source"),
                "last_transition_target_gui",observed==null?JSONObject.NULL:tree(observed.target,step,version,"crash_retained_target"),
                "live_crash_screenshot",capture(ape,null,step,version,"crash_live_unpaired_with_last_tree")));
            save("jev-audit/crashes/crash-"+timestamp+"-"+pid+"-"+sequence.incrementAndGet()+".json",value);
        }catch(Exception error){failures.incrementAndGet();}
    }
    void drain() {
        try {Future<?> barrier=writer.submit(new Runnable(){public void run(){}});barrier.get(5,TimeUnit.SECONDS);}
        catch(Exception error){failures.incrementAndGet();}
    }
    public void close() {
        if(closed)return;drain();closed=true;writer.shutdown();
        try{writer.awaitTermination(5,TimeUnit.SECONDS);}catch(InterruptedException error){Thread.currentThread().interrupt();}
        try{write("jev-audit/complete.json",JevOutput.freeze(JevRegistry.fields("schema","guitaboo-local-audit-v1","run_id",runId,"failures",failures.get(),
            "rejected",rejected.get(),"bytes",bytes.get(),"queue_capacity",64,"coverage_not_implied_by_io_health",true,"pending",writer.getQueue().size(),"healthy",failures.get()==0&&rejected.get()==0&&writer.isTerminated())));}
        catch(Exception error){System.err.println("Local audit completion unavailable: "+error.getClass().getSimpleName());}
    }
}
