"""Exact current CommonHeader close/back controls for material return.

The supplier shares an already-open read-only session. The pure binder joins a
current model callback to its current VM element and requires target clearance.
Neither function opens the game, captures pixels, or sends input.
"""
from copy import deepcopy
import math

from .material_return import MMML_REVISION,LEGACY_ESCAPE_SOURCE
from .arbeit_operations import _fresh

CLOSE_TAGS=('Button.X','Button.Back')
CALLBACK_SOURCE='current LayoutVM.elementIdFromElementIndex/runtimeForest'


def _pointer(value):
    if isinstance(value,str):value=int(value,16) if value.startswith('0x') else int(value)
    if type(value) is not int or value<=0:raise ValueError('current UI pointer required')
    return value


def _close_semantics(ui):
    return dict(source_family=ui['source_family'],recipe_id=ui['recipe_id'],surface=ui['surface'],
        panel_id=_pointer(ui['panel_id']),event_id=ui['close']['callback']['event_id'],
        control=ui['close']['control'],can_go_back=ui.get('can_go_back'),source_revision=ui['source_revision'])


def decorate_material_close_layout(session,handle,layout):
    """Attach actual element callbacks using the caller's existing read guard."""
    from .quest_ui_actions import quest_ui_session
    selected=[c for c in layout.get('candidates',[]) if c.get('tag') in CLOSE_TAGS]
    if not selected:return layout
    view=dict(layout,candidates=selected,missing=[])
    try:
        proxy=quest_ui_session(session)
        proxy.bind_runtime_callbacks(view,handle=handle)
    except (OSError,RuntimeError,ValueError,KeyError,TypeError,AttributeError) as exc:
        layout.setdefault('close_missing',[]).append('current close callback binding: '+str(exc))
    return layout


def attach_material_close_geometry(session,return_ui,frame):
    """Read exact RootObject/Graphic geometry before the owned session closes.

    Geometry and pointer evidence are delegated to the shared UI supplier. Raw
    blocker observations never become an unoccluded flag here.
    """
    from .quest_ui_actions import quest_ui_session
    if return_ui.get('surface')=='detail':
        from .material_dimmer import attach_material_dimmer
        from .material_escape import attach_material_escape
        attach_material_dimmer(session,return_ui,frame)
        if return_ui.get('dimmer_close'):
            return_ui['key_receiver_confirmed']=False
            return return_ui
        return attach_material_escape(quest_ui_session(session),return_ui,frame)
    layout=return_ui.get('current_layout') or {}
    candidates=[c for c in layout.get('candidates',[]) if c.get('tag') in CLOSE_TAGS]
    if not candidates:return return_ui
    view=dict(layout,candidates=candidates,source_hash=return_ui.get('source_revision'),
              ui_pointer=_pointer(return_ui.get('ui_pointer')),missing=[])
    try:
        proxy=quest_ui_session(session)
        proxy.attach_current_geometry(view,frame)
        return_ui['close_geometry']=view
    except (OSError,RuntimeError,ValueError,KeyError,TypeError,AttributeError) as exc:
        return_ui.setdefault('close_missing',[]).append('current close geometry: '+str(exc))
    return return_ui


