"""Current default delivery policy and exact per-instance selection effects.

Reads only through the caller's guarded session. No inventory balance or catalog
quantity is substituted for the actual DeliverContext and CommonItemData.
"""
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/source-pins.json'
SOURCE_HASH = 'dca09621f87a2096ec9096cddef990dd12910ab51f11a7183c61d86dbf8d9117'
_raw = (root()/SOURCE_REF).read_bytes()
if hashlib.sha256(_raw).hexdigest() != SOURCE_HASH:
    raise ValueError('Delivery policy source pins changed')
PINS = json.loads(_raw)
del _raw


def _equipped_ids(session, pointer):
    """Actual closed MMList<Int64>; both array type and stride must agree."""
    r = session.reader
    klass = r.u64(pointer)
    if r.class_name(klass) != ('MMList`1', 'Silvervine.ManualMemory'):
        raise ValueError('Current equipped item list type changed')
    for _ in range(4):
        fields = {x['name'].lstrip('_'): x for x in r.layout(klass)['fields']}
        if {'array', 'size', 'version'} <= fields.keys():
            break
        klass = r.u64(klass+0x58)
    else:
        raise ValueError('Current equipped item list fields missing')
    if fields['size']['type_code'] != 8 or fields['version']['type_code'] != 8:
        raise ValueError('Current equipped list count type changed')
    count = struct.unpack('<i', session.watch(pointer+fields['size']['offset'], 4))[0]
    session.watch(pointer+fields['version']['offset'], 4)
    if not 0 <= count <= 512:
        raise ValueError('Current equipped list exceeds bound')
    if not count:
        return []
    array = struct.unpack('<Q', session.watch(pointer+fields['array']['offset'], 8))[0]
    length, stride = session.array(array, name='Int64[]', stride=8, limit=30000)
    if stride != 8 or count > length:
        raise ValueError('Current equipped Int64 array changed')
    return list(struct.unpack('<'+'q'*count, session.watch(array+32, count*8)))


def read_delivery_policy(session, component):
    """Exact component -> DefaultDeliverPolicy -> same context collection."""
    if any(session.plan.get(k) != v for k, v in PINS['source_version'].items()):
        raise ValueError('Current delivery policy source version changed')
    policy = session.field(component, 'DeliverUIViewModelComponent', 'deliverPolicy')
    shape = PINS['classes'][0]
    if not policy or session.reader.u64(policy) != _class(session.reader, session.base, shape):
        raise ValueError('Current delivery policy is not the supported DefaultDeliverPolicy')
    fields = session.fields(session.reader.u64(policy), shape)
    contexts = session.field(component, 'DeliverUIViewModelComponent', 'deliverContexts')
    current = struct.unpack('<Q', session.watch(policy+fields['deliverContexts'], 8))[0]
    if current != contexts or not contexts:
        raise ValueError('Current delivery policy belongs to a different context collection')
    for method in PINS['methods']:
        raw = bytes.fromhex(method['raw_hex'])
        if (hashlib.sha256(raw).hexdigest() != method['sha256'] or
                session.reader.read(session.base+method['rva'], len(raw)) != raw):
            raise ValueError('Current delivery method differs from source: '+method['name'])
    equipped = _equipped_ids(session, session.field(component, 'DeliverUIViewModelComponent', 'equippedItemList'))
    session.assert_stable()
    return dict(kind='DefaultDeliverPolicy', confirmed=True, source=SOURCE_REF,
                source_hash=SOURCE_HASH, source_version=deepcopy(PINS['source_version']),
                policy_pointer=policy, component_pointer=component, contexts_pointer=contexts,
                equipped_item_ids=[str(x) for x in equipped], allow_used_by_quest=True,
                allow_locked_item=False, allow_preserved_item=False, allow_equipped_item=False)


def selection_instances(contexts):
    """Keep actual selected identity, domain and quantity, not only sheet totals."""
    rows = []
    for context in contexts:
        for item in context['selected_items']:
            rows.append(dict(context_index=context['index'], item_pointer=item['item_pointer'],
                             item_id=item['item_id'], table_domain=item.get('table_domain'),
                             sheet_id=item['sheet_id'] & 0xffffffff, count=item['count']))
    return sorted(rows, key=lambda x: (x['context_index'], x['item_pointer']))


def verify_current_policy(details):
    policy = details.get('delivery_policy') or {}
    if (policy.get('confirmed') is not True or policy.get('kind') != 'DefaultDeliverPolicy'
            or policy.get('source_hash') != SOURCE_HASH
            or policy.get('source_version') != PINS['source_version']):
        raise ValueError('Current default delivery policy evidence unavailable')
    equipped = set(policy['equipped_item_ids'])
    selected = {x['item_pointer'] for x in selection_instances(details['delivery_contexts'])}
    if any(x['item_pointer'] in selected and (x['item_id'] in equipped or x['locked'] or x['preserved'])
           for x in details['deliverable_items']):
        raise ValueError('Selected current item is equipped, locked or preserved; default delivery forbids it')
    return policy


def choose_default_item(details, reservations):
    policy = verify_current_policy(details)
    selected = selection_instances(details['delivery_contexts'])
    selected_ids = {x['item_pointer'] for x in selected}
    equipped = set(policy['equipped_item_ids'])
    items = details['deliverable_items']
    if len({x['index'] for x in items}) != len(items):
        raise ValueError('Current deliverable source order is ambiguous')
    matched = {x['context_index']: x for x in reservations['contexts']}
    # Consume the current source list order. Do not invent item quality ranks.
    for item in sorted(items, key=lambda x: x['index']):
        if (item['item_pointer'] in selected_ids or item['locked'] or item['preserved']
                or item['item_id'] in equipped or item['count'] <= 0):
            continue
        compatible = [c for c in details['delivery_contexts']
                      if item['sheet_id'] & 0xffffffff in {s & 0xffffffff for s in c['allowed_sheet_ids']}]
        if not compatible:
            continue
        # Default TryAdd may replace an already full compatible context. Never
        # route a new selection through that source replacement branch.
        context = compatible[0]
        required = matched.get(context['index'])
        if (not required or required['table_domain'] not in (None, item.get('table_domain'))
                or required['selected'] >= required['required']):
            continue
        return item, required
    raise ValueError('No unselected, unequipped current item can advance the owned delivery contexts')


def expected_selection(details, item, context):
    """Pinned AddToDeliver signed min; exact unchanged peers and chosen amount."""
    rows = selection_instances(details['delivery_contexts'])
    if any(x['item_pointer'] == item['item_pointer'] for x in rows):
        raise ValueError('Selected current item must not be clicked again')
    count = min(item['count'], context['required']-context['selected'])
    if type(count) is not int or count <= 0:
        raise ValueError('Current source selection amount is not positive')
    rows.append(dict(context_index=context['context_index'], item_pointer=item['item_pointer'],
                     item_id=item['item_id'], table_domain=item.get('table_domain'),
                     sheet_id=item['sheet_id'] & 0xffffffff, count=count))
    return sorted(rows, key=lambda x: (x['context_index'], x['item_pointer']))
