"""Source-owned logical group delivery; never invent a selected NPC entity.

The client request carries the current group name and quest occurrence directly.
All reads use the caller's already-open guarded session; no network API is called.
"""
from copy import deepcopy
import hashlib
import json
import struct
from .paths import root
from .vendor.map_character_context import _class

SOURCE_REF = 'data/r4-item-delivery/group-request-pins.json'
SOURCE_HASH = '1cf0f215ee42dab5c6cf456063cbe25af1df25d439f16860c9276c777afe1b15'
_raw = (root()/SOURCE_REF).read_bytes()
if hashlib.sha256(_raw).hexdigest() != SOURCE_HASH:
    raise ValueError('Group delivery request source pins changed')
PINS = json.loads(_raw)
del _raw


def _fields(session, pointer, name):
    shape = next(c for c in PINS['classes'] if c['name'] == name)
    if not pointer or session.reader.u64(pointer) != _class(session.reader, session.base, shape):
        raise ValueError('Current group delivery source class changed: '+name)
    return session.fields(session.reader.u64(pointer), shape)


def _ref(session, pointer, fields, name):
    return struct.unpack('<Q', session.watch(pointer+fields[name], 8))[0]



def _unpatched_request(session):
    rule=PINS['patch_guard'];shape=next(c for c in PINS['classes'] if c['name']=='ILFixDynamicMethodWrapper')
    klass=struct.unpack('<Q',session.watch(session.base+rule['wrapper_slot_rva'],8))[0]
    if session.reader.class_name(klass)!=('ILFixDynamicMethodWrapper','IFix'):
        raise ValueError('Current request patch wrapper class changed')
    offsets=session.fields(klass,shape)
    static=struct.unpack('<Q',session.watch(klass+0xb8,8))[0]
    if not static:raise ValueError('Current request patch state unavailable')
    array=struct.unpack('<Q',session.watch(static+offsets['wrapperArray'],8))[0]
    if not array:raise ValueError('Current request patch array unavailable')
    length=struct.unpack('<Q',session.watch(array+24,8))[0]
    if length>100000:raise ValueError('Current request patch array exceeds bound')
    for slot in rule['patch_slots']:
        if length>slot and struct.unpack('<Q',session.watch(array+32+slot*8,8))[0]:
            raise ValueError('Current group delivery request is patched; original request semantics unavailable')


def read_group_request(session, component, group_name):
    """Current UI -> controller -> same component -> concrete batch supplier."""
    if any(session.plan.get(k) != v for k, v in PINS['source_version'].items()):
        raise ValueError('Current group delivery source version changed')
    ui = getattr(session, '_current_ui', None)
    uf = _fields(session, ui, 'DeliverUIInstance')
    controller = _ref(session, ui, uf, 'controller')
    cf = _fields(session, controller, 'DeliverUIController')
    actual_component = _ref(session, controller, cf, 'viewModelComponent')
    if actual_component != component:
        raise ValueError('Current group delivery component belongs to another UI')
    _fields(session, component, 'DeliverUIViewModelComponent')
    supplier = _ref(session, controller, cf, 'logicSupplier')
    _fields(session, supplier, 'DeliverLogicSupplier')
    for method in PINS['methods']:
        raw = bytes.fromhex(method['raw_hex'])
        if hashlib.sha256(raw).hexdigest() != method['sha256'] or session.watch(session.base+method['rva'],len(raw)) != raw:
            raise ValueError('Current group delivery request method changed: '+method['owner']+'.'+method['name'])
    _unpatched_request(session)
    # Recheck the source group even when called directly; the argument is never
    # an independent semantic fact supplied by a caller.
    from .group_npc_observer import GroupNPCSession
    view=GroupNPCSession.__new__(GroupNPCSession)
    for key in ('reader','base','plan','character_plan','_guards'):setattr(view,key,getattr(session,key))
    group_offset=session.typed_fields(component,'DeliverUIViewModelComponent')['targetNPCGroupTagName']
    actual_group=view.string(struct.unpack('<Q',session.watch(component+group_offset,8))[0])
    if actual_group!=group_name:raise ValueError('Current request group differs from actual component string')
    npc = session.inline_value(component,'DeliverUIViewModelComponent','targetNPCSheetId','NPCSheetId','Id')
    if type(npc) is not int or npc != 0 or not isinstance(group_name,str) or not group_name:
        raise ValueError('Group-only request requires the actual zero NPC ID and nonempty group')
    instance = session.inline_value(component,'DeliverUIViewModelComponent','targetQuestInstanceId','QuestInstanceId','int64Value')
    step = session.field(component,'DeliverUIViewModelComponent','targetStepId')
    objective = session.field(component,'DeliverUIViewModelComponent','targetObjectiveIndex')
    if type(instance) is not int or instance == 0 or type(step) is not int or type(objective) is not int or objective < 0:
        raise ValueError('Current group delivery request occurrence missing')
    session.assert_stable()
    return dict(confirmed=True,kind='current-delivery-group-request',source=SOURCE_REF,source_hash=SOURCE_HASH,
        source_version=deepcopy(PINS['source_version']),ui_pointer=ui,controller_pointer=controller,
        component_pointer=component,logic_supplier_pointer=supplier,group_name=group_name,npc_id=0,
        quest_instance_id=instance,step_id=step,objective_index=objective,entity_key_available=False,
        request_target='NPCGroupTagName',basis='source GetDeliverItems copies the current component group and occurrence to DeliverItems')


