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

import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/** Compact immutable wire snapshots and atomic V1 validation. No native model access. */
public final class JevProtocol {
    private JevProtocol() {}
    private static final int NO_OUTCOME_HARD_LIMIT=32768;
    static final Charset UTF8 = Charset.forName("UTF-8");
    public static final String DEFAULT_ENDPOINT = "http://211.71.15.47:8090/v1/systemone";
    public static final String DEFAULT_MODEL = "StartLux-Decision-4B";
    static final String AFTER_SATURATION="after_saturation";
    public static final String NO_OUTCOME_RULES = JevSemanticProtocol.INSTRUCTIONS;
    static final String CANDIDATE_RULES = "Determine whether executing the candidate is likely to add an application operation that is not yet represented by confirmed APE executions, or whether its likely operation is already covered.\n\n"
        + "`candidates` are unexecuted concrete members awaiting review; candidates in the same review share one ModelAction and provide context for understanding its concrete variations. Use them as semantic context, but other candidates and review conclusions are not execution history, and an unexecuted candidate cannot establish coverage by itself. Structural touch-routing checks are handled separately and do not establish semantic representation.\n\n"
        + "First understand the candidate from its page and local UI relations. Then use `group.action_type`, the first node of the resolved `role_signature`, interaction attributes, and `source_context` to infer the likely operation. Do not infer the operation from text or control type alone. `target_subtree_labels` contains remaining descendant `[text, content_desc]` pairs; labels elsewhere in a larger container are only page context unless a local relation supports the association. For scroll actions, use the recorded scroll direction and context.\n\n"
        + "`current_siblings` contains executed members of the current group. `executed_history_other_pages` contains confirmed executions selected as structurally related history. Structural similarity alone does not establish semantic coverage. Interpret each recorded execution from its own `source_snapshot` before comparing its operation, source context, observed destination, and behavior with the candidate. Repeated executions retain their counts and observed outcomes.\n\n"
        + "Compare the candidate with executed group members and relevant history using the selected control's local role, source page or functional context, action type, observed destination or result, and concrete behavior. Determine whether differences are only data, item, text, or value variations within one function, or indicate a different operation, destination, or behavior branch. `self_loop`, `activity_unchanged`, `gui_unchanged`, unchanged State, or shared structure do not by themselves establish coverage; null or missing evidence is not evidence of no change.\n\n"
        + "Any recorded execution may represent the candidate when the evidence supports the same application operation and behavior branch. Choose `REPRESENTED` only when observed evidence establishes coverage. Choose `PROBE` when the candidate may add a distinct operation, destination, or behavior branch, or when evidence is insufficient. Judge each candidate independently.\n\n"
        + "After saturation, compare remaining candidates with executed group members and relevant history. Answer each question independently.";
    public static final class Request {
        public final String requestId, stateJson, bodyJson, evidenceDigest, inputFingerprint, localTargetStatesJson;
        public final int requestBytes, historyCount;
        public final List<String> candidateIds;
        public final boolean semantic;
        public final List<String> modelActionIds;
        private Request(String id, String state, String body, List<String> ids, int history) {
            this(id,state,body,ids,history,"{}",false,Collections.<String>emptyList());
        }
        private Request(String id, String state, String body, List<String> ids, int history, String localTargets) {
            this(id,state,body,ids,history,localTargets,false,Collections.<String>emptyList());
        }
        private Request(String id, String state, String body, List<String> ids, int history, String localTargets, boolean semantic, List<String> actionIds) {
            localTargetStatesJson=localTargets;
            requestId = id; stateJson = state; bodyJson = body;
            candidateIds = Collections.unmodifiableList(new ArrayList<String>(ids));
            requestBytes = body.getBytes(UTF8).length; historyCount = history;
            this.semantic=semantic;modelActionIds=Collections.unmodifiableList(new ArrayList<String>(actionIds));
            evidenceDigest = digest(state);
            try { inputFingerprint = JevSharedEvidence.fingerprint(new JSONObject(body)); }
            catch (JSONException error) { throw new IllegalStateException(error); }
        }
    }

    /** Adapter used by the synchronous transport; the wire body remains the semantic page contract. */
    public static Request semantic(String requestId, String model, JSONObject page, int maxBytes) throws JSONException {
        JevSemanticProtocol.Request semantic=JevSemanticProtocol.request(requestId,model,page,Integer.MAX_VALUE);
        return new Request(requestId,page.toString(),semantic.bodyJson,semantic.candidateIds,0,"{}",true,semantic.modelActionIds);
    }

