"""Current owned InstantProp interaction through the actual HUD action control.

Import is inert. Readers are opt-in, use bounded read-only roots and never call
game functions. Static quest IDs never become screen positions or input keys.
"""
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,struct
from .paths import root
from .material_escape import EscapeView
from .quest_ui_actions import QuestUIActionSession,prepare_pointer_proposal,pointer_effect_contract
from .vendor.map_character_context import active_context
from .vendor.map_character_identity import CharacterMemoryReader
from .vendor.map_memory import MapReadPending

SOURCE_REF='data/arbeit-world-object.json'
TARGET_ID='world_object.interact'
KEY_TYPE='MM.Client.Framework.ComponentProcessing.EntityKey'

@lru_cache(maxsize=1)
def world_ui_source():return json.loads((root()/SOURCE_REF).read_text(encoding='utf8'))

def read_nullable_entity_key(session,pointer,schema,field_name):
    """Verify the actual closed Nullable<EntityKey>, preserving both key parts."""
    r=session.reader;klass=r.u64(pointer);fields=session.fields(klass,schema)
    declaration=next(f for f in schema['fields'] if f['name']==field_name)
    ty=declaration['type'];args=ty.get('arguments',[])
    if ty.get('name')!='Nullable`1' or ty.get('type_code')!=21 or len(args)!=1 or args[0].get('definition_index')!=55869:
        raise ValueError('source Nullable<EntityKey> type required')
    table=r.u64(klass+0x80);tp=r.u64(table+declaration['index']*32+8)
    if ((r.u32(tp+8)>>16)&255)!=21:raise ValueError('current EntityKey nullable type changed')
    generic=r.u64(tp);closed=struct.unpack('<Q',session.watch(generic+24,8))[0]
    if not closed:raise MapReadPending('current Nullable<EntityKey> closed layout not initialized')
    if r.class_name(closed)!=('Nullable`1','System'):raise ValueError('current EntityKey nullable class changed')
    inst=r.u64(generic+8);argc=r.u64(inst);arguments=r.u64(inst+8)
    if argc!=1:raise ValueError('current EntityKey nullable generic argument count changed')
    arg=r.u64(arguments)
    if ((r.u32(arg+8)>>16)&255)!=17 or type_definition_index(session, arg)!=55869:
        raise ValueError('current EntityKey nullable generic argument domain changed')
    rows={f['name']:f for f in r.layout(closed,['hasValue','value'])['fields']};hf,vf=rows['hasValue'],rows['value']
    if (hf['type_code'],vf['type_code'])!=(2,17):raise ValueError('current EntityKey nullable field type changed')
    ho,vo=hf['offset']-16,vf['offset']-16
    size=min(f['offset'] for f in schema['fields'] if f['offset']>fields[field_name])-fields[field_name]
    if size!=12 or not(0<=ho<size and 0<=vo<=size-8 and not vo<=ho<vo+8):raise ValueError('current EntityKey nullable layout exceeds source span')
    raw=session.watch(pointer+fields[field_name],size);flag=raw[ho]
    if flag not in (0,1):raise ValueError('current EntityKey nullable presence invalid')
    if not flag:return None
    a,b=struct.unpack_from('<ii',raw,vo)
    return dict(declared_type=KEY_TYPE,Type=a,InnerKey=b,raw_hex=raw[vo:vo+8].hex())

def _key(value):
    if not isinstance(value,dict) or value.get('declared_type')!=KEY_TYPE:return None
    if any(type(value.get(k)) is not int or not -(2**31)<=value[k]<2**31 for k in ('Type','InnerKey')):return None
    raw=struct.pack('<ii',value['Type'],value['InnerKey']).hex()
    return (value['Type'],value['InnerKey']) if value.get('raw_hex')==raw else None

OCCURRENCE=('quest_id','instance_id','accept_time','step_id','objective_index')

