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

import java.util.*;
import java.security.MessageDigest;
import org.json.*;

/** Content interning for the legacy evidence and synchronous no-result schemas. */
public final class JevSharedEvidence {
    private JevSharedEvidence() {}
    static final String GUIDE = "Resolve shared_evidence references before applying the rules: role_signature is an ID in role_signatures; each page_labels item is an ID in page_labels; each outcomes item is an ID in outcomes; local_context and source_snapshot use local_contexts and source_snapshots; result_labels is an ID in result_label_sets, whose arrays preserve label order and repetitions. Tables are shared across the whole request. Sharing identical content does not merge members, source pages, executions, counts or outcome occurrences; each reference belongs to its original member. In controls and page labels, omitted resource_id, text, content_desc and inputText mean empty strings. Boolean false and null are explicit and distinct. Internal target State IDs are kept only in local receipts; target_activity, result_labels and transition flags retain the observed outcome evidence.";
    static String fingerprint(JSONObject body)throws JSONException {
        JSONObject copy=new JSONObject(body.toString());stripIdentityFields(copy);
        try {
            byte[] bytes=MessageDigest.getInstance("SHA-256").digest(fingerprintCanonical(copy).getBytes("UTF-8"));
            StringBuilder out=new StringBuilder();for(byte b:bytes)out.append(String.format(java.util.Locale.ROOT,"%02x",b & 255));return out.toString();
        } catch(Exception error) {throw new IllegalStateException(error);}
    }
    private static void stripIdentityFields(Object value)throws JSONException {
        if(value instanceof JSONObject) {
            JSONObject object=(JSONObject)value;
            for(String key:new ArrayList<String>(keys(object))) {
                if("state_id".equals(key)||"model_action_id".equals(key)||"candidate_id".equals(key)||"input_signature".equals(key)||"request_id".equals(key)||"step".equals(key)||"modelVersion".equals(key)||"model_version".equals(key)||"source_context_id".equals(key)||"history_context_id".equals(key)||"member_id".equals(key)||"sibling_id".equals(key))object.remove(key);
                else stripIdentityFields(object.get(key));
            }
        } else if(value instanceof JSONArray) {
            JSONArray array=(JSONArray)value;for(int i=0;i<array.length();i++)stripIdentityFields(array.get(i));
        }
    }
    /** Canonical form used only for request de-duplication.  Logical question
     * IDs and prompt prose are normalized without changing provider input. */
    private static String fingerprintCanonical(Object value)throws JSONException {
        if(value instanceof JSONObject) {
            JSONObject object=(JSONObject)value;List<String> names=keys(object);Collections.sort(names);StringBuilder out=new StringBuilder("{");int logical=0;
            for(String key:names) {
                String normalized=key;
                if(key.matches("c[1-9][0-9]*"))normalized="candidate_"+(++logical);
                out.append(JSONObject.quote(normalized)).append(':');
                Object child=object.get(key);
                if("representative_model_action_id".equals(key) && child instanceof JSONArray) {
                    JSONArray ids=(JSONArray)child;StringBuilder array=new StringBuilder("[");
                    for(int i=0;i<ids.length();i++)array.append(JSONObject.quote("model_action_"+i)).append(',');
                    out.append(array.append(']'));
                } else out.append(fingerprintCanonical(child));
                out.append(',');
            }
            return out.append('}').toString();
        }
        if(value instanceof JSONArray) {
            JSONArray array=(JSONArray)value;StringBuilder out=new StringBuilder("[");for(int i=0;i<array.length();i++)out.append(fingerprintCanonical(array.get(i))).append(',');return out.append(']').toString();
        }
        if(value instanceof String) {
            String text=((String)value).replaceAll("candidate(?:_id)?[ ]*[`']?c[1-9][0-9]*[`']?","candidate");
            return JSONObject.quote(text);
        }
        return String.valueOf(value);
    }
    static final class Packed {
        final JSONObject state,localTargets;
        Packed(JSONObject state,JSONObject targets){this.state=state;localTargets=targets;}
    }
    private static final class Table {
        final JSONObject values=new JSONObject();
        final Map<String,String> ids=new LinkedHashMap<String,String>();
        final String prefix;
        Table(String prefix){this.prefix=prefix;}
        String intern(Object value)throws JSONException {
            String key=canonical(value),id=ids.get(key);
            if(id==null){id=prefix+(ids.size()+1);ids.put(key,id);values.put(id,value);}
            return id;
        }
    }
    static Packed pack(JSONObject original)throws JSONException {
        JSONObject state=new JSONObject(original.toString()),targets=new JSONObject();
        Table roles=new Table("r"),pages=new Table("p"),outcomes=new Table("o"),locals=new Table("lc"),snapshots=new Table("ss"),labels=new Table("rl");
        packObject(state,"",roles,pages,outcomes,locals,snapshots,labels,targets);
        // Android JSONObject retains insertion order. Define references before their uses.
        JSONObject ordered=new JSONObject().put("candidate_review_rules",GUIDE+"\n\n"+JevProtocol.CANDIDATE_RULES)
            .put("evidence_schema","ape_probe_v3")
            .put("shared_evidence",new JSONObject().put("role_signatures",roles.values).put("page_labels",pages.values)
                .put("outcomes",outcomes.values).put("local_contexts",locals.values).put("source_snapshots",snapshots.values).put("result_label_sets",labels.values));
        for(String key:keys(state))if(!"candidate_review_rules".equals(key))ordered.put(key,state.get(key));
        return new Packed(ordered,targets);
    }
    /** Compact packer for the synchronous v4 review contract. */
    static Packed packNoOutcome(JSONObject original)throws JSONException {
        JSONObject state=new JSONObject(original.toString()),targets=new JSONObject();
        Table roles=new Table("r"),pages=new Table("p"),locals=new Table("lc");
        packNoOutcomeObject(state,roles,pages,locals);
        JSONObject ordered=new JSONObject().put("candidate_review_rules",JevProtocol.NO_OUTCOME_RULES)
            .put("evidence_schema","ape_probe_v4")
            .put("shared_evidence",new JSONObject().put("role_signatures",roles.values)
                .put("page_labels",pages.values).put("local_contexts",locals.values));
        for(String key:keys(state))if(!"candidate_review_rules".equals(key))ordered.put(key,state.get(key));
        return new Packed(ordered,targets);
    }
    private static void packNoOutcomeObject(JSONObject object,Table roles,Table pages,Table locals)throws JSONException {
        for(String key:keys(object)) {
            Object value=object.get(key);
            if("local_context".equals(key) && value instanceof JSONObject)object.put(key,locals.intern(value));
            else if("role_signature".equals(key)) {
                JSONArray array=(JSONArray)value;for(int i=0;i<array.length();i++)omit(array.getJSONObject(i),"resource_id","text","content_desc","inputText");
                object.put(key,roles.intern(array));
            } else if("page_labels".equals(key)) {
                JSONArray array=(JSONArray)value,refs=new JSONArray();
                for(int i=0;i<array.length();i++) {JSONObject label=array.getJSONObject(i);omit(label,"resource_id","text","content_desc");refs.put(pages.intern(label));}
                object.put(key,refs);
            } else if(value instanceof JSONObject)packNoOutcomeObject((JSONObject)value,roles,pages,locals);
            else if(value instanceof JSONArray) {
                JSONArray array=(JSONArray)value;for(int i=0;i<array.length();i++)if(array.get(i) instanceof JSONObject)packNoOutcomeObject(array.getJSONObject(i),roles,pages,locals);
            }
        }
    }
    private static void omit(JSONObject value,String... keys)throws JSONException {
        for(String key:keys)if(value.has(key) && "".equals(value.get(key)))value.remove(key);
    }
    private static void packObject(JSONObject object,String path,Table roles,Table pages,Table outcomes,Table locals,Table snapshots,Table labels,JSONObject targets)throws JSONException {
        List<String> keys=keys(object);
        for(String key:keys) {
            Object value=object.get(key);String at=path+"/"+key;
            if("local_context".equals(key) && value instanceof JSONObject) {
                object.put(key,locals.intern(value));
            } else if("source_snapshot".equals(key) && value instanceof JSONObject) {
                packObject((JSONObject)value,at,roles,pages,outcomes,locals,snapshots,labels,targets);
                object.put(key,snapshots.intern(value));
            } else if("result_labels".equals(key)) {
                object.put(key,labels.intern((JSONArray)value));
            } else if("role_signature".equals(key)) {
                JSONArray array=(JSONArray)value;
                for(int i=0;i<array.length();i++)omit(array.getJSONObject(i),"resource_id","text","content_desc","inputText");
                object.put(key,roles.intern(array));
            } else if("page_labels".equals(key)) {
                JSONArray array=(JSONArray)value,refs=new JSONArray();
                for(int i=0;i<array.length();i++) {
                    JSONObject label=array.getJSONObject(i);omit(label,"resource_id","text","content_desc");
                    refs.put(pages.intern(label));
                }
                object.put(key,refs);
            } else if("outcomes".equals(key)) {
                JSONArray array=(JSONArray)value,refs=new JSONArray();
                for(int i=0;i<array.length();i++) {
                    JSONObject outcome=array.getJSONObject(i);
                    targets.put(at+"/"+i,outcome.getString("target_state"));outcome.remove("target_state");
                    packObject(outcome,at+"/"+i,roles,pages,outcomes,locals,snapshots,labels,targets);
                    refs.put(outcomes.intern(outcome));
                }
                object.put(key,refs);
            } else if(value instanceof JSONObject)packObject((JSONObject)value,at,roles,pages,outcomes,locals,snapshots,labels,targets);
            else if(value instanceof JSONArray) {
                JSONArray array=(JSONArray)value;
                for(int i=0;i<array.length();i++)if(array.get(i) instanceof JSONObject)
                    packObject(array.getJSONObject(i),at+"/"+i,roles,pages,outcomes,locals,snapshots,labels,targets);
            }
        }
    }
    /** Expand the current wire schema. Internal IDs are deliberately not reconstructed. */
    public static JSONObject expand(JSONObject wire)throws JSONException {
        if("ape_probe_v4".equals(wire.optString("evidence_schema")))return expandNoOutcome(wire);
        JSONObject state=new JSONObject(wire.toString());
        if(!(GUIDE+"\n\n"+JevProtocol.CANDIDATE_RULES).equals(state.getString("candidate_review_rules")))throw new JSONException("invalid shared evidence instructions");
        JSONObject tables=state.getJSONObject("shared_evidence");
        if(!new HashSet<String>(keys(tables)).equals(new HashSet<String>(Arrays.asList("role_signatures","page_labels","outcomes","local_contexts","source_snapshots","result_label_sets"))))throw new JSONException("invalid shared tables");
        Map<String,Set<String>> used=new HashMap<String,Set<String>>();
        String[] names={"role_signatures","page_labels","outcomes","local_contexts","source_snapshots","result_label_sets"},prefixes={"r","p","o","lc","ss","rl"};
        for(int i=0;i<names.length;i++) {
            JSONObject table=tables.getJSONObject(names[i]);Set<String> contents=new HashSet<String>();
            for(String id:keys(table)) {
                if(!id.matches(prefixes[i]+"[1-9][0-9]*"))throw new JSONException("invalid shared ID");
                if(!contents.add(canonical(table.get(id))))throw new JSONException("duplicate shared content");
            }
            used.put(names[i],new HashSet<String>());
        }
        state.remove("shared_evidence");
        expandObject(state,tables,used);
        for(String name:names)if(!used.get(name).equals(new HashSet<String>(keys(tables.getJSONObject(name)))))throw new JSONException("unreferenced shared evidence");
        state.put("candidate_review_rules",JevProtocol.CANDIDATE_RULES);
        return state;
    }
    /** Expand and validate the compact no-result synchronous schema. */
    public static JSONObject expandNoOutcome(JSONObject wire)throws JSONException {
        JSONObject state=new JSONObject(wire.toString());
        if(!"ape_probe_v4".equals(state.optString("evidence_schema")))throw new JSONException("invalid no-result evidence schema");
        if(!JevProtocol.NO_OUTCOME_RULES.equals(state.getString("candidate_review_rules")))throw new JSONException("invalid no-result instructions");
        JSONObject tables=state.getJSONObject("shared_evidence");
        if(!new HashSet<String>(keys(tables)).equals(new HashSet<String>(Arrays.asList("role_signatures","page_labels","local_contexts"))))throw new JSONException("invalid no-result shared tables");
        Map<String,Set<String>> used=new HashMap<String,Set<String>>();
        String[] names={"role_signatures","page_labels","local_contexts"},prefixes={"r","p","lc"};
        for(int i=0;i<names.length;i++) {
            JSONObject table=tables.getJSONObject(names[i]);Set<String> contents=new HashSet<String>();
            for(String id:keys(table)) {if(!id.matches(prefixes[i]+"[1-9][0-9]*"))throw new JSONException("invalid no-result shared ID");if(!contents.add(canonical(table.get(id))))throw new JSONException("duplicate no-result shared content");}
            used.put(names[i],new HashSet<String>());
        }
        state.remove("shared_evidence");expandNoOutcomeObject(state,tables,used);
        for(String name:names)if(!used.get(name).equals(new HashSet<String>(keys(tables.getJSONObject(name)))))throw new JSONException("unreferenced no-result evidence");
        state.put("candidate_review_rules",JevProtocol.NO_OUTCOME_RULES);return state;
    }
    private static Object noOutcomeLookup(JSONObject tables,Map<String,Set<String>> used,String table,Object ref)throws JSONException {
        if(!(ref instanceof String))throw new JSONException("no-result shared reference must be a string");
        if(!tables.getJSONObject(table).has((String)ref))throw new JSONException("missing no-result shared reference");
        used.get(table).add((String)ref);Object value=tables.getJSONObject(table).get((String)ref);
        if(value instanceof JSONObject)return new JSONObject(value.toString());if(value instanceof JSONArray)return new JSONArray(value.toString());
        throw new JSONException("invalid no-result shared value");
    }
    private static void expandNoOutcomeObject(JSONObject object,JSONObject tables,Map<String,Set<String>> used)throws JSONException {
        for(String key:keys(object)) {
            Object value=object.get(key);
            if("outcomes".equals(key)||"source_snapshot".equals(key)||"target_state".equals(key)||"target_activity".equals(key)||"result_labels".equals(key))throw new JSONException("execution result field on no-result wire");
            if("local_context".equals(key) && value instanceof String)object.put(key,noOutcomeLookup(tables,used,"local_contexts",value));
            else if("role_signature".equals(key)) {
                Object expanded=noOutcomeLookup(tables,used,"role_signatures",value);if(!(expanded instanceof JSONArray))throw new JSONException("no-result role table requires array");
                JSONArray array=(JSONArray)expanded;for(int i=0;i<array.length();i++){JSONObject role=array.getJSONObject(i);defaults(role,"resource_id","text","content_desc");if(i==0)defaults(role,"inputText");}object.put(key,array);
            } else if("page_labels".equals(key)) {
                JSONArray refs=(JSONArray)value,array=new JSONArray();for(int i=0;i<refs.length();i++){Object label=noOutcomeLookup(tables,used,"page_labels",refs.get(i));if(!(label instanceof JSONObject))throw new JSONException("no-result page table requires object");defaults((JSONObject)label,"resource_id","text","content_desc");array.put(label);}object.put(key,array);
            } else if(value instanceof JSONObject)expandNoOutcomeObject((JSONObject)value,tables,used);
            else if(value instanceof JSONArray){JSONArray array=(JSONArray)value;for(int i=0;i<array.length();i++)if(array.get(i) instanceof JSONObject)expandNoOutcomeObject(array.getJSONObject(i),tables,used);}
        }
    }
    private static Object lookup(JSONObject tables,Map<String,Set<String>> used,String table,Object ref)throws JSONException {
        if(!(ref instanceof String))throw new JSONException("shared reference must be a string");
        Object value=tables.getJSONObject(table).get((String)ref);used.get(table).add((String)ref);
        if(value instanceof JSONObject)return new JSONObject(value.toString());
        if(value instanceof JSONArray)return new JSONArray(value.toString());
        throw new JSONException("invalid shared value type");
    }
    private static void defaults(JSONObject object,String... fields)throws JSONException {
        for(String field:fields)if(!object.has(field))object.put(field,"");
    }
    private static void expandObject(JSONObject object,JSONObject tables,Map<String,Set<String>> used)throws JSONException {
        for(String key:keys(object)) {
            Object value=object.get(key);
            if("target_state".equals(key))throw new JSONException("internal target state on wire");
            if("local_context".equals(key) && value instanceof String) {
                object.put(key,lookup(tables,used,"local_contexts",value));
            } else if("source_snapshot".equals(key) && value instanceof String) {
                Object expanded=lookup(tables,used,"source_snapshots",value);
                if(!(expanded instanceof JSONObject))throw new JSONException("source snapshot table requires object");
                expandObject((JSONObject)expanded,tables,used);object.put(key,expanded);
            } else if("result_labels".equals(key)) {
                Object expanded=lookup(tables,used,"result_label_sets",value);
                if(!(expanded instanceof JSONArray))throw new JSONException("result labels table requires array");
                object.put(key,expanded);
            } else if("role_signature".equals(key)) {
                Object expanded=lookup(tables,used,"role_signatures",value);
                if(!(expanded instanceof JSONArray))throw new JSONException("role table requires array");
                JSONArray array=(JSONArray)expanded;
                for(int i=0;i<array.length();i++) {
                    JSONObject role=array.getJSONObject(i);defaults(role,"resource_id","text","content_desc");
                    if(i==0)defaults(role,"inputText");
                }
                object.put(key,array);
            }else if("page_labels".equals(key)) {
                if(!(value instanceof JSONArray))throw new JSONException("page labels requires array");
                JSONArray refs=(JSONArray)value,array=new JSONArray();
                for(int i=0;i<refs.length();i++) {
                    Object label=lookup(tables,used,"page_labels",refs.get(i));
                    if(!(label instanceof JSONObject))throw new JSONException("page table requires object");
                    defaults((JSONObject)label,"resource_id","text","content_desc");array.put(label);
                }
                object.put(key,array);
            }else if("outcomes".equals(key)) {
                if(!(value instanceof JSONArray))throw new JSONException("outcomes requires array");
                JSONArray array=(JSONArray)value,expanded=new JSONArray();
                for(int i=0;i<array.length();i++) {
                    Object item=lookup(tables,used,"outcomes",array.get(i));
                    if(!(item instanceof JSONObject) || ((JSONObject)item).has("target_state"))throw new JSONException("invalid shared outcome");
                    expandObject((JSONObject)item,tables,used);
                    expanded.put(item);
                }
                object.put(key,expanded);
            }else if(value instanceof JSONObject)expandObject((JSONObject)value,tables,used);
            else if(value instanceof JSONArray) {
                JSONArray array=(JSONArray)value;
                for(int i=0;i<array.length();i++)if(array.get(i) instanceof JSONObject)expandObject(array.getJSONObject(i),tables,used);
            }
        }
    }
    static List<String> keys(JSONObject object) {
        List<String> keys=new ArrayList<String>();for(Iterator<?> it=object.keys();it.hasNext();)keys.add((String)it.next());return keys;
    }
    /** JSONObject field order is not evidence; arrays, types, false and null are. */
    public static String canonical(Object value)throws JSONException {
        if(value instanceof JSONObject) {
            JSONObject object=(JSONObject)value;List<String> keys=keys(object);Collections.sort(keys);StringBuilder result=new StringBuilder("{");
            for(String key:keys)result.append(JSONObject.quote(key)).append(':').append(canonical(object.get(key))).append(',');
            return result.append('}').toString();
        }
        if(value instanceof JSONArray) {
            JSONArray array=(JSONArray)value;StringBuilder result=new StringBuilder("[");
            for(int i=0;i<array.length();i++)result.append(canonical(array.get(i))).append(',');return result.append(']').toString();
        }
        return value instanceof String?JSONObject.quote((String)value):String.valueOf(value);
    }
}
