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

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

/**
 * Frozen structural nominations, shared by Shadow observation and Region decision review. R(d) is the full non-entry dominator subtree.
 * AvgVisits uses actual captures/confirmed arrivals reprojected through native GUI lineage.
 * Nomination requires AvgVisits > 2, >= 3 confirmed arrivals in the last 120s, and
 * a full quiet window since a confirmed action inside the Region discovered a previously unseen UI State.
 * No saturation or reachability guard filters new nominations. Identity-valid mixed
 * outcomes remain reviewable. The locator never binds or samples; accepted evidence is handed to the existing reviewer when enabled.
 */
final class JevRegionLocator {
    static final class Region {
        final int header;final Set<Integer> members;
        Region(int header,Set<Integer> members){this.header=header;this.members=Collections.unmodifiableSet(new TreeSet<Integer>(members));}
    }
    static long minutes(){return bounded("ape.jev.region_warmup_minutes",10,0,Long.MAX_VALUE/TimeUnit.MINUTES.toNanos(1));}
    static long windowSeconds(){return bounded("ape.jev.region_recent_window_seconds",120,1,Long.MAX_VALUE/TimeUnit.SECONDS.toNanos(1));}
    static int minimumRecent(){return (int)bounded("ape.jev.region_min_recent_visits",3,1,Integer.MAX_VALUE);}
    static long bounded(String key,long fallback,long low,long high){
        long value=Long.parseLong(Config.get(key,Long.toString(fallback)));
        if(value<low || value>high)throw new IllegalArgumentException("Invalid "+key);return value;
    }
    static void validateConfiguration(){minutes();windowSeconds();minimumRecent();
        if(Config.getLong("ape.jev.regionBlocking.updateIntervalMs",30000)<=0)throw new IllegalArgumentException("Invalid Region interval");}

