"""Source candidates and run-owned inspection decisions; never eligibility by name."""
from copy import deepcopy


def validated_material_hold_request(node,snapshot):
    """Read only the separate source-recipe material channel, with no fallback."""
    if 'material_requirement' not in node.get('params',{}):return None
    from .material_supply import validate_material_request
    supplied=snapshot.get('material_hold_requests',[])
    if not isinstance(supplied,list):return None
    rows=[r for r in supplied
          if isinstance(r,dict) and validate_material_request(node,snapshot,r)]
    return deepcopy(rows[0]) if len(rows)==1 else None


def collect_supply_candidates(node,snapshot=None):
    from .arbeit_operations import _item, _fresh, _scope, supply_candidates
    material=None
    if 'material_requirement' in node.get('params',{}):
        material=validated_material_hold_request(node,snapshot or {})
        if material is None:return []
        node=deepcopy(node);node.setdefault('params',{})['item']=deepcopy(material['item'])
    elif snapshot:
        requests=[r for r in snapshot.get('inventory_hold_requests',[]) if _fresh(r,snapshot) and _scope(r,node,objective=True)]
        if len(requests)==1:
            node=deepcopy(node);node.setdefault('params',{})['item']=deepcopy(requests[0]['item'])
    item=_item(node)
    rows=deepcopy(supply_candidates(item)) if (item or {}).get('table_domain') in (None,'ConsumableSheet') else []
    # The optional current-sheet craft supplier is independent of old life IDs.
    from .craft_observer import recipe_candidates
    for recipe in recipe_candidates(node):
        rows.append(dict(kind='native_craft',route_id='crafting:'+str(recipe['id']),recipe_id=recipe['id'],
            recipe=deepcopy(recipe),source_refs=[recipe['source_ref']]))
    seen=set();result=[]
    for row in rows:
        key=(row['kind'],row['route_id'])
        if key not in seen:
            if material is not None:
                row['supply_requirement']={k:deepcopy(material[k]) for k in
                    ('requirement_origin','material_state_id','parent_node_id','required_quantity','quantity_unit','quest_objective')}
            result.append(row);seen.add(key)
    return result