def bind_world_target(typed,frame,node):
    """Join the actual selected entity to the owned current objective program."""
    from .arbeit_operations import _fresh
    sem=typed['pointer_semantics'];current=frame.get('current_quest') or {}
    if current.get('confirmed') is not True or current.get('quest_id')!=node.get('quest_id'):
        raise ValueError('current owned quest occurrence required for world interaction')
    owner={k:node.get(k,current.get(k)) for k in OCCURRENCE}
    if any(owner.get(k) is None for k in OCCURRENCE):raise ValueError('current world objective occurrence incomplete')
    if any(current.get(k)!=owner[k] for k in ('quest_id','instance_id','accept_time','step_id')):
        raise ValueError('current world objective accepted occurrence changed')
    process=frame.get('process_started_at')
    rows=[r for r in frame.get('current_world_targets',[]) if
        r.get('confirmed') is True and r.get('owner_verified') is True and _fresh(r,frame) and
        all(r.get(k)==owner[k] for k in OCCURRENCE) and r.get('process_started_at')==process and
        r.get('session_id')==frame.get('session_id') and r.get('character_id')==frame.get('character_id') and
        (r.get('target') or {}).get('domain')=='InstantProp' and
        r['target'].get('definition_id')==sem['instant_prop_id'] and _key(r['target'].get('entity_key'))==_key(sem['entity_key'])]
    if len(rows)!=1:raise MapReadPending('current selected entity is not one owned spawned InstantProp target')
    from .world_target_observer import owned_instant_prop_program
    program,normalized=owned_instant_prop_program(node,frame)
    data=program.get('data') or {};value=(data.get('values') or {}).get('InstantPropName') or {}
    if data.get('type') not in ('MM.QDBL.CodeGenerated.InteractInstantPropQuestObjectiveData','MM.QDBL.CodeGenerated.GainFakeItemByInteractInstantPropQuestObjectiveData'):
        raise ValueError('current typed objective is not an InstantProp interaction')
    if value.get('value')!=sem['instant_prop_id'] or value.get('domain_mapping_verified') is not True or value.get('table_domain')!='InstantPropSheet':
        raise ValueError('current objective InstantProp definition differs from selected target')
    effect=program.get('typed_effect') or {}
    if effect.get('complete') is True:raise MapReadPending('current world objective already complete')
    bound=dict(owner,node_id=node['id'],ownership_verified=True,current_identity_bound=True,source_bound=True,
        source=typed['source'],source_refs=typed['source_refs'],entity_key=deepcopy(sem['entity_key']),instant_prop_id=sem['instant_prop_id'],
        target_identity_pointer=rows[0]['target'].get('identity_pointer'),hud_action_id=sem['hud_action_id'],interaction_id=sem['interaction_id'])
    from .objective_inventory_policy import bind_fake_acquisition
    acquisition=bind_fake_acquisition(node,frame)
    if acquisition is not None:bound['fake_acquisition']=acquisition
    count=effect.get('count') if effect.get('confirmed') is True else None
    typed['spawned_target']=deepcopy(rows[0]['target'])
    typed['pointer_semantics'].update(occurrence=owner,objective_count=count)
    return bound,dict(objective_count=count,objective_complete=effect.get('complete') if effect.get('confirmed') is True else None)

