"""Current Talk HUD entry; opening a menu is not accepting/reporting a quest."""
from copy import deepcopy
import hashlib,struct
from .direct_dialogue_source import direct_source
from .paths import root
from .dialogue_binding import DialogueBindingSession,validate_current_dialogue
from .world_object_observer import WorldObjectSession,WorldUIView,world_ui_source,_key,read_nullable_entity_key
from .quest_ui_actions import prepare_pointer_proposal,pointer_effect_contract
from .vendor.map_character_context import active_context,_class
from .vendor.map_character_identity import CharacterMemoryReader
from .vendor.map_memory import MapReadPending

SOURCE_REF='data/direct-dialogue-source.json'
TARGET='direct_dialogue.menu.open'
SCOPE=('quest_id','instance_id','accept_time','step_id','objective_index')


def entry_request(node,frame):
    from .arbeit_operations import _fresh
    from .objective_binding import current_objective_occurrence
    now=dict(frame,now=frame.get('captured_at',frame.get('now')))
    accept=node.get('op')=='dialogue.accept' or (node.get('op')=='move.target' and node.get('params',{}).get('role')=='acceptance')
    if accept:
        rows=[r for r in frame.get('offers',[]) if r.get('quest_id')==node.get('quest_id')]
        if len(rows)!=1:raise MapReadPending('direct_entry_unique_current_offer_required')
        row=rows[0];giver=row.get('giver_definition') or {};version=row.get('source_version') or {}
        if (row.get('source')!='current-root-typed-quest-collections' or not _fresh(row,now)
            or row.get('process_started_at')!=frame.get('process_started_at')
            or any(row.get(k) is not True for k in ('confirmed','eligible','conditions_met','eligibility_verified','available_candidate'))
            or any(version.get(k)!=direct_source()[k] for k in ('game_assembly_sha256','metadata_sha256'))
            or row.get('accept_time') not in (None,0) or row.get('instance_id') is None
            or giver.get('kind')!='npc' or giver.get('declared_type')!='Shared.Symbols.NPCSheetId'
            or type(giver.get('npc_id')) is not int):
            raise MapReadPending('direct_entry_current_eligible_offer_sender_required')
        return dict(node_id=node['id'],quest_id=node['quest_id'],instance_id=row['instance_id'],
            waiting_instance_id=row['instance_id'],npc_ids=[giver['npc_id']],ownership_scope='waiting_instance_menu_only',
            accepted_occurrence=False,source_evidence=deepcopy(row))
    if node.get('op') not in ('dialogue.talk','dialogue.report','objective.deliver'):
        raise ValueError('direct_entry_operation_unsupported')
    programs=[r for r in frame.get('objective_programs',[]) if r.get('confirmed') is True and r.get('owner_verified') is True
        and _fresh(r,now) and current_objective_occurrence(r,frame,node)]
    if len(programs)!=1:raise MapReadPending('direct_entry_current_objective_occurrence_required')
    program=programs[0];data=program.get('data') or {};npc=(data.get('values') or {}).get('NpcName') or {}
    ids=[]
    if (data.get('available') is True and data.get('discriminant',{}).get('verified') is True
        and npc.get('declared_type')=='Shared.Symbols.NPCSheetId' and npc.get('member')=='Id'
        and npc.get('domain_mapping_verified') is True and npc.get('interpreted') is True and type(npc.get('value')) is int):
        ids=[npc['value']]
    groups=[]
    if not ids:
        from .group_npc_observer import owned_group_program,source_pins,SOURCE as GROUP_SOURCE
        _,normal=owned_group_program(node,frame);group=normal['operands']['npc_group_tag']
        pins=source_pins()
        groups=[r for r in frame.get('current_group_npc_targets',[]) if r.get('confirmed') is True and r.get('owner_verified') is True
            and _fresh(r,now) and current_objective_occurrence(r,frame,node)
            and r.get('source')==GROUP_SOURCE and r.get('source_hash')==pins['source_hash']
            and r.get('group_tag')==group and (r.get('membership') or {}).get('group_name')==group['value']]
        ids=sorted({r['target']['definition_id'] for r in groups if r.get('target',{}).get('domain')=='NPC'})
    if not ids:raise MapReadPending('direct_entry_current_explicit_NpcName_or_group_membership_required')
    return dict({k:program[k] for k in SCOPE},node_id=node['id'],npc_ids=ids,
        ownership_scope='accepted_occurrence_menu_only',accepted_occurrence=True,
        source_evidence=deepcopy(program),group_targets=deepcopy(groups))


