"""Current replicated InstantProp targets; inert until explicit read opt-in.

WorldTargetObservationProvider(...)(window=...,node=...,snapshot=...,frame=...)
returns current_world_targets and current_world_target_observation. A target is
an owned objective candidate, not a confirmed active interaction. The UI must
join its current TargetKey and InstantPropSheetId. World positions never become
pixels here. No heap scan, client function call, native invocation or writes.
"""
from mini_masukja.native_type_reference import type_definition_index
from copy import deepcopy
from datetime import datetime, timezone
from functools import lru_cache
import hashlib,json,math,struct
from .paths import root
from .vendor.map_memory import MapMemorySession,MapReadPending
from .vendor.map_character_context import active_context
from .vendor.map_character_identity import CharacterMemoryReader

SOURCE_REF='data/arbeit-world-targets/source-pins.json'
SOURCE='current_owned_replicated_InstantProp_entity'
KEY_TYPE='MM.Client.Framework.ComponentProcessing.EntityKey'
VERSION=('game_assembly_sha256','metadata_sha256')
OWNER=('quest_id','instance_id','accept_time','step_id','step_index','objective_index',
       'session_id','character_id','process_started_at')

@lru_cache(maxsize=1)
def source_pins():
    raw=(root()/SOURCE_REF).read_bytes();digest=hashlib.sha256(raw).hexdigest()
    if digest!='c13d91dee5de3f67110529d835cf69216cc242dc22ca7877c17c529f7a3068ed':
        raise ValueError('world target source pins changed')
    value=json.loads(raw);value['source_hash']=digest;return value


def entity_key(raw):
    a,b=struct.unpack('<ii',raw)
    return dict(declared_type=KEY_TYPE,Type=a,InnerKey=b,raw_hex=raw.hex())


