"""Current rooted quest-item and identified equipment stock. OFF by default.

No game method is called. Reads use the existing bounded external reader and
the current singular entity, not an arbitrary item/UI/heap owner. Static item
names and recipe previews cannot become current quantities or output promises.
"""
from copy import deepcopy
from datetime import datetime, timezone
from functools import lru_cache
import hashlib
import json
import struct
import math

from .paths import root
from .vendor.map_memory import MapMemorySession, MapReadPending
from .vendor.map_character_context import active_context, _class
from .vendor.map_character_identity import CharacterMemoryReader


@lru_cache(maxsize=1)
def inventory_sources():
    shapes=json.loads((root()/'data/objective-inventory/source-shapes.json').read_text('utf8'))
    extra=json.loads((root()/'data/objective-inventory/rune-source-shapes.json').read_text('utf8'))
    if any(extra[k]!=shapes[k] for k in ('metadata_sha256','game_assembly_sha256')):raise ValueError('inventory_rune_source_revision_changed')
    known={(r['name'],r['namespace']):r for r in shapes['classes']}
    for row in extra['classes']:
        key=row['name'],row['namespace']
        if key in known:
            if known[key]['fields']!=row['fields']:raise ValueError('inventory_rune_source_declaration_conflict')
        else:shapes['classes'].append(row);known[key]=row
    return shapes


@lru_cache(maxsize=1)
def inventory_rules():
    return json.loads((root()/'data/objective-inventory/semantic-rules.json').read_text('utf8'))


@lru_cache(maxsize=1)
def inventory_definitions():
    fake_path=root()/'data/arbeit-craft/fake-items.json'
    equipment_path=root()/'data/arbeit-craft/equipment.json'
    return dict(fake=json.loads(fake_path.read_text('utf8')),equipment=json.loads(equipment_path.read_text('utf8')),
        source_refs=[dict(path=str(p.relative_to(root())).replace('\\','/'),sha256=hashlib.sha256(p.read_bytes()).hexdigest())
                     for p in (fake_path,equipment_path)])


def _unknown(reason):
    return dict(status='unknown',complete=False,rows=[],missing=[reason],quantity=None,available_quantity=None)


