"""Pure owned processing-menu -> actual HUD return planner.

Reuses the retained mini27 Escape close sequence, one current surface at a
time. Never executes input, declares the next screen observed, or retries an
uncertain input. Exact close-click clearance or actual key-receiver ownership
is a separate prerequisite.
"""
from copy import deepcopy
import hashlib,json
from .arbeit_operations import _fresh,_stamp

MMML_REVISION='14a26db4dd960856114643dfa730efcee4d6de38e15c2f5033b0c94706a200e5'
LEGACY_ESCAPE_SOURCE={'snapshot':'data/arbeit-material-return/source-pins.json',
    'sha256':'8daf03a047e03604e5284706394c8ffa677575bd4b7462dfcb5bed183271aeab','original_sha256':'7fbadf40c0ac148e938622cd1299f8afb1a21707ce0b1a0614d9f00c35f3276f',
    'symbols':['_close_once','_close_detail','_leave'],'meaning':'owned Escape sequence only; sent is not a next-screen effect'}
DEPTH={'hud':0,'hub':1,'facility':2,'detail':3}
OCCURRENCE=('quest_id','instance_id','accept_time','step_id','objective_index')


def _waiting(reason,receipt=None,missing=None):
    return {'status':'waiting','reason':reason,'missing':missing or [reason],
        'next_receipt':deepcopy(receipt),'sent_to_game':False,'field_return_evidence':None}


def _current_owner(node,snapshot,ui,recipe_id):
    quest=snapshot.get('current_quest') or {};proof=snapshot.get('input_owner') or {};identity=proof.get('owner') or {}
    if not recipe_id or ui.get('recipe_id')!=recipe_id:raise ValueError('current_return_recipe_owner_required')
    family=ui.get('source_family')
    if family not in ('native_alter','native_craft'):raise ValueError('current_return_source_family_required')
    if (quest.get('confirmed') is not True or quest.get('source')!='current-root-typed-quest-collections'
        or not _fresh(quest,snapshot) or quest.get('quest_id')!=node.get('quest_id')
        or any(quest.get(k) is None for k in ('instance_id','accept_time'))):raise ValueError('current_return_accepted_occurrence_required')
    process=snapshot.get('process_started_at') or snapshot.get('window_preflight',{}).get('selected',{}).get('process_started_at')
    if not process or quest.get('process_started_at')!=process or ui.get('process_started_at')!=process:
        raise ValueError('current_return_process_owner_changed')
    for key in ('session_id','character_id'):
        if snapshot.get(key) is None or ui.get(key)!=snapshot[key] or identity.get(key)!=snapshot[key]:
            raise ValueError('current_return_character_session_changed')
    if (proof.get('continuous') is not True or proof.get('epoch') is None or proof.get('epoch')!=ui.get('input_epoch')
        or not _fresh(dict(proof,session_id=identity.get('session_id'),character_id=identity.get('character_id')),snapshot)):
        raise ValueError('current_return_input_epoch_required')
    if node.get('step_id') is not None and quest.get('step_id')!=node['step_id']:
        raise ValueError('suspended_parent_quest_step_changed')
    occurrence={k:quest.get(k) for k in OCCURRENCE}
    if node.get('objective_index') is not None:occurrence['objective_index']=node['objective_index']
    return {**occurrence,'parent_node_id':node['id'],'recipe_id':recipe_id,'source_family':family,
        'input_epoch':proof['epoch'],'session_id':snapshot['session_id'],'character_id':snapshot['character_id'],
        'process_started_at':process}


def _revision(ui):
    value={k:ui.get(k) for k in ('surface','panel_id','window_id')}
    value['event_id']=(ui.get('close') or {}).get('callback',{}).get('event_id')
    return hashlib.sha256(json.dumps(value,sort_keys=True,separators=(',',':')).encode()).hexdigest()


def _close_ready(ui,snapshot):
    close=ui.get('close') or {};surface=ui['surface'];callback=close.get('callback') or {}
    if close.get('enabled') is not True or callback.get('event_present') is not True or type(callback.get('event_id')) is not int:
        raise ValueError('current_return_close_callback_enabled_required')
    pin=close.get('static_source_pinned') or {}
    expected={'native_alter':{'detail':921,'facility':943,'hub':937},
              'native_craft':{'detail':918,'facility':927,'hub':937}}[ui['source_family']]
    if surface=='detail':
        if pin.get('function')!=expected['detail'] or close.get('control')!='onclose' or close.get('closable') is not True:
            raise ValueError('current_detail_onclose_closable_source_required')
    elif surface=='facility':
        if pin.get('function')!=expected['facility'] or type(ui.get('can_go_back')) is not bool:
            raise ValueError('current_facility_back_branch_required')
        control='backEvent' if ui['can_go_back'] else 'closeEvent'
        if close.get('control')!=control:raise ValueError('current_facility_close_branch_changed')
    elif surface=='hub':
        if pin.get('function')!=expected['hub'] or close.get('control')!='closeEvent':raise ValueError('current_hub_close_source_required')
    if close.get('key')!='ESC' or not close.get('source_refs'):raise ValueError('authored_owned_escape_source_required')
    return close


