"""Typed UI first; caption OCR only inside unresolved current source controls.

OCR may identify a source rectangle but never supplies click geometry.
"""
from copy import deepcopy
import math
from .arbeit_targets import OfflineOCR, build_target, ui_map
from .dialogue_binding import DialogueBindingReader

TARGETS={'quest.offer':'arbeit.apply','dialogue.accept':'dialogue.accept',
    'dialogue.talk':'dialogue.keyword','dialogue.report':'dialogue.report',
    'objective.deliver':'delivery.submit','reward.claim':'reward.confirm'}
DIALOGUE_OPS={'dialogue.accept','dialogue.report','dialogue.talk'}


def _semantic(image,frame):
    return {'frame':{**frame,'width':image.width,'height':image.height},'facts':{},
        'source':'current typed source descriptors','uncertainty':[],'ocr_regions':[],
        'coordinate_bindings_supplied':False,'game_completion_confirmed':False}


def _dialogue_descriptors(snapshot,node):
    rows=[r['values'] for r in snapshot.get('details',{}).get('model',{}).get('rows',[])]
    rows=[r for r in rows if r.get('keywordId') and isinstance(r.get('displayName'),str)]
    if node['op']!='dialogue.talk':
        label=ui_map()['targets'][TARGETS[node['op']]]['label']
        rows=[r for r in rows if r['displayName']==label]
    return rows


def bind_named_dialogue(current_ui,node,semantic=None):
    """Exact current Name equality; no inherited Name/parent assumption."""
    result=deepcopy(current_ui);snapshot=result.get('layout_snapshot',{});target_id=TARGETS[node['op']]
    target=ui_map()['targets'][target_id];descriptors=_dialogue_descriptors(snapshot,node)
    if len(descriptors)!=1:return result
    desc=descriptors[0];candidates=[c for c in snapshot.get('candidates',[]) if c.get('tag') in target['runtime_layout_tags']]
    event=desc.get('onClickEvent',{})
    chosen=[c for c in candidates if event.get('event_present') is True and type(event.get('event_id')) is int
            and c.get('model_event_id')==event['event_id']]
    matched_by='current runtimeForest onclick equals current model onClickEvent'
    if len(chosen)!=1:
        chosen=[c for c in candidates if c.get('name')==desc['keywordId']]
        matched_by='current ElementBasicInfo.name equals descriptor.keywordId'
    if len(chosen)!=1:
        chosen=[];scale=snapshot.get('scale',0)
        for fact in (semantic or {}).get('facts',{}).values():
            if fact.get('observed_text')!=desc['displayName'] or not fact.get('box'):continue
            l,t,r,b=fact['box']
            for c in candidates:
                x,y,w,h=[v*scale for v in c['rect_ui']]
                if x<=l and y<=t and r<=x+w and b<=y+h:chosen.append(c)
        matched_by='current caption contained by one source rectangle'
    if len(chosen)!=1:return result
    c=chosen[0];binding=dict(c,frame_id=result.get('frame_id'),screen=target['screen'],
        label=desc['displayName'],source_bound=True,layout_stable=snapshot.get('layout_stable') is True,
        semantic_unique=True,enabled=desc.get('isEnable'),occluded=None,current_identity_bound=False,
        descriptor=desc,rect_source=c.get('rect_source','current-mmml-output'),layout_source=snapshot.get('source'),
        layout_revision=snapshot.get('source_hash'),semantic_binding=matched_by)
    result['bindings']={target_id:binding}
    result['missing']=[m for m in result.get('missing',[]) if not m.startswith('semantic glyph requires one source rectangle')]
    return result


