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

import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import org.json.*;
import com.android.commands.monkey.ape.model.*;
import com.android.commands.monkey.ape.tree.*;
import com.android.commands.monkey.ape.utils.Config;

/** Optional passive journal. Never calls selection, resolution, Naming, RNG, gates or providers. */
public final class JevFlowShadow implements Closeable {
    private final String runId;
    private final ArrayBlockingQueue<String> queue;
    private final Thread worker;
    private final File file;
    private volatile boolean closing,failed;
    private long seq,inputSeq,offered,dropped; private int peak;
    private final IdentityHashMap<GUITreeAction,JSONObject> inputs=new IdentityHashMap<GUITreeAction,JSONObject>();
    private final Set<String> emittedInputIds=new HashSet<String>();
    private final Set<String> transitionIds=new HashSet<String>();
    private final Set<String> states=new HashSet<String>(),triples=new HashSet<String>();
    private static Object value(Object v){return v==null?JSONObject.NULL:v;}
    static JSONObject fields(Object... kv) throws JSONException {JSONObject o=new JSONObject();for(int i=0;i<kv.length;i+=2)o.put((String)kv[i],value(kv[i+1]));return o;}
    public static JevFlowShadow open(File directory,String defaultRunId) {
        if(!Config.getBoolean("ape.jev.flow_shadow",false))return null;
        try {return new JevFlowShadow(new File(directory,"region-flow-events.jsonl"),Config.get("ape.jev.flow_shadow.run_id",defaultRunId),Config.getInteger("ape.jev.flow_shadow.queue",2048));}
        catch(Throwable error){System.err.println("FLOW_SHADOW_DISABLED "+error.getClass().getSimpleName());return null;}
    }
    JevFlowShadow(File file,String runId,int capacity) throws IOException {
        if(capacity<1 || file.exists())throw new IOException("use a new run-specific flow journal");
        this.file=file;this.runId=runId;queue=new ArrayBlockingQueue<String>(capacity);
        final Writer out=new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file),"UTF-8"));
        worker=new Thread(new Runnable(){public void run(){try{while(!closing || !queue.isEmpty()){
            String s=queue.poll(100,TimeUnit.MILLISECONDS);if(s!=null){out.write(s);out.write('\n');out.flush();}
        }}catch(Throwable error){failed=true;}finally{try{out.close();}catch(IOException error){failed=true;}}}},"region-flow-shadow-writer");
        worker.setDaemon(true);worker.start();
    }
    private synchronized void emit(String kind,JSONObject facts) {
        if(closing || failed)return;
        try {
            facts.put("schema",1).put("run_id",runId).put("event_seq",++seq).put("event_kind",kind)
                .put("timestamp",System.currentTimeMillis()).put("monotonic_ns",System.nanoTime())
                .put("dropped_before",dropped).put("execution_confirmed",facts.optBoolean("execution_confirmed",false));
            offered++;if(!queue.offer(facts.toString()))dropped++;peak=Math.max(peak,queue.size());
        }catch(Throwable error){failed=true;}
    }
    public void selection(ModelAction action,int step,String strategy) {
        try{emit("SELECTION",fields("step",step,"source_state",action.getState().getGraphId(),"source_activity",action.getState().getActivity(),
            "model_action_identity",action.getGraphId(),"action_type",action.getType().name(),"strategy",strategy));}catch(Throwable error){failed=true;}
    }
    public void beforeInput(Action action,long step) {
        try {
            if(action.getType().canStartApp()){inputs.clear();emit(action.getType()==ActionType.EVENT_CLEAN_RESTART?"RESET":"RESTART",fields("step",step,"action_type",action.getType().name(),"is_restart",true,"boundary_stage","input_requested_not_execution_proof"));return;}
            if(!(action instanceof ModelAction))return;
            ModelAction a=(ModelAction)action;GUITreeAction concrete=a.getResolvedGUITreeAction();if(concrete==null)return;
            JSONObject progress=fields("selected_model_action_visits",a.getVisitedCount(),"selected_model_action_saturated",a.isSaturated(),"selected_model_action_unvisited",a.isUnvisited(),"stage","before_input");
            JSONObject input=fields("input_id","flow-input-"+(++inputSeq),"source_state",a.getState().getGraphId(),"source_activity",a.getState().getActivity(),
                "model_action_identity",a.getGraphId(),"action_type",a.getType().name(),"step",step,"model_progress",progress);
            inputs.put(concrete,input);emit("INPUT_REQUESTED",new JSONObject(input.toString()));
        }catch(Throwable error){failed=true;}
    }
    public void cachedInput(GUITreeAction action,JSONObject cached) {
        try {
            JSONObject in=inputs.get(action);if(in==null || cached==null)return;
            in.put("audit_input_id",cached.opt("audit_input_id"));JSONObject c=cached.optJSONObject("concrete");
            if(c!=null){in.put("concrete_identity",c.opt("identity_digest"));in.put("concrete_full_name",c.opt("concrete_identity"));
                in.put("control",new JSONObject(c.toString()));}
        }catch(Throwable error){failed=true;}
    }
    public void generated(GUITreeAction action,int total) {
        try{JSONObject in=inputs.get(action);emit("INPUT_GENERATED",fields("input_id",in==null?null:in.opt("input_id"),"event_count",total));}catch(Throwable error){failed=true;}
    }
    public void inputCompleted(GUITreeAction action,boolean confirmed,int total,int success,int failure,int discarded) {
        try{JSONObject in=inputs.get(action);if(in!=null)in.put("input_confirmed",confirmed);emit("INPUT_COMPLETED",fields("input_id",in==null?null:in.opt("input_id"),"execution_confirmed",confirmed,
            "event_count",total,"injected_success",success,"injected_failed",failure,"discarded",discarded));}catch(Throwable error){failed=true;}
    }
    public void boundary(String reason) {try{inputs.clear();emit("TRACE_BOUNDARY",fields("reason",reason,"is_restart",JSONObject.NULL));}catch(Throwable error){failed=true;}}
    public void transition(GUITreeTransition transition,String id,boolean inputConfirmed,boolean rebuilding,boolean nativeNewState,boolean nativeNewAction) {
        try {
            if(transitionIds.contains(id))return;transitionIds.add(id);
            ModelAction a=transition.getAction().getModelAction();State source=transition.getSource().getCurrentState(),target=transition.getTarget().getCurrentState();
            String u=source.getGraphId(),v=target.getGraphId(),triple=u+"\u0000"+a.getGraphId()+"\u0000"+v;
            JSONObject in=inputs.remove(transition.getAction());
            boolean live=inputConfirmed && in!=null && in.optBoolean("input_confirmed",false) && emittedInputIds.add(in.getString("input_id"));
            JSONObject row=fields("transition_identity",id,"native_transition_identity",transition.getCurrentStateTransition().getGraphId(),
                "source_state",u,"target_state",v,"source_activity",source.getActivity(),"target_activity",target.getActivity(),
                "model_action_identity",a.getGraphId(),"action_type",a.getType().name(),"execution_confirmed",live,
                "native_input_confirmed",inputConfirmed,"history_rebuild",rebuilding && in==null,"model_rebinding_pass",rebuilding,"input_id",in==null?null:in.opt("input_id"),
                "is_back",a.getType()==ActionType.MODEL_BACK,"is_restart",false,"is_new_state",live?!states.contains(v):JSONObject.NULL,
                "is_new_transition",live?!triples.contains(triple):JSONObject.NULL,"native_new_state",nativeNewState,"native_new_model_action",nativeNewAction,
                "model_progress",in==null?null:in.opt("model_progress"),"source_gui_id",transition.getSource().getId(),"target_gui_id",transition.getTarget().getId(),
                "source_gui_timestamp",transition.getSource().getTimestamp(),"target_gui_timestamp",transition.getTarget().getTimestamp(),
                "audit_input_id",in==null?null:in.opt("audit_input_id"),"concrete_identity",in==null?null:in.opt("concrete_identity"),
                "control",in==null?null:in.opt("control"),"concrete_identity_status","cached existing audit input only; no new Naming/identity computation");
            if(live){states.add(u);states.add(v);triples.add(triple);}emit("TRANSITION",row);
        }catch(Throwable error){failed=true;}
    }
    public void close() {
        closing=true;try{worker.join(3000);}catch(InterruptedException e){Thread.currentThread().interrupt();}
        try {Writer out=new OutputStreamWriter(new FileOutputStream(new File(file.getParentFile(),"region-flow-status.json")),"UTF-8");
            out.write(fields("run_id",runId,"offered",offered,"dropped",dropped,"queue_peak",peak,"failed",failed,"writer_alive",worker.isAlive()).toString());out.close();
        }catch(Throwable ignored){}
    }
}