    static final class Visit {
        final GUITree tree;final long time;final boolean confirmed;
        Visit(GUITree tree,long time,boolean confirmed){this.tree=tree;this.time=time;this.confirmed=confirmed;}
    }
    static final class Observation {
        final GUITreeTransition transition;final long time,version;final String state,action,targetState;
        boolean confirmed,newState,discoveryKnown;long confirmedTime,confirmedVersion;
        Integer concrete;int source,target;String transitionId,outcomePattern;
        Observation(GUITreeTransition t,long time,long version){transition=t;this.time=time;this.version=version;
            state=t.getSource().getCurrentState().getGraphId();action=t.getAction().getModelAction().getGraphId();targetState=t.getTarget().getCurrentState().getGraphId();}
    }
    static String fullName(final GUITreeNode node){return node==null?"<lifecycle>":com.android.commands.monkey.ape.naming.NameManager.withoutInterning(new java.util.function.Supplier<String>(){
        public String get(){return JevIdentity.fullName(node).toString();}});}
    /** Worker value. A discovery belongs to its actual source, irrespective of target membership. */
    static final class Discovery {
        final long time,version;final int source,target,sourceGui,targetGui;final boolean known;final String action;
        Discovery(long time,long version,int source,int target,int sourceGui,int targetGui,boolean known,String action){
            this.time=time;this.version=version;this.source=source;this.target=target;this.sourceGui=sourceGui;this.targetGui=targetGui;this.known=known;this.action=action;}
        JSONObject json(){return JevRegistry.fields("time_ns",time,"observed_version",version,"source",source,"target",target,
            "source_gui_id",sourceGui,"target_gui_id",targetGui,"discovery_known",known,"observed_model_action_id",action);}
    }
    /** GUI objects are native lineage anchors. No state/action ID string establishes identity. */
    static final class History {
        final IdentityHashMap<GUITree,Long> captures=new IdentityHashMap<GUITree,Long>();
        final IdentityHashMap<GUITreeTransition,Observation> observations=new IdentityHashMap<GUITreeTransition,Observation>();
        final Set<State> seenStates=Collections.newSetFromMap(new IdentityHashMap<State,Boolean>());
        final List<Visit> visits=new ArrayList<Visit>();final List<long[]> refinements=new ArrayList<long[]>();
        long version=Long.MIN_VALUE,refinement=Long.MIN_VALUE;boolean lineageKnown=true;
        void refined(long version,long now){
            if(this.version!=Long.MIN_VALUE && version!=this.version){refinement=now;refinements.add(new long[]{version,now});
                seenStates.clear();lineageKnown=true;for(GUITree tree:captures.keySet()){
                    State state=tree.getCurrentState();if(state==null)lineageKnown=false;else seenStates.add(state);}}
            this.version=version;
        }
        void capture(GUITree tree,long now){if(!captures.containsKey(tree)){captures.put(tree,now);seenStates.add(tree.getCurrentState());}}
        void observed(GUITree tree,long now){if(!captures.containsKey(tree)){capture(tree,now);visits.add(new Visit(tree,now,false));}}
        boolean transition(GUITreeTransition t,boolean confirmed,Integer concrete,int source,int target,long now){
            Observation o=observations.get(t);if(o==null){o=new Observation(t,now,version);observations.put(t,o);}
            o.concrete=concrete;o.source=source;o.target=target;
            if(confirmed && !o.confirmed){
                observed(t.getSource(),now);
                o.discoveryKnown=lineageKnown && t.getTarget().getCurrentState()!=null;
                o.newState=o.discoveryKnown && !seenStates.contains(t.getTarget().getCurrentState());
                capture(t.getTarget(),now);visits.add(new Visit(t.getTarget(),now,true));
                o.confirmedTime=now;o.confirmedVersion=version;o.confirmed=true;return true;
            }
            return false;
        }
        Stats snapshot(List<State> states,long now,long window){
            Stats stats=new Stats(states.size(),now,window,refinement);
            IdentityHashMap<State,Integer> ids=new IdentityHashMap<State,Integer>();for(int i=0;i<states.size();i++)ids.put(states.get(i),i);
            boolean missingLineage=false;
            for(Map.Entry<GUITree,Long> e:captures.entrySet()){
                Integer id=ids.get(e.getKey().getCurrentState());if(id==null){missingLineage=true;continue;}
                stats.first[id]=Math.min(stats.first[id],e.getValue());
            }
            if(missingLineage)for(int i=0;i<states.size();i++)stats.unknown.put(i,"capture_lineage_outside_snapshot");
            for(Visit v:visits){Integer id=ids.get(v.tree.getCurrentState());if(id!=null){stats.visits[id]++;if(v.confirmed && v.time>now-window && v.time<=now)stats.recent[id]++;}}
            for(Observation o:observations.values()){
                Integer source=ids.get(o.transition.getSource().getCurrentState()),target=ids.get(o.transition.getTarget().getCurrentState());
                if(source==null){if(o.confirmed && o.confirmedTime>now-window)for(int i=0;i<states.size();i++)stats.unknown.put(i,"discovery_source_unresolved");continue;}
                if(target==null || source!=o.source || target!=o.target)stats.unknown.put(source,"transition_lineage_snapshot_mismatch");
                long time=o.confirmed?o.confirmedTime:o.time;
                JSONObject transitionRow=JevRegistry.fields("source",source,"target",target==null?JSONObject.NULL:target,"source_gui_id",o.transition.getSource().getId(),"target_gui_id",o.transition.getTarget().getId(),
                    "first_observed_ns",o.time,"confirmed_time_ns",o.confirmed?o.confirmedTime:JSONObject.NULL,"observed_version",o.version,"confirmed_version",o.confirmed?o.confirmedVersion:JSONObject.NULL,"confirmed",o.confirmed,
                    "observed_state_id",o.state,"observed_target_state_id",o.targetState,"observed_model_action_id",o.action,"concrete_id",o.concrete==null?JSONObject.NULL:o.concrete,
                    "action_type",o.transition.getAction().getActionType().name(),"full_name",fullName(o.transition.getAction().getGUITreeNode()),"discovery_known",o.discoveryKnown,"new_ui_state",o.newState,"transition_id",o.transitionId,
                    "result_labels",JSONObject.NULL,"target_activity",JSONObject.NULL,"self_loop",JSONObject.NULL,"gui_unchanged",JSONObject.NULL);
                if(o.outcomePattern!=null)try {
                    JSONArray pattern=new JSONArray(o.outcomePattern);transitionRow.put("target_activity",pattern.getString(0)).put("result_labels",pattern.getJSONArray(1))
                        .put("self_loop",pattern.getBoolean(2)).put("gui_unchanged",pattern.getBoolean(3));
                }catch(JSONException impossible){throw new IllegalStateException(impossible);}
                stats.transitionRows.put(transitionRow);
                if(o.confirmed){
                    if(o.newState || !o.discoveryKnown)stats.discoveries.add(new Discovery(time,o.confirmedVersion,source,target==null?-1:target,o.transition.getSource().getId(),o.transition.getTarget().getId(),o.discoveryKnown,o.action));
                    if(!o.discoveryKnown && time>now-window)stats.unknown.put(source,"state_discovery_unknown");
                    if(time>now-window && time<=now && target!=null)stats.recentTransitions.add(new int[]{source,target});
                }
                if(o.concrete==null){stats.unknown.put(source,"transition_projection_unresolved");continue;}
                String key=o.source+":"+o.concrete+":"+o.target;Long old=stats.links.get(key);if(old==null || o.time<old)stats.links.put(key,o.time);
                Map<String,Integer> counts=o.confirmed?stats.confirmedLinks:stats.weakLinks;Integer count=counts.get(key);counts.put(key,count==null?1:count+1);
            }
            for(int i=0;i<states.size();i++){
                if(stats.first[i]==Long.MAX_VALUE)stats.unknown.put(i,"state_without_observation_lineage");
                for(ModelAction action:states.get(i).getActions())if(ActionFilter.ENABLED_VALID.include(action)){stats.actionCount[i]++;if(action.isUnvisited())stats.unvisited[i]++;}}
            for(long[] ref:refinements)stats.refinementRows.put(JevRegistry.fields("version",ref[0],"time_ns",ref[1]));
            return stats;
        }
    }
    static final class Stats {
        final long now,window,refinement;final long[] visits,first;final int[] recent,actionCount,unvisited;
        final Map<String,Long> links=new HashMap<String,Long>();
        final Map<String,Integer> confirmedLinks=new HashMap<String,Integer>(),weakLinks=new HashMap<String,Integer>();
        final List<int[]> recentTransitions=new ArrayList<int[]>();final List<Discovery> discoveries=new ArrayList<Discovery>();
        final Map<Integer,String> unknown=new TreeMap<Integer,String>();
        final JSONArray refinementRows=new JSONArray(),transitionRows=new JSONArray();
        Stats(int n,long now,long window,long refinement){this.now=now;this.window=window;this.refinement=refinement;
            visits=new long[n];first=new long[n];Arrays.fill(first,Long.MAX_VALUE);recent=new int[n];actionCount=new int[n];unvisited=new int[n];}
    }
    /** Kept on the main thread; no native objects are sent to the analysis worker. */
    static final class Evidence {
        final Map<Integer,JevRegion.Bucket> buckets=new HashMap<Integer,JevRegion.Bucket>();
        final Map<Integer,JSONObject> candidates=new HashMap<Integer,JSONObject>();
        final Map<Integer,GUITreeAction> anchors=new HashMap<Integer,GUITreeAction>();
        final Map<Integer,JSONObject> evidence=new HashMap<Integer,JSONObject>();
        final JSONArray stateRows=new JSONArray();
        final Map<Integer,String> watchIds=new HashMap<Integer,String>();
    }
    static final class Entry {
        final JevRegion.Concrete concrete;final Set<Integer> inside,outside;
        String watchId;boolean firstNomination;
        String status="not_nominated";
        Entry(JevRegion.Concrete c,Region r){concrete=c;inside=new TreeSet<Integer>(c.targets);inside.retainAll(r.members);outside=new TreeSet<Integer>(c.targets);outside.removeAll(r.members);}
    }
    static final class Candidate {
        final Region region;final List<Entry> entries=new ArrayList<Entry>();
        long visits,lastProgress;int recent,reentries,newStates,actions,unvisited;double average;