    static Request semanticPacket(Request logical,List<String> ids,int index,int maxBytes)throws JSONException {
        JSONObject body=new JSONObject(logical.bodyJson),page=body.getJSONObject("page");
        Set<String> selected=new HashSet<String>(ids);JSONArray groups=page.getJSONArray("model_actions");
        for(int i=0;i<groups.length();i++) {
            JSONObject group=groups.getJSONObject(i);JSONArray source=group.getJSONArray("unexecuted_concrete_actions"),kept=new JSONArray();
            for(int j=0;j<source.length();j++) {
                JSONObject control=source.getJSONObject(j);
                if(!control.has("candidate_id")||selected.contains(control.getString("candidate_id")))kept.put(control);
            }
            group.put("unexecuted_concrete_actions",kept);
        }
        JSONArray selectedIds=new JSONArray();for(String id:ids)selectedIds.put(id);page.put("candidate_ids",selectedIds);
        JSONObject questions=new JSONObject();JSONObject original=body.getJSONObject("questions");
        for(String id:ids)questions.put(id,original.get(id));
        String requestId=logical.requestId+":p"+index;
        JSONObject packet=new JSONObject().put("model",body.get("model")).put("instructions",body.get("instructions"))
            .put("page",page).put("questions",questions);
        String json=packet.toString();if(json.getBytes(UTF8).length>maxBytes)throw new BudgetExceeded(new Request(requestId,page.toString(),json,ids,0,"{}",true,logical.modelActionIds));
        return new Request(requestId,page.toString(),json,ids,0,"{}",true,logical.modelActionIds);
    }
    public static final class BudgetExceeded extends JSONException {
        public final Request request;
        BudgetExceeded(Request request) { super("REQUEST_BUDGET_EXCEEDED"); this.request=request; }
    }
    public static JSONObject budget(Request request,int limit) throws JSONException {
        JSONObject body=new JSONObject(request.bodyJson),state=body.getJSONObject("state");
        JSONObject sizes=new JSONObject().put("body_bytes",request.requestBytes).put("limit_bytes",limit);
        for(String key:new String[]{"state","questions"})sizes.put(key+"_bytes",body.get(key).toString().getBytes(UTF8).length);
        for(String key:JevSharedEvidence.keys(state))sizes.put(key+"_bytes",state.get(key).toString().getBytes(UTF8).length);
        int outcomes=0;
        if(!"ape_probe_v4".equals(state.optString("evidence_schema"))) {
            JSONArray reviews=state.has("reviews")?state.getJSONArray("reviews"):new JSONArray().put(state);
            for(int i=0;i<reviews.length();i++)for(String key:new String[]{"current_siblings","executed_history_other_pages"}) {
                JSONArray members=reviews.getJSONObject(i).getJSONArray(key);
                for(int j=0;j<members.length();j++)outcomes+=members.getJSONObject(j).getJSONArray("outcomes").length();
            }
        }
        return sizes.put("outcome_count",outcomes);
    }
    public static final class Decision {
        public final String candidateId, decision;
        public final String representativeModelActionId;
        public final double pProbe, pRepresented;
        public final Double confidence;
        private Decision(String id, String choice, double probe, double represented, Double confidence) {
            this(id,choice,probe,represented,confidence,null);
        }
        private Decision(String id, String choice, double probe, double represented, Double confidence, String representative) {
            candidateId = id; decision = choice; pProbe = probe; pRepresented = represented; this.confidence = confidence; representativeModelActionId=representative;
        }
    }
    public static final class Result {
        public final Map<String, Decision> decisions;
        private Result(Map<String, Decision> values) {
            decisions = Collections.unmodifiableMap(new LinkedHashMap<String, Decision>(values));
        }
        public JSONObject toJson() throws JSONException {
            JSONArray ds = new JSONArray();
            for (Decision d : decisions.values()) ds.put(new JSONObject().put("candidate_id", d.candidateId)
                .put("decision", d.decision).put("p_probe", d.pProbe).put("p_represented", d.pRepresented).put("confidence", d.confidence)
                .put("representative_model_action_id", d.representativeModelActionId==null?JSONObject.NULL:d.representativeModelActionId));
            return new JSONObject().put("decisions", ds);
        }
    }

    public static Request snapshot(String requestId, JSONObject compactState, int maxBytes, String model) throws JSONException {
        if (requestId == null || requestId.length() == 0 || maxBytes <= 0 || model == null || model.length() == 0)
            throw new JSONException("invalid request configuration");
        // Clone before validation: no mutable app object is retained by the worker.
        JSONObject state = StrictJson.object(compactState.toString());
        exact(state, "review_source", "candidate_review_rules", "source_context", "history_contexts", "group", "candidates", "current_siblings", "executed_history_other_pages");
        if(!CANDIDATE_RULES.equals(string(state,"candidate_review_rules")))throw new JSONException("invalid candidate rules");
        if(!AFTER_SATURATION.equals(string(state,"review_source")))throw new JSONException("invalid review source");
        JSONObject contexts=state.getJSONObject("history_contexts");
        for(Iterator<?> it=contexts.keys();it.hasNext();) {
            String id=(String)it.next();
            if(!id.matches("p[1-9][0-9]*"))throw new JSONException("invalid context ID");
            sourceContext(contexts.getJSONObject(id));
        }
        sourceContext(state.getJSONObject("source_context"));
        JSONObject group = state.getJSONObject("group"); exact(group, "action_type"); string(group, "action_type");
        JSONArray cs = state.getJSONArray("candidates"), siblings = state.getJSONArray("current_siblings"), hs = state.getJSONArray("executed_history_other_pages");
        if (hs.length() > 8) throw new JSONException("invalid compact member count");
        List<String> ids = new ArrayList<String>(); Set<String> seen = new HashSet<String>();
        JSONObject questions = new JSONObject();
        for (int i = 0; i < cs.length(); i++) {
            JSONObject c = cs.getJSONObject(i);
            if(c.has("local_context")) exact(c, "candidate_id", "target_subtree_labels", "role_signature", "local_context");
            else exact(c, "candidate_id", "target_subtree_labels", "role_signature");
            member(c); String id = string(c, "candidate_id");
            if (!id.matches("c[1-9][0-9]*") || !seen.add(id)) throw new JSONException("invalid candidate ID");
            ids.add(id);
            questions.put(id, choice("Apply `candidate_review_rules` to candidate_id `" + id + "`.",
                "REPRESENTED", "The candidate's likely application operation is already covered by observed behavior. Differences, if any, are consistent with data, item, text, or value variations within the same function.",
                "PROBE", "The candidate may expose a distinct application operation, destination, or behavior branch, or existing observations do not establish that it is already represented. One native APE execution is warranted."));
        }
        seen.clear();
        for (int i = 0; i < hs.length(); i++) {
            JSONObject h = hs.getJSONObject(i);
            if(!contexts.has(string(h,"source_context_id")))throw new JSONException("unknown history context");
            exact(h, "source_context_id", "member_id", "execution_count", "outcomes", "outcomes_truncated");
            string(h,"source_context_id"); String id = string(h, "member_id");
            if (!id.matches("h[1-9][0-9]*") || !seen.add(id)) throw new JSONException("invalid history ID");
            count(h, "execution_count");bool(h,"outcomes_truncated");validateOutcomes(h,contexts);
        }
        int executedSiblings=0;
        for (int i = 0; i < siblings.length(); i++) {
            JSONObject s = siblings.getJSONObject(i);
            exact(s,"sibling_id","execution_count","outcomes","outcomes_truncated");
            count(s,"execution_count");bool(s,"outcomes_truncated");validateOutcomes(s,contexts);
            executedSiblings++;
            if(s.getJSONArray("outcomes").length()==0)throw new JSONException("inconsistent sibling execution");
        }
        return packRequest(requestId,state,questions,ids,hs.length()+executedSiblings,model,maxBytes);
    }