class WorldTargetSession(MapMemorySession):
    """Bounded current-root reader; source-pinned closed generic layout only."""
    def source(self,name):
        rows=[r for r in source_pins()['classes'] if r['name']==name]
        if len(rows)!=1:raise ValueError('world target schema unavailable:'+name)
        return rows[0]

    def u64(self,at):return struct.unpack('<Q',self.watch(at,8))[0]
    def i32(self,at):return struct.unpack('<i',self.watch(at,4))[0]
    def bit(self,at):
        v=self.watch(at,1)[0]
        if v not in (0,1):raise ValueError('world target bool is invalid')
        return bool(v)

    def wfields(self,pointer,name):
        if not pointer:raise MapReadPending('current world target reference unavailable:'+name)
        klass=self.u64(pointer);schema=self.source(name);r=self.reader
        if r.class_name(klass)!=(name,schema['namespace']):raise ValueError('world target class changed:'+name)
        if struct.unpack('<H',self.watch(klass+0x120,2))[0]!=schema['field_count']:
            raise ValueError('world target field count changed:'+name)
        table=self.u64(klass+0x80);out={}
        for field in schema['fields']:
            f=self.watch(table+32*field['index'],32);np,tp,owner,offset,_=struct.unpack('<QQQiI',f)
            data,flags=struct.unpack('<QI',self.watch(tp,12));ty=field['type'];code=(flags>>16)&255
            if (r.ascii(np)!=field['name'] or owner!=klass or offset!=field['offset'] or code!=ty['type_code']
                    or code in (17,18) and data!=ty['definition_index']):
                raise ValueError('world target field changed:'+name+'.'+field['name'])
            out[field['name']]=offset
        return out

    def wref(self,pointer,name,field):
        return self.u64(pointer+self.wfields(pointer,name)[field])

    def verify_native(self):
        pins=source_pins()
        if any(self.plan.get(k)!=pins[k] for k in VERSION):raise ValueError('world target source version mismatch')
        for probe in pins['native_probes']:
            raw=bytes.fromhex(probe['raw_hex'])
            if self.watch(self.base+probe['rva'],len(raw))!=raw:raise ValueError('world target native source changed:'+probe['name'])

    def container(self,replicated):
        """Validate real closed class plus current RGCTX and generic method bodies."""
        fields=self.wfields(replicated,'ReplicatedSystem');pointer=self.u64(replicated+fields['multipleReplicatedEntityContainer'])
        if not pointer:raise MapReadPending('current replicated entity container missing')
        klass=self.u64(pointer);r=self.reader;schema=self.source('EntityContainer`1')
        if r.class_name(klass)!=('EntityContainer`1',schema['namespace']):raise ValueError('current world entity container type changed')
        owner_class=self.u64(replicated);table=self.u64(owner_class+0x80)
        field=self.source('ReplicatedSystem')['fields'][0];tp=self.u64(table+field['index']*32+8)
        generic,flags=struct.unpack('<QI',self.watch(tp,12))
        if (flags>>16)&255!=21:raise ValueError('current world container generic field changed')
        generic_raw=self.watch(generic,32);inst=struct.unpack_from('<Q',generic_raw,8)[0];closed=struct.unpack_from('<Q',generic_raw,24)[0]
        if not closed or closed!=klass or not inst:raise MapReadPending('current world container closed generic unavailable')
        instraw=self.watch(inst,16);argc=struct.unpack_from('<I',instraw)[0];argv=struct.unpack_from('<Q',instraw,8)[0]
        if argc!=1 or not argv:raise ValueError('current world entity generic argument count changed')
        atp=self.u64(argv);definition,bits=struct.unpack('<QI',self.watch(atp,12))
        if type_definition_index(self, atp)!=56374 or (bits>>16)&255!=18:raise ValueError('current world entity generic argument changed')
        method=self.u64(self.base+source_pins()['container']['current_instantiation']['cache_rva'])
        if not method or self.u64(method+0x20)!=klass:raise MapReadPending('current world generic method cache uninitialized or changed')
        rgctx=self.u64(klass+0xc0)
        if not rgctx:raise MapReadPending('current world entity RGCTX unavailable')
        for slot,rva in ((24,0x4eb6290),(32,0x4eb5e50),(48,0x2b57a10),(56,0x15ddae0)):
            entry=self.u64(rgctx+slot)
            if not entry or self.u64(entry+8)!=self.base+rva or self.u64(entry+0x20)!=klass:
                raise ValueError('current world generic source method changed:'+str(slot))
        names=['EntityType','<LastIndex>k__BackingField','<Entities>k__BackingField','ages']
        shape=r.layout(klass,names);actual={f['name']:f for f in shape['fields']}
        for name,offset,code in zip(names,(16,20,24,32),(17,8,29,29)):
            if name not in actual or (actual[name]['offset'],actual[name]['type_code'])!=(offset,code):
                raise ValueError('current closed entity container layout changed:'+name)
        kind=self.i32(pointer+16);last=self.i32(pointer+20)
        if kind!=self.source('MultipleEntityType')['enum_constants']['StageObject']:
            raise ValueError('current replicated entity container is not StageObject')
        if not -1<=last<65536:raise ValueError('current entity index exceeds native UInt16 domain')
        array=self.u64(pointer+24);ages=self.u64(pointer+32)
        length,_=self.array(array,name='MultipleReplicatedEntity[]',stride=8,limit=65536)
        age_count,_=self.array(ages,name='UInt16[]',stride=2,limit=65536)
        self.watch(array,32);self.watch(ages,32)
        element=self.u64(self.u64(array)+0x40)
        if r.class_name(element)!=('MultipleReplicatedEntity',self.source('MultipleReplicatedEntity')['namespace']):
            raise ValueError('current entity array element class changed')
        if length<=last or age_count<=last:raise ValueError('current entity container arrays truncated')
        pointers=struct.unpack('<'+'Q'*(last+1),self.watch(array+32,8*(last+1))) if last>=0 else []
        age_values=struct.unpack('<'+'H'*(last+1),self.watch(ages+32,2*(last+1))) if last>=0 else []
        return [dict(index=i,age=age_values[i],pointer=p,key=struct.pack('<II',kind,(i<<16)|age_values[i])) for i,p in enumerate(pointers) if p]

    def spawned(self,replicated,channel,*,definition_id=None):
        result=[];keys=set();state=self.source('ReplicatedUpdateState')
        if not any(f['name']=='hasValue' and f['offset']==16 and f['type']['type_code']==2 for f in state['fields']):
            raise ValueError('replicated presence source changed')
        for slot in self.container(replicated):
            pointer=slot['pointer'];fields=self.wfields(pointer,'MultipleReplicatedEntity')
            presence=[self.bit(pointer+fields[k+'UpdateState']) for k in ('InteractablePropIdentityReplicated','PropLocationReplicated')]
            if not all(presence):continue
            ident=self.u64(pointer+fields['InteractablePropIdentityReplicated']);loc=self.u64(pointer+fields['PropLocationReplicated'])
            a=self.wfields(ident,'InteractablePropIdentityReplicated');b=self.wfields(loc,'PropLocationReplicated')
            key=self.watch(ident+a['<EntityKey>k__BackingField'],8)
            if key!=slot['key'] or self.watch(loc+b['<EntityKey>k__BackingField'],8)!=key:
                raise MapReadPending('current entity key/slot generation differs')
            if key in keys:raise ValueError('duplicate current replicated entity key')
            keys.add(key)
            kind=self.i32(ident+a['<InteractablePropType>k__BackingField'])
            if kind!=self.source('InteractablePropType')['enum_constants']['InstantProp']:continue
            definition=self.i32(ident+a['<InstantPropSheetId>k__BackingField'])
            if definition_id is not None and definition!=definition_id:continue
            x,y=struct.unpack('<ff',self.watch(loc+b['<Position>k__BackingField'],8))
            if not all(math.isfinite(v) for v in (x,y)):raise ValueError('nonfinite current prop position')
            result.append(dict(domain='InstantProp',definition_id=definition,table_domain='InstantPropSheet',
                entity_key=entity_key(key),current_channel=dict(table_domain='ChannelSheet',id=channel),
                position=dict(coordinate_space='System.Numerics.Vector2/current channel',x=x,y=y),
                identity_pointer=ident,location_pointer=loc,entity_pointer=pointer,
                collection_index=slot['index'],collection_age=slot['age'],replicated_present=True,
                membership_verified=True,activation_confirmed=False,movement_ready=False))
        return result

    def fields(self,klass,schema):
        # Projection and map readers use different bounded subsets of the same
        # generated container. Never reuse a cached smaller subset as full.
        names={f['name'] for f in schema['fields']};key=(klass,schema['type_index'])
        cache=getattr(self,'_validated',None)
        if cache is None:self._validated={};cache=self._validated
        if key in cache and not names.issubset(cache[key]):cache.pop(key)
        return super().fields(klass,schema)

    def read_targets(self,*,current_map_snapshot=None,definition_id=None):
        self._guards=[];projection_map=None;projection_missing=None
        if current_map_snapshot is not None:
            try:
                projection_map=super().read(include_map=True)
                validate_map_successor(current_map_snapshot,projection_map)
                projection_map['session_id']=current_map_snapshot['session_id']
            except (OSError,RuntimeError,ValueError,KeyError,TypeError) as exc:
                projection_missing='current_spawn_projection_map_unavailable:'+str(exc)
                projection_map=None;self._guards=[]
        self.verify_native();r=self.reader
        context=active_context(r,self.base,self.character_plan)
        identity=CharacterMemoryReader._value(r,context['identity_pointer'],self.identity_class,self.identity_shape)
        singleton,entity=context['chain'][1],context['chain'][3]
        replicated=self.wref(singleton,'IntegrationSingleton','<Replicated>k__BackingField')
        channel_object=self.wref(entity,'SingularEntity','IChannelIdentifierComponent')
        fields=self.wfields(channel_object,'ChannelIdentifierComponent')
        if self.bit(channel_object+fields['<IsChangingChannel>k__BackingField']):raise MapReadPending('current channel is changing')
        channel=self.i32(channel_object+fields['<ChannelSheetId>k__BackingField'])
        targets=self.spawned(replicated,channel,definition_id=definition_id)
        if projection_map is not None:
            space=channel_space(channel)
            from .typed_region_movement import project_current_world_position
            for target in targets:
                try:
                    if not space:raise ValueError('current_ChannelSheet_GameSpace_definition_missing')
                    proof=project_current_world_position(self,projection_map,[target['position']['x'],target['position']['y']],
                        context=context,expected_world=space['world_data_key'],expected_geo=space['geo_data_key'])
                    target['projection']=proof;target['projection_map_identity']=map_identity(projection_map)
                    target['world_definition']=deepcopy(space)
                except (OSError,RuntimeError,ValueError,KeyError,TypeError,struct.error) as exc:
                    target['projection_missing']=str(exc)
        elif projection_missing:
            for target in targets:target['projection_missing']=projection_missing
        if active_context(r,self.base,self.character_plan)!=context or CharacterMemoryReader._value(r,context['identity_pointer'],self.identity_class,self.identity_shape)!=identity:
            raise MapReadPending('current world target character root changed')
        if any(r.read(a,len(raw))!=raw for a,raw in self._guards):raise MapReadPending('current world targets changed during read')
        return dict(pid=r.pid,process_started_at=r.started_at.isoformat(),identity=identity,targets=targets,
            current_channel=channel,observed_at=datetime.now(timezone.utc).isoformat(),source=SOURCE,
            source_ref=SOURCE_REF,source_hash=source_pins()['source_hash'],
            source_version={k:self.plan[k] for k in VERSION},stable=True)