class SourceUiObserver:
    def __init__(self,*,current_layout_reader=None,dialogue_reader=None,quest_ui_reader=None):
        self.current_layout_reader=current_layout_reader;self._ocr=None
        enabled=bool(getattr(current_layout_reader,'enabled',False))
        self.dialogue_reader=dialogue_reader or DialogueBindingReader(enabled=enabled)
        self.quest_ui_reader=quest_ui_reader;self._read_enabled=enabled

    def _captions(self,image,frame,snapshot,semantic,ocr):
        """Only bounded source rectangles, never an entire screen."""
        scale=snapshot.get('scale');rects=[]
        if type(scale) not in (int,float) or not math.isfinite(scale) or scale<=0:return semantic
        for row in snapshot.get('candidates',[]):
            rect=row.get('rect_ui')
            if not isinstance(rect,list) or len(rect)!=4 or not all(type(v) in (int,float) and math.isfinite(v) for v in rect):continue
            x,y,w,h=[v*scale for v in rect]
            box=(math.floor(x),math.floor(y),math.ceil(x+w),math.ceil(y+h))
            if (0<=box[0]<box[2]<=image.width and 0<=box[1]<box[3]<=image.height
                and box not in rects):rects.append(box)
        if not rects:return semantic
        if ocr is None:
            if self._ocr is None:self._ocr=OfflineOCR()
            ocr=self._ocr
        texts=[]
        for box in rects:
            for row in ocr.read(image.crop(box)):
                if float(row.get('score',0))<.82:continue
                a,b,c,d=row['box'];mapped=[a+box[0],b+box[1],c+box[0],d+box[1]]
                if not(box[0]<=mapped[0]<mapped[2]<=box[2] and box[1]<=mapped[1]<mapped[3]<=box[3]):continue
                fact={'observed_text':row['text'],'box':mapped,'score':row['score'],'source':'current source control crop OCR'}
                if fact not in texts:texts.append(fact)
        semantic['ocr_regions']=[list(r) for r in rects]
        semantic['source']='current source control crops plus typed descriptors'
        semantic['facts'].update({f'current_caption_{i}':f for i,f in enumerate(texts)})
        # Partial rows must never masquerade as whole-frame bag evidence.
        frame['ui_caption_rows']=texts;frame['ui_caption_rows_frame_id']=frame.get('frame_id') or frame.get('observation_id')
        return semantic

    def read(self,image,*,frame,graph,node,ocr=None):
        result={'source':'current_typed_UI_first','action_proposals':{},'ui_effect_proofs':[],
            'interaction_ready':[],'missing':[],'safe_rect':None}
        interaction_only=node.get('op')=='move.target' and node.get('params',{}).get('role')=='acceptance'
        if interaction_only:node=dict(node,op='dialogue.accept')
        if node['op'] not in TARGETS:return result
        semantic=_semantic(image,frame);result['semantic']=semantic
        if node['op'] in ('objective.deliver','reward.claim'):
            if self.quest_ui_reader is None:
                from .quest_ui_actions import QuestUIActionReader
                self.quest_ui_reader=QuestUIActionReader(enabled=self._read_enabled)
            current_ui=self.quest_ui_reader.read(frame=frame,node=node,semantic=semantic)
            snapshot=current_ui.get('layout_snapshot',{})
            if current_ui.get('proposal',{}).get('status')!='ready' and snapshot.get('candidates'):
                semantic=self._captions(image,frame,snapshot,semantic,ocr)
                model=snapshot.get('details',{}).get('model',{})
                labels={v.get('deliverButtonText') for v in (r.get('values',{}) for r in model.get('rows',[]))}
                for fact in list(semantic['facts'].values()):
                    if fact['observed_text'] in labels:semantic['facts']['delivery_submit_caption']=fact
                    if any(fact['observed_text']==i.get('name') for i in snapshot.get('details',{}).get('deliverable_items',[])):
                        semantic['facts']['delivery_item']=fact
                from .quest_ui_actions import bind_quest_ui_action
                current_ui=bind_quest_ui_action(snapshot,frame,node,semantic)
            result['ui_effect_proofs'].extend(current_ui.get('effect_proofs',[]))
        elif self.current_layout_reader is not None:
            read_node=dict(node,op='dialogue.report') if node['op']=='dialogue.talk' else node
            current_ui=self.current_layout_reader.read(frame=frame,node=read_node,semantic=semantic)
            if node['op'] in DIALOGUE_OPS:
                current_ui=bind_named_dialogue(current_ui,node)
                if not current_ui.get('bindings') and current_ui.get('layout_snapshot',{}).get('candidates'):
                    semantic=self._captions(image,frame,current_ui['layout_snapshot'],semantic,ocr)
                    current_ui=bind_named_dialogue(current_ui,node,semantic)
                dialogue=self.dialogue_reader.read(frame=frame,node=node,current_ui=current_ui,receipt=frame.get('pending_input'))
                result['dialogue_observation']=dialogue;result['ui_effect_proofs'].extend(dialogue.get('ui_effect_proofs',[]))
                result['missing'].extend(dialogue.get('missing',[]))
                if dialogue.get('dialogue_offer'):result['dialogue_offer']=dialogue['dialogue_offer']
                owner=deepcopy(dialogue.get('action_binding'));target=dialogue.get('target_id')
                if interaction_only and owner:
                    result['interaction_ready'].append({**owner,'kind':'owned_accept_dialogue',
                        'confirmed':True,'source':'current owned accept dialogue after native quest autoplay',
                        'quest_accepted_inferred':False})
                request=deepcopy(dialogue.get('request_binding'))
                if request and not interaction_only and target in current_ui.get('bindings',{}):
                    from .direct_dialogue_input_guard import bind_direct_request_semantics
                    from .quest_ui_actions import prepare_pointer_proposal,pointer_effect_contract
                    binding=current_ui['bindings'][target]
                    requested=dict(request,**{k:v for k,v in request['before_scope'].items() if k not in request})
                    requested.update(npc_id=request['transaction']['npc_id'],npc=str(request['transaction']['npc_id']))
                    binding.update(requested,ownership_verified=False,current_identity_bound=True,
                        request_binding=deepcopy(request))
                    try:
                        bind_direct_request_semantics(current_ui['layout_snapshot'],request)
                        pointer=prepare_pointer_proposal(binding,current_ui['layout_snapshot'],frame)
                        from .direct_dialogue_input_guard import request_identity
                        probe_request=pointer['probe_context'].get('request_binding')
                        if probe_request is not None and request_identity(probe_request)!=request_identity(request):
                            raise ValueError('direct request transaction changed after pointer probe')
                        pointer['probe_context'].update(source_guard_kind='direct-dialogue-request')
                        if probe_request is None:pointer['probe_context']['request_binding']=deepcopy(request)
                        binding.update(pointer)
                        state=binding.get('pointer_state') or current_ui['layout_snapshot'].get('pointer_state',{})
                        contract=pointer_effect_contract(pointer,requested,node,frame,state['observed_at'])
                        result['ui_effect_proofs'].extend(contract['effect_proofs'])
                        requested['phase']='direct_dialogue.request_probe' if pointer['input_kind']=='pointer_probe' else 'direct_dialogue.request_sent'
                        if pointer['input_kind']=='pointer_probe':requested['expected_effect']=contract['expected_effect']
                        binding['expected_effect']=requested['expected_effect']
                        current_ui['goal']={'quest_id':requested['quest_id'],'npc':requested['npc']}
                        current_ui['current_action_binding']=requested
                        current_ui['direct_request_binding']=deepcopy(request)
                        legacy_requirements=('current exact quest/node ownership',
                            'current dialogue owner NPC differs conceptually from current speaker; giver ownership requires exact dialogue context',
                            current_ui.get('layout_snapshot',{}).get('details',{}).get('dialogue_context',{}).get('quest_binding_gap'))
                        current_ui['legacy_owner_diagnostics']=[m for m in current_ui.get('missing',[]) if m in legacy_requirements]
                        current_ui['missing']=[m for m in current_ui.get('missing',[]) if m not in legacy_requirements]
                        frame['direct_dialogue_request_node']=deepcopy(node)
                        frame['direct_dialogue_request_ui']=deepcopy(current_ui)
                    except (KeyError,ValueError) as exc:
                        result['missing'].append(str(exc))
                        from .direct_dialogue_input_guard import direct_dialogue_probe_wait
                        progress=direct_dialogue_probe_wait(binding,current_ui['layout_snapshot'],frame,node,request,frame.get('pending_input') or {})
                        if progress:result['direct_dialogue_probe_progress']=progress
                if owner and target in current_ui.get('bindings',{}):
                    binding=current_ui['bindings'][target];binding.update(owner,ownership_verified=True,current_identity_bound=True)
                    try:
                        from .quest_ui_actions import _occlusion,prepare_pointer_proposal,pointer_effect_contract
                        if binding.get('native_geometry_proof') and not interaction_only:
                            pointer=prepare_pointer_proposal(binding,current_ui['layout_snapshot'],frame)
                            binding.update(pointer)
                            state=binding.get('pointer_state') or current_ui['layout_snapshot'].get('pointer_state',{})
                            contract=pointer_effect_contract(pointer,owner,node,frame,state['observed_at'])
                            result['ui_effect_proofs'].extend(contract['effect_proofs'])
                            if pointer['input_kind']=='pointer_probe':owner['expected_effect']=contract['expected_effect']
                            binding['expected_effect']=owner['expected_effect']
                        else:
                            _occlusion(binding,current_ui['layout_snapshot'],frame);binding['occluded']=False
                    except (KeyError,ValueError) as exc:result['missing'].append(str(exc))
                    current_ui['goal']={'quest_id':owner['quest_id'],'npc':owner['npc']}
                    current_ui['current_action_binding']=owner
                    resolved=('current exact quest/node ownership','current dialogue owner NPC differs conceptually from current speaker; giver ownership requires exact dialogue context',
                        current_ui.get('layout_snapshot',{}).get('details',{}).get('dialogue_context',{}).get('quest_binding_gap'))
                    current_ui['missing']=[m for m in current_ui.get('missing',[]) if m not in resolved]
        else:
            result['missing'].append('current_active_viewmodel_parent_and_branch_fields');return result
        result.update(semantic=semantic,layout_observation=current_ui,current_ui=current_ui,safe_rect=current_ui.get('safe_rect'))
        result['missing'].extend(current_ui.get('missing',[]))
        bound=current_ui.get('current_action_binding') or {}
        if (node['op'] in ('dialogue.talk','dialogue.report','objective.deliver')
            and bound.get('current_dialogue_input_owned') is not True
            and bound.get('node_id')==node['id'] and bound.get('quest_id')==node['quest_id']
            and bound.get('source') and bound.get('instance_id') is not None and bound.get('accept_time') is not None):
            result['interaction_ready'].append({**{k:bound.get(k) for k in ('node_id','quest_id','instance_id','accept_time',
                'step_id','objective_index','npc_id','ownership_basis')},'kind':'owned_objective_interaction',
                'confirmed':True,'ownership_verified':True,'source':bound['source'],
                'observed_at':bound['observed_at'],'session_id':frame['session_id'],'character_id':frame['character_id'],
                'process_started_at':frame.get('process_started_at'),'quest_completion_inferred':False})
        if interaction_only:return result
        if current_ui.get('evidence_kind')!='current-frame':
            result['missing'].append('current_source_layout_observation_unavailable');return result
        target_id=current_ui.get('selected_target_id') or TARGETS[node['op']]
        proposal=build_target(target_id,{'width':image.width,'height':image.height,'origin':current_ui.get('viewport',{}).get('origin',[0,0])},current_ui)
        result['target_evaluation']=proposal
        if proposal.get('status')!='ready':result['missing'].extend(proposal.get('missing',[]));return result
        binding=current_ui.get('current_action_binding',{})
        if binding.get('node_id')!=node['id'] or binding.get('quest_id')!=graph['quest_id']:
            result['missing'].append('current_exact_action_node_binding');return result
        if not binding.get('expected_effect'):
            result['missing'].append('current_expected_action_effect');return result
        proposal.update(current_binding=binding,observation_id=frame.get('observation_id'),
            expected_effect=binding['expected_effect'],source='current_ui_source_geometry')
        if current_ui.get('direct_request_binding'):
            proposal['request_binding']=deepcopy(current_ui['direct_request_binding'])
        proposal['context']={'source':'current_ui_source_geometry','observed_at':frame['captured_at'],
            'session_id':frame['session_id'],'character_id':frame['character_id']}
        proposal['action']={'kind':proposal['input_kind'],'role':target_id,'point':proposal.get('point_client'),'key':proposal.get('key')}
        for key in ('probe_context','source_rect','safe_rect','expected_effect'):
            if proposal.get(key) is not None:proposal['action'][key]=deepcopy(proposal[key])
        verified=proposal.get('pointer_verified') or proposal.get('raycast_proof')
        if verified is not None:proposal['action']['pointer_verified']=deepcopy(verified)
        if proposal.get('probe_context'):
            proposal['action']['input_epoch']=proposal['probe_context']['input_epoch']
        result['action_proposals'][node['id']]=proposal
        return result