    /** One synchronous page request; each group retains its own confirmed history. */
    public static Request page(String requestId, List<JSONObject> groups, int maxBytes, String model) throws JSONException {
        if(groups.isEmpty())throw new JSONException("empty page request");
        JSONArray reviews=new JSONArray();JSONObject questions=new JSONObject();
        JSONObject historyContexts=new JSONObject();Map<String,String> contextIds=new LinkedHashMap<String,String>();
        List<String> ids=new ArrayList<String>();int histories=0;
        JSONObject context=null;
        for(int i=0;i<groups.size();i++) {
            JSONObject group=StrictJson.object(groups.get(i).toString());
            JSONArray candidates=group.getJSONArray("candidates");
            for(int j=0;j<candidates.length();j++)candidates.getJSONObject(j).put("candidate_id","c"+(ids.size()+j+1));
            Request checked=snapshot(requestId,group,Integer.MAX_VALUE,model);
            histories+=checked.historyCount;
            JSONObject source=group.getJSONObject("source_context");
            if(context==null)context=source;
            else if(!context.toString().equals(source.toString()))throw new JSONException("mixed page contexts");
            JSONObject groupQuestions=new JSONObject(checked.bodyJson).getJSONObject("questions");
            JSONObject localContexts=group.getJSONObject("history_contexts");Map<String,String> remap=new HashMap<String,String>();
            Set<String> localIds=new TreeSet<String>();
            for(Iterator<?> it=localContexts.keys();it.hasNext();)localIds.add((String)it.next());
            for(String localId:localIds) {
                JSONObject historyContext=localContexts.getJSONObject(localId);
                // Compare validated values, independent of JSONObject field insertion order.
                JSONArray key=new JSONArray().put(historyContext.getString("activity")).put(historyContext.getBoolean("truncated"));
                JSONArray labels=historyContext.getJSONArray("page_labels");
                for(int j=0;j<labels.length();j++) {
                    JSONObject label=labels.getJSONObject(j);
                    key.put(new JSONArray().put(label.get("resource_id")).put(label.get("text")).put(label.get("content_desc")).put(label.get("selected")));
                }
                String globalId=contextIds.get(key.toString());
                if(globalId==null){globalId="p"+(historyContexts.length()+1);contextIds.put(key.toString(),globalId);historyContexts.put(globalId,historyContext);}
                remap.put(localId,globalId);
            }
            JSONArray history=group.getJSONArray("executed_history_other_pages");
            for(int j=0;j<history.length();j++) {
                JSONObject member=history.getJSONObject(j);member.put("source_context_id",remap.get(member.getString("source_context_id")));
            }
            for(String kind:new String[]{"current_siblings","executed_history_other_pages"}) {
                JSONArray members=group.getJSONArray(kind);
                for(int j=0;j<members.length();j++) {
                    JSONArray outcomes=members.getJSONObject(j).getJSONArray("outcomes");
                    for(int k=0;k<outcomes.length();k++) {
                        JSONObject sourceSnapshot=outcomes.getJSONObject(k).getJSONObject("source_snapshot");
                        sourceSnapshot.put("source_context_id",remap.get(sourceSnapshot.getString("source_context_id")));
                    }
                }
            }
            for(String id:checked.candidateIds) {
                JSONObject question=groupQuestions.getJSONObject(id);
                questions.put(id,question);ids.add(id);
            }
            group.remove("source_context");group.remove("candidate_review_rules");group.remove("history_contexts");reviews.put(group);
        }
        JSONObject state=new JSONObject().put("candidate_review_rules",CANDIDATE_RULES).put("source_context",context).put("history_contexts",historyContexts).put("reviews",reviews);
        return packRequest(requestId,state,questions,ids,histories,model,maxBytes);
    }