class CurrentObjectiveInventorySession(MapMemorySession):
    def __init__(self,pid,**options):
        super().__init__(pid,**options)
        try:
            self.inventory_schema=inventory_sources()
            if any(self.inventory_schema[k]!=self.plan[k] for k in ('metadata_sha256','game_assembly_sha256')):
                raise ValueError('objective_inventory_source_revision_changed')
            self._inventory_shapes={(s['name'],s['namespace']):s for s in self.inventory_schema['classes']}
        except BaseException:
            self.close();raise

    def stock_shape(self,name):
        matches=[s for (n,_),s in self._inventory_shapes.items() if n==name]
        if len(matches)!=1:raise ValueError('objective_inventory_source_type_ambiguous:'+name)
        return matches[0]

    def stock_fields(self,pointer,name):
        if not pointer:raise MapReadPending('objective_inventory_current_reference_missing:'+name)
        return self.fields(self.reader.u64(pointer),self.stock_shape(name))

    def value(self,pointer,name,member):
        fields=self.stock_fields(pointer,name);spec=next(f for f in self.stock_shape(name)['fields'] if f['name']==member)
        code=spec['type']['type_code'];address=pointer+fields[member]
        if code in (18,21,28):return struct.unpack('<Q',self.watch(address,8))[0]
        if code==14:
            ref=struct.unpack('<Q',self.watch(address,8))[0]
            return self.reader.string(ref) if ref else None
        if code==2:
            value=self.watch(address,1)[0]
            if value not in (0,1):raise ValueError('objective_inventory_boolean_invalid')
            return bool(value)
        if code in (8,9,10,11):
            return struct.unpack({8:'<i',9:'<I',10:'<q',11:'<Q'}[code],self.watch(address,8 if code in (10,11) else 4))[0]
        if code==17:
            nested=self.stock_shape(spec['type']['name'])
            if nested['type_index']!=spec['type']['definition_index'] or nested['value_type'] is not True:
                raise ValueError('objective_inventory_value_wrapper_definition_changed')
            self.fields(_class(self.reader,self.base,nested),nested)
            members=[f for f in nested['fields'] if not f['type'].get('attrs',0)&16]
            if len(members)!=1 or members[0]['offset']!=16 or members[0]['type']['type_code'] not in (8,9,10,11):
                raise ValueError('objective_inventory_value_wrapper_not_scalar:'+nested['name'])
            code=members[0]['type']['type_code']
            return struct.unpack({8:'<i',9:'<I',10:'<q',11:'<Q'}[code],self.watch(address,8 if code in (10,11) else 4))[0]
        raise ValueError('objective_inventory_unsupported_field:'+name+'.'+member)

    def dense_rows(self,pointer,*,key_name,value_name,key_width,value_width,boolean=False):
        """Native manual dictionaries with their actual typed parallel arrays."""
        if not pointer:raise MapReadPending('objective_inventory_dictionary_reference_missing')
        r=self.reader;klass=r.u64(pointer);name,namespace=r.class_name(klass)
        if namespace!='Silvervine.ManualMemory' or name not in ('MMDictionary`2','MMDerivedDictionary`2'):
            raise MapReadPending('objective_inventory_dictionary_layout_unsupported')
        shape=r.layout(klass);common=None
        for _ in range(6):
            if shape.get('name')=='CommonDictionary`2' and shape.get('namespace')=='Silvervine.ManualMemory.Internal':
                common=shape;break
            parent=int(shape.get('parent_class','0x0'),16)
            if not parent:break
            shape=r.layout(parent)
        fields={f['name']:f for f in (common or {}).get('fields',[])}
        if set(fields)!={'keys','values','hashTable','version','count'}:
            raise ValueError('objective_inventory_dictionary_common_fields_changed')
        if any(fields[k].get('type_code')!=8 for k in ('version','count')):
            raise ValueError('objective_inventory_dictionary_count_type_changed')
        count=struct.unpack('<i',self.watch(pointer+fields['count']['offset'],4))[0]
        self.watch(pointer+fields['version']['offset'],4)
        if not 0<=count<=8192:raise ValueError('objective_inventory_dictionary_count_bound')
        kp=struct.unpack('<Q',self.watch(pointer+fields['keys']['offset'],8))[0]
        vp=struct.unpack('<Q',self.watch(pointer+fields['values']['offset'],8))[0]
        if not kp or not vp:raise MapReadPending('objective_inventory_dictionary_arrays_unavailable')
        kk,vk=r.u64(kp),r.u64(vp)
        if (r.class_name(kk)[0],r.class_name(vk)[0],r.u32(kk+0x100),r.u32(vk+0x100))!=(key_name+'[]',value_name+'[]',key_width,value_width):
            raise ValueError('objective_inventory_dictionary_typed_array_changed')
        if r.u64(kp+24)<count or r.u64(vp+24)<count:raise ValueError('objective_inventory_dictionary_capacity_changed')
        self.watch(kp+24,8);self.watch(vp+24,8)
        kr=self.watch(kp+32,count*key_width);vr=self.watch(vp+32,count*value_width)
        rows=[]
        for i in range(count):
            key=struct.unpack_from('<i' if key_width==4 else '<q',kr,i*key_width)[0]
            value=vr[i] if boolean else struct.unpack_from('<Q',vr,i*value_width)[0]
            if boolean and value not in (0,1):raise ValueError('objective_inventory_target_boolean_invalid')
            rows.append((key,bool(value) if boolean else value))
        if len({r[0] for r in rows})!=len(rows):raise ValueError('objective_inventory_dictionary_duplicate_key')
        return rows

    def fake_stock(self,component):
        listing=self.value(component,'QuestFakeItemInventoryComponent','fakeItemList')
        if not listing:raise MapReadPending('quest_fake_item_current_list_unavailable')
        pointers=self.refs(listing,element='QuestFakeItemInfo',limit=4096)
        target_pointer=self.value(component,'QuestFakeItemInventoryComponent','useTargetfakeItemMap')
        targets=dict(self.dense_rows(target_pointer,key_name='QuestFakeItemSheetId',value_name='Boolean',key_width=4,value_width=1,boolean=True))
        rows=[]
        for pointer in pointers:
            name='QuestFakeItemInfo'
            raw_id=self.value(pointer,name,'<QuestFakeItemSheetId>k__BackingField')
            counts={key:self.value(pointer,name,'<'+field+'>k__BackingField') for key,field in
                    (('quantity','Amount'),('quest_quantity','QuestAmount'),('scenario_quantity','ScenarioAmount'))}
            if raw_id==0 or any(type(v) is not int or v<0 for v in counts.values()):
                raise ValueError('quest_fake_item_id_or_quantity_invalid')
            rows.append(dict(table_domain='QuestFakeItemSheet',item_id=raw_id,
                name=self.value(pointer,name,'<DisplayName>k__BackingField'),**counts,
                use_target=targets.get(raw_id,False),use_target_basis='current typed useTargetfakeItemMap TryGetValue && value',
                source_pointer=hex(pointer),quantity_semantics='QuestFakeItemInfo.Amount',
                available_quantity=None,availability_scope='not yet joined to current recipe and goal protection'))
        if len({r['item_id'] for r in rows})!=len(rows):raise ValueError('quest_fake_item_current_duplicate_ID')
        return dict(status='observed',complete=True,rows=rows,missing=[],use_targets={str(k):v for k,v in targets.items()},
            scope='current QuestFakeItemInventoryComponent.fakeItemList; no storage/name substitution')

    def equipment_stock(self,component):
        pointer=self.value(component,'EquipmentInventoryComponent','equipments')
        entries=self.dense_rows(pointer,key_name='NonStackableId',value_name='Equipment',key_width=8,value_width=8)
        rows=[]
        for identifier,item in entries:
            ident=self.value(item,'Equipment','<Id>k__BackingField')
            if identifier!=ident or ident<=0:raise ValueError('current_equipment_dictionary_identity_mismatch')
            sheet=self.value(item,'Equipment','sheetData')
            raw_id=self.value(sheet,'EquipmentSheetData','<Id>k__BackingField')
            fields=self.stock_fields(item,'Equipment')
            rune_shape=self.stock_shape('CarvedRune');rune_fields=self.fields(_class(self.reader,self.base,rune_shape),rune_shape)
            # Value-type field offsets include the boxed object header. Its
            # runeSheetId is another single Int32 ID; no struct pointer read.
            rune_at=item+fields['carvedRune']+rune_fields['runeSheetId']-16
            rune_id=struct.unpack('<i',self.watch(rune_at,4))[0]
            attributes=self.equipment_attributes(rune_id,equipment_pointer=item)
            rows.append(dict(table_domain='EquipmentSheet',item_id=raw_id,non_stackable_id=ident,
                locked=self.value(item,'Equipment','<IsLocked>k__BackingField'),
                is_override=self.value(item,'Equipment','<IsOverride>k__BackingField'),
                carved_rune_id=rune_id,source_pointer=hex(item),**attributes,
                future_crafting_output_guaranteed=False))
        return dict(status='observed',complete=True,rows=rows,missing=[],scope='identified current character equipment; storage and unidentified excluded')

    def equipment_attributes(self,rune_id,*,equipment_pointer=None):
        rule=inventory_rules()['equipment_uncarved'];r=self.reader
        unknown=dict(rarity=None,season=None,constraints_status='unknown')
        if rune_id!=rule['rune_sheet_id'] and equipment_pointer is None:
            return dict(unknown,missing=['nonzero_rune_current_equipment_instance_required'])
        try:
            for check in rule['code_checks']:
                wanted=bytes.fromhex(check['bytes_hex'])
                if self.watch(self.base+check['rva'],len(wanted))!=wanted:
                    raise ValueError('current_equipment_attribute_code_changed:'+check['name'])
            klass=struct.unpack('<Q',self.watch(self.base+rule['wrapper_slot_rva'],8))[0]
            if r.class_name(klass)!=('ILFixDynamicMethodWrapper','IFix'):
                raise ValueError('current_equipment_attribute_patch_owner_changed')
            statics=struct.unpack('<Q',self.watch(klass+0xb8,8))[0]
            array=struct.unpack('<Q',self.watch(statics,8))[0]
            if not array:raise MapReadPending('current_equipment_patch_array_unavailable')
            count=struct.unpack('<Q',self.watch(array+24,8))[0]
            if count>100000:raise ValueError('current_equipment_patch_array_bound')
            for slot in rule['patch_slots']:
                if slot<count and struct.unpack('<Q',self.watch(array+32+8*slot,8))[0]:
                    raise MapReadPending('current_equipment_attribute_getter_patched')
            if rune_id!=rule['rune_sheet_id']:
                from .objective_inventory_equipment import nonzero_equipment_attributes
                return nonzero_equipment_attributes(self,rune_id,equipment_pointer)
            return dict(rarity=rule['rarity'],season=rule['season'],constraints_status='observed',missing=[],
                attribute_basis=rule['basis'],attribute_source='data/objective-inventory/semantic-rules.json',
                attribute_source_sha256=hashlib.sha256((root()/'data/objective-inventory/semantic-rules.json').read_bytes()).hexdigest())
        except (OSError,ValueError,RuntimeError,KeyError,TypeError) as exc:
            return dict(unknown,missing=[str(exc)])

    def read_inventory(self):
        r=self.reader;self._guards=[]
        context=active_context(r,self.base,self.character_plan)
        identity=CharacterMemoryReader._value(r,context['identity_pointer'],self.identity_class,self.identity_shape)
        entity=context['chain'][3];fields=self.stock_fields(entity,'SingularEntity')
        result={}
        for name,field,reader in (('fake_items','IQuestFakeItemInventoryComponent',self.fake_stock),
                                  ('equipment','IEquipmentInventoryComponent',self.equipment_stock)):
            try:
                component=struct.unpack('<Q',self.watch(entity+fields[field],8))[0]
                result[name]=reader(component)
            except (OSError,ValueError,RuntimeError,KeyError,TypeError) as exc:result[name]=_unknown(str(exc))
        if (context!=active_context(r,self.base,self.character_plan)
                or identity!=CharacterMemoryReader._value(r,context['identity_pointer'],self.identity_class,self.identity_shape)
                or any(r.read(a,len(data))!=data for a,data in self._guards)):
            raise MapReadPending('objective_inventory_owner_or_collection_changed_during_read')
        return dict(result,pid=r.pid,process_started_at=r.started_at.isoformat(),character_id=str(identity['character_id']),
            observed_at=datetime.now(timezone.utc).isoformat(),confirmed=True,source='current-root-typed-objective-inventory',
            source_version={k:self.inventory_schema[k] for k in ('metadata_sha256','game_assembly_sha256')},
            source_refs=deepcopy(self.inventory_schema['source_refs']),game_inputs=0)