def delivery_group_request(snapshot, node):
    """Validate source group request evidence without converting it to NPCSheet."""
    typed = (node.get('params') or {}).get('typed_operands') or {}
    group = (typed.get('operands') or {}).get('npc_group_tag') or {}
    details = snapshot.get('details') or {};proof = details.get('delivery_group') or {}
    if not group or proof.get('kind') != 'current-delivery-group-request':
        return None
    try:component = int(details['component_pointer'],0) if isinstance(details['component_pointer'],str) else details['component_pointer']
    except (KeyError,TypeError,ValueError):component = None
    if (proof.get('confirmed') is not True or not group.get('value') or proof.get('group_name') != group['value']
            or proof.get('source') != SOURCE_REF or proof.get('source_hash') != SOURCE_HASH
            or proof.get('source_version') != PINS['source_version'] or proof.get('request_target') != 'NPCGroupTagName'
            or type(details.get('target_npc_id')) is not int or details['target_npc_id'] != 0 or proof.get('npc_id') != 0
            or proof.get('ui_pointer') != snapshot.get('ui_pointer') or proof.get('component_pointer') != component
            or any(type(proof.get(k)) is not int or proof[k] <= 0 for k in
                   ('ui_pointer','controller_pointer','component_pointer','logic_supplier_pointer'))
            or any(proof.get(k) != details.get(v) for k,v in
                   (('quest_instance_id','target_quest_instance_id'),('step_id','targetStepId'),('objective_index','targetObjectiveIndex')))):
        raise ValueError('Current logical group delivery request owner/source changed')
    return dict(table_domain='NPCGroupTagName',value=group['value'],request_proof=deepcopy(proof))


def group_request_ownership(snapshot, frame, node, target):
    """Same accepted lifetime checks as exact-NPC delivery, with the source target."""
    current=frame.get('current_quest') or {};details=snapshot.get('details') or {}
    required={'confirmed':True,'source':'current-root-typed-quest-collections',
        'session_id':frame.get('session_id'),'process_started_at':frame.get('process_started_at')}
    if any(current.get(k)!=v or v is None for k,v in required.items()) or str(current.get('character_id'))!=str(frame.get('character_id')):
        return None,['current accepted quest observation for the same character/process/session']
    if not current.get('observed_at') or not current.get('accept_time'):
        return None,['current accepted quest observation time and accepted occurrence']
    checks=[(details.get('target_quest_instance_id'),current.get('instance_id')),
        (current.get('quest_id'),node.get('quest_id')),(details.get('targetStepId'),current.get('step_id')),
        (details.get('targetStepId'),node.get('step_id')),(details.get('targetObjectiveIndex'),node.get('objective_index'))]
    if any(a is None or b is None or a!=b for a,b in checks):
        return None,['current group delivery quest/step/objective differs from selected goal']
    accepted=[p for p in frame.get('quest_state_proofs',[]) if p.get('confirmed') is True and p.get('state')=='accepted'
        and p.get('source')==required['source'] and p.get('session_id')==required['session_id']
        and str(p.get('character_id'))==str(current.get('character_id'))
        and all(p.get(k)==current[k] for k in ('quest_id','instance_id','accept_time'))]
    if len(accepted)!=1:return None,['exact current accepted occurrence proof unavailable or ambiguous']
    identity={'table_domain':'NPCGroupTagName','value':target['value']}
    return dict(quest_id=current['quest_id'],instance_id=current['instance_id'],accept_time=current['accept_time'],
        step_id=current['step_id'],objective_index=details['targetObjectiveIndex'],npc_id=0,npc=identity,
        npc_identity=identity,npc_label=None,ownership_basis='current source group request joined to the exact current accepted quest occurrence'),[]
