"""Durable material subgraphs using the same observer, planner and guarded port.

Parent navigation intent is retained. Each material child owns an independent
journal; a crash cannot turn an uncertain child input into a new input. No I/O
adapter, fresh observation, native quest effect or current UI is fabricated here.
"""
from copy import deepcopy
from .arbeit_graph import digest


class MaterialExecution:
    def _material_child_engine(self):
        state=self.data.get('material_supply')
        if not state or state.get('phase')!='supplying' or not state.get('child_graph'):
            return None
        cached=getattr(self,'_material_child',None)
        if cached is not None:return cached
        from .graph_executor import GraphExecutor
        path=self.journal.path.with_name(self.journal.path.stem+'.material.'+
            state['id'][:16]+'.pass'+str(state.get('pass_index',0))+'.'+str(state['index'])+'.json')
        if state.get('pass_index',0)==0:
            legacy=self.journal.path.with_name(self.journal.path.stem+'.material.'+
                state['id'][:16]+'.'+str(state['index'])+'.json')
            if legacy.exists():
                if path.exists():raise ValueError('material_pass_zero_child_journal_ambiguous')
                path=legacy
        # The parent stores graph identity before the first child input. The
        # child journal holds intent before dispatch and is the authority after
        # a crash between child and parent journal writes.
        child=GraphExecutor(state['child_graph'],path,mode=self.data['mode'],resume=path.exists())
        child.data.setdefault('material_ancestry',deepcopy(state['ancestry']))
        child.save()
        self._material_child=child
        return child

    def observation_node(self):
        child=self._material_child_engine()
        return (child.observation_node() or child.graph['nodes'][0]) if child else self.current_node()

    def observation_pending(self):
        child=self._material_child_engine()
        if child:return child.observation_pending()
        state=self.data.get('material_supply')
        return (state.get('return_pending') or state['parent_pending']) if state else self.data.get('pending')

    def observation_recovery(self):
        child=self._material_child_engine()
        return child.observation_recovery() if child else self.data.get('recovery')

    def reported_pending(self):
        """Reporting is read-only: never open a child journal from a UI poll."""
        child=getattr(self,'_material_child',None)
        if child:return child.reported_pending()
        state=self.data.get('material_supply') or {}
        return state.get('child_pending') or state.get('return_pending') or self.data.get('pending')

    def material_observation_context(self):
        child=self._material_child_engine()
        if child:return child.material_observation_context()
        state=self.data.get('material_supply')
        if not state:
            pending=self.data.get('pending') or {}
            receipt=pending.get('planner_receipt') or {}
            detail=receipt.get('child_receipt') or {}
            if receipt.get('phase')=='alter.cleanup' and detail.get('recipe_id'):
                return {'processing_cleanup_requested':True,'processing_route_id':detail['recipe_id'],
                    'material_return_requested':True,'material_return_kind':'native_alter',
                    'material_return_recipe_id':detail['recipe_id'],
                    'material_return_parent_pending':deepcopy(pending)}
            return {}
        context={'material_return_requested':True,
            'material_return_kind':state['source_recipe']['kind'],
            'material_return_recipe_id':state['source_recipe']['recipe_id'],
            'material_return_parent_pending':deepcopy(state['parent_pending'])}
        if state['source_recipe']['kind']=='native_alter':
            context['processing_route_id']=state['source_recipe']['recipe_id']
        return context

    def _material_protection(self,snapshot):
        from .material_supply import _state_scope
        states=list(self.data.get('material_commitments',[]))
        if self.data.get('material_supply'):states.append(self.data['material_supply'])
        inherited=snapshot.setdefault('material_protections',[])
        for state in states:
            gap=None
            try:_state_scope(state,snapshot)
            except (ValueError,KeyError,TypeError) as exc:gap=str(exc)
            for need in state['requirements']:
                row=dict(source='source_recipe_material_requirement',
                    requirement_origin='source_recipe_material',quest_objective=False,
                    material_state_id=state['id'],parent_node_id=state['parent_node']['id'],
                    item_name=need['item']['name'],item=deepcopy(need['item']),
                    required_quantity=need['required_quantity'],source_recipe=deepcopy(state['source_recipe']),
                    source_required_at=state['created_at'],parent_scope=deepcopy(state['parent_scope']),
                    confirmed=gap is None,missing=[] if gap is None else [gap],
                    observed_at=(snapshot.get('current_quest') or {}).get('observed_at'),
                    session_id=snapshot['session_id'],character_id=snapshot['character_id'])
                key=(row['material_state_id'],row['item_name'])
                if not any((p['material_state_id'],p['item_name'])==key for p in inherited):inherited.append(row)

    def _begin_materials(self,node,snapshot,result):
        if self.data.get('material_supply'):return None
        reason=result.get('reason') or ''
        if result.get('status')!='waiting' or (reason not in (
                'processing_material_shortage','processing_material_or_slot_unconfirmed',
                'current_craft_confirm_disabled_or_unbound','current_craft_material_shortage_or_amount_changed')
                and not reason.startswith(('craft_material_bag_shortage:','craft_material_goal_reserved_shortage:',
                    'processing_material_goal_reserved_shortage:'))):
            return None
        from .material_supply import begin_material_supply
        begun=begin_material_supply(node,snapshot,self.data.get('pending'),
            ancestry=self.data.get('material_ancestry',()))
        if begun['status']=='ready':
            self.data['material_supply']=begun['state']
            self.event('material_goal_created',begun['reason'],state=begun['state'])
            return self.boundary('material_supply_started','running')
        if begun['status']=='waiting':
            self.event('material_goal_gap',begun['reason'],missing=begun.get('missing',[]))
            return self.boundary(begun['reason'])
        return None

    def _dispatch_material_return(self,node,snapshot,port,result):
        state=self.data['material_supply'];previous=state.get('return_pending')
        if previous and not result.get('effect_evidence'):
            return self.boundary('owned_close_effect_not_observed')
        if (self.data['mode']=='offline-contract')!=bool(getattr(port,'offline_contract',False)):
            return self.boundary('material_return_port_mode_mismatch')
        try:prepared=port.prepare(result['proposal'],snapshot['input_context'])
        except Exception as exc:return self.boundary('material_return_preparation_failed:'+str(exc))
        operation_id=f"{self.data['run_id']}:{node['id']}:material-return:{len(self.data['events'])}"
        state['return_pending']={'operation_id':operation_id,'node_id':node['id'],
            'status':'intent_persisted','planner_receipt':result['next_receipt'],
            'proposal':result['proposal'],'after_sequence':snapshot['sequence'],'previous':previous}
        self.save()
        try:sent=port.send(prepared,operation_id)
        except Exception as exc:sent={'status':'unknown','reason':type(exc).__name__+':'+str(exc)}
        state['return_pending'].update(status=sent.get('status','unknown'),input_receipt=sent)
        if sent.get('status')=='blocked' and sent.get('attempted') is False:
            self.event('material_return_not_attempted',sent.get('reason'),intent=state['return_pending'])
            state['return_pending']=previous
            return self.boundary('material_return_blocked_before_dispatch')
        return self.boundary('material_return_input_requires_effect_observation')

    def _tick_materials(self,snapshot,observer,port,planner):
        from .material_supply import advance_material_supply,_state_scope
        from .material_return import plan_material_return
        state=self.data['material_supply']
        try:_state_scope(state,snapshot)
        except (ValueError,KeyError,TypeError) as exc:
            return self.boundary(str(exc))
        child=self._material_child_engine()
        completion=None;field=None
        if child:
            if child.data['status']=='paused':child.start(explicit=True)
            class CurrentObservation:
                def read(_,node):return snapshot
            report=child.tick(CurrentObservation(),port,planner=planner)
            state['child_pending']=deepcopy(child.data.get('pending'))
            state['child_report']=deepcopy(report)
            if report['status'] not in ('complete','offline_contract_passed'):
                return self.boundary(report['reason'],report['status'],wait_contract=report.get('wait_contract'))
            record=child.data['nodes'].get(child.graph['nodes'][0]['id'],{})
            completion={'status':'complete','effect_evidence':record.get('effect_evidence'),
                'child_journal':str(child.journal.path),'source_material_goal':True,
                'supply_receipt':deepcopy((record.get('pending') or {}).get('planner_receipt')),
                'preserved_input':deepcopy(record.get('pending'))}
        elif state['phase'] in ('return_to_field','return_to_parent'):
            returned=plan_material_return(state['parent_node'],snapshot,
                (state.get('return_pending') or {}).get('planner_receipt'),
                recipe_id=state['source_recipe']['recipe_id'],parent_pending=state['parent_pending'])
            self.event('material_return',returned['reason'],result=returned)
            if returned['status']=='action':
                return self._dispatch_material_return(state['parent_node'],snapshot,port,returned)
            if returned['status']!='complete':return self.boundary(returned['reason'])
            field=returned['field_return_evidence']
        outcome=advance_material_supply(state,snapshot,field_return_evidence=field,child_result=completion)
        self.data['material_supply']=outcome.get('state') or state
        state=self.data['material_supply']
        if outcome['status']=='waiting':return self.boundary(outcome['reason'])
        if completion:
            child.close();self._material_child=None
            state.pop('child_graph',None)
            state['return_pending']=None
        if outcome['status']=='child':
            state['child_graph']={'quest_id':self.graph['quest_id'],'title':self.graph['title']+' / recipe material',
                'source_fingerprint':self.graph['source_fingerprint'],
                'parent_graph_fingerprint':digest(self.graph),'nodes':[deepcopy(outcome['node'])]}
            self.save()
            return self.boundary('material_child_ready','running')
        if outcome['status']=='resume_parent':
            self.event('material_parent_resumed',outcome['reason'],evidence=outcome['resume'])
            self.data.setdefault('material_commitments',[]).append(deepcopy(state))
            self.data['material_supply']=None
            self.data['pending']=None # Only resolved navigation; original is retained above.
            return self.boundary('material_parent_replan_from_current_field','running')
        if outcome['status']=='return_to_field':
            # Child completion may require a new current HUD observation.
            # Never reuse the HUD proof from before that child ran.
            return self.boundary(outcome['reason'],'running')
        return self.boundary(outcome['reason'])

    def _material_node_complete(self,node,evidence):
        keep=[]
        for state in self.data.get('material_commitments',[]):
            if state['parent_node']['id']==node['id']:
                self.event('material_protection_released','parent_effect_confirmed',
                    material_state_id=state['id'],effect_evidence=evidence)
            else:keep.append(state)
        self.data['material_commitments']=keep

    def _close_materials(self):
        child=getattr(self,'_material_child',None)
        if child is not None:child.close();self._material_child=None