class WorldObjectSession(QuestUIActionSession):
    def source_shape(self,name,namespace=None):
        source=world_ui_source()
        choices=[c for c in source['classes'] if c['name']==name and (namespace is None or c['namespace']==namespace)]
        if len(choices)==1:return choices[0]
        if choices:raise ValueError('ambiguous world UI source class: '+name)
        return super().source_shape(name,namespace)

    def current_hud_instances(self):
        # Same bounded UIInstanceHandleTable traversal, exact Hud namespace.
        from .vendor.map_character_context import _class
        r=self.reader;shape=self.source_shape('PresentationSingleton');klass=_class(r,self.base,shape)
        fields=self.fields(klass,shape);statics=r.u64(klass+0xb8)
        instance=struct.unpack('<Q',self.watch(statics+fields['instance'],8))[0]
        if not instance or r.u64(instance)!=klass:raise MapReadPending('current presentation singleton missing')
        service=self.field(instance,'PresentationSingleton','<UIInstanceService>k__BackingField')
        f=self.object_fields(service,'UIInstanceService');table=self.ref(service,f,'handleTable')
        f=self.object_fields(table,'UIInstanceHandleTable');dictionary=self.ref(table,f,'handleTable');rows=[]
        for _,handle in self.dictionary_values(dictionary,limit=self.plan['limits']['instances']):
            if self.field(handle,'UIInstanceHandle','<Status>k__BackingField')!=2:continue
            pointer=self.field(handle,'UIInstanceHandle','<Instance>k__BackingField')
            if pointer and r.class_name(r.u64(pointer))==('HudActionUIInstance','MM.Client.Presentation.UI.Instances.StageScene.Hud'):rows.append(pointer)
        return rows

    def current_world_ui(self,frame):
        self._guards=[];context=active_context(self.reader,self.base,self.character_plan)
        identity=CharacterMemoryReader._value(self.reader,context['identity_pointer'],self.identity_class,self.identity_shape)
        if identity['character_id']!=str(frame.get('character_id')):
            raise MapReadPending('current HUD reader character differs from bound observation')
        guard=self.character_plan['context']['patch_guard'];wrapper=self.reader.u64(self.base+guard['wrapper_slot_rva'])
        if self.reader.class_name(wrapper)!=('ILFixDynamicMethodWrapper','IFix'):raise ValueError('current HUD input patch registry changed')
        array=self.reader.u64(self.reader.u64(wrapper+0xb8));count=self.reader.u64(array+24)
        if not 0<=count<=100000:raise ValueError('current HUD input patch registry count invalid')
        patch_slot=world_ui_source()['hud_input_patch_slot']
        if count>patch_slot and struct.unpack('<Q',self.watch(array+32+patch_slot*8,8))[0]:
            raise MapReadPending('current HudActionUIComponent.UpdateInput patched implementation unsupported')
        self.watch(context['chain'][2],8)
        value=WorldUIView(self).read(context['chain'][2],frame)
        node=frame.get('world_object_node')
        if not isinstance(node,dict):raise ValueError('current world interaction node required')
        bind_world_target(value,frame,node)
        from .world_target_observer import verify_current_spawned_target
        value['spawned_target_recheck']=verify_current_spawned_target(self,value['spawned_target'],context=context)
        if active_context(self.reader,self.base,self.character_plan)['chain']!=context['chain']:
            raise MapReadPending('current world interaction character root changed')
        if CharacterMemoryReader._value(self.reader,context['identity_pointer'],self.identity_class,self.identity_shape)['character_id']!=identity['character_id']:
            raise MapReadPending('current world interaction character identity changed during read')
        self.assert_stable();return value