    /**
     * New synchronous contract: preserve candidate and local/history control
     * evidence while removing all post-execution result fields.  The legacy
     * page() method remains available for old fixtures and non-probe clients.
     */
    public static Request pageNoOutcome(String requestId, List<JSONObject> groups, int maxBytes, String model) throws JSONException {
        if(groups.isEmpty())throw new JSONException("empty page request");
        JSONArray reviews=new JSONArray();JSONObject questions=new JSONObject();
        JSONObject historyContexts=new JSONObject();Map<String,String> contextIds=new LinkedHashMap<String,String>();
        List<String> ids=new ArrayList<String>();int histories=0;JSONObject context=null;
        for(int i=0;i<groups.size();i++) {
            JSONObject original=StrictJson.object(groups.get(i).toString());
            JSONObject group=noOutcomeGroup(original,ids.size());
            JSONArray localCandidates=group.getJSONArray("candidates");
            for(int j=0;j<localCandidates.length();j++) {
                String id=localCandidates.getJSONObject(j).getString("candidate_id");ids.add(id);
            }
            JSONObject source=group.getJSONObject("source_context");
            if(context==null)context=source;
            else if(!JevSharedEvidence.canonical(context).equals(JevSharedEvidence.canonical(source)))throw new JSONException("mixed page contexts");
            JSONObject localContexts=group.getJSONObject("history_contexts");
            Map<String,String> remap=new HashMap<String,String>();
            for(String localId:JevSharedEvidence.keys(localContexts)) {
                JSONObject value=localContexts.getJSONObject(localId);
                String key=JevSharedEvidence.canonical(value),global=contextIds.get(key);
                if(global==null){global="p"+(historyContexts.length()+1);contextIds.put(key,global);historyContexts.put(global,value);}
                remap.put(localId,global);
            }
            for(String kind:new String[]{"current_siblings","executed_history_other_pages"}) {
                JSONArray values=group.getJSONArray(kind);
                for(int j=0;j<values.length();j++) {
                    JSONObject value=values.getJSONObject(j);
                    if(value.has("source_context_id"))value.put("source_context_id",remap.get(value.getString("source_context_id")));
                }
            }
            JSONObject localQuestions=new JSONObject();
            for(int j=0;j<localCandidates.length();j++) {
                JSONObject candidate=localCandidates.getJSONObject(j);String id=candidate.getString("candidate_id");
                localQuestions.put(id,choice("Apply the synchronous concrete review rules to this candidate.",
                    "REPRESENTED","The supplied concrete control evidence establishes the same application operation.",
                    "PROBE","The candidate may be a distinct operation or the supplied evidence is insufficient."));
            }
            histories+=group.getJSONArray("executed_history_other_pages").length()+group.getJSONArray("current_siblings").length();
            group.remove("source_context");group.remove("candidate_review_rules");group.remove("history_contexts");
            reviews.put(group);
            for(String id:ids)if(!questions.has(id))questions.put(id,localQuestions.get(id));
        }
        JSONObject state=new JSONObject().put("candidate_review_rules",NO_OUTCOME_RULES).put("source_context",context)
            .put("history_contexts",historyContexts).put("reviews",reviews);
        return packNoOutcomeRequest(requestId,state,questions,ids,histories,model,maxBytes);
    }

    /** Convert the native legacy compact evidence into the v4 no-result shape. */
    private static JSONObject noOutcomeGroup(JSONObject original,int idOffset)throws JSONException {
        JSONObject group=StrictJson.object(original.toString());
        exact(group,"review_source","candidate_review_rules","source_context","history_contexts","group","candidates","current_siblings","executed_history_other_pages");
        if(!AFTER_SATURATION.equals(string(group,"review_source")))throw new JSONException("invalid review source");
        JSONObject contexts=group.getJSONObject("history_contexts");
        sourceContext(group.getJSONObject("source_context"));
        for(String id:JevSharedEvidence.keys(contexts)){if(!id.matches("p[1-9][0-9]*"))throw new JSONException("invalid context ID");sourceContext(contexts.getJSONObject(id));}
        JSONObject groupInfo=group.getJSONObject("group");exact(groupInfo,"action_type");string(groupInfo,"action_type");
        JSONArray candidates=group.getJSONArray("candidates");
        for(int i=0;i<candidates.length();i++) {
            JSONObject candidate=candidates.getJSONObject(i);candidate.put("candidate_id","c"+(idOffset+i+1));
            if(candidate.has("local_context"))exact(candidate,"candidate_id","target_subtree_labels","role_signature","local_context");
            else exact(candidate,"candidate_id","target_subtree_labels","role_signature");
            member(candidate);
        }
        group.put("candidates",candidates);
        group.put("current_siblings",noOutcomeMembers(group.getJSONArray("current_siblings"),"sibling_id",groupInfo.getString("action_type"),contexts,false));
        group.put("executed_history_other_pages",noOutcomeMembers(group.getJSONArray("executed_history_other_pages"),"member_id",groupInfo.getString("action_type"),contexts,true));
        group.put("candidate_review_rules",NO_OUTCOME_RULES);
        return group;
    }

    private static JSONArray noOutcomeMembers(JSONArray source,String idField,String actionType,JSONObject contexts,boolean history)throws JSONException {
        JSONArray out=new JSONArray();
        for(int i=0;i<source.length();i++) {
            JSONObject old=source.getJSONObject(i),value=new JSONObject();
            String id=string(old,idField);value.put(idField,id).put("action_type",actionType);
            JSONObject snapshot=null;String contextId=null;
            if(old.has("outcomes")) {
                JSONArray outcomes=old.getJSONArray("outcomes");
                if(outcomes.length()>0) {
                    JSONObject first=outcomes.getJSONObject(0);snapshot=first.optJSONObject("source_snapshot");
                    if(snapshot!=null)contextId=snapshot.optString("source_context_id",null);
                }
            }
            if(snapshot==null) {
                // A confirmed execution normally has a source snapshot.  If
                // an old receipt lacks one, it is not safe to invent a GUI
                // binding, so retain only the stable member shell.
                snapshot=new JSONObject().put("target_subtree_labels",new JSONArray()).put("role_signature",new JSONArray());
            }
            value.put("target_subtree_labels",snapshot.getJSONArray("target_subtree_labels"))
                .put("role_signature",snapshot.getJSONArray("role_signature"))
                .put("local_context",snapshot.has("local_context")?snapshot.get("local_context"):JSONObject.NULL);
            if(contextId!=null) {
                if(!contexts.has(contextId))throw new JSONException("unknown history context");
                value.put("source_context_id",contextId).put("source_activity",contexts.getJSONObject(contextId).getString("activity"));
            } else if(history)throw new JSONException("history source context missing");
            out.put(value);
        }
        return out;
    }

    private static Request packRequest(String id,JSONObject state,JSONObject questions,List<String> ids,int history,String model,int maxBytes)throws JSONException {
        JevSharedEvidence.Packed packed=JevSharedEvidence.pack(state);
        String body=new JSONObject().put("model",model).put("state",packed.state).put("questions",questions).toString();
        Request request=new Request(id,packed.state.toString(),body,ids,history,packed.localTargets.toString());
        if(request.requestBytes>maxBytes || request.stateJson.getBytes(UTF8).length>maxBytes)throw new BudgetExceeded(request);
        return request;
    }