        boolean membershipChanged;String noveltyStatus="KNOWN";
        boolean mature,selected;String reason;
        Candidate(Region region){this.region=region;}
    }
    static final class Stamp {
        final Set<Integer> members;
        Stamp(Set<Integer> members){this.members=members;}
    }
    static final class Analysis {
        final JevRegion.Snapshot graph;final Stats stats;final List<Candidate> candidates;final JevRegion.Result legacy;final Set<Integer> reviewable;
        Analysis(JevRegion.Snapshot s,Stats stats,List<Candidate> c,JevRegion.Result legacy,Set<Integer> reviewable){graph=s;this.stats=stats;candidates=c;this.legacy=legacy;this.reviewable=reviewable;}
    }
    /** Worker owns stamps. Every input consists solely of immutable/copy-only values. */
    static final class Analyzer {
        final JevRegion.Calculator calculator;final int minRecent;boolean compareLegacy=true;
        long version=Long.MIN_VALUE;final Map<Integer,Stamp> stamps=new HashMap<Integer,Stamp>();
        Analyzer(JevRegion.Calculator calculator,int minRecent){this.calculator=calculator;this.minRecent=minRecent;}
        Analysis compute(JevRegion.Snapshot s,Stats stats){
            if(version!=s.abstraction){stamps.clear();version=s.abstraction;}
            List<Candidate> candidates=new ArrayList<Candidate>();
            List<Region> regions=calculator.structure(s);
            for(Region region:regions){
                if(Thread.currentThread().isInterrupted())throw new CancellationException();
                Candidate c=new Candidate(region);c.lastProgress=Long.MIN_VALUE;
                for(int v:region.members){c.visits+=stats.visits[v];c.recent+=stats.recent[v];c.actions+=stats.actionCount[v];c.unvisited+=stats.unvisited[v];
                }
                for(int v:region.members)if(stats.unknown.containsKey(v))c.noveltyStatus="UNKNOWN";
                for(int[] edge:stats.recentTransitions)if(!region.members.contains(edge[0]) && region.members.contains(edge[1]))c.reentries++;
                c.average=(double)c.visits/region.members.size();
                for(Discovery event:stats.discoveries)if(region.members.contains(event.source)){
                    if(event.known){c.lastProgress=Math.max(c.lastProgress,event.time);if(recent(event.time,stats))c.newStates++;}
                    else if(recent(event.time,stats))c.noveltyStatus="UNKNOWN";
                }
                Stamp old=stamps.get(region.header);c.membershipChanged=old!=null && !old.members.equals(region.members);
                stamps.put(region.header,new Stamp(region.members));
                for(JevRegion.Concrete concrete:s.entries)if(!region.members.contains(concrete.source)){
                    Entry entry=new Entry(concrete,region);if(entry.inside.contains(region.header))c.entries.add(entry);
                }
                Collections.sort(c.entries,new Comparator<Entry>(){public int compare(Entry a,Entry b){return Integer.compare(a.concrete.id,b.concrete.id);}});
                boolean reviewable=false;for(Entry e:c.entries)reviewable |= e.concrete.eligible;
                c.reason=c.entries.isEmpty()?"no_entry":!reviewable?"no_reviewable_entry":c.average<=2?"avg_visits_not_above_2":c.recent<minRecent?"insufficient_recent_visits":
                    !c.noveltyStatus.equals("KNOWN")?"novelty_unknown":c.lastProgress>stats.now-stats.window?"recent_region_progress":"mature";
                c.mature=c.reason.equals("mature");candidates.add(c);
            }
            List<Candidate> ranked=new ArrayList<Candidate>(candidates);
            Collections.sort(ranked,new Comparator<Candidate>(){public int compare(Candidate a,Candidate b){int size=Integer.compare(a.region.members.size(),b.region.members.size());return size!=0?size:Integer.compare(a.region.header,b.region.header);}});
            Set<Integer> selectedMembers=new HashSet<Integer>(),requested=new HashSet<Integer>();
            for(Candidate c:ranked)if(c.mature){
                if(!Collections.disjoint(selectedMembers,c.region.members)){c.reason="nested_deferred";continue;}
                c.selected=true;selectedMembers.addAll(c.region.members);
                for(Entry e:c.entries){
                    e.status=e.concrete.eligible?"ready_for_review":"unconfirmed_or_unmatchable";
                    if(e.concrete.eligible)requested.add(e.concrete.id);
                }
            }
            // Legacy saturation/guard computation is comparison telemetry only. It
            // cannot filter new nominations, including mixed-target concretes.
            return new Analysis(s,stats,candidates,compareLegacy?calculator.compute(s):null,Collections.unmodifiableSet(new TreeSet<Integer>(requested)));
        }
        static boolean recent(long time,Stats stats){return time>stats.now-stats.window && time<=stats.now;}
    }