def map_identity(snapshot):
    return {k:deepcopy(snapshot.get(k)) for k in ('pid','process_started_at','world_data_key','geo_data_key','viewing_channel','pose')}


def validate_map_successor(expected,current):
    if expected.get('certainty')!='confirmed' or not expected.get('observation_source'):
        raise ValueError('current spawn projection map source unconfirmed')
    stamp=datetime.fromisoformat(expected['captured_at'].replace('Z','+00:00'))
    if stamp.tzinfo is None or not -.5<=(datetime.now(timezone.utc)-stamp).total_seconds()<=5:
        raise ValueError('current spawn projection map is stale')
    if map_identity(expected)!=map_identity(current) or expected.get('identity')!=current.get('identity'):
        raise MapReadPending('current spawn projection map pose/world/owner changed')
    if not expected.get('session_id'):raise ValueError('current spawn projection session missing')


def channel_space(channel_id):
    from .arbeit_movement import movement_definition
    domains=movement_definition()['descriptor_domains']
    if not domains.get('channel_game_space_join',{}).get('verified') or any(domains.get(k)!=source_pins()[k] for k in VERSION):
        raise ValueError('Channel_GameSpace_source_join_required')
    rows=[r for r in domains['domains']['ChannelName']['rows'] if r['id']==channel_id&0xffffffff]
    return deepcopy(rows[0]['game_space']) if len(rows)==1 else None