class ObjectiveInventoryProvider:
    def __init__(self,*,allow_process_read=False,session_factory=CurrentObjectiveInventorySession):
        self.allow_process_read=allow_process_read;self.session_factory=session_factory

    def __call__(self,*,snapshot,node=None,session=None,**_):
        result=dict(ok=False,source='current-root-typed-objective-inventory',confirmed=False,
            fake_items=_unknown('objective_inventory_not_read'),equipment=_unknown('objective_inventory_not_read'),
            missing=[],game_inputs=0,read_attempted=False,live_verified=False)
        if not self.allow_process_read:return dict(result,missing=['read_only_objective_inventory_access_disabled'])
        window=(snapshot.get('window_preflight') or {}).get('selected') or {}
        identity=snapshot.get('identity') or {};pid=window.get('pid');started=identity.get('process_started_at') or window.get('process_started_at')
        if type(pid) is not int or pid<=0 or not started or not all(snapshot.get(k) for k in ('session_id','character_id')):
            return dict(result,missing=['current_objective_inventory_process_character_owner'])
        result['read_attempted']=True
        try:
            if session is not None:observed=session.read_inventory()
            else:
                with self.session_factory(pid) as opened:observed=opened.read_inventory()
            if (observed.get('pid')!=pid or observed.get('process_started_at')!=started
                    or observed.get('character_id')!=snapshot['character_id'] or observed.get('confirmed') is not True
                    or any((observed.get('source_version') or {}).get(k)!=inventory_sources()[k] for k in ('metadata_sha256','game_assembly_sha256'))):
                raise ValueError('objective_inventory_owner_or_source_mismatch')
            return dict(result,**observed,ok=True,session_id=snapshot['session_id'])
        except (OSError,ValueError,RuntimeError,KeyError,TypeError) as exc:return dict(result,missing=[str(exc)])