    private static Request packNoOutcomeRequest(String id,JSONObject state,JSONObject questions,List<String> ids,int history,String model,int maxBytes)throws JSONException {
        JevSharedEvidence.Packed packed=JevSharedEvidence.packNoOutcome(state);
        String body=new JSONObject().put("model",model).put("state",packed.state).put("questions",questions).toString();
        Request request=new Request(id,packed.state.toString(),body,ids,history,packed.localTargets.toString());
        // The synchronous Registry builds one in-memory logical request and
        // immediately hands it to JevProbePackets for physical splitting.
        // Only that internal sentinel may exceed the wire cap; public/wire
        // callers remain hard-limited to 32 KiB.
        int limit=maxBytes==Integer.MAX_VALUE?Integer.MAX_VALUE:Math.min(maxBytes,NO_OUTCOME_HARD_LIMIT);
        if(request.requestBytes>limit || request.stateJson.getBytes(UTF8).length>limit)throw new BudgetExceeded(request);
        return request;
    }

    /** Validate only the current shared wire protocol; expanded snapshots are internal input only. */
    public static Request wire(String id,String raw,int maxBytes,String model)throws JSONException {
        if(id==null || id.isEmpty() || maxBytes<=0)throw new JSONException("invalid request configuration");
        JSONObject body=StrictJson.object(raw);exact(body,"model","state","questions");
        if(!model.equals(string(body,"model")))throw new JSONException("model mismatch");
        if("ape_probe_v4".equals(body.getJSONObject("state").optString("evidence_schema")))return wireNoOutcome(id,body,raw,maxBytes,model);
        JSONObject state=JevSharedEvidence.expand(body.getJSONObject("state"));
        if(!"ape_probe_v3".equals(state.optString("evidence_schema")))throw new JSONException("invalid evidence schema");
        JSONObject questions=body.getJSONObject("questions"),expectedQuestions=new JSONObject();
        List<String> ids=new ArrayList<String>();int histories=0;
        boolean page=state.has("reviews");
        JSONArray groups;
        if(page) {
            exact(state,"evidence_schema","candidate_review_rules","source_context","history_contexts","reviews");
            sourceContext(state.getJSONObject("source_context"));
            JSONObject contexts=state.getJSONObject("history_contexts");
            for(String key:JevSharedEvidence.keys(contexts)) {
                if(!key.matches("p[1-9][0-9]*"))throw new JSONException("invalid context ID");
                sourceContext(contexts.getJSONObject(key));
            }
            groups=state.getJSONArray("reviews");
            if(groups.length()==0)throw new JSONException("empty page request");
        }else groups=new JSONArray().put(state);
        for(int i=0;i<groups.length();i++) {
            JSONObject group=new JSONObject(groups.getJSONObject(i).toString());
            group.remove("evidence_schema");
            if(page) {
                exact(group,"review_source","group","candidates","current_siblings","executed_history_other_pages");
                JSONObject contexts=new JSONObject();JSONArray history=group.getJSONArray("executed_history_other_pages");
                for(int j=0;j<history.length();j++) {
                    String key=string(history.getJSONObject(j),"source_context_id");
                    contexts.put(key,state.getJSONObject("history_contexts").getJSONObject(key));
                }
                for(String kind:new String[]{"current_siblings","executed_history_other_pages"}) {
                    JSONArray members=group.getJSONArray(kind);
                    for(int j=0;j<members.length();j++) {
                        JSONArray outcomes=members.getJSONObject(j).getJSONArray("outcomes");
                        for(int k=0;k<outcomes.length();k++) {
                            String key=outcomes.getJSONObject(k).getJSONObject("source_snapshot").getString("source_context_id");
                            contexts.put(key,state.getJSONObject("history_contexts").getJSONObject(key));
                        }
                    }
                }
                group.put("candidate_review_rules",CANDIDATE_RULES).put("source_context",state.getJSONObject("source_context")).put("history_contexts",contexts);
            }
            // IDs are intentionally absent from model evidence. An empty local placeholder
            // permits reuse of the exact internal evidence validator, never an inferred ID.
            for(String kind:new String[]{"current_siblings","executed_history_other_pages"}) {
                JSONArray members=group.getJSONArray(kind);
                for(int j=0;j<members.length();j++) {
                    JSONArray outcomes=members.getJSONObject(j).getJSONArray("outcomes");
                    for(int k=0;k<outcomes.length();k++)outcomes.getJSONObject(k).put("target_state","");
                }
            }
            Request validated=snapshot(id,group,Integer.MAX_VALUE,model);histories+=validated.historyCount;
            JSONObject localQuestions=new JSONObject(validated.bodyJson).getJSONObject("questions");
            for(String candidate:validated.candidateIds) {
                if(ids.contains(candidate))throw new JSONException("duplicate page candidate ID");
                ids.add(candidate);JSONObject question=localQuestions.getJSONObject(candidate);
                expectedQuestions.put(candidate,question);
            }
        }
        if(!JevSharedEvidence.canonical(questions).equals(JevSharedEvidence.canonical(expectedQuestions)))throw new JSONException("question binding or criteria mismatch");
        Request request=new Request(id,body.getJSONObject("state").toString(),body.toString(),ids,histories);
        if(request.requestBytes>maxBytes || raw.getBytes(UTF8).length>maxBytes)throw new BudgetExceeded(request);
        return request;
    }