def descriptor_channel_value(auto):
    """Return the declared destination constraint, or None for source absence."""
    channel=(auto.get('values') or {}).get('ChannelName')
    if channel is None:return None
    if channel.get('optional') is True:
        arg=channel.get('optional_argument_type') or {}
        if (channel.get('interpreted') is not True or type(channel.get('has_value')) is not bool
                or (arg.get('type_code'),arg.get('namespace'),arg.get('name'))!=(17,'Shared.Symbols','ChannelSheetId')):
            raise ValueError('current objective Optional ChannelSheetId unverified')
        if not channel['has_value']:return None
    if (channel.get('interpreted') is not True or channel.get('table_domain')!='ChannelSheet'
            or channel.get('declared_type')!='Shared.Symbols.ChannelSheetId'
            or channel.get('domain_mapping_verified') is not True or type(channel.get('value')) is not int):
        raise ValueError('current objective AutoPlay ChannelSheetId unverified')
    return channel['value']


def owned_instant_prop_program(node,snapshot):
    """Recheck actual typed Data and owned occurrence; never trust params alone."""
    from .objective_binding import current_objective_occurrence
    from .typed_objective_operands import normalize_objective_operands
    rows=[p for p in snapshot.get('objective_programs',[]) if current_objective_occurrence(p,snapshot,node)]
    if len(rows)!=1:raise ValueError('one fresh current owned world objective program required')
    program=rows[0];normalized=normalize_objective_operands(program,expected=node)
    item=normalized['operands'].get('instant_prop') or {}
    if not normalized['ready'] or normalized['operation']!='objective.inspect' or item.get('table_domain')!='InstantPropSheet':
        raise ValueError('current typed InstantProp objective required:'+','.join(normalized['missing']))
    auto=program.get('auto_play') or {};values=auto.get('values') or {};target=values.get('InstantPropName')
    # Some valid objectives use a multi-objective/no-destination descriptor.
    # Preserve it, do not synthesize a prop ID or claim activation from it.
    if target is not None:
        if (auto.get('available') is not True or target.get('interpreted') is not True
                or target.get('domain_mapping_verified') is not True or target.get('table_domain')!='InstantPropSheet'
                or target.get('declared_type')!='Shared.Symbols.InstantPropSheetId' or target.get('value')!=item['id']):
            raise ValueError('current objective Data/AutoPlay InstantProp mismatch')
    descriptor_channel_value(auto)
    return program,normalized