class WorldUIView(EscapeView):
    def __init__(self,session):
        self.s=session;self.r=session.reader;self.source=world_ui_source()
        if any(session.plan.get(k)!=self.source[k] for k in ('game_assembly_sha256','metadata_sha256')):
            raise ValueError('world interaction source revision mismatch')

    def refs(self,pointer,element,limit=32):
        r=self.r;klass=r.u64(pointer);name,ns=r.class_name(klass)
        if name not in ('List`1','MMList`1','MMReadOnlyList`1'):raise ValueError('unexpected HUD list type')
        for _ in range(4):
            fields={f['name'].lstrip('_'):f for f in r.layout(klass)['fields']}
            if {'size','version'}<=fields.keys() and ('items' in fields or 'array' in fields):break
            klass=r.u64(klass+0x58)
        else:raise ValueError('HUD list current fields absent')
        a=fields.get('items',fields.get('array'));count=fields['size'];version=fields['version']
        if (a['type_code'],count['type_code'],version['type_code'])!=(29,8,8):raise ValueError('HUD list current field types changed')
        n=struct.unpack('<i',self.s.watch(pointer+count['offset'],4))[0];self.s.watch(pointer+version['offset'],4)
        if not 0<=n<=limit:raise ValueError('current HUD list exceeds bound')
        array=struct.unpack('<Q',self.s.watch(pointer+a['offset'],8))[0]
        capacity,stride=self.s.array(array,name=element+'[]',stride=8,limit=128)
        if n>capacity:raise ValueError('current HUD list exceeds array')
        return list(struct.unpack('<'+'Q'*n,self.s.watch(array+32,8*n))) if n else []

    def int_pairs(self,pointer,limit=32):
        d=self.r.dictionary(pointer)
        if d.get('kind')!='manual_dense' or not 0<=d['count']<=limit:raise ValueError('current HUD action dictionary type/count changed')
        keys,values=d['arrays']['keys'],d['arrays']['values']
        if (keys['name'],keys['stride'],values['name'],values['stride'])!=('Int32[]',4,'HudActionClickActionType[]',4):raise ValueError('current HUD action ID domain changed')
        self.s.watch(pointer+d['version_offset'],4);self.s.watch(pointer+d['count_offset'],4)
        n=d['count'];a=self.s.watch(int(keys['pointer'],16)+32,n*4);b=self.s.watch(int(values['pointer'],16)+32,n*4)
        return list(zip(struct.unpack('<'+'i'*n,a),struct.unpack('<'+'i'*n,b)))

    def delegate(self,pointer):
        if self.value(pointer,'MulticastDelegate','delegates'):raise MapReadPending('current HUD callback multicast unsupported')
        return dict(pointer=pointer,method=self.value(pointer,'Delegate','method_ptr'),target=self.value(pointer,'Delegate','m_target'))

    def read(self,system,frame):
        self.verify_methods();s=self.s
        interaction=self.value(system,'SystemContainers','InteractionComponent')
        hud=self.value(system,'SystemContainers','HudActionComponent')
        main=self.value(system,'SystemContainers','MainButtonStateComponent')
        key=read_nullable_entity_key(s,interaction,self.shape('InteractionComponent'),'<TargetKey>k__BackingField')
        old=read_nullable_entity_key(s,hud,self.shape('HudActionComponent'),'lastTargetKey')
        if key is None or _key(key)!=_key(old):raise MapReadPending('current HUD target key is absent or not updated')
        state=self.value(main,'MainButtonStateComponent','<MainButtonState>k__BackingField')
        if state!=self.shape('MainButtonState')['enum_constants']['Interaction'] or self.value(hud,'HudActionComponent','lastMainButtonState')!=state:
            raise MapReadPending('current HUD main button is not the Interaction branch')
        interaction_id=self.value(interaction,'InteractionComponent','<TargetInteractionSheetId>k__BackingField')
        prop=self.value(interaction,'InteractionComponent','<TargetInstantPropSheetId>k__BackingField')
        if not prop or interaction_id!=self.value(hud,'HudActionComponent','lastTargetInteractionId'):
            raise MapReadPending('current HUD InstantProp interaction target not updated')
        if self.value(interaction,'InteractionComponent','currentRunningInteractionSheetId'):
            raise MapReadPending('current world interaction is already running')
        ids=self.int_pairs(self.value(hud,'HudActionComponent','hudActionIds'))
        data=[]
        for pointer in self.refs(self.value(hud,'HudActionComponent','hudActionDatas'),'IHudActionData'):
            if self.r.class_name(self.r.u64(pointer))!=('InteractHudActionData','MM.Client.PresentationContracts.ComponentInterfaces.Types'):continue
            ident=self.value(pointer,'InteractHudActionData','<Id>k__BackingField')
            if ids.count((ident,self.shape('HudActionClickActionType')['enum_constants']['MainButton']))!=1:continue
            data.append(dict(id=ident,sheet_id=self.value(pointer,'InteractHudActionData','<HudActionSheetId>k__BackingField')))
        if len(data)!=1:raise MapReadPending('current one Interaction HUD action required')
        panels=s.current_hud_instances()
        if len(panels)!=1:raise MapReadPending('current one HudActionUIInstance required')
        panel=panels[0];ui=self.value(panel,'HudActionUIInstance','ui')
        entity=self.value(panel,'HudActionUIInstance','entityComponents')
        component=self.value(entity,'HudActionUIEntityComponents','<HudActionUIComponent>k__BackingField')
        if self.value(component,'HudActionUIComponent','ui')!=ui:raise ValueError('current HUD component UI root differs')
        if not self.value(component,'HudActionUIComponent','isVisible') or any(self.value(component,'HudActionUIComponent',k) for k in ('isTweening','isNeedUpdate')):
            raise MapReadPending('current HUD UI not visible/stable')
        group=self.value(ui,'HudActionUI','buttonSlotGroupControl');matches=[]
        for slot_name in ('mainSlotControl','subSlotControl','interactionSlotControl'):
            slot=self.value(group,'HudActionButtonSlotGroupControl',slot_name)
            name,ns=self.r.class_name(self.r.u64(slot))
            if name not in ('HudActionButtonSlotControl','HudActionPageableButtonSlotControl'):raise MapReadPending('current HUD slot implementation unsupported')
            container=self.value(slot,name,'<Container>k__BackingField')
            creator=self.value(container,'HudActionButtonSlotButtonControlContainer','creatorHolder')
            mapped=s.dictionary_values(self.value(creator,'HudActionButtonCreatorHolder','controls'),key_type='Int32',value_type='IHudActionButtonControl',limit=64)
            for control in self.refs(self.value(container,'HudActionButtonSlotButtonControlContainer','buttonControls'),'IHudActionButtonControl'):
                if (data[0]['id'],control) not in mapped:continue
                if self.r.class_name(self.r.u64(control))!=('BasicHudActionButtonControl','MM.Client.Presentation.UI.Controls.MainHud.HudActionUI'):continue
                base=self.value(control,'BasicHudActionButtonControl','baseControl')
                if not self.value(base,'BaseHudActionButtonControl','<IsVisible>k__BackingField') or self.value(base,'BaseHudActionButtonControl','<CurrentSlot>k__BackingField')!=slot:continue
                button=self.value(base,'BaseHudActionButtonControl','<Button>k__BackingField')
                if any(not self.value(button,'ButtonControl',k) for k in ('enabled','interactable')):continue
                callback=self.delegate(self.value(control,'BasicHudActionButtonControl','onClick'))
                if callback['method']!=s.base+self.source['callback_rva']:raise ValueError('current HUD callback source changed')
                closure=callback['target'];slot_pointer=self.value(control,'BasicHudActionButtonControl','uiSlot')
                if self.value(closure,'<>c__DisplayClass19_6','slot')!=slot_pointer:raise ValueError('current HUD callback slot differs')
                outer=self.value(closure,'<>c__DisplayClass19_6','CS$<>8__locals6')
                bound=self.value(outer,'<>c__DisplayClass19_0','data')
                if self.value(bound,'BasicHudActionButtonData','<Id>k__BackingField')!=data[0]['id']:raise ValueError('current HUD callback data ID differs')
                if self.value(bound,'BasicHudActionButtonData','<HudActionSheetId>k__BackingField')!=data[0]['sheet_id']:raise ValueError('current HUD callback action sheet differs')
                selection=self.value(button,'ButtonControl','<SelectionControl>k__BackingField')
                if self.value(selection,'DefaultButtonSelectionControl','ClickedWithTouchInputMethod')!=callback['pointer']:
                    raise ValueError('current native button does not own the HUD callback')
                if any(self.value(selection,'DefaultButtonSelectionControl',k) for k in ('ClickedWithTouchDeviceType','ClickedWithMouseButtonType')):
                    raise MapReadPending('current HUD button has additional input callbacks')
                geometry=s.read_control_geometry(button,panel,frame)
                geometry.update(button_pointer=button,window_id=panel,window_kind='native-ui-root',index=0,
                    event_kind='world-object-hud-action',model_event_id=callback['pointer'])
                matches.append((geometry,dict(callback=callback,hud_action_id=data[0]['id'],hud_action_sheet=data[0]['sheet_id'],
                    interaction_id=interaction_id,instant_prop_id=prop,entity_key=key,system_pointer=system,slot_pointer=slot_pointer,
                    hud_pointer=hud,interaction_pointer=interaction,main_state=state)))
        if len(matches)!=1:raise MapReadPending('current exact owned Interaction HUD control not unique')
        candidate,semantics=matches[0];pointer=s.current_pointer_state()
        return dict(candidate=candidate,pointer_semantics=semantics,pointer_state=pointer,ui_pointer=panel,
            source='current rooted HUD action callback and InteractionComponent target',source_hash=self.source['game_assembly_sha256'],
            observed_at=pointer['observed_at'],source_refs=[SOURCE_REF+'#sha256='+hashlib.sha256((root()/SOURCE_REF).read_bytes()).hexdigest()])

