"""Bind one current typed objective to local routing, without changing its goal.

The catalog node remains immutable. This does not add game commands, infer
operands from text, or turn acquisition/event objectives into stock predicates.
"""
from copy import deepcopy

DEDICATED = frozenset(('dialogue.talk','objective.inspect','objective.read_story',
    'objective.use_quest_item',
    'life.gather','life.craft','life.alter','inventory.hold','objective.deliver',
    'objective.move','objective.dungeon_clear'))
OWNER = ('quest_id','instance_id','accept_time','step_id','step_index','objective_index',
         'process_started_at','session_id','character_id')


def _retired_life_binding(node,snapshot,receipt,previous):
    """Reconcile an owned activity; never reinterpret a new current objective."""
    from .arbeit_operations import _fresh
    from .quest_program import load_schema
    from .arbeit_crafting import _step_termination
    if not isinstance(previous,dict):return None
    schema=load_schema();current=(snapshot.get('quest_state_read') or {}).get('current_quest') or snapshot.get('current_quest') or {}
    lifetime=('quest_id','instance_id','accept_time','process_started_at')
    if ((snapshot.get('quest_state_read') or {}).get('ok') is False
            or current.get('confirmed') is not True or current.get('source')!='current-root-typed-quest-collections'
            or not _fresh(current,snapshot) or previous.get('confirmed') is not True
            or any(previous.get(k)!=node.get(k) for k in ('quest_id','step_id','objective_index'))
            or any(previous.get(k) is None or current.get(k)!=previous[k] for k in lifetime)
            or any(previous.get(k)!=snapshot.get(k) for k in ('session_id','character_id'))
            or any((previous.get('source_version') or {}).get(k)!=schema[k] for k in ('metadata_sha256','game_assembly_sha256'))):return None
    phase=receipt.get('phase');child=receipt.get('child_receipt') or {};kind=None
    if previous.get('dispatch_op') in ('life.alter','inventory.hold'):
        from .processing_completion import processing_termination_evidence
        original=receipt
        if phase=='alter.pointer_probe' and child.get('processing_stage')=='collect':
            original=(child.get('before_probe_pending') or {}).get('planner_receipt') or {}
        if processing_termination_evidence(node,snapshot,original):
            kind='reconciling_processing_termination'
    if phase=='alter.cleanup' and previous.get('dispatch_op') in ('life.alter','inventory.hold'):
        goal=child.get('goal_effect') or {}
        if (child.get('goal_native') is True and goal.get('confirmed') is True and goal.get('complete') is True
                and goal.get('semantics')=='current_quest_objective_native_IsAchieved'
                and all(goal.get(k)==previous.get(k) for k in OWNER)):
            kind='reconciling_processing_cleanup'
    if phase in ('gather.native_activity','gather.stop') and previous.get('dispatch_op') in ('life.gather','inventory.hold'):
        if _step_termination(node,snapshot,dict(child,goal_scope={k:previous[k] for k in lifetime+('step_id',)})):
            kind='reconciling_terminated_gather'
    if kind:
        selected=deepcopy(node)
        selected.update(op=previous['dispatch_op'],definition_op=previous['definition_op'],runtime_operation_binding=deepcopy(previous))
        if kind=='reconciling_processing_termination':
            selected['operation_reconciliation']='owned_processing_termination'
        return dict(status=kind,node=selected,binding=deepcopy(previous),missing=[])
    return None