def current_fake_item_stock(item,snapshot):
    """Exact typed current stock; read gaps never imply zero available items."""
    from .arbeit_operations import _fresh
    raw=snapshot.get('objective_inventory') or {};domain=raw.get('fake_items') or {}
    result=dict(status='unknown',quantity=None,available_quantity=None,missing=[])
    current=snapshot.get('identity') or snapshot.get('current_quest') or {}
    if (raw.get('ok') is not True or raw.get('confirmed') is not True or not _fresh(raw,snapshot)
            or raw.get('source')!='current-root-typed-objective-inventory' or not raw.get('process_started_at')
            or raw['process_started_at']!=current.get('process_started_at')
            or any((raw.get('source_version') or {}).get(k)!=inventory_sources()[k] for k in ('metadata_sha256','game_assembly_sha256'))
            or domain.get('complete') is not True or domain.get('status')!='observed'):
        return dict(result,missing=['fresh_owned_complete_quest_fake_item_inventory'])
    if item.get('table_domain')!='QuestFakeItemSheet' or type(item.get('item_id')) is not int:
        return dict(result,missing=['exact_QuestFakeItemSheet_domain_and_ID'])
    source=inventory_definitions()['fake'];definitions=source.get('items',source.get('rows',[]))
    definitions=[r for r in definitions if r['raw_id']==item['item_id']]
    if len(definitions)!=1 or definitions[0]['display_name']!=item.get('name'):
        return dict(result,missing=['quest_fake_item_current_source_name_ID_join'])
    rows=[r for r in domain['rows'] if r.get('table_domain')=='QuestFakeItemSheet' and r.get('item_id')==item['item_id']]
    if len(rows)>1:return dict(result,missing=['quest_fake_item_current_duplicate_ID'])
    if rows and rows[0].get('name')!=item['name']:return dict(result,missing=['quest_fake_item_observed_name_changed'])
    quantity=rows[0]['quantity'] if rows else 0
    if type(quantity) is not int or quantity<0:return dict(result,missing=['quest_fake_item_current_quantity_invalid'])
    use_target=rows[0].get('use_target') if rows else (domain['use_targets'].get(str(item['item_id']),False) if isinstance(domain.get('use_targets'),dict) else None)
    return dict(result,status='observed',quantity=quantity,use_target=use_target,definition=deepcopy(definitions[0]),
        current_row=deepcopy(rows[0]) if rows else None,absence_confirmed=not rows,
        observation={k:deepcopy(raw[k]) for k in ('source','observed_at','process_started_at','session_id','character_id','source_version','source_refs')})