def world_object_scene_reader(session,binding,frame):
    """Last-moment target/control/callback re-observation for the shared guard."""
    if binding.get('source_guard_kind')!='world-object-interaction':raise ValueError('wrong world interaction guard domain')
    node=frame.get('world_object_node')
    if not node or node.get('id')!=binding.get('node_id'):raise ValueError('current world interaction node changed')
    typed=session.current_world_ui(frame);bind_world_target(typed,frame,node)
    return typed['candidate'],typed

def build_world_proposal(typed,frame,node):
    bound,fields=bind_world_target(typed,frame,node);candidate=typed['candidate']
    pointer=prepare_pointer_proposal(candidate,typed,frame)
    pointer['probe_context'].update(source_guard_kind='world-object-interaction',target_id=TARGET_ID,node_id=node['id'])
    observed=typed['observed_at'];contract=pointer_effect_contract(pointer,bound,node,frame,observed)
    effect=dict(kind='ui_state',phase='world_interaction',node_id=node['id'],**{k:bound[k] for k in OCCURRENCE},
        fields=fields,source=typed['source'],source_revision=typed['source_hash'],source_refs=typed['source_refs'],
        observed_at=observed,session_id=frame['session_id'],character_id=frame['character_id'])
    expected={k:v for k,v in effect.items() if k not in ('fields','source','source_refs','observed_at','session_id','character_id')}
    if fields['objective_count'] is not None:expected['changed_fields']={'objective_count':fields['objective_count']}
    else:expected['fields']={'objective_complete':True}
    if pointer['input_kind']=='pointer_probe':expected=contract['expected_effect']
    x,y,w,h=candidate['rect_client'];point=pointer['point_client']
    safe=(frame.get('current_context') or {}).get('safe_rect') or frame.get('safe_rect')
    if not safe:raise ValueError('current world interaction safe rectangle required')
    action=dict(kind=pointer['input_kind'],role=TARGET_ID,point=list(point),
        source_rect=[x,y,x+w,y+h],safe_rect=list(safe),
        probe_context=pointer['probe_context'],pointer_verified=pointer.get('pointer_verified'),input_epoch=pointer['probe_context']['input_epoch'])
    proposal=dict(status='ready',action=action,source_refs=typed['source_refs'],current_binding=bound,
        expected_effect=expected,observed_at=observed,session_id=frame['session_id'],character_id=frame['character_id'],
        observation_source=typed['source'],
        context=dict(frame_id=frame['frame_id'],window_signature=frame['window_signature'],observed_at=observed,
            session_id=frame['session_id'],character_id=frame['character_id'],source_revision=typed['source_hash'],
            input_owner=frame['input_owner']))
    return dict(action_proposals={node['id']:proposal},ui_effect_proofs=[effect]+contract['effect_proofs'],
        interaction_ready=[dict(bound,kind='owned_objective_interaction',confirmed=True,observed_at=observed,
            session_id=frame['session_id'],character_id=frame['character_id'],process_started_at=frame['process_started_at'])],
        world_ui=typed,missing=[])