class DirectDialogueUIView(WorldUIView):
    def __init__(self,session):
        super().__init__(session)
        if any(session.plan.get(k)!=direct_source()[k] for k in ('game_assembly_sha256','metadata_sha256')):
            raise ValueError('direct dialogue source/transport revision mismatch')
        self.source=dict(self.source,classes=self.source['classes']+[c for c in direct_source()['classes']
            if c['name'] not in {x['name'] for x in self.source['classes']}])

    def talk_interaction_id(self):
        shape=self.shape('InteractionSheetId_');r=self.r;s=self.s
        klass=_class(r,s.base,shape);fields=s.fields(klass,shape)
        statics=struct.unpack('<Q',s.watch(klass+0xb8,8))[0]
        instance=struct.unpack('<Q',s.watch(statics+fields['_instance'],8))[0] if statics else 0
        if not instance or r.u64(instance)!=klass:raise MapReadPending('current_InteractionSheetId_Talk_symbol_uninitialized')
        value=self.value(instance,'InteractionSheetId_','Talk')
        if not value:raise ValueError('current Talk interaction symbol is invalid')
        return value

    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 prop or interaction_id!=self.talk_interaction_id() or interaction_id!=self.value(hud,'HudActionComponent','lastTargetInteractionId'):
            raise MapReadPending('current HUD Talk 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='direct-dialogue-entry',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=[ref+'#sha256='+hashlib.sha256((root()/ref).read_bytes()).hexdigest()
                for ref in ('data/arbeit-world-object.json',SOURCE_REF)])



def bind_current_entry_npc(session,typed,request,*,context=None):
    """Join an actual HUD key to the current rooted NPC, never a nearby guess."""
    from .group_npc_observer import read_current_npc_target
    key=typed['pointer_semantics']['entity_key'];matches=[]
    if _key(key) is None:raise ValueError('direct entry typed HUD entity key required')
    for npc in request['npc_ids']:
        groups=[g for g in request.get('group_targets',[]) if g.get('target',{}).get('definition_id')==npc
                and _key(g['target'].get('entity_key'))==_key(key)]
        if request.get('group_targets') and len(groups)!=1:continue
        group=((groups[0]['target'].get('group_membership') or {}).get('group_name')) if groups else None
        try:proof=read_current_npc_target(session,key,npc,context=context,group_name=group)
        except MapReadPending:continue
        matches.append(proof)
    if len(matches)!=1:raise MapReadPending('direct entry current HUD target is not one owned NPC')
    return matches[0]


class _EntryDialogueSession(DialogueBindingSession):
    def verify_name_source(self):
        # Entry uses current NPC identity only, so no QDBL file read is needed.
        return {'verified':False,'missing':'static quest name binding not used for menu entry'}