class SupplySelector:
    """Choose a read/selection path, retaining current eligible or owned routes.

    A new candidate may only be inspected. Actual work still requires the
    typed current detail/unlock/material predicates of its concrete supplier.
    A locked attempt is excluded only inside this uninterrupted goal session;
    invalidate on stop, foreign input, owner/epoch changes or an explicit retry.
    """
    def __init__(self):self.invalidate()
    def invalidate(self):self.scope=None;self.observations={};self.owned_ui=None;self.last_choice=None
    def _scope(self,node,snapshot):
        owner=snapshot.get('input_owner') or {}
        current=snapshot.get('current_quest') or {}
        return (node['id'],snapshot.get('session_id'),snapshot.get('character_id'),owner.get('epoch'),owner.get('continuous'),
            *(current.get(k) for k in ('quest_id','instance_id','accept_time','step_id','step_index','process_started_at')))
    def _bind(self,node,snapshot):
        scope=self._scope(node,snapshot)
        if scope!=self.scope:self.scope=scope;self.observations={};self.owned_ui=None;self.last_choice=None
    def observe(self,node,snapshot,candidate,*,eligible,evidence):
        from .arbeit_operations import _fresh
        self._bind(node,snapshot)
        if type(eligible) is not bool or not _fresh(evidence,snapshot) or not evidence.get('identity_binding'):
            raise ValueError('current_owned_supply_eligibility_evidence_required')
        for name in ('kind','route_id','recipe_id'):
            if name in evidence and evidence[name]!=candidate.get(name):raise ValueError('supply_feedback_candidate_identity_changed')
        if evidence.get('node_id') not in (None,node['id']):raise ValueError('supply_feedback_goal_changed')
        transient=evidence.get('material_shortage') is True or evidence.get('eligibility_kind') in ('material_shortage','unresolved_current_condition')
        self.observations[(candidate['kind'],candidate['route_id'])]=dict(eligible=eligible,
            excluded=eligible is False and not transient,transient=transient,evidence=deepcopy(evidence),scope=self.scope)

    def observe_context(self,node,snapshot,candidate):
        """Retain a craft UI owner until typed absence plus current HUD."""
        from .arbeit_operations import _fresh
        self._bind(node,snapshot)
        if candidate.get('kind')!='native_craft':return
        scene=snapshot.get('crafting') or {};raw=(snapshot.get('craft_read') or {}).get('typed_current') or {}
        if (scene.get('recipe_id')!=candidate.get('recipe_id') or scene.get('confirmed') is not True
                or not _fresh(scene,snapshot) or raw.get('stable') is not True):return
        key=(candidate['kind'],candidate['route_id']);panels=raw.get('panels')
        if not isinstance(panels,dict):return
        if panels:
            self.owned_ui=dict(key=key,evidence=dict(source=scene['source'],observed_at=scene['observed_at'],
                session_id=scene['session_id'],character_id=scene['character_id'],recipe_id=scene['recipe_id'],
                active_ui_types=sorted(panels)),scope=self.scope)
            return
        context=snapshot.get('current_context') or {}
        if (self.owned_ui and self.owned_ui['key']==key and context.get('ok') is True
                and context.get('field_hud') is True and context.get('profile',{}).get('visual_confirmed') is True):
            self.owned_ui=None
    def choose(self,node,snapshot,candidates=None,*,kind=None,pending=None):
        from .arbeit_operations import _fresh
        self._bind(node,snapshot)
        rows=collect_supply_candidates(node,snapshot) if candidates is None else deepcopy(candidates)
        if kind:rows=[r for r in rows if r['kind']==kind]
        def selected(status,row,state=None,missing=None):
            self.last_choice=(row['kind'],row['route_id'])
            return dict(status=status,candidate=row,eligible=state.get('eligible') if state else None,
                evidence=deepcopy(state),missing=missing or [])
        current=snapshot.get('supply_evidence',[])
        for row in rows:
            proofs=[p for p in current if p.get('route_id')==row['route_id'] and p.get('kind')==row['kind']]
            if len(proofs)==1:
                try:self.observe(node,snapshot,row,eligible=proofs[0].get('eligible'),evidence=proofs[0])
                except ValueError:pass
        owned=((pending or {}).get('planner_receipt') or {}).get('child_receipt') or {}
        route=owned.get('route_id') or owned.get('recipe_id')
        scope_ok=not pending or (all(((pending.get('planner_receipt') or {}).get(k)==snapshot.get(k)) for k in ('session_id','character_id'))
            and (pending.get('planner_receipt') or {}).get('node_id') in (None,node['id']))
        for row in rows:
            state=self.observations.get((row['kind'],row['route_id']))
            if (row['route_id']==route or row.get('recipe_id') is not None and row['recipe_id']==route) and scope_ok:
                return selected('owned',row,state,['pending_supply_input_must_be_resolved'] if state and state['eligible'] is False else [])
        if pending:return dict(status='blocked',candidate=None,eligible=None,missing=['unresolved_pending_supply_owner'])
        if self.owned_ui:
            held=[r for r in rows if (r['kind'],r['route_id'])==self.owned_ui['key']]
            if len(held)==1:return selected('owned',held[0],self.observations.get(self.owned_ui['key']),['owned_supply_ui_cleanup_required'])
            return dict(status='blocked',candidate=None,eligible=None,missing=['owned_supply_ui_candidate_unavailable'])
        transient=[r for r in rows if (r['kind'],r['route_id'])==self.last_choice
            and self.observations.get(self.last_choice,{}).get('transient') is True]
        if transient:return selected('inspect',transient[0],self.observations[self.last_choice],['current_supply_material_or_condition_recovery'])
        eligible=[r for r in rows if self.observations.get((r['kind'],r['route_id']),{}).get('eligible') is True
                  and _fresh(self.observations[(r['kind'],r['route_id'])]['evidence'],snapshot)]
        unknown=[r for r in rows if (r['kind'],r['route_id']) not in self.observations or
                 self.observations[(r['kind'],r['route_id'])].get('transient') is True or
                 self.observations[(r['kind'],r['route_id'])]['eligible'] is True and r not in eligible]
        if eligible:return selected('eligible',eligible[0],self.observations[(eligible[0]['kind'],eligible[0]['route_id'])])
        if unknown:return selected('inspect',unknown[0],None,['current_selected_candidate_eligibility'])
        return dict(status='blocked',candidate=None,eligible=False,missing=['all_current_supply_candidates_locked' if rows else 'source_supply_candidate_unavailable'],
            observations=deepcopy(list(self.observations.values())))


CRAFT_NAVIGATION=('craft.open','craft.category','craft.recipe')