def fake_item_craft_materials(recipe,batches,snapshot,*,node_id):
    """Current source-count + exact recipe row + native goal commitments.

    The permitted action is that currently selected recipe's existing confirm
    input. This does not authorize quest-item use, disposal, or an API call.
    """
    from .arbeit_operations import _fresh
    from .goal_reservations import goal_commitment_for_item
    scene=snapshot.get('crafting') or {}
    if (scene.get('confirmed') is not True or not _fresh(scene,snapshot) or scene.get('stage')!='detail'
            or scene.get('selected_recipe_id')!=recipe.get('id')
            or scene.get('current_binding_kind')!='typed current native recipe'
            or recipe.get('source_ref') not in scene.get('source_refs',[])
            or type(batches) is not int or batches<=0 or (scene.get('quantity') or {}).get('value')!=batches):
        raise ValueError('quest_fake_item_current_source_recipe_required')
    proofs=[]
    for material in recipe.get('fake_item_ingredients',[]):
        item=dict(table_domain='QuestFakeItemSheet',item_id=material['raw_id'],name=material['display_name'])
        stock=current_fake_item_stock(item,snapshot)
        if stock['status']!='observed':raise ValueError('quest_fake_item_stock_unknown:'+','.join(stock['missing']))
        if type(material.get('amount')) is not int or material['amount']<=0:raise ValueError('quest_fake_item_source_recipe_amount_invalid')
        required=material['amount']*batches
        rows=[r for r in scene.get('materials',[]) if (r.get('item') or {}).get('displayName')==item['name']]
        # Fake item labels share the UI with Consumables. A colliding recipe
        # label cannot identify a typed quantity; do not choose the first row.
        if (len(rows)!=1 or any(r['name']==item['name'] for r in recipe.get('consumable_ingredients',[]))
                or sum(r['display_name']==item['name'] for r in recipe.get('fake_item_ingredients',[]))!=1):
            raise ValueError('quest_fake_item_current_recipe_label_ambiguous')
        ui=rows[0];count=ui.get('inventoryCount');need=ui.get('requiredCount')
        if (type(count) not in (int,float) or not math.isfinite(count) or count!=stock['quantity']
                or type(need) not in (int,float) or not math.isfinite(need) or need!=required):
            raise ValueError('quest_fake_item_current_Amount_and_recipe_UI_count_changed')
        if stock['quantity']<required:raise ValueError('craft_quest_fake_item_stock_shortage:'+item['name'])
        if (stock.get('current_row') or {}).get('use_target') is not False:
            raise ValueError('craft_quest_fake_item_current_use_target_protected:'+item['name'])
        reference_protection=fake_item_goal_protection(item,snapshot,exclude_node_id=node_id)
        if reference_protection['status']=='unknown':
            raise ValueError('craft_quest_fake_item_goal_scope_unknown:'+','.join(reference_protection['missing']))
        if reference_protection['status']=='protected':
            raise ValueError('craft_quest_fake_item_other_current_goal_reference_protected:'+item['name'])
        commitment=goal_commitment_for_item(snapshot,item,exclude_node_id=node_id)
        free=max(0,stock['quantity']-commitment['required_quantity'])
        if free<required:raise ValueError('craft_quest_fake_item_goal_reserved_shortage:'+item['name'])
        proofs.append(dict(status='available',item=item,quantity=stock['quantity'],available_quantity=stock['quantity'],
            required_quantity=required,uncommitted_quantity=free,goal_commitment=commitment,
            stock_evidence=stock,current_material_row=deepcopy(ui),batches=batches,
            current_reference_protection=reference_protection,
            quantity_semantics='current QuestFakeItemInfo.Amount; current native recipe UI agrees',
            permitted_action='current_owned_native_craft_confirm_only',inventory_quantity_modified=False,
            **stock['observation']))
    return proofs