def bind_current_world_targets(node,snapshot,observation):
    from .arbeit_operations import _fresh
    program,normalized=owned_instant_prop_program(node,snapshot);pins=source_pins()
    if (observation.get('stable') is not True or observation.get('source')!=SOURCE or observation.get('source_hash')!=pins['source_hash']
            or any((observation.get('source_version') or {}).get(k)!=pins[k] for k in VERSION)
            or observation.get('process_started_at')!=program['process_started_at']
            or str((observation.get('identity') or {}).get('character_id'))!=str(program['character_id'])
            or not _fresh(dict(observation,session_id=program['session_id'],character_id=program['character_id']),snapshot)):
        raise ValueError('current replicated target observation owner/time/version unverified')
    definition=normalized['operands']['instant_prop']['id'];auto=program.get('auto_play') or {}
    channel=descriptor_channel_value(auto)
    if channel is not None and channel!=observation['current_channel']:
        raise MapReadPending('current objective destination channel differs from current channel')
    rows=[]
    for target in observation['targets']:
        if target.get('definition_id')!=definition:continue
        key=target.get('entity_key') or {}
        if (target.get('domain')!='InstantProp' or target.get('membership_verified') is not True or target.get('replicated_present') is not True
                or key.get('declared_type')!=KEY_TYPE or key.get('raw_hex')!=struct.pack('<ii',key['Type'],key['InnerKey']).hex()
                or (target.get('current_channel') or {}).get('id')!=observation['current_channel']):
            raise ValueError('current spawned target membership/key/channel unverified')
        stamp=min(datetime.fromisoformat(row['observed_at'].replace('Z','+00:00')) for row in (program,observation)).isoformat()
        rows.append(dict({k:deepcopy(program[k]) for k in OWNER},node_id=node['id'],confirmed=True,owner_verified=True,
            source=SOURCE,source_ref=SOURCE_REF,source_hash=pins['source_hash'],source_version=deepcopy(observation['source_version']),
            observed_at=stamp,target=deepcopy(target),identity_pointer=target['identity_pointer'],
            location_pointer=target['location_pointer'],entity_pointer=target['entity_pointer'],
            objective_source=program['source'],objective_observed_at=program['observed_at'],
            auto_play=deepcopy(auto),data=deepcopy(program['data']),normalized_operands=deepcopy(normalized),
            identity_binding='current_owned_typed_Data_to_present_replicated_entity_same_definition_and_key_generation',
            activation_confirmed=False,interaction_ready=False,movement_ready=False,
            missing=['current_same_entity_interaction_target_and_callback','source_world_to_current_map_projection_for_direct_movement']))
    return rows