def _escape_receiver_ready(ui,snapshot,callback):
    receiver=ui.get('key_receiver') or {}
    if (ui.get('key_receiver_confirmed') is not True or receiver.get('confirmed') is not True or receiver.get('key')!='ESC'
        or not _fresh(receiver,snapshot) or receiver.get('process_started_at')!=ui['process_started_at']
        or any(receiver.get(k)!=ui.get(k) for k in ('panel_id','window_id','source_revision'))
        or receiver.get('event_id')!=callback['event_id']):
        raise ValueError('current_exact_escape_key_receiver_required')
    binding=ui.get('escape_binding') or {}
    if (binding.get('source_guard_kind')!='material-escape' or binding.get('target_id')!='material_return.close.'+ui['surface']
        or not binding.get('scene_signature') or binding.get('event_id')!=callback['event_id']
        or binding.get('input_epoch')!=ui.get('input_epoch')):
        raise ValueError('current_owned_escape_dispatch_guard_required')
    return binding


def plan_material_return(node,snapshot,receipt=None,*,recipe_id=None,parent_pending=None):
    """Return one source-owned Escape, a wait, or current field evidence.

    The caller preserves the original parent/pending and dispatches the proposal
    using its normal guarded port. `recipe_id` is the suspended parent's source
    recipe, not the material child recipe. No I/O occurs here.
    """
    if snapshot.get('stop_requested'):return _waiting('user_stop',receipt)
    ui=snapshot.get('material_return_ui') or {}
    try:
        if (ui.get('confirmed') is not True or ui.get('stable') is not True or not _fresh(ui,snapshot)
            or ui.get('source_revision')!=MMML_REVISION or ui.get('surface') not in DEPTH):
            raise ValueError('current_source_return_surface_required')
        recipe_id=recipe_id or node.get('params',{}).get('recipe_id')
        owner=_current_owner(node,snapshot,ui,recipe_id)
        if parent_pending and parent_pending.get('operation_id') is not None:
            owner['parent_pending_operation_id']=parent_pending['operation_id']
        epoch_changed=False
        if receipt:
            if any(receipt.get(k)!=v for k,v in owner.items() if k!='input_epoch'):
                raise ValueError('suspended_parent_return_owner_changed')
            epoch_changed=receipt.get('input_epoch')!=owner['input_epoch']
            if receipt.get('phase')!='material_return.close':raise ValueError('wrong_return_receipt_phase')
        surface=ui['surface'];revision=_revision(ui);effect=None
        if receipt and receipt.get('dispatch_state')!='not_sent':
            previous=receipt.get('surface')
            probe_pending=receipt.get('input_kind')=='pointer_probe'
            lower=previous in DEPTH and DEPTH[surface]<DEPTH[previous] and revision!=receipt.get('surface_revision')
            if not lower and (not probe_pending or epoch_changed):
                return _waiting('owned_close_effect_not_observed',receipt)
            if _stamp(ui['observed_at'])<=_stamp(receipt['observed_at']):return _waiting('new_current_surface_observation_required',receipt)
            if lower:
                if (receipt.get('expected_effect') or {}).get('source_revision',ui['source_revision'])!=ui['source_revision']:
                    raise ValueError('previous_close_source_revision_changed')
                effect={'source':ui['source'],'observed_at':ui['observed_at'],'from_surface':previous,
                        'current_surface':surface,'current_surface_revision':revision,'confirmed':True,**owner}
                if epoch_changed:effect.update(previous_input_epoch=receipt.get('input_epoch'),
                    revalidated_after_epoch_change=True,basis='same occurrence and source family; actual newer lower owned surface')
        elif epoch_changed:
            raise ValueError('suspended_parent_return_owner_changed')
        if surface=='hud':
            hud=snapshot.get('field_hud') or {}
            if (ui.get('panels_absent_confirmed') is not True or ui.get('active_owned_panels')!=[]
                or hud.get('confirmed') is not True or not _fresh(hud,snapshot)):
                raise ValueError('actual_empty_owned_panels_and_current_field_HUD_required')
            observed=min(_stamp(ui['observed_at']),_stamp(hud['observed_at'])).isoformat()
            if receipt and _stamp(observed)<=_stamp(receipt['observed_at']):return _waiting('post_close_current_HUD_required',receipt)
            evidence={**owner,'source':'current typed empty owned panels plus current HUD',
                'observed_at':observed,'confirmed':True,'field_hud':True,'quest_completion_inferred':False}
            return {'status':'complete','reason':'actual_parent_UI_returned_to_field','missing':[],
                'field_return_evidence':evidence,'effect_evidence':[effect] if effect else [],
                'next_receipt':None,'sent_to_game':False}
        close=_close_ready(ui,snapshot)
        from .material_close import bind_material_close_target
        target=bind_material_close_target(ui,snapshot)
        if target['status']=='ready':
            action={'kind':target['input_kind'],'point':target['point_client'],'role':'material_return.close.'+surface}
            reason='owned_close_callback_current_rectangle_and_clearance_confirmed'
            if receipt and receipt.get('input_kind')=='pointer_probe' and effect is None:
                if action['kind']!='pointer_click':return _waiting('current_exact_probe_hit_not_observed',receipt)
                effect={**owner,'confirmed':True,'source':'current pointer target verified after exact material close probe',
                    'observed_at':target['pointer_observed_at'],'pointer_verified':target['pointer_verified']}
        else:
            if receipt and receipt.get('input_kind')=='pointer_probe' and effect is None:
                return _waiting('current_exact_probe_hit_not_observed',receipt,target['missing'])
            try:escape_binding=_escape_receiver_ready(ui,snapshot,close['callback'])
            except ValueError as exc:return _waiting(str(exc),receipt,[str(exc),*target['missing']])
            action={'kind':'owned_escape','key':'ESC','role':'material_return.close.'+surface,
                'escape_binding':deepcopy(escape_binding),'input_epoch':owner['input_epoch']}
            reason='owned_escape_receiver_and_current_surface_confirmed'
        refs=[LEGACY_ESCAPE_SOURCE,*close['source_refs']]
        expected={'kind':'ui_state','phase':'material_return.close','node_id':node['id'],
            **{k:owner[k] for k in OCCURRENCE},'source_revision':ui['source_revision'],
            'changed_fields':{'material_return_surface_revision':revision}}
        if action['kind']=='pointer_probe':
            expected.update(phase='pointer_probe',fields={'pointer_verified':True,
                'probe_target_signature':target['probe_context']['scene_signature'],'probe_point':target['point_client']})
            expected.pop('changed_fields',None)
            reason='current_exact_close_control_requires_pointer_hit_observation'
        current_binding={**owner,'node_id':node['id'],'phase':'material_return.close','surface':surface,
            'panel_id':ui.get('panel_id'),'window_id':ui.get('window_id'),'event_id':close['callback']['event_id']}
        proposal={'status':'ready','target_id':action['role'],'action':action,'input_kind':action['kind'],
            'source_refs':refs,'current_binding':current_binding,'expected_effect':expected,
            'observed_at':ui['observed_at'],'session_id':owner['session_id'],
            'context':{'source':ui['source'],'observed_at':ui['observed_at'],
                'session_id':owner['session_id'],'character_id':owner['character_id']},
            'runtime_input_authorized':False,'live_verified':False}
        if action['kind']=='owned_escape':
            proposal.update(key='ESC',escape_binding=deepcopy(escape_binding),input_epoch=owner['input_epoch'])
        else:
            for key in ('point_client','point_desktop','rect_client','window_signature','frame_id','occlusion'):
                proposal[key]=target[key]
            proposal['observed_at']=target['observed_at']
            current_binding.update(candidate_index=target['candidate_index'],tag=target['tag'])
            for key in ('probe_context','pointer_verified','source_rect','safe_rect'):
                if target.get(key) is not None:
                    proposal[key]=deepcopy(target[key]);action[key]=deepcopy(target[key])
            if target.get('probe_context'):
                action.update(input_epoch=owner['input_epoch'],expected_effect=deepcopy(expected))
        saved={**owner,'phase':'material_return.close','surface':surface,'surface_revision':revision,
            'observed_at':ui['observed_at'],'dispatch_state':'prepared','expected_effect':expected,'input_kind':action['kind']}
        return {'status':'action','reason':reason,
            'action':action,'proposal':proposal,'next_receipt':saved,'missing':[],
            'effect_evidence':[effect] if effect else [],'sent_to_game':False,'field_return_evidence':None}
    except (ValueError,TypeError,KeyError) as exc:
        return _waiting(str(exc),receipt)