def bind_material_close_target(ui,snapshot):
    """Produce one exact current close click, or explain the missing evidence."""
    try:
        if ui.get('source_revision')!=MMML_REVISION or ui.get('stable') is not True or not _fresh(ui,snapshot):
            raise ValueError('current_close_source_revision_and_stable_frame_required')
        if ui.get('surface')=='detail':
            from .material_dimmer import bind_dimmer_target
            return bind_dimmer_target(ui,snapshot)
        control=(ui.get('close') or {}).get('control')
        # Detail onclose is an enclosing popup callback, not a Header onclick.
        # No Header rectangle may be borrowed for a detail popup close event.
        if ui.get('surface') not in ('facility','hub') or control not in ('closeEvent','backEvent'):
            raise ValueError('current_detail_popup_close_control_not_yet_source_joined')
        tag='Button.Back' if control=='backEvent' else 'Button.X'
        callback=ui['close']['callback']
        if callback.get('event_present') is not True or type(callback.get('event_id')) is not int:
            raise ValueError('current_close_model_callback_required')
        layout=ui.get('current_layout') or {}
        frame=ui.get('frame') or {}
        if (layout.get('rect_source')!='current-mmml-output' or layout.get('window_id')!=ui.get('window_id')
            or ui.get('ui_pointer')!=ui.get('panel_id')):
            raise ValueError('current_close_same_parent_layout_required')
        for key in ('session_id','character_id','process_started_at'):
            if not frame.get(key) or frame[key]!=ui.get(key):raise ValueError('current_close_frame_owner_required')
        if not frame.get('frame_id') or not frame.get('window_signature'):
            raise ValueError('current_close_frame_identity_required')
        if not _fresh(dict(frame,source='bound current capture metadata',observed_at=frame.get('captured_at')),snapshot):
            raise ValueError('current_close_frame_stale')
        candidates=[]
        for row in layout.get('candidates',[]):
            event=(row.get('runtime_attributes') or {}).get('onclick') or {}
            if (row.get('tag')==tag and row.get('callback_source')==CALLBACK_SOURCE
                and row.get('model_event_id')==callback['event_id']
                and event.get('event_present') is True and event.get('event_id')==callback['event_id']):
                candidates.append(row)
        if len(candidates)!=1:raise ValueError('current_unique_close_callback_element_required')
        candidate=candidates[0]
        # CommonHeader 186 emits onclick directly and no disabled attribute.
        # If a current descriptor adds a disabled value, it must explicitly be false.
        attrs=candidate['runtime_attributes']
        if 'disabled' in attrs and attrs['disabled'] is not False:
            raise ValueError('current_close_control_disabled_or_unknown')
        scale=layout.get('scale');screen=layout.get('screen_ui');rect=candidate.get('rect_ui')
        physical=frame.get('physical_client_rect');safe=(frame.get('current_context') or {}).get('safe_rect')
        if (type(scale) not in (int,float) or not math.isfinite(scale) or not 0<scale<20
            or not isinstance(screen,list) or len(screen)!=2 or not isinstance(rect,list) or len(rect)!=4
            or not isinstance(physical,(list,tuple)) or len(physical)!=4
            or not isinstance(safe,(list,tuple)) or len(safe)!=4):
            raise ValueError('current_close_scale_viewport_and_safe_area_required')
        if any(type(x) not in (int,float) or not math.isfinite(x) for x in [*screen,*rect,*physical,*safe]):
            raise ValueError('current_close_geometry_nonfinite')
        width,height=physical[2]-physical[0],physical[3]-physical[1]
        if min(width,height,*rect[2:])<=0 or any(abs(screen[i]*scale-(width,height)[i])>2 for i in (0,1)):
            raise ValueError('current_close_layout_viewport_mismatch')
        left,top,w,h=[x*scale for x in rect]
        if not 0<=safe[0]<=left<left+w<=safe[2]<=width or not 0<=safe[1]<=top<top+h<=safe[3]<=height:
            raise ValueError('current_close_control_clipped')
        from .quest_ui_actions import _occlusion,prepare_pointer_proposal
        typed=dict(layout,source_hash=ui['source_revision'],ui_pointer=_pointer(ui['ui_pointer']),
            ui_type=ui.get('ui_type'),target_id='material_return.close.'+ui['surface'],pointer_semantics=_close_semantics(ui))
        pointer=None
        if candidate.get('native_geometry_proof'):
            pointer=prepare_pointer_proposal(candidate,typed,dict(frame,input_owner=snapshot.get('input_owner'),
                pointer_probe_receipt=frame.get('pointer_probe_receipt')))
            point=pointer['point_client'];proof=pointer.get('occlusion')
        else:
            proof=_occlusion(candidate,typed,frame)
            point=[round(left+w/2),round(top+h/2)]
        if not left<=point[0]<left+w or not top<=point[1]<top+h:raise ValueError('current_close_no_interior_pixel')
        result=dict(status='ready',input_kind='click',point_client=point,
            point_desktop=[point[0]+physical[0],point[1]+physical[1]],rect_client=[left,top,w,h],
            window_signature=frame['window_signature'],frame_id=frame['frame_id'],observed_at=frame['captured_at'],
            source_refs=[LEGACY_ESCAPE_SOURCE,*ui['close'].get('source_refs',[])],
            source='current CommonHeader 186 onclick + exact VM element + current target clearance',
            event_id=callback['event_id'],candidate_index=candidate['index'],tag=tag,
            occlusion=deepcopy(proof),runtime_input_authorized=False,live_verified=False)
        if pointer:
            pointer['probe_context'].update(source_family=ui['source_family'],recipe_id=ui['recipe_id'])
            rx,ry,rw,rh=candidate['rect_client']
            result.update(pointer,source_rect=[rx,ry,rx+rw,ry+rh],safe_rect=list(safe),
                pointer_observed_at=(candidate.get('pointer_state') or typed.get('pointer_state',{})).get('observed_at'))
        return result
    except (ValueError,KeyError,TypeError) as exc:
        return dict(status='blocked',missing=[str(exc)],sent_to_game=False)