    private static Request wireNoOutcome(String id,JSONObject body,String raw,int maxBytes,String model)throws JSONException {
        JSONObject expanded=JevSharedEvidence.expandNoOutcome(body.getJSONObject("state"));
        JSONObject questions=body.getJSONObject("questions"),expected=new JSONObject();
        List<String> ids=new ArrayList<String>();int histories=0;
        boolean page=expanded.has("reviews");JSONArray groups;
        if(page) {
            exact(expanded,"evidence_schema","candidate_review_rules","source_context","history_contexts","reviews");
            sourceContext(expanded.getJSONObject("source_context"));
            JSONObject contexts=expanded.getJSONObject("history_contexts");
            for(String key:JevSharedEvidence.keys(contexts)){if(!key.matches("p[1-9][0-9]*"))throw new JSONException("invalid context ID");sourceContext(contexts.getJSONObject(key));}
            groups=expanded.getJSONArray("reviews");if(groups.length()==0)throw new JSONException("empty page request");
        } else groups=new JSONArray().put(expanded);
        for(int i=0;i<groups.length();i++) {
            JSONObject group=new JSONObject(groups.getJSONObject(i).toString());
            if(page) {
                exact(group,"review_source","group","candidates","current_siblings","executed_history_other_pages");
                if(!AFTER_SATURATION.equals(string(group,"review_source")))throw new JSONException("invalid review source");
                JSONObject contexts=new JSONObject();
                for(String kind:new String[]{"current_siblings","executed_history_other_pages"}) {
                    JSONArray values=group.getJSONArray(kind);histories+=values.length();
                    for(int j=0;j<values.length();j++) {
                        JSONObject value=values.getJSONObject(j);
                        if(value.has("source_context_id")) {
                            String key=string(value,"source_context_id");contexts.put(key,expanded.getJSONObject("history_contexts").getJSONObject(key));
                        }
                    }
                }
                group.put("source_context",expanded.getJSONObject("source_context")).put("history_contexts",contexts);
            } else histories+=group.getJSONArray("current_siblings").length()+group.getJSONArray("executed_history_other_pages").length();
            if(!AFTER_SATURATION.equals(string(group,"review_source")))throw new JSONException("invalid review source");
            JSONObject groupInfo=group.getJSONObject("group");exact(groupInfo,"action_type");string(groupInfo,"action_type");
            JSONArray candidates=group.getJSONArray("candidates");Set<String> localSeen=new HashSet<String>();
            for(int j=0;j<candidates.length();j++) {
                JSONObject candidate=candidates.getJSONObject(j);
                if(candidate.has("local_context"))exact(candidate,"candidate_id","target_subtree_labels","role_signature","local_context");
                else exact(candidate,"candidate_id","target_subtree_labels","role_signature");
                member(candidate);String candidateId=string(candidate,"candidate_id");
                if(!candidateId.matches("c[1-9][0-9]*")||!localSeen.add(candidateId)||ids.contains(candidateId))throw new JSONException("invalid candidate ID");
                ids.add(candidateId);expected.put(candidateId,choice("Apply the synchronous concrete review rules to this candidate.",
                    "REPRESENTED","The supplied concrete control evidence establishes the same application operation.",
                    "PROBE","The candidate may be a distinct operation or the supplied evidence is insufficient."));
            }
            validateNoOutcomeMembers(group.getJSONArray("current_siblings"),"sibling_id",false,expanded.getJSONObject("history_contexts"));
            validateNoOutcomeMembers(group.getJSONArray("executed_history_other_pages"),"member_id",true,expanded.getJSONObject("history_contexts"));
        }
        if(!JevSharedEvidence.canonical(questions).equals(JevSharedEvidence.canonical(expected)))throw new JSONException("question binding or criteria mismatch");
        Request request=new Request(id,body.getJSONObject("state").toString(),body.toString(),ids,histories);
        int limit=Math.min(maxBytes,NO_OUTCOME_HARD_LIMIT);
        if(request.requestBytes>limit || body.toString().getBytes(UTF8).length>limit || raw.getBytes(UTF8).length>limit)throw new BudgetExceeded(request);
        return request;
    }

    private static void validateNoOutcomeMembers(JSONArray values,String idField,boolean history,JSONObject contexts)throws JSONException {
        Set<String> seen=new HashSet<String>();
        for(int i=0;i<values.length();i++) {
            JSONObject value=values.getJSONObject(i);
            if(history)exact(value,idField,"action_type","source_context_id","source_activity","target_subtree_labels","role_signature","local_context");
            else exact(value,idField,"action_type","source_context_id","source_activity","target_subtree_labels","role_signature","local_context");
            String id=string(value,idField);if(!seen.add(id))throw new JSONException("duplicate history ID");
            string(value,"action_type");String contextId=string(value,"source_context_id");string(value,"source_activity");if(!contexts.has(contextId))throw new JSONException("unknown history context");
            JSONObject member=new JSONObject().put("candidate_id","c1").put("target_subtree_labels",value.get("target_subtree_labels"))
                .put("role_signature",value.get("role_signature")).put("local_context",value.get("local_context"));member(member);
        }
    }

    /** Text is already validated and bounded by snapshot(); images have a separate wire cap. */
    public static Request withImages(Request text, JSONObject visual, JSONArray images, int maxBytes) throws JSONException {
        if(images.length()<1 || images.length()>4 || maxBytes<=0)throw new JSONException("invalid image count or budget");
        JSONArray panels=visual.getJSONArray("panels");
        if(panels.length()!=images.length())throw new JSONException("image mapping mismatch");
        for(int i=0;i<images.length();i++) {
            if(!images.getString(i).startsWith("data:image/jpeg;base64,") || !"<image>".equals(panels.getJSONObject(i).getString("image")))
                throw new JSONException("invalid image encoding or marker");
        }
        JSONObject state=new JSONObject(text.stateJson).put("visual_evidence",visual);
        JSONObject body=new JSONObject(text.bodyJson).put("state",state).put("images",images);
        Request request=new Request(text.requestId,state.toString(),body.toString(),text.candidateIds,text.historyCount,text.localTargetStatesJson);
        if(request.requestBytes>maxBytes)throw new BudgetExceeded(request);
        return request;
    }