def fake_item_goal_protection(item,snapshot,*,exclude_node_id=None):
    """Protect exact current quest references without inventing an item quota.

    Gain/use counters measure events. They are retained as native semantics,
    never converted to a stock target. Existing durable commitments remain a
    separate required consumer and survive a current-step change.
    """
    from .arbeit_operations import _fresh
    from .objective_binding import current_objective_occurrence
    from .quest_program import load_schema
    result=dict(status='clear',references=[],acquisition_events=[],missing=[],quota_inferred=False)
    schema=load_schema();current=snapshot.get('current_quest') or (snapshot.get('quest_state_read') or {}).get('current_quest') or {}
    if snapshot.get('quest_state_read') and snapshot['quest_state_read'].get('ok') is not True:
        return dict(result,status='unknown',missing=['current_quest_collection_unavailable'])
    def atoms(value,depth=0):
        if depth>12:raise ValueError('typed_fake_item_reference_depth_bound')
        if isinstance(value,dict):
            if value.get('declared_type')=='Shared.Symbols.QuestFakeItemSheetId':yield value
            for v in value.values():
                if isinstance(v,(dict,list)):yield from atoms(v,depth+1)
        elif isinstance(value,list):
            if len(value)>1024:raise ValueError('typed_fake_item_reference_count_bound')
            for v in value:yield from atoms(v,depth+1)
    for program in snapshot.get('objective_programs',[]):
        data=program.get('data') or {}
        try:references=list(atoms(data.get('values') or {}))
        except ValueError as exc:return dict(result,status='unknown',missing=[str(exc)])
        # A partial same-current program may hide a consumption requirement.
        if data.get('missing') and program.get('quest_id')==current.get('quest_id'):
            return dict(result,status='unknown',missing=['partial_current_typed_program_cannot_clear_fake_item_protection'])
        references=[v for v in references if v.get('value')==item.get('item_id')]
        if not references:continue
        node=dict(quest_id=program.get('quest_id'),step_id=program.get('step_id'),objective_index=program.get('objective_index'))
        own_id=program.get('node_id') or f"q{node['quest_id']}.s{node['step_id']}.o{node['objective_index']}"
        if (not current_objective_occurrence(program,snapshot,node) or program.get('owner_verified') is not True
                or any((program.get('source_version') or {}).get(k)!=schema[k] for k in ('metadata_sha256','game_assembly_sha256'))
                or any(v.get('interpreted') is not True or v.get('domain_mapping_verified') is not True
                    or v.get('table_domain')!='QuestFakeItemSheet' for v in references)):
            return dict(result,status='unknown',missing=['current_typed_fake_item_reference_owner_or_domain'])
        if own_id==exclude_node_id:continue
        if data.get('type')=='MM.QDBL.CodeGenerated.GainFakeItemByInteractInstantPropQuestObjectiveData':
            # This native objective counts acquisition events. Its output
            # reference is not a promise to retain the acquired stock. Keep
            # the event separately; Use/Deliver and durable quotas still bind.
            from .typed_objective_operands import normalize_objective_operands
            normalized=normalize_objective_operands(program,expected=node)
            output=normalized.get('operands',{}).get('output_item') or {}
            if (normalized.get('ready') is not True or output.get('table_domain')!='QuestFakeItemSheet'
                    or output.get('id')!=item.get('item_id')):
                return dict(result,status='unknown',missing=['current_typed_fake_acquisition_output_unverified'])
            result['acquisition_events'].append(dict(node_id=own_id,scope=deepcopy(normalized['scope']),
                output_item=deepcopy(output),native_effect=deepcopy(program.get('typed_effect')),
                quota_inferred=False,stock_retention_required=False))
            continue
        result['references'].append(dict(node_id=own_id,data_type=data.get('type'),
            objective_scope={k:program[k] for k in ('quest_id','instance_id','accept_time','step_id','step_index','objective_index','process_started_at')},
            source=program['source'],observed_at=program['observed_at'],typed_references=deepcopy(references),
            protection_policy='preserve_current_stock_for_other_current_goal; no event_count_to_stock_quota'))
    if result['references']:result['status']='protected'
    return result