class WorldTargetObservationProvider:
    def __init__(self,*,allow_process_read=False,session_factory=None):
        self.enabled=allow_process_read is True;self.session_factory=session_factory or WorldTargetSession

    def __call__(self,*,window,node,snapshot,frame,image=None):
        result=dict(ok=False,current_world_targets=[],current_world_target_observation={},missing=[],read_attempted=False,
                    game_inputs=0,memory_writes=0,client_functions_called=0,heap_scans=0,live_tested=False)
        if node.get('op')!='objective.inspect':return result
        if not self.enabled:result['missing']=['process_read_not_explicitly_enabled'];return result
        try:
            program,normalized=owned_instant_prop_program(node,snapshot)
            pid=getattr(window,'pid',None) or frame.get('pid') or (frame.get('window') or {}).get('pid')
            if type(pid) is not int or pid<=0:raise ValueError('bound world target PID required')
            if any(frame.get(k)!=program.get(k) for k in ('process_started_at','character_id','session_id')):
                raise ValueError('bound world target frame owner differs')
            result['read_attempted']=True
            with self.session_factory(pid) as session:
                map_result=snapshot.get('map') or {};current_map=map_result.get('snapshot') if map_result.get('ok') is True else None
                observation=session.read_targets(current_map_snapshot=current_map,definition_id=normalized['operands']['instant_prop']['id'])
            if observation.get('pid')!=pid:raise ValueError('world target process changed')
            targets=bind_current_world_targets(node,snapshot,observation)
            result.update(ok=True,current_world_targets=targets,current_world_target_observation=observation)
            if not targets:result['missing']=['current_owned_definition_has_no_present_spawned_entity']
        except (OSError,RuntimeError,ValueError,TypeError,KeyError,AttributeError,struct.error) as exc:
            result['missing']=[str(exc)]
        return result