    /** Monotonic elapsed-time schedule. A slow worker skips slots; it never creates debt. */
    static final class Schedule {
        final long interval;long next;
        Schedule(long warmup,long interval){this.next=warmup;this.interval=interval;}
        boolean due(long elapsed){return elapsed>=next;}
        long scheduled(long elapsed){return next+((elapsed-next)/interval)*interval;}
        long submitted(long elapsed){long skipped=(elapsed-next)/interval;next=elapsed+interval;return skipped;}
        long completed(long elapsed){
            if(elapsed<next)return 0;
            long skipped=(elapsed-next)/interval+1;next+=skipped*interval;return skipped;
        }
    }
    static final class Request {
        final long id,runStart,scheduled,submitted,skipped;final JevRegion.Snapshot graph;
        volatile long completed=Long.MIN_VALUE;
        Request(long id,long runStart,long scheduled,long submitted,long skipped,JevRegion.Snapshot graph){
            this.id=id;this.runStart=runStart;this.scheduled=scheduled;this.submitted=submitted;this.skipped=skipped;this.graph=graph;}
        String receipt(){return "region-locator-"+id;}
    }
    private final JevRuntime runtime;private final JevTiming timing;private final JevEvents events;private final ExecutorService worker;
    final History history=new History();final Analyzer analyzer;
    final long warmup=TimeUnit.MINUTES.toNanos(minutes()),window=TimeUnit.SECONDS.toNanos(windowSeconds());
    private final long interval=TimeUnit.MILLISECONDS.toNanos(Config.getLong("ape.jev.regionBlocking.updateIntervalMs",30000));
    final Schedule schedule=new Schedule(warmup,interval);
    private long sequence,analyses;private Future<Analysis> pending;private Evidence pendingEvidence;private Request pendingRequest;
    private boolean warmupRecorded;
    private long structures,mature,nominations,reviewableEntries,identityRejected,mixedEntries;
    private final Map<Integer,Long> legacyFirst=new HashMap<Integer,Long>();
    private final List<Watch> watches=new ArrayList<Watch>();
    static final class Watch {
        final GUITreeAction anchor;final String id,receipt;final int concrete;final long trackedSince,version;Long first,legacy;
        long available,selected,executed,nominations,lastAvailableStep=Long.MIN_VALUE,lastSelectedStep=Long.MIN_VALUE;
        Watch(GUITreeAction anchor,String id,long trackedSince,String receipt,int concrete,long version){this.anchor=anchor;this.id=id;this.trackedSince=trackedSince;this.receipt=receipt;this.concrete=concrete;this.version=version;}
    }
    JevRegionLocator(JevRuntime runtime,JevTiming timing,JevEvents events,ExecutorService worker,JevRegion.Calculator calculator){
        this.runtime=runtime;this.timing=timing;this.events=events;this.worker=worker;analyzer=new Analyzer(calculator,minimumRecent());analyzer.compareLegacy=runtime.regionShadowAudit();
        emit("REGION_LOCATOR_CONFIG",0,0,JevRegistry.fields("locator_schema",7,"region_warmup_minutes",minutes(),"recent_window_seconds",windowSeconds(),
            "min_recent_visits",minimumRecent(),"avg_visits_threshold",2,"shadow_only",runtime.regionShadowAudit(),
            "locator_period_ms",TimeUnit.NANOSECONDS.toMillis(interval),"schedule_schema",1,"novelty_definition","confirmed_new_ui_state_v1"));
    }
    void refined(long version){history.refined(version,runtime.regionNowNanos());legacyFirst.clear();
        for(Watch watch:watches){ModelAction action=JevRegion.shadowAction(watch.anchor.getGUITree(),watch.anchor.getGUITreeNode(),watch.anchor.getActionType());
            if(action!=null)watch.anchor.rebuild(action);}
    }
    void transitionUnmapped(GUITreeTransition transition,boolean confirmed,int source,int target){observeTransition(transition,confirmed,null,source,target);}
    void transition(GUITreeTransition transition,boolean confirmed,int concrete,int source,int target,String transitionId,String outcomePattern){
        observeTransition(transition,confirmed,concrete,source,target);
        Observation observation=history.observations.get(transition);observation.transitionId=transitionId;
        if(confirmed)observation.outcomePattern=outcomePattern;
    }
    private void observeTransition(GUITreeTransition transition,boolean confirmed,Integer concrete,int source,int target){
        boolean sourceMissing=!history.captures.containsKey(transition.getSource());
        long now=runtime.regionNowNanos();boolean fresh=history.transition(transition,confirmed,concrete,source,target,now);
        if(fresh){if(sourceMissing)visit(transition.getSource(),"observed_capture",now,transition.getSource().getTimestamp());visit(transition.getTarget(),"confirmed_arrival",now,transition.getTarget().getTimestamp());}
        ModelAction currentAction=fresh && !watches.isEmpty()?JevRegion.shadowAction(transition.getSource(),transition.getAction().getGUITreeNode(),transition.getAction().getActionType()):null;
        if(fresh && currentAction!=null)for(Watch w:watches)if(JevRegionMatcher.match(w.anchor,transition.getSource(),currentAction).exact(transition.getAction().getGUITreeNode())){w.executed++;opportunity(w,"confirmed_execution",transition.getSource(),currentAction,transition.getTarget().getTimestamp());}
    }
    private long invalidSnapshots,rebindings;
    void rebound(long step,long version,JSONObject detail){
        String receipt="region-snapshot-rebound-"+(++rebindings);
        try{runtime.save(receipt,detail);}catch(java.io.IOException error){runtime.receiptFailed(error);}
        emit("REGION_LOCATOR_REBOUND",step,version,JevRegistry.fields("locator_receipt",receipt,"concrete_id",detail.opt("bucket_id")));
    }
    void snapshotInvalid(long step,long version,JSONObject detail){
        long start=JevRegion.runStartNanos(),elapsed=runtime.regionNowNanos()-start;
        long scheduled=schedule.due(elapsed)?schedule.scheduled(elapsed):schedule.next;
        if(schedule.due(elapsed))schedule.submitted(elapsed);
        String receipt="region-snapshot-invalid-"+(++invalidSnapshots);
        try{detail.put("attempt_elapsed_ns",elapsed);detail.put("scheduled_elapsed_ns",scheduled);runtime.save(receipt,detail);}
        catch(Exception error){runtime.receiptFailed(error);}
        timing.count("locator_snapshot_invalid",1);
        emit("REGION_LOCATOR_SNAPSHOT_INVALID",step,version,JevRegistry.fields("locator_receipt",receipt,"reason",detail.optString("reason"),"attempt_elapsed_ns",elapsed));
    }
    private void emit(String event,long step,long version,JSONObject fields){events.emit(event,step,version,null,null,fields);}
    boolean needsSnapshot(){long start=JevRegion.runStartNanos(),now=runtime.regionNowNanos();return pending==null && start!=Long.MIN_VALUE && schedule.due(now-start);}
    void safePoint(GUITree tree,JevRegion.Snapshot graph,List<State> states,long step){
        int namesBefore=com.android.commands.monkey.ape.naming.NameManager.registeredNameCount();
        List<ModelAction> nativeActions=tree.getCurrentState().getActions();Object[][] bindings=new Object[nativeActions.size()][3];int[] counters=new int[nativeActions.size()];
        for(int i=0;i<nativeActions.size();i++){ModelAction a=nativeActions.get(i);bindings[i]=new Object[]{a.getResolvedGUITreeAction(),a.getResolvedNode(),a.getResolvedNodes()};counters[i]=a.getVisitedCount();}
        long start=System.nanoTime(),now=runtime.regionNowNanos();boolean firstCapture=!history.captures.containsKey(tree);history.observed(tree,now);
        if(firstCapture)visit(tree,"observed_capture",now,step);
        poll(step);
        for(Watch watch:watches)if(now>watch.trackedSince && watch.lastAvailableStep!=step){
            ModelAction action=watch.anchor.getModelAction();JevRegionMatcher.Match match=JevRegionMatcher.match(watch.anchor,tree,action);
            if(match.concrete!=null && match.concrete.isEnabled() && action.isEnabled() && action.isValid()
                    && action.getResolvedGUITree()==tree && action.getResolvedNodes()!=null && Arrays.asList(action.getResolvedNodes()).contains(match.concrete)){
                watch.available++;watch.lastAvailableStep=step;opportunity(watch,"available",tree,action,step);
            }
        }
        long runStart=JevRegion.runStartNanos();
        if(runStart!=Long.MIN_VALUE && !warmupRecorded){warmupRecorded=true;emit("REGION_LOCATOR_WARMUP",step,history.version,
            JevRegistry.fields("run_start_monotonic_ns",runStart,"warmup_ms",TimeUnit.NANOSECONDS.toMillis(warmup),"elapsed_ms",TimeUnit.NANOSECONDS.toMillis(now-runStart)));}
        if(graph!=null && runStart!=Long.MIN_VALUE && pending==null && schedule.due(now-runStart)){
            final Stats stats=history.snapshot(states,now,window);pendingEvidence=new Evidence();runtime.region().locatorEvidence(pendingEvidence);final JevRegion.Snapshot snapshot=graph;
            // Track every exact action before the worker runs, including deferred ancestors'
            // entries. This adds observation only; selection/nomination order is unchanged.
            for(JevRegion.Concrete c:graph.entries)if(c.eligible){GUITreeAction anchor=pendingEvidence.anchors.get(c.id);Watch found=findWatch(anchor);
                if(found==null){found=new Watch(anchor,"locator-entry-"+(watches.size()+1),now,"region-locator-"+(sequence+1),c.id,graph.abstraction);watches.add(found);}
                pendingEvidence.watchIds.put(c.id,found.id);}
            long elapsed=runtime.regionNowNanos()-runStart,scheduled=schedule.scheduled(elapsed),skipped=schedule.submitted(elapsed);
            final Request request=new Request(++sequence,runStart,scheduled,elapsed,skipped,graph);pendingRequest=request;
            emit("REGION_LOCATOR_REQUEST",step,graph.abstraction,requestFields(request));
            pending=worker.submit(new Callable<Analysis>(){public Analysis call(){long begin=System.nanoTime();try{return analyzer.compute(snapshot,stats);}
                finally{request.completed=runtime.regionNowNanos()-request.runStart;timing.record("region_locator_worker",System.nanoTime()-begin);}}});
        }
        long nameGrowth=com.android.commands.monkey.ape.naming.NameManager.registeredNameCount()-namesBefore,bindingChanges=0,countChanges=0;
        for(int i=0;i<nativeActions.size();i++){ModelAction a=nativeActions.get(i);if(bindings[i][0]!=a.getResolvedGUITreeAction() || bindings[i][1]!=a.getResolvedNode() || bindings[i][2]!=a.getResolvedNodes())bindingChanges++;if(counters[i]!=a.getVisitedCount())countChanges++;}
        timing.count("locator_name_registry_growth",nameGrowth);timing.count("locator_binding_changes",bindingChanges);timing.count("locator_native_counter_changes",countChanges);
        if(nameGrowth!=0 || bindingChanges!=0 || countChanges!=0)runtime.receiptFailed(new IllegalStateException("Shadow observer altered native identity/binding/counts"));
        timing.record("region_locator_safe_point",System.nanoTime()-start);
    }
    private Watch findWatch(GUITreeAction anchor){if(anchor==null)return null;for(Watch w:watches)if(JevRegionMatcher.match(w.anchor,anchor.getGUITree(),anchor.getModelAction()).exact(anchor.getGUITreeNode()))return w;return null;}
    private void poll(long step){
        if(pending==null || !pending.isDone())return;
        try {
            Analysis result=pending.get();
            analyses++;structures+=result.candidates.size();int matureCount=0,selectedCount=0,entryCount=0,rejected=0,mixed=0;
            for(Candidate c:result.candidates){if(c.mature)matureCount++;if(c.selected){selectedCount++;for(Entry e:c.entries){entryCount++;
                if(!e.concrete.eligible)rejected++;
                if(!e.outside.isEmpty())mixed++;}}}
            mature+=matureCount;nominations+=selectedCount;reviewableEntries+=result.reviewable.size();identityRejected+=rejected;mixedEntries+=mixed;
            String receipt=pendingRequest.receipt();
            // Accept frozen analysis evidence; matching uses the existing concrete anchors.
            if(result.legacy!=null)for(int id:result.legacy.blocked)if(!legacyFirst.containsKey(id))legacyFirst.put(id,result.stats.now);
            for(Candidate c:result.candidates)for(Entry e:c.entries){
                GUITreeAction anchor=pendingEvidence.anchors.get(e.concrete.id);if(anchor==null || !e.concrete.eligible)continue;
                Watch found=findWatch(anchor);
                if(found==null){found=new Watch(anchor,"locator-entry-"+(watches.size()+1),result.stats.now,receipt,e.concrete.id,result.graph.abstraction);watches.add(found);}
                e.watchId=found.id;
                // Installed rules share the existing observer anchor that follows GUI lineage.
                JevRegion.Bucket bucket=pendingEvidence.buckets.get(e.concrete.id);if(bucket!=null)bucket.ruleAnchor=found.anchor;
                if(c.selected){if(found.first==null){found.first=result.stats.now;e.firstNomination=true;}found.nominations++;}
                Long old=legacyFirst.get(e.concrete.id);if(old!=null && found.legacy==null)found.legacy=old;
            }
            for(Map.Entry<Integer,Long> e:legacyFirst.entrySet()) {GUITreeAction anchor=pendingEvidence.anchors.get(e.getKey());if(anchor!=null)for(Watch w:watches)
                if(w.legacy==null && JevRegionMatcher.match(w.anchor,anchor.getGUITree(),anchor.getModelAction()).exact(anchor.getGUITreeNode()))w.legacy=e.getValue();}
            JSONObject record=record(result,pendingEvidence);
            record.put("schedule",requestFields(pendingRequest));runtime.save(receipt,record);
            emit("REGION_LOCATOR_ANALYSIS",step,result.graph.abstraction,JevRegistry.fields("locator_receipt",receipt,"run_elapsed_ms",TimeUnit.NANOSECONDS.toMillis(result.stats.now-JevRegion.runStartNanos()),
                "structural_regions",result.candidates.size(),"mature_regions",matureCount,"nominated_regions",selectedCount,"nominated_entries",entryCount,
                "reviewable_entries",result.reviewable.size(),"identity_rejected_entries",rejected,"mixed_target_entries",mixed,"legacy_candidates",(result.legacy==null?0:result.legacy.blocked.size()),"analysis_accepted",true,
                "model_approved_entries",0,"rules_installed",0));
            // Only nominated identity-valid entries enter review; keep their frozen evidence and anchors.
            pendingEvidence.candidates.keySet().retainAll(result.reviewable);
            runtime.region().acceptLocator(record,pendingEvidence);
            complete(step,"accepted");
        }catch(Exception error){runtime.receiptFailed(error);emit("REGION_LOCATOR_ERROR",step,history.version,JevRegistry.fields("reason",error.getClass().getSimpleName()));}
        finally{if(pendingRequest!=null)complete(step,"error");pending=null;pendingEvidence=null;}
    }
    private JSONObject requestFields(Request r){return JevRegistry.fields("locator_receipt",r.receipt(),"scheduled_elapsed_ns",r.scheduled,
        "submit_elapsed_ns",r.submitted,"completion_elapsed_ns",r.completed==Long.MIN_VALUE?JSONObject.NULL:r.completed,
        "skipped_before_submit",r.skipped,"snapshot_abstraction_version",r.graph.abstraction,"snapshot_topology_version",r.graph.topology,
        "snapshot_concrete_version",r.graph.concretes,"snapshot_fact_version",r.graph.facts);}
    private void complete(long step,String status){
        Request r=pendingRequest;long skipped=r.completed==Long.MIN_VALUE?0:schedule.completed(r.completed);
        JSONObject fields=requestFields(r);
        try{fields.put("status",status);
            fields.put("observed_elapsed_ns",runtime.regionNowNanos()-r.runStart);fields.put("skipped_while_busy",skipped);fields.put("next_scheduled_elapsed_ns",schedule.next);
        }catch(JSONException error){throw new IllegalStateException(error);}
        emit("REGION_LOCATOR_COMPLETION",step,r.graph.abstraction,fields);pendingRequest=null;
    }
    private JSONObject record(Analysis a,Evidence evidenceSnapshot)throws JSONException {
        JSONArray rows=new JSONArray();
        for(Candidate c:a.candidates){JSONArray entries=new JSONArray();for(Entry e:c.entries){
            JSONObject evidence=evidenceSnapshot.evidence.get(e.concrete.id);
            entries.put(JevRegistry.fields("concrete_id",e.concrete.id,"source",e.concrete.source,"known_targets",new JSONArray(new TreeSet<Integer>(e.concrete.targets)),
                "locator_entry_id",e.watchId==null?evidenceSnapshot.watchIds.getOrDefault(e.concrete.id,null):e.watchId,"first_nomination",e.firstNomination,
                "inside_targets",new JSONArray(e.inside),"outside_targets",new JSONArray(e.outside),"mixed_targets",!e.outside.isEmpty(),"eligible_identity",e.concrete.eligible,"entry_status",e.status,
                "concrete_evidence",evidence==null?JSONObject.NULL:evidence,"model_approved",false,"rule_installed",false));}
            rows.put(JevRegistry.fields("header",c.region.header,"members",new JSONArray(c.region.members),"visits",c.visits,"avg_visits",c.average,
                "avg_visits_pass",c.average>2,"recent_arrivals_pass",c.recent>=analyzer.minRecent,"novelty_pass",c.noveltyStatus.equals("KNOWN") && c.lastProgress<=a.stats.now-a.stats.window,
                "novelty_status",c.noveltyStatus,"state_progress_ns",c.lastProgress,
                "membership_changed",c.membershipChanged,"refinement_ns",a.stats.refinement,
                "recent_visits",c.recent,"recent_reentries",c.reentries,"recent_new_states",c.newStates,
                "quiet_ms",c.lastProgress==Long.MIN_VALUE?JSONObject.NULL:TimeUnit.NANOSECONDS.toMillis(a.stats.now-c.lastProgress),
                "known_model_actions",c.actions,"unvisited_model_actions",c.unvisited,"mature",c.mature,"nominated",c.selected,"reason",c.reason,"entries",entries));
        }
        JSONArray links=new JSONArray();for(JevRegion.Link link:a.graph.links){String key=link.source+":"+link.concrete+":"+link.target;
            links.put(JevRegistry.fields("source",link.source,"target",link.target,"concrete_id",link.concrete,"first_seen_ns",a.stats.links.get(key),"confirmed_supports",a.stats.confirmedLinks.getOrDefault(key,0),"weak_supports",a.stats.weakLinks.getOrDefault(key,0)));}
        return JevRegistry.fields("schema",7,"novelty_definition","confirmed_new_ui_state_v1","transition_observations",a.stats.transitionRows,"refinement_events",a.stats.refinementRows,"state_discoveries",discoveryRows(a.stats),"unknown_states",unknownRows(a.stats),"shadow_only",runtime.regionShadowAudit(),"run_start_monotonic_ns",JevRegion.runStartNanos(),"snapshot_monotonic_ns",a.stats.now,"links",links,
            "elapsed_ms",TimeUnit.NANOSECONDS.toMillis(a.stats.now-JevRegion.runStartNanos()),"abstraction_version",a.graph.abstraction,"topology_version",a.graph.topology,
            "concrete_version",a.graph.concretes,"fact_version",a.graph.facts,"analysis_accepted",true,"states",evidenceSnapshot.stateRows,"regions",rows,"roots",new JSONArray(a.graph.roots),"current",a.graph.current,
            "legacy_candidates",new JSONArray(new TreeSet<Integer>(a.legacy==null?Collections.<Integer>emptySet():a.legacy.blocked)),"reviewable_entries",new JSONArray(a.reviewable),"model_requests",0,"rules_installed",0);
    }
    static JSONObject unknownRows(Stats stats){JSONObject rows=new JSONObject();try{for(Map.Entry<Integer,String> e:stats.unknown.entrySet())rows.put(e.getKey().toString(),e.getValue());}catch(JSONException e){throw new IllegalStateException(e);}return rows;}
    static JSONArray discoveryRows(Stats stats){JSONArray rows=new JSONArray();for(Discovery event:stats.discoveries)rows.put(event.json());return rows;}
    void beforeInput(Action action,long step){
        if(!(action instanceof ModelAction))return;ModelAction model=(ModelAction)action;
        for(Watch w:watches)if(step!=w.lastSelectedStep && JevRegionMatcher.match(w.anchor,model.getResolvedGUITree(),model).exact(model.getResolvedNode())){w.selected++;w.lastSelectedStep=step;opportunity(w,"selected",model.getResolvedGUITree(),model,step);}
    }
    private void opportunity(Watch w,String kind,GUITree tree,ModelAction action,long step){
        events.emit("REGION_LOCATOR_OPPORTUNITY",step,history.version,tree.getCurrentState().getGraphId(),action.getGraphId(),JevRegistry.fields(
            "locator_entry_id",w.id,"opportunity_kind",kind,"source_gui_id",tree.getId(),"run_elapsed_ms",TimeUnit.NANOSECONDS.toMillis(runtime.regionNowNanos()-JevRegion.runStartNanos())));
    }
    private void visit(GUITree tree,String kind,long now,long step){events.emit("REGION_LOCATOR_VISIT",step,history.version,tree.getCurrentState().getGraphId(),null,
        JevRegistry.fields("source_gui_id",tree.getId(),"opportunity_kind",kind,"observation_ns",now,"run_elapsed_ms",TimeUnit.NANOSECONDS.toMillis(now-JevRegion.runStartNanos())));}
    void close(long step){
        if(pending!=null){pending.cancel(true);complete(step,"cancelled");pending=null;pendingEvidence=null;}
        for(Watch w:watches)emit("REGION_LOCATOR_ENTRY_TOTAL",step,w.version,JevRegistry.fields("locator_entry_id",w.id,"locator_receipt",w.receipt,"concrete_id",w.concrete,
            "first_nomination_elapsed_ms",w.first==null?JSONObject.NULL:TimeUnit.NANOSECONDS.toMillis(w.first-JevRegion.runStartNanos()),"legacy_first_elapsed_ms",w.legacy==null?JSONObject.NULL:TimeUnit.NANOSECONDS.toMillis(w.legacy-JevRegion.runStartNanos()),
            "lead_ms",w.legacy==null || w.first==null?JSONObject.NULL:TimeUnit.NANOSECONDS.toMillis(w.legacy-w.first),"future_exact_availability_count",w.available,
            "future_model_action_selection_count",w.selected,"future_confirmed_execution_count",w.executed,"nomination_occurrences",w.nominations));
        emit("REGION_LOCATOR_TOTAL",step,history.version,JevRegistry.fields("analyses",analyses,"structural_regions",structures,"mature_regions",mature,
            "nominated_regions",nominations,"reviewable_entries",reviewableEntries,"identity_rejected_entries",identityRejected,"mixed_target_entries",mixedEntries,"unique_candidates",watches.size(),"model_invocations",runtime.region().modelInvocationCount(),"rules_installed",runtime.region().installedRuleCount()));
    }
}