class DirectDialogueEntrySession(WorldObjectSession):
    def source_shape(self,name,namespace=None):
        rows=[c for c in direct_source()['classes'] if c['name']==name and (namespace is None or c['namespace']==namespace)]
        if len(rows)==1:return rows[0]
        return super().source_shape(name,namespace)

    def current_direct_entry(self,frame):
        self._guards=[];r=self.reader
        proxy=_EntryDialogueSession.__new__(_EntryDialogueSession);proxy.__dict__.update(self.__dict__)
        dialogue=proxy.current_dialogue();self._guards=proxy._guards
        p=dialogue.get('player') or {};rep=dialogue.get('replicated') or {}
        if p.get('is_active') is True or rep.get('is_playing') is True:
            return dict(stage='dialogue',dialogue_snapshot=dialogue,observed_at=dialogue['observed_at'])
        if p and p.get('is_active') is not False:raise MapReadPending('current dialogue active state unavailable')
        node=frame['direct_dialogue_entry_node'];request=entry_request(node,frame)
        context=active_context(r,self.base,self.character_plan)
        identity=CharacterMemoryReader._value(r,context['identity_pointer'],self.identity_class,self.identity_shape)
        if str(identity['character_id'])!=str(frame.get('character_id')):raise MapReadPending('direct entry character changed')
        guard=self.character_plan['context']['patch_guard'];wrapper=r.u64(self.base+guard['wrapper_slot_rva'])
        if r.class_name(wrapper)!=('ILFixDynamicMethodWrapper','IFix'):raise ValueError('current HUD patch registry changed')
        array=r.u64(r.u64(wrapper+0xb8));count=r.u64(array+24)
        if not 0<=count<=100000:raise ValueError('current HUD patch registry count invalid')
        slot=world_ui_source()['hud_input_patch_slot']
        if count>slot and struct.unpack('<Q',self.watch(array+32+slot*8,8))[0]:
            raise MapReadPending('current HUD UpdateInput implementation patched')
        typed=DirectDialogueUIView(self).read(context['chain'][2],frame)
        proof=bind_current_entry_npc(self,typed,request,context=context)
        target=proof['target'];typed.update(stage='hud',entry_request=request,npc_target_proof=proof,dialogue_snapshot=dialogue)
        typed['pointer_semantics'].update(action_kind='direct-dialogue-entry',npc_id=target['definition_id'],
            npc_target=deepcopy(target),npc_identity_source_hash=proof['source_hash'],entry_scope={k:request.get(k) for k in SCOPE},node_id=node['id'],
            direct_source_revision=hashlib.sha256((root()/SOURCE_REF).read_bytes()).hexdigest())
        typed['source_refs'].append(SOURCE_REF)
        if active_context(r,self.base,self.character_plan)!=context:raise MapReadPending('direct entry roots changed')
        if CharacterMemoryReader._value(r,context['identity_pointer'],self.identity_class,self.identity_shape)!=identity:
            raise MapReadPending('direct entry identity changed')
        self.assert_stable();return typed


def direct_dialogue_entry_scene(session,binding,frame):
    if binding.get('source_guard_kind')!='direct-dialogue-entry':raise ValueError('wrong direct entry guard family')
    node=(binding.get('direct_dialogue_entry_request') or {}).get('node')
    if not isinstance(node,dict) or node.get('id')!=binding.get('node_id'):raise ValueError('direct entry node changed')
    actual=dict(frame,direct_dialogue_entry_node=deepcopy(node))
    typed=session.current_direct_entry(actual)
    if typed.get('stage')!='hud':raise MapReadPending('dialogue already opened before HUD input')
    return typed['candidate'],typed


def menu_effect(dialogue,frame,node,receipt):
    inner=(receipt or {}).get('planner_receipt',receipt or {});expected=inner.get('expected_effect') or {}
    if expected.get('phase')!='direct_dialogue_menu_opened' or expected.get('node_id')!=node.get('id'):
        return None
    if expected.get('quest_id')!=node.get('quest_id') or validate_current_dialogue(dialogue,frame):return None
    try:request=entry_request(node,frame)
    except (ValueError,RuntimeError):return None
    if any(expected.get(k) is not None and request.get(k)!=expected[k] for k in SCOPE):return None
    player=dialogue['player'];rep=dialogue['replicated'];fields=expected.get('fields') or {}
    if (_key(player.get('target_entity_key'))!=_key(fields.get('dialogue_entity_key'))
        or _key(player.get('target_entity_key')) is None or player.get('current_npc_id')!=fields.get('dialogue_npc_id')
        or player.get('target_object_id') is None or player.get('target_object_id')!=rep.get('target_object_id')):return None
    if any(inner.get(k)!=frame.get(k) for k in ('session_id','character_id')):return None
    if expected.get('process_started_at')!=frame.get('process_started_at'):return None
    return dict({k:v for k,v in expected.items() if k!='fields'},fields=deepcopy(fields),confirmed=True,owner_verified=True,
        source='current owned dialogue after exact Talk HUD target',observed_at=dialogue['observed_at'],
        session_id=frame['session_id'],character_id=frame['character_id'],
        quest_completion_inferred=False,quest_accepted_inferred=False,action_owner_verified=False)