def bind_equipment_reward(reward,snapshot):
    """Resolve an actual reward NonStackableId, without promising its attributes."""
    from .arbeit_operations import _fresh
    raw=snapshot.get('objective_inventory') or {};stock=raw.get('equipment') or {}
    current=snapshot.get('identity') or snapshot.get('current_quest') or {}
    if (reward.get('type')!='Equipment' or type(reward.get('non_stackable_id')) is not int
            or raw.get('ok') is not True or raw.get('confirmed') is not True or not _fresh(raw,snapshot)
            or raw.get('source')!='current-root-typed-objective-inventory'
            or not raw.get('process_started_at') or raw['process_started_at']!=current.get('process_started_at')
            or any((raw.get('source_version') or {}).get(k)!=inventory_sources()[k] for k in ('metadata_sha256','game_assembly_sha256'))
            or stock.get('status')!='observed' or stock.get('complete') is not True):
        return dict(ready=False,missing=['fresh_current_identified_equipment_reward_owner'])
    rows=[r for r in stock.get('rows',[]) if r.get('non_stackable_id')==reward['non_stackable_id']]
    if len(rows)!=1:return dict(ready=False,missing=['exact_current_equipment_NonStackableId'])
    row=rows[0]
    return dict(ready=True,item_id=row['item_id'],table_domain='EquipmentSheet',non_stackable_id=row['non_stackable_id'],
        observed_instance=deepcopy(row),rarity_confirmed=row.get('constraints_status')=='observed',
        season_confirmed=row.get('constraints_status')=='observed',future_output_guaranteed=False,
        observation={k:deepcopy(raw.get(k)) for k in ('source','observed_at','process_started_at','session_id','character_id','source_version','source_refs')})


def bind_crafting_equipment_rewards(result,snapshot):
    """Bind reported instances separately from immutable native UI semantics.

    A failed/expired collection is kept as such. Neither an existing instance
    nor the reward's presentation rarity establishes creation or quest success.
    """
    bindings=[]
    for collection,rows in (result.get('rewards') or {}).items():
        if collection not in ('successRewards','criticalSuccessRewards','additionalRewards','failedRewards','expiredRewards'):
            continue
        for index,reward in enumerate(rows):
            if reward.get('type')!='Equipment':continue
            bindings.append(dict(collection=collection,reward_index=index,reported_reward=deepcopy(reward),
                **bind_equipment_reward(reward,snapshot),new_instance_created_verified=False,
                native_objective_effect_supplied=False))
    return bindings