def material_close_scene_reader(session,probe_context,frame):
    """Last-moment current rooted material close for IO.make_pointer_guard.

    A new session is not opened here. Read the allowed UIInstanceService panel
    roots and same handle model/layout again, rather than trusting saved pointers.
    """
    from .processing_observer import CurrentProcessingSession,processing_return_ui,processing_current_sources
    from .craft_observer import CurrentCraftSession
    from .quest_ui_actions import quest_ui_session
    target=probe_context.get('target_id','')
    if target=='material_return.close.detail' and probe_context.get('event_kind')=='registered-click-popup-close':
        from .material_dimmer import dimmer_scene_reader
        return dimmer_scene_reader(session,probe_context,frame)
    if not target.startswith('material_return.close.'):
        snapshot=session.current_layout(target);session.attach_current_geometry(snapshot,frame)
        if target=='reward.confirm':snapshot['candidates']=[snapshot['reward_geometry']]
    else:
        surface=target.rsplit('.',1)[-1];kind=probe_context.get('source_family')
        if frame.get('material_return_kind',kind)!=kind:raise ValueError('current return source family changed')
        classes={'native_alter':CurrentProcessingSession,'native_craft':CurrentCraftSession}
        if kind not in classes:raise ValueError('current material return source family required at dispatch')
        typed_session=classes[kind].__new__(classes[kind]);typed_session.__dict__=session.__dict__
        if kind=='native_alter':
            current=typed_session.current_processing();close=processing_return_ui(current)
            if current['mmml_revision_sha256']!=processing_current_sources()['mmml_revision_sha256']:
                raise ValueError('current processing return source checks changed')
            source_hash=processing_current_sources()['mmml_source_sha256']
        else:
            # CurrentCraftSession has its own rooted enumeration and model API;
            # never use an arbitrary previously saved UI pointer as a root.
            current=typed_session.current_crafting();source_hash=current['mmml_sha256']
            panels=current['panels'];names={'detail':'CraftingPanelPopupUIInstance','facility':'CraftingFacilityPopupUIInstance','hub':'CraftingHubPopupUIInstance'}
            if 'CraftResultUIInstance' in panels:raise ValueError('current craft result owns this surface')
            selected=next((s for s in ('detail','facility','hub') if names[s] in panels),None)
            if selected!=surface:raise ValueError('current craft return surface changed')
            panel=panels[names[surface]];from .craft_observer import Model
            model=Model(panel['model']);values=model.values(model.root)
            callback={'facility':'backButtonClick','hub':'closeButtonEvent','detail':'closeEvent'}[surface]
            control='backEvent' if surface=='facility' and values.get('canGoBack') is True else 'closeEvent'
            close=dict(surface=surface,panel_id=panel['ui_pointer'],can_go_back=values.get('canGoBack'),
                close={'callback':values.get(callback),'control':control})
        if close.get('surface')!=surface or source_hash!=MMML_REVISION:
            raise ValueError('current material close source surface changed before dispatch')
        event=(close.get('close') or {}).get('callback') or {}
        if event.get('event_present') is not True or event.get('event_id')!=probe_context['model_event_id']:
            raise ValueError('current material close callback changed before dispatch')
        panels=current['panels'];selected=[p for p in panels.values() if _pointer(p.get('ui_pointer'))==probe_context['target_ui_pointer']]
        if len(selected)!=1 or _pointer(close['panel_id'])!=probe_context['target_ui_pointer']:
            raise ValueError('current material close root owner changed before dispatch')
        panel=selected[0];snapshot=dict(panel['layout'],ui_pointer=_pointer(panel['ui_pointer']),
            source_hash=source_hash,ui_type=panel.get('ui_type'),target_id=target,
            pointer_semantics=_close_semantics(dict(close,source_family=kind,recipe_id=probe_context['recipe_id'],source_revision=source_hash)))
        quest_ui_session(typed_session).attach_current_geometry(snapshot,frame)
    matches=[c for c in snapshot.get('candidates',[]) if
        (c.get('control_pointer'),c.get('root_object_pointer'),c.get('model_event_id'),c.get('index'))==
        (probe_context['control_pointer'],probe_context['root_object_pointer'],probe_context['model_event_id'],probe_context['target_index'])]
    if len(matches)!=1:raise ValueError('current exact material/quest control no longer unique')
    return matches[0],snapshot