def build_entry_proposal(typed,frame,node):
    if typed.get('stage')!='hud':raise ValueError('current Talk HUD required')
    request=entry_request(node,frame)
    if request!=typed.get('entry_request'):raise ValueError('current entry request changed')
    proof=typed.get('npc_target_proof') or {};target=proof.get('target') or {};sem=typed.get('pointer_semantics') or {}
    if (proof.get('confirmed') is not True or target.get('domain')!='NPC' or target.get('definition_id') not in request['npc_ids']
        or _key(target.get('entity_key')) is None or _key(target['entity_key'])!=_key(sem.get('entity_key'))):
        raise ValueError('current source NPC target proof required')
    owner=dict({k:request.get(k) for k in SCOPE},node_id=node['id'],npc_id=target['definition_id'],entity_key=deepcopy(target['entity_key']),
        ownership_verified=True,entry_only=True,action_owner_verified=False,ownership_scope=request['ownership_scope'])
    pointer=prepare_pointer_proposal(typed['candidate'],typed,frame)
    pointer['probe_context'].update(source_guard_kind='direct-dialogue-entry',target_id=TARGET,node_id=node['id'],
        direct_dialogue_entry_request={'node':deepcopy(node)})
    contract=pointer_effect_contract(pointer,owner,node,frame,typed['observed_at'])
    expected=dict({k:owner[k] for k in SCOPE},node_id=node['id'],kind='ui_state',phase='direct_dialogue_menu_opened',
        process_started_at=frame['process_started_at'],source_revision=typed['source_hash'],
        fields=dict(dialogue_menu_open=True,dialogue_npc_id=owner['npc_id'],dialogue_entity_key=owner['entity_key']))
    if pointer['input_kind']=='pointer_probe':expected=contract['expected_effect']
    from .coordinate_geometry import CLIENT_XYWH,safe_rect_xywh_from_frame
    x,y,w,h=typed['candidate']['rect_client'];safe=safe_rect_xywh_from_frame(frame)
    action=dict(kind=pointer['input_kind'],role=TARGET,point=pointer['point_client'],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'],
        safe_rect_format=CLIENT_XYWH)
    proposal=dict(status='ready',action=action,source_refs=typed['source_refs'],current_binding=owner,expected_effect=expected,
        observed_at=typed['observed_at'],session_id=frame['session_id'],character_id=frame['character_id'],observation_source=typed['source'],
        context=dict(source=typed['source'],observed_at=typed['observed_at'],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=contract['effect_proofs'],missing=[],entry_observation=typed)


class DirectDialogueEntryProvider:
    def __init__(self,*,enabled=False,session_factory=DirectDialogueEntrySession):
        self.enabled=enabled;self.session_factory=session_factory

    def __call__(self,*,window,node,snapshot,frame,image=None):
        out=dict(action_proposals={},ui_effect_proofs=[],missing=[],game_inputs=0)
        if node.get('op') not in ('dialogue.accept','dialogue.report','dialogue.talk','objective.deliver','move.target'):return out
        if node.get('op')=='move.target' and node.get('params',{}).get('role')!='acceptance':return out
        if not self.enabled:out['missing']=['direct dialogue entry transport disabled'];return out
        receipt=snapshot.get('pending_input') or frame.get('pending_input') or {}
        expected=(receipt.get('planner_receipt',receipt).get('expected_effect') or {})
        existing=snapshot.get('action_proposals',{}).get(node['id'])
        if existing and expected.get('phase')!='direct_dialogue_menu_opened':return out
        try:
            from .arbeit_operations import _stamp
            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 direct entry PID required')
            actual=dict(frame)
            for key in ('offers','objective_programs','current_quest','quest_state_read','current_group_npc_targets','input_owner','pending_input'):
                if key in snapshot:actual[key]=deepcopy(snapshot[key])
            actual['direct_dialogue_entry_node']=deepcopy(node)
            with self.session_factory(pid) as session:
                if session.reader.pid!=pid or session.reader.started_at!=_stamp(actual['process_started_at']):
                    raise ValueError('direct entry current process lifetime changed')
                typed=session.current_direct_entry(actual)
            out['entry_observation']=typed
            if typed.get('stage')=='dialogue':
                effect=menu_effect(typed['dialogue_snapshot'],actual,node,receipt)
                if effect:out['ui_effect_proofs'].append(effect)
                return out
            if existing:return out
            out.update(build_entry_proposal(typed,actual,node))
        except (OSError,RuntimeError,ValueError,KeyError,TypeError,AttributeError) as exc:out['missing'].append(str(exc))
        return out