def plan_world_target_movement(node,snapshot,receipt=None,*,now=None):
    """Current unique spawned target -> existing map input planner.

    Receipt scope is the current quest occurrence AND the full generation key.
    Map opening/view selection and point navigation have separate stages.
    An uncertain prior action is retained; no candidate reselection/re-send.
    Arrival is approach evidence only, never objective or interaction success.
    """
    from .arbeit_operations import _fresh
    from .arbeit_movement import plan_movement
    from .typed_region_movement import SOURCE as PROJECTION_SOURCE,SOURCE_HASH as PROJECTION_HASH
    def wait(reason):
        return dict(status='waiting',action=None,wait_reason=reason,evidence={'objective_arrival_confirmed':False},
            next_receipt=deepcopy(receipt),sent_to_game=False,game_tested=False)
    try:
        program,normalized=owned_instant_prop_program(node,snapshot)
        owner={k:program[k] for k in OWNER};definition=normalized['operands']['instant_prop']['id']
        outer=snapshot.get('map') or {};current=outer.get('snapshot') if outer.get('ok') is True else None
        if not isinstance(current,dict):return wait('current_world_target_map_observation_required')
        if ((current.get('identity') or {}).get('character_id'),current.get('session_id'),current.get('process_started_at'))!=(program['character_id'],program['session_id'],program['process_started_at']):
            return wait('current_world_target_map_owner_mismatch')
        if receipt and receipt.get('world_target_owner')!=owner:return wait('pending_world_target_objective_occurrence_changed')
        candidates=[r for r in snapshot.get('current_world_targets',[]) if r.get('confirmed') is True and r.get('owner_verified') is True
            and r.get('source')==SOURCE and r.get('source_hash')==source_pins()['source_hash'] and _fresh(r,snapshot)
            and all((r.get('source_version') or {}).get(k)==source_pins()[k] for k in VERSION)
            and all(r.get(k)==v for k,v in owner.items()) and (r.get('target') or {}).get('definition_id')==definition]
        if receipt:
            candidates=[r for r in candidates if r['target'].get('entity_key')==receipt.get('world_target_key')]
        if len(candidates)!=1:return wait('current_spawned_target_missing_or_ambiguous:'+str(len(candidates)))
        row=candidates[0];target=row['target'];key=target.get('entity_key') or {}
        if (target.get('membership_verified') is not True or target.get('replicated_present') is not True
            or target.get('domain')!='InstantProp' or key.get('declared_type')!=KEY_TYPE
            or key.get('raw_hex')!=struct.pack('<ii',key['Type'],key['InnerKey']).hex()):
            return wait('current_spawned_target_membership_unverified')
        channel=target['current_channel']['id'];space=channel_space(channel)
        if space is None:return wait('current_spawned_channel_world_definition_missing')
        world=[target['position']['x'],target['position']['y']]
        local=current.get('map_ready') and current.get('map_visible') and (current.get('pose') or {}).get('map_type')!=1 and current.get('world_data_key')==space['world_data_key']
        phase=(receipt or {}).get('phase');prior=receipt
        replanning=bool(receipt and receipt.get('world_target_stage')=='replan' and phase=='replan_ready')
        if replanning:
            # This receipt records an already-observed effect of the original
            # navigation. Keep its owner/key and evidence; only the next action
            # may use a newly observed position of that same entity.
            resolved=receipt.get('resolved_navigation_receipt') or {}
            arrival=receipt.get('original_goal_arrival') or {}
            if (resolved.get('phase')!='move_sent' or resolved.get('world_target_owner')!=owner
                or resolved.get('world_target_key')!=key or arrival.get('confirmed') is not True
                or datetime.fromisoformat(current['captured_at'].replace('Z','+00:00'))<datetime.fromisoformat(arrival['observed_at'].replace('Z','+00:00'))):
                return wait('pending_world_target_replan_effect_receipt_invalid')
            prior=None
        proof=target.get('projection') or {}
        projection_valid=(proof.get('confirmed') is True and proof.get('source')==PROJECTION_SOURCE and proof.get('source_hash')==PROJECTION_HASH
            and all((proof.get('source_version') or {}).get(k)==source_pins()[k] for k in VERSION)
            and proof.get('world_position')==world and proof.get('world_data_key')==space['world_data_key']
            and proof.get('geo_data_key')==space['geo_data_key'] and target.get('projection_map_identity')==map_identity(current)
            and proof.get('process_started_at')==program['process_started_at'] and proof.get('character_id')==program['character_id']
            and proof.get('session_id')==program['session_id'] and _fresh(proof,snapshot))
        if receipt and receipt.get('world_target_stage')=='point':
            goal=deepcopy(receipt['world_target_goal']);stage='point'
            if world!=receipt.get('spawned_world_position'):
                # A moved target is not permission to replace/re-send an
                # uncertain input. Resolve only the original selected-location
                # effect, using a newer current character/channel observation.
                if phase=='move_sent':
                    original=plan_movement(current,goal,receipt,now=now)
                    newer=datetime.fromisoformat(current['captured_at'].replace('Z','+00:00'))>datetime.fromisoformat(receipt['captured_at'].replace('Z','+00:00'))
                    if original.get('status')=='arrived' and newer:
                        arrival=dict(deepcopy(original['evidence']),confirmed=True,observed_at=current['captured_at'])
                        successor=deepcopy(receipt)
                        successor.update(phase='replan_ready',world_target_stage='replan',captured_at=current['captured_at'],
                            dispatch_state='effect_confirmed',resolved_navigation_receipt=deepcopy(receipt),original_goal_arrival=arrival)
                        return dict(status='transition',action=None,wait_reason='original_world_target_position_arrived_replan_same_entity',
                            next_receipt=successor,sent_to_game=False,game_tested=False,
                            evidence=dict(world_target_owner=owner,world_target_key=deepcopy(key),world_target_definition_id=definition,
                                source=SOURCE,source_hash=source_pins()['source_hash'],original_goal_arrival=arrival,
                                current_spawned_world_position=world,objective_arrival_confirmed=False,interaction_completed=False))
                return wait('current_spawned_target_moved_pending_navigation_no_resend')
            if phase!='move_sent':
                if not local or not projection_valid or proof.get('map_position')!=goal['map_position']:
                    return wait('pending_world_target_point_projection_changed_no_resend')
        elif not local:
            token=hashlib.sha256(json.dumps(dict(owner=owner,key=key),sort_keys=True).encode()).hexdigest()
            goal=dict(kind='marker',marker_id='world-target-view:'+token,world_data_key=space['world_data_key'],region_names=[space['name']])
            stage='view'
        else:
            if receipt and not replanning:
                if (receipt.get('world_target_stage')!='view' or phase not in ('open_sent','world_sent','region_sent','pan_sent','zoom_sent')
                    or datetime.fromisoformat(current['captured_at'].replace('Z','+00:00'))<=datetime.fromisoformat(receipt['captured_at'].replace('Z','+00:00'))):
                    return wait('pending_world_target_view_fresh_effect_required')
                prior=None
            if not projection_valid:
                return wait('current_spawned_world_projection_required:'+str(target.get('projection_missing','no current projection')))
            goal=dict(kind='map_position',coordinate_space='current-map',world_data_key=space['world_data_key'],map_position=deepcopy(proof['map_position']),
                region_names=[space['name']],channel_id=channel,title='current owned InstantProp '+str(definition))
            stage='point'
        selected=current.get('selected') or {};detail=selected.get('detail') or {}
        if stage=='point' and selected.get('long_press') and detail.get('target_channel') is not None and detail['target_channel']&0xffffffff!=channel&0xffffffff:
            return wait('selected_spawned_target_move_channel_mismatch')
        answer=plan_movement(current,goal,prior,now=now)
        if answer.get('next_receipt') is not None:
            answer['next_receipt'].update(world_target_owner=owner,world_target_key=deepcopy(key),world_target_stage=stage,
                world_target_goal=deepcopy(goal),spawned_world_position=world,world_target_definition_id=definition)
            if receipt and prior is None:
                answer['next_receipt']['resolved_replan_receipt' if replanning else 'resolved_view_receipt']=deepcopy(receipt)
        answer.setdefault('evidence',{}).update(world_target_owner=owner,world_target_key=deepcopy(key),world_target_definition_id=definition,
            source=SOURCE,source_hash=source_pins()['source_hash'],objective_arrival_confirmed=False,interaction_completed=False,
            target_choice='unique current same-objective spawned definition; retained exact generation key after first action')
        return answer
    except (ValueError,RuntimeError,KeyError,TypeError,struct.error) as exc:return wait('world_target_movement_unavailable:'+str(exc))