class WorldObjectUIProvider:
    def __init__(self,*,enabled=False,session_factory=WorldObjectSession):
        self.enabled=enabled;self.session_factory=session_factory

    def __call__(self,*,window,node,snapshot,frame,image=None):
        out=dict(action_proposals={},ui_effect_proofs=[],interaction_ready=[],missing=[],game_inputs=0)
        if node.get('op')!='objective.inspect':return out
        if not self.enabled:out['missing']=['world interaction read transport disabled'];return out
        from .arbeit_operations import _stamp
        pid=getattr(window,'pid',None) or frame.get('pid') or (frame.get('window') or {}).get('pid')
        try:
            if type(pid) is not int or pid<=0:raise ValueError('bound world interaction PID required')
            for k in ('current_world_targets','objective_programs','current_quest','input_owner'):
                if k in snapshot:frame[k]=deepcopy(snapshot[k])
            frame['world_object_node']=deepcopy(node);frame['now']=snapshot.get('now',frame.get('captured_at'))
            with self.session_factory(pid) as session:
                if session.reader.pid!=pid or session.reader.started_at!=_stamp(frame['process_started_at']):
                    raise ValueError('current world interaction process lifetime changed')
                typed=session.current_world_ui(frame)
            out.update(build_world_proposal(typed,frame,node))
        except (OSError,RuntimeError,ValueError,TypeError,KeyError,AttributeError) as exc:out['missing'].append(str(exc))
        return out