def bind_operation(node, snapshot, receipt=None):
    result = dict(status='not_applicable', node=deepcopy(node), binding=None, missing=[])
    if node.get('objective_index') is None or 'material_requirement' in node.get('params', {}):
        return result
    rows = [p for p in snapshot.get('objective_programs', []) if isinstance(p,dict)
            and all(p.get(k)==node.get(k) for k in ('quest_id','step_id','objective_index'))]
    if not rows:
        receipt=receipt or ((snapshot.get('pending_input') or {}).get('planner_receipt'))
        previous=(receipt or {}).get('operation_binding')
        retired=_retired_life_binding(node,snapshot,receipt,previous) if previous else None
        if retired:return retired
        from .supply_selection import CRAFT_NAVIGATION
        cleanup_phases=CRAFT_NAVIGATION+('craft.supply_cleanup','craft.submit','craft.result.probe','craft.result.ack')
        if previous and (receipt or {}).get('phase') in cleanup_phases:
            from .arbeit_crafting import _step_termination
            from .quest_program import load_schema
            child=receipt.get('child_receipt') or {}
            original=((child.get('supply_cleanup') or {}).get('original_pending') or {}).get('planner_receipt') or receipt
            saved_child=original.get('child_receipt') or {}
            scope=saved_child.get('goal_scope') or original.get('goal_scope') or {};schema=load_schema()
            # Only finish an already submitted operation after exact native step
            # termination. No current recipe, target or new craft is inferred.
            if (previous.get('confirmed') is True and previous.get('dispatch_op') in ('life.craft','inventory.hold')
                    and all(previous.get(k)==node.get(k) for k in ('quest_id','step_id','objective_index'))
                    and all(scope.get(k)==previous.get(k) and previous.get(k) is not None for k in
                            ('quest_id','instance_id','accept_time','step_id','process_started_at'))
                    and all(previous.get(k)==snapshot.get(k) for k in ('session_id','character_id'))
                    and all((previous.get('source_version') or {}).get(k)==schema[k] for k in
                            ('metadata_sha256','game_assembly_sha256'))
                    and _step_termination(node,snapshot,dict(saved_child,goal_scope=scope))):
                selected=deepcopy(node)
                selected.update(op=previous['dispatch_op'],definition_op=previous['definition_op'],
                                runtime_operation_binding=deepcopy(previous))
                return dict(result,status='reconciling_terminated_craft',node=selected,binding=deepcopy(previous))
        return dict(result,status='unavailable',missing=['current_typed_operation_not_observed'])
    from .objective_binding import current_objective_occurrence
    from .quest_program import load_schema, _OPERATIONS
    if len(rows)!=1:
        return dict(result,status='invalid',missing=['current_typed_operation_ambiguous'])
    program=rows[0];schema=load_schema();data=program.get('data') or {}
    if (program.get('owner_verified') is not True
            or not current_objective_occurrence(program,snapshot,node)
            or any((program.get('source_version') or {}).get(k)!=schema[k]
                   for k in ('metadata_sha256','game_assembly_sha256'))):
        return dict(result,status='invalid',missing=['current_typed_operation_occurrence_or_source'])
    declarations=[c for c in schema['classes']
        if c['namespace']+'.'+c['name']==data.get('type') and c['name'].endswith('QuestObjectiveData')]
    source=declarations[0] if len(declarations)==1 else {}
    getters=[g for g in source.get('getters',[]) if g['name']=='get_Type' and 'constant' in g]
    discriminant=data.get('discriminant') or {}
    if (data.get('available') is not True or len(getters)!=1 or discriminant.get('verified') is not True
            or discriminant.get('value')!=getters[0]['constant']):
        return dict(result,status='invalid',missing=['current_typed_operation_discriminant'])
    operation=_OPERATIONS.get(source['name'],'objective.native')
    dispatch=operation if operation in DEDICATED else 'objective.unresolved'
    from .typed_objective_operands import normalize_objective_operands
    normalized=normalize_objective_operands(program,expected=node)
    binding={k:deepcopy(program.get(k)) for k in OWNER}
    binding.update(confirmed=True,source=program['source'],observed_at=program['observed_at'],
        source_version=deepcopy(program['source_version']),data_type=data['type'],
        discriminant=deepcopy(discriminant),operands=deepcopy(data.get('values',{})),
        operand_missing=deepcopy(data.get('missing',[])),definition_op=node.get('definition_op',node['op']),
        operation=operation,dispatch_op=dispatch,
        route_kind='dedicated_provider' if operation in DEDICATED else 'native_program_only',
        goal_semantics='current_typed_objective; neither inventory delta nor catalog text',
        native_autoplay_readiness_inferred=False,normalized_operands=normalized)
    selected=deepcopy(node)
    selected.update(op=dispatch,definition_op=binding['definition_op'],runtime_operation_binding=deepcopy(binding))
    selected.setdefault('params',{})['typed_operands']=deepcopy(normalized)
    if normalized['status'] in ('supported','partial'):
        params=selected['params']
        params['definition_targets']=deepcopy(params.get('targets',params.get('objective',{}).get('targets',[])))
        # Runtime handlers consume typed identities. Display-description targets
        # cannot silently stand in for a missing NPC/group/region/prop operand.
        params['targets']=[]
        npc=normalized.get('operands',{}).get('npc')
        if normalized['ready'] and npc and npc.get('table_domain')=='NPCSheet' and type(npc.get('id')) is int:
            params['targets']=[dict(kind='npc',table_data={'raw_id':npc['id']&0xffffffff},
                identity_resolution={'source_ref':dict(source=program['source'],
                    source_version=deepcopy(program['source_version']),observed_at=program['observed_at'],
                    operand=deepcopy(npc),scope={k:program.get(k) for k in OWNER})},
                target_evidence_kind='current_owned_typed_NPC_operand_not_display_text')]
    return dict(status='bound',node=selected,binding=binding,missing=[])


def same_operation(previous, current):
    """A fresh observation may revalidate an old route, never silently replace it."""
    keys=OWNER+('data_type','discriminant','source_version','operation','dispatch_op','operands')
    return all(previous.get(k)==current.get(k) for k in keys)