def verify_current_spawned_target(session,target,*,context=None):
    """Revalidate a selected generation key inside an already-open UI guard.

    This shares the caller's read guards and opens no process. The caller must
    perform its final stable-root/guard check before proposing or dispatching.
    Prior frame timestamps do not refresh an entity that was removed/recycled.
    """
    if target.get('domain')!='InstantProp' or type(target.get('definition_id')) is not int:
        raise ValueError('current InstantProp target declaration required')
    expected=target.get('entity_key') or {}
    if expected.get('declared_type')!=KEY_TYPE or expected.get('raw_hex')!=struct.pack('<ii',expected['Type'],expected['InnerKey']).hex():
        raise ValueError('current InstantProp generation key required')
    view=WorldTargetSession.__new__(WorldTargetSession)
    view.reader=session.reader;view.base=session.base;view.plan=session.plan;view.character_plan=session.character_plan
    view._guards=session._guards
    view.verify_native()
    context=context if context is not None else active_context(view.reader,view.base,view.character_plan)
    replicated=view.wref(context['chain'][1],'IntegrationSingleton','<Replicated>k__BackingField')
    channel_obj=view.wref(context['chain'][3],'SingularEntity','IChannelIdentifierComponent')
    fields=view.wfields(channel_obj,'ChannelIdentifierComponent')
    if view.bit(channel_obj+fields['<IsChangingChannel>k__BackingField']):raise MapReadPending('current interaction channel changing')
    channel=view.i32(channel_obj+fields['<ChannelSheetId>k__BackingField'])
    if channel!=(target.get('current_channel') or {}).get('id'):raise MapReadPending('current interaction entity channel changed')
    rows=[r for r in view.spawned(replicated,channel,definition_id=target['definition_id']) if r['entity_key']==expected]
    if len(rows)!=1:raise MapReadPending('current interaction entity removed/recycled or not present')
    return dict(confirmed=True,source=SOURCE,source_hash=source_pins()['source_hash'],target=rows[0],
        observed_at=datetime.now(timezone.utc).isoformat(),caller_final_stability_check_required=True)