def _cleanup_goal_effect(node,snapshot,receipt):
    from .arbeit_operations import _objective_effect
    from .arbeit_crafting import _step_termination
    effect=_objective_effect(node,snapshot,receipt)
    if effect and effect.get('confirmed') is True and effect.get('semantics')=='current_quest_objective_native_IsAchieved':return effect
    child=(receipt or {}).get('child_receipt') or {}
    termination=_step_termination(node,snapshot,dict(child,goal_scope=child.get('goal_scope') or (receipt or {}).get('goal_scope')))
    if termination:return termination
    if 'material_requirement' in node.get('params',{}):
        request=validated_material_hold_request(node,snapshot)
        if request and request.get('current_inventory_evidence',{}).get('available_quantity',-1)>=request['required_quantity']:return request
    return None


def plan_supply_cleanup(node,snapshot,receipt=None,*,effect=None):
    """Resolve an observed navigation, close its owned UI, then retry this goal.

    Never resolves a consuming submission. The original navigation ledger and
    current false eligibility survive every close/probe receipt. A returned
    supply_retry clears that pending navigation only, not the objective.
    """
    from .arbeit_operations import _fresh,_waiting,_authored_action,_complete
    from .arbeit_crafting import _owner,_validate_pending,_stamp
    from .material_return import plan_material_return
    phase=(receipt or {}).get('phase') or ''
    if node.get('op') not in ('inventory.hold','life.craft') or phase not in (*CRAFT_NAVIGATION,'craft.supply_cleanup'):return None
    continuing=phase=='craft.supply_cleanup';saved=(receipt.get('child_receipt') or {}).get('supply_cleanup') or {}
    original=saved.get('original_receipt') if continuing else receipt
    goal_effect=_cleanup_goal_effect(node,snapshot,original)
    if effect is not None and goal_effect is None:
        return _waiting(node,'craft_cleanup_current_goal_effect_required',['craft_cleanup_current_goal_effect_required'],receipt)
    proof=None
    if not continuing and goal_effect is None:
        candidate=(snapshot.get('supply_selection') or {}).get('candidate') or {}
        rows=[p for p in snapshot.get('supply_evidence',[]) if p.get('kind')=='native_craft'
            and p.get('recipe_id')==candidate.get('recipe_id') and p.get('route_id')==candidate.get('route_id')]
        if not rows:
            remembered=(snapshot.get('supply_selection') or {}).get('evidence') or {}
            rows=[remembered['evidence']] if remembered.get('excluded') is True and remembered.get('evidence') else []
        if len(rows)!=1:return None
        proof=rows[0]
        if (proof.get('eligible') is not False or proof.get('material_shortage') is True
                or proof.get('eligibility_kind')!='current_recipe_condition_unavailable'):return None
    wait=lambda why,missing=None:_waiting(node,why,missing or [why],receipt)
    try:
        if snapshot.get('stop_requested'):return wait('user_stop')
        scene=snapshot.get('crafting') or {};pending=snapshot.get('pending_input') or {}
        if scene.get('confirmed') is not True or not _fresh(scene,snapshot):raise ValueError('current_owned_supply_cleanup_scene_required')
        owner=_owner(scene,snapshot)
        child=_validate_pending(node,snapshot,receipt,scene,owner,allow_attempted=True)
        original_child=original.get('child_receipt') or {}
        scope=original_child.get('goal_scope') or original.get('goal_scope') or {}
        quest=snapshot.get('current_quest') or {}
        if (quest.get('confirmed') is not True or not _fresh(quest,snapshot)
                or quest.get('source')!='current-root-typed-quest-collections'
                or any(scope.get(k) is None or scope[k]!=quest.get(k) for k in ('quest_id','instance_id','accept_time','process_started_at'))):
            raise ValueError('supply_cleanup_original_accepted_occurrence_required')
        step_changed=scope.get('step_id')!=quest.get('step_id')
        if step_changed and (not goal_effect or goal_effect.get('state')!='step_complete'):
            raise ValueError('supply_cleanup_original_step_changed')
        if original.get('phase') not in CRAFT_NAVIGATION:raise ValueError('supply_cleanup_requires_nonconsuming_navigation')
        if continuing:
            if saved.get('mode')=='goal_complete' and goal_effect is None:raise ValueError('craft_cleanup_current_goal_effect_required')
            candidate=saved['candidate'];proof=saved.get('eligibility');nav=saved['navigation_effect']
            if (candidate.get('recipe_id')!=scene['recipe_id'] or nav.get('craft_owner')!=owner
                    or saved['original_pending'].get('planner_receipt')!=original):raise ValueError('supply_cleanup_original_navigation_changed')
        else:
            candidate=dict(kind='native_craft',route_id='crafting:'+str(scene['recipe_id']),recipe_id=scene['recipe_id'])
            detail=scene
            if goal_effect is None:
                if not _fresh(proof,snapshot) or not proof.get('identity_binding'):raise ValueError('current_locked_supply_evidence_required')
                if any(proof.get(k)!=candidate[k] for k in ('kind','route_id','recipe_id')):raise ValueError('locked_supply_recipe_changed')
                if scene.get('stage')=='field_hud':
                    detail=proof.get('current_detail') or {}
                    if detail.get('pending_operation_id')!=pending.get('operation_id'):raise ValueError('supply_cleanup_previous_navigation_effect_required')
                if (detail.get('stage')!='detail' or detail.get('selected_recipe_id')!=scene['recipe_id']
                        or detail.get('current_binding_kind')!='typed current native recipe'
                        or (detail.get('input_owner') or {}).get('epoch')!=owner['epoch']
                        or detail.get('window_signature')!=scene['window_signature']):
                    raise ValueError('supply_cleanup_exact_selected_recipe_effect_required')
            elif scene.get('stage') not in ('detail','facility','hub','field_hud'):
                raise ValueError('supply_cleanup_current_navigation_surface_required')
            if _stamp(detail['observed_at'])<=_stamp(original['observed_at']):raise ValueError('supply_cleanup_new_navigation_observation_required')
            nav=dict(source=detail['source'],observed_at=detail['observed_at'],confirmed=True,
                recipe_id=scene['recipe_id'],stage=detail['stage'],operation_id=pending['operation_id'],
                input_owner=deepcopy(snapshot['input_owner']),craft_owner=deepcopy(owner),goal_scope=deepcopy(scope),
                basis='same owned navigation ledger and source-bound current successor',individual_production_inferred=False)
            saved=dict(original_pending=deepcopy(pending),original_receipt=deepcopy(receipt),candidate=candidate,
                eligibility=deepcopy(proof),navigation_effect=nav,return_receipt=None,
                mode='goal_complete' if goal_effect else 'retry_candidate')
        ui=snapshot.get('material_return_ui') or {}
        if ui.get('source_family')!='native_craft':raise ValueError('current_craft_return_source_family_required')
        return_node=node;return_receipt=deepcopy(saved.get('return_receipt'))
        if step_changed:
            return_node=dict(node,step_id=quest['step_id'])
            if return_receipt:return_receipt.update(step_id=quest['step_id'])
        if return_receipt:return_receipt['dispatch_state']=pending.get('status')
        returned=plan_material_return(return_node,snapshot,return_receipt,recipe_id=candidate['recipe_id'],parent_pending=saved['original_pending'])
        if returned['status']=='waiting':return wait(returned['reason'],returned.get('missing'))
        evidence=[deepcopy(nav),*deepcopy(returned.get('effect_evidence') or [])]
        if returned['status']=='complete':
            evidence.append(deepcopy(returned['field_return_evidence']))
            if goal_effect:
                result=_complete(node,goal_effect,receipt);result['effect_evidence']+=evidence
                if goal_effect.get('state')=='step_complete':result.update(individual_craft_success_verified=False,completion_basis='native step termination after owned navigation UI cleanup')
                return result
            return dict(status='supply_retry',reason='owned_locked_supply_UI_closed',prior_effect_confirmed=True,
                released_candidate=deepcopy(candidate),effect_evidence=evidence,next_receipt=None,
                action=None,proposal=None,missing=[],sent_to_game=False)
        saved['return_receipt']=deepcopy(returned['next_receipt'])
        action=deepcopy(returned['action']);next_child=deepcopy(child)
        next_child.update(actions=[action],supply_cleanup=saved,goal_scope=deepcopy(scope))
        result=_authored_action(node,snapshot,[action],returned['proposal']['source_refs'],'craft.supply_cleanup',receipt,
            child=next_child,prior_effect=True,observed_at=ui['observed_at'])
        result.update(reason=returned['reason'],effect_evidence=evidence)
        result['proposal']['expected_effect']=deepcopy(returned['proposal']['expected_effect'])
        return result
    except (ValueError,KeyError,TypeError,AttributeError) as exc:return wait(str(exc))