    public static Result validate(String raw, List<String> expectedIds) throws JSONException {
        return validateObject(StrictJson.object(raw), expectedIds);
    }
    private static Result validateObject(JSONObject root, List<String> expectedIds) throws JSONException {
        exact(root, "decisions");
        Set<String> expected = new HashSet<String>(expectedIds);
        if (expected.size() != expectedIds.size()) throw new JSONException("invalid expected IDs");
        JSONArray decisions = root.getJSONArray("decisions");
        if (decisions.length() != expected.size()) throw new JSONException("incomplete decisions");
        Map<String, Decision> validated = new LinkedHashMap<String, Decision>();
        for (int i = 0; i < decisions.length(); i++) {
            JSONObject d = decisions.getJSONObject(i); if(d.has("confidence")) exact(d, "candidate_id", "decision", "p_probe", "p_represented", "confidence");
            else exact(d, "candidate_id", "decision", "p_probe", "p_represented");
            String id = string(d, "candidate_id"), decision = string(d, "decision");
            if (!expected.contains(id) || validated.containsKey(id)) throw new JSONException("unknown or duplicate candidate");
            oneOf(decision, "REPRESENTED", "PROBE");
            validated.put(id, new Decision(id, decision, probability(d, "p_probe"), probability(d, "p_represented"), d.has("confidence") ? probability(d,"confidence") : null));
        }
        return new Result(validated);
    }
    /** Direct TypeSafe provider response -> the exact frozen V1 schema -> atomic validation. */
    public static Result fromProvider(String raw, Request request) throws JSONException {
        if(request.semantic) {
            JSONObject body=new JSONObject(request.bodyJson);
            JevSemanticProtocol.Request semanticRequest=JevSemanticProtocol.request(request.requestId,body.getString("model"),body.getJSONObject("page"),Integer.MAX_VALUE);
            JevSemanticProtocol.Result semantic=JevSemanticProtocol.fromProvider(raw,semanticRequest);
            Map<String,Decision> converted=new LinkedHashMap<String,Decision>();
            for(JevSemanticProtocol.Decision d:semantic.decisions.values()) {
                String choice=d.representativeModelActionId;
                converted.put(d.candidateId,new Decision(d.candidateId,JevSemanticProtocol.PROMOTE.equals(choice)?"PROBE":"REPRESENTED",
                    JevSemanticProtocol.PROMOTE.equals(choice)?1.0:0.0,JevSemanticProtocol.PROMOTE.equals(choice)?0.0:1.0,d.confidence,choice));
            }
            return new Result(converted);
        }
        JSONObject provider = StrictJson.object(raw);
        JSONObject answers = provider.getJSONObject("answers");
        List<String> keys = request.candidateIds;
        exact(answers, keys.toArray(new String[keys.size()]));
        JSONArray decisions = new JSONArray();
        for (String id : request.candidateIds) {
            JSONObject a = providerChoice(answers.getJSONObject(id), "PROBE", "REPRESENTED");
            JSONObject p = a.getJSONObject("probabilities");
            decisions.put(new JSONObject().put("candidate_id", id).put("decision", string(a, "choice"))
                .put("confidence", probability(a,"confidence")).put("p_probe", probability(p, "PROBE")).put("p_represented", probability(p, "REPRESENTED")));
        }
        return validateObject(new JSONObject().put("decisions", decisions), request.candidateIds);
    }

    private static JSONObject providerChoice(JSONObject a, String first, String second) throws JSONException {
        exact(a, "type", "choice", "probabilities", "confidence");
        if (!"choice".equals(string(a, "type"))) throw new JSONException("invalid provider type");
        oneOf(string(a, "choice"), first, second); probability(a, "confidence");
        exact(a.getJSONObject("probabilities"), first, second);
        return a;
    }
    private static JSONObject choice(String instructions, String a, String da, String b, String db) throws JSONException {
        return new JSONObject().put("type", "choice").put("instructions", instructions)
            .put("criteria", new JSONObject().put(a, da).put(b, db));
    }
    private static void sourceContext(JSONObject context)throws JSONException {
        exact(context,"activity","page_labels","truncated");string(context,"activity");bool(context,"truncated");
        JSONArray labels=context.getJSONArray("page_labels");
        if(labels.length()>8)throw new JSONException("page context label limit");
        for(int i=0;i<labels.length();i++) {
            JSONObject label=labels.getJSONObject(i);exact(label,"resource_id","text","content_desc","selected");
            for(String field:new String[]{"resource_id","text","content_desc"}) {
                String value=string(label,field);
                if(value.codePointCount(0,value.length())>128)throw new JSONException("page context text limit");
            }
            if(label.get("selected")!=JSONObject.NULL)bool(label,"selected");
        }
    }
    private static void member(JSONObject o) throws JSONException {
        JSONArray labels = o.getJSONArray("target_subtree_labels");
        if (labels.length() > 33) throw new JSONException("subtree label limit");
        for (int j = 0; j < labels.length(); j++) {
            JSONArray pair = labels.getJSONArray(j);
            if (pair.length() != 2 || !(pair.get(0) instanceof String) || !(pair.get(1) instanceof String)) throw new JSONException("invalid subtree label");
        }
        JSONArray roles = o.getJSONArray("role_signature");
        if (roles.length() < 1 || roles.length() > 8) throw new JSONException("role signature limit");
        for (int j = 0; j < roles.length(); j++) {
            JSONObject r = roles.getJSONObject(j);
            string(r,"class"); string(r,"resource_id"); string(r,"text"); string(r,"content_desc"); bool(r,"enabled");
            if(r.get("selected")!=JSONObject.NULL)bool(r,"selected");
            if(j==0) {
                exact(r,"class","resource_id","index","clickable","long_clickable","checkable","scrollable","enabled","checked","focusable","focused","bounds","text","content_desc","inputText","password","selected");
                countNonNegative(r,"index"); string(r,"inputText"); bool(r,"password");
                bool(r,"clickable"); bool(r,"long_clickable"); bool(r,"checkable"); bool(r,"scrollable");
                bool(r,"checked"); bool(r,"focusable"); bool(r,"focused");
                if(r.get("bounds")!=JSONObject.NULL) {
                    JSONArray b=r.getJSONArray("bounds"); if(b.length()!=4) throw new JSONException("invalid role bounds");
                    for(int k=0;k<4;k++) if(!(b.get(k) instanceof Number)) throw new JSONException("invalid role bounds");
                }
            } else {
                exact(r,"class","resource_id","text","content_desc","enabled","checked","selected");
                if(r.get("checked")!=JSONObject.NULL)bool(r,"checked");
                else if(!Boolean.TRUE.equals(r.get("selected")))throw new JSONException("descendant has no check or selection state");
            }
        }
        if(o.has("local_context") && !o.isNull("local_context")) {
            JSONObject local=o.getJSONObject("local_context");
            exact(local,"parent","labels","truncated"); bool(local,"truncated");
            Object parent=local.get("parent");
            if(parent!=JSONObject.NULL) { JSONObject p=(JSONObject)parent; exact(p,"class","resource_id"); string(p,"class"); string(p,"resource_id"); }
            JSONArray localLabels=local.getJSONArray("labels"); if(localLabels.length()>8)throw new JSONException("local context label limit");
            for(int i=0;i<localLabels.length();i++) {
                JSONObject label=localLabels.getJSONObject(i); exact(label,"path","text","content_desc");
                JSONArray path=label.getJSONArray("path"); if(path.length()>2)throw new JSONException("local label path limit");
                for(int j=0;j<path.length();j++)countNonNegativeValue(path.get(j));
                string(label,"text");string(label,"content_desc");
            }
        }
    }
    private static void validateOutcomes(JSONObject member, JSONObject contexts)throws JSONException {
        JSONArray outcomes=member.getJSONArray("outcomes");
        if(outcomes.length()>4)throw new JSONException("too many outcomes");
        for(int j=0;j<outcomes.length();j++) {
            JSONObject o=outcomes.getJSONObject(j);
            if(!o.has("source_snapshot")) throw new JSONException("EVIDENCE_SOURCE_SNAPSHOT_MISSING");
            exact(o,"target_activity","target_state","count","result_labels","self_loop","activity_unchanged","gui_unchanged","local_change","source_snapshot");
            string(o,"target_activity");string(o,"target_state");count(o,"count");
            bool(o,"self_loop");bool(o,"activity_unchanged");if(!o.isNull("gui_unchanged"))bool(o,"gui_unchanged");
            if(!o.isNull("local_change"))o.getJSONObject("local_change");
            if(o.has("source_snapshot")) {
                JSONObject source=o.getJSONObject("source_snapshot");
                exact(source,"source_context_id","target_subtree_labels","role_signature","local_context");
                String contextId=string(source,"source_context_id");
                if(!contexts.has(contextId))throw new JSONException("EVIDENCE_SOURCE_SNAPSHOT_MISSING");
                member(source);
            }
            JSONArray labels=o.getJSONArray("result_labels");
            for(int k=0;k<labels.length();k++)if(!(labels.get(k) instanceof String))throw new JSONException("invalid result label");
        }
    }
    private static void count(JSONObject o, String key) throws JSONException {
        Object n = o.get(key);
        if (!(n instanceof Number) || ((Number)n).longValue() <= 0 || ((Number)n).doubleValue() != ((Number)n).longValue()) throw new JSONException("invalid count");
    }
    private static void countNonNegative(JSONObject o,String key) throws JSONException {
        Object n=o.get(key); if(!(n instanceof Number) || ((Number)n).longValue()<0 || ((Number)n).doubleValue()!=((Number)n).longValue()) throw new JSONException("invalid non-negative count");
    }
    private static void countNonNegativeValue(Object n) throws JSONException {
        if(!(n instanceof Number) || ((Number)n).longValue()<0 || ((Number)n).doubleValue()!=((Number)n).longValue()) throw new JSONException("invalid non-negative count");
    }
    private static void bool(JSONObject o,String key) throws JSONException { if(!(o.get(key) instanceof Boolean)) throw new JSONException("expected boolean"); }
    private static String string(JSONObject o, String key) throws JSONException {
        Object value = o.get(key);
        if (!(value instanceof String)) throw new JSONException("expected string");
        return (String)value;
    }
    private static double probability(JSONObject o, String key) throws JSONException {
        Object value = o.get(key);
        if (!(value instanceof Number)) throw new JSONException("expected probability");
        double p = ((Number)value).doubleValue();
        if (Double.isNaN(p) || Double.isInfinite(p) || p < 0 || p > 1) throw new JSONException("probability out of range");
        return p;
    }
    private static void oneOf(String value, String a, String b) throws JSONException {
        if (!a.equals(value) && !b.equals(value)) throw new JSONException("unknown decision");
    }
    private static void exact(JSONObject object, String... fields) throws JSONException {
        Set<String> expected = new HashSet<String>(Arrays.asList(fields));
        if (object.length() != expected.size()) throw new JSONException("unexpected field set");
        for (Iterator<?> it = object.keys(); it.hasNext();) if (!expected.contains(it.next())) throw new JSONException("unknown field");
    }
    private static String digest(String value) {
        try {
            byte[] bytes = MessageDigest.getInstance("SHA-256").digest(value.getBytes(UTF8));
            StringBuilder hex = new StringBuilder();
            for (byte b : bytes) hex.append(String.format(java.util.Locale.ROOT, "%02x", b & 255));
            return hex.toString();
        } catch (java.security.NoSuchAlgorithmException e) { throw new AssertionError(e); }
    }
}
