"""Read the selected action's declared operands, without inventing its quest owner.

The caller supplies its already-owned DialogueBindingSession. This module opens
no transport and executes no client method. A continuation's QuestName is never
the enclosing AcceptQuest/AchieveQuestObjective effect target.
"""
from functools import lru_cache
from copy import deepcopy
import hashlib
import json
import struct
from .paths import root

NS = 'MM.QDBL.CodeGenerated'
ROOT_ACTIONS = {'AcceptQuestDialogueActionData', 'AchieveQuestObjectiveDialogueActionData'}
ACTION_TYPE = 37497
VERSION = ('metadata_sha256', 'game_assembly_sha256')


@lru_cache(maxsize=1)
def operand_source():
    return json.loads((root()/'data/r6-dialogue-operands/declarations.json').read_text(encoding='utf8'))


def _pointer(session, at):
    return struct.unpack('<Q', session.watch(at, 8))[0]


def _optional_action(session, pointer, klass, schema, field):
    """Use the current closed Optional layout; absent payloads stay unread.

    This is the named-field path in direct_dialogue_source.action_branches,
    extended to return the present continuation pointer rather than its label.
    """
    r = session.reader
    table = r.u64(klass+0x80)
    tp = _pointer(session, table+field['index']*32+8)
    if ((r.u32(tp+8)>>16)&255) != 21:
        raise ValueError('dialogue_operand_optional_field_type_changed')
    generic = _pointer(session, tp)
    closed = _pointer(session, generic+24) if generic else 0
    if not closed or r.class_name(closed) != ('Optional`1', 'MM.QDBL.Definitions.ValueType'):
        raise ValueError('dialogue_operand_optional_closed_class_unavailable')
    inst = r.u64(generic+8)
    if r.u64(inst) != 1:
        raise ValueError('dialogue_operand_optional_arity_changed')
    arg = r.u64(r.u64(inst+8))
    if ((r.u32(arg+8)>>16)&255) != 18 or r.u64(arg) != ACTION_TYPE:
        raise ValueError('dialogue_operand_optional_argument_domain_changed')
    layout = r.layout(closed, ['value', '<HasValue>k__BackingField'])
    if r.class_name(int(layout['parent_class'], 16)) != ('ValueType', 'System'):
        raise ValueError('dialogue_operand_optional_not_value_type')
    fields = {f['name']: f for f in layout['fields']}
    value, presence = fields['value'], fields['<HasValue>k__BackingField']
    if (value['type_code'], presence['type_code']) != (18, 2):
        raise ValueError('dialogue_operand_optional_named_fields_changed')
    width = layout['instance_size']-16
    span = min([f['offset'] for f in schema['fields'] if f['offset'] > field['offset']]
        + [schema['type_definition_size_raw'][0]])-field['offset']
    vo, po = value['offset']-16, presence['offset']-16
    if not (0 < width <= span and 0 <= vo <= width-8 and 0 <= po < width and not vo <= po < vo+8):
        raise ValueError('dialogue_operand_optional_layout_outside_source_span')
    raw = session.watch(pointer+field['offset'], width)
    if raw[po] not in (0, 1):
        raise ValueError('dialogue_operand_optional_presence_invalid')
    if not raw[po]:
        return 0
    child = struct.unpack_from('<Q', raw, vo)[0]
    if not child:
        raise ValueError('dialogue_operand_optional_present_reference_null')
    return child


def read_direct_dialogue_operands(session, pointer):
    """Return a watched current structure, not input authorization or quest success.

    Only declared references/strings/action lists are decoded. Other payloads
    stay explicitly unread; conditions and static names are never evaluated.
    Exact action pointers are transient and are excluded from the revision.
    The enclosing reader still validates character/player/prompt roots.
    """
    source = operand_source()
    if any(session.plan.get(k) != source[k] for k in VERSION):
        raise ValueError('dialogue_operand_source_version_changed')
    schemas = {(row['name'], row['namespace']): row for row in source['classes']}
    start = len(session._guards)
    strings = []
    seen = set()
    validated = {}

    def fields(klass, schema):
        # The enclosing reader may have cached only a smaller declaration of
        # this class. Validate this helper's exact schema in its own cache;
        # restore the enclosing cache even when a current field has changed.
        missing = object()
        enclosing = getattr(session, '_validated', missing)
        session._validated = validated
        try:
            return session.fields(klass, schema)
        finally:
            if enclosing is missing:
                del session._validated
            else:
                session._validated = enclosing

    def read(at, path, depth=0):
        if depth > 16 or len(seen) >= 128:
            raise ValueError('dialogue_operand_structure_exceeds_source_read_bound')
        if at in path:
            raise ValueError('dialogue_operand_reference_cycle')
        seen.add(at)
        klass = _pointer(session, at)
        name, namespace = session.reader.class_name(klass)
        schema = schemas.get((name, namespace))
        row = dict(type=name, namespace=namespace, explicit_operands={}, branches=[],
            root_effect_owner=False, decoded=False, unread_fields=[])
        if schema is None or namespace != NS:
            row['missing'] = 'current_nested_action_schema_not_in_bounded_source'
            return row
        fields(klass, schema)
        row['decoded'] = True
        row['direct_quest_operand_declared'] = any(f['name'] == '<QuestName>k__BackingField'
            for f in schema['fields'])
        for field in schema['fields']:
            ty = field['type']
            if ty.get('attrs', 0) & 16:  # Static singleton, not an instance operand.
                continue
            label = field['name']
            label = label[1:label.index('>')] if label.startswith('<') and '>' in label else label
            address = at+field['offset']
            if ty['type_code'] == 14:
                text_pointer = _pointer(session, address)
                text = session.reader.string(text_pointer) if text_pointer else None
                if text_pointer:
                    strings.append((text_pointer, text))
                row['explicit_operands'][label] = text
            elif ty['type_code'] == 18 and ty.get('definition_index') == ACTION_TYPE:
                child = _pointer(session, address)
                row['branches'].append(dict(field=label, present=bool(child),
                    **(read(child, path | {at}, depth+1) if child else {})))
            elif (ty['type_code'] == 21 and ty.get('name') == 'Optional`1'
                    and ty.get('namespace') == 'MM.QDBL.Definitions.ValueType'
                    and ty.get('arguments') == [dict(type_code=18, definition_index=ACTION_TYPE,
                        name='IDialogueActionData', namespace=NS, attrs=0)]):
                child = _optional_action(session, at, klass, schema, field)
                row['branches'].append(dict(field=label, present=bool(child),
                    **(read(child, path | {at}, depth+1) if child else {})))
            elif (ty['type_code'] == 21 and ty.get('name') == 'IReadOnlyList`1'
                    and ty.get('namespace') == 'System.Collections.Generic'
                    and len(ty.get('arguments', [])) == 1
                    and ty['arguments'][0].get('definition_index') == ACTION_TYPE):
                collection = _pointer(session, address)
                children = session.refs(collection, element='IDialogueActionData', limit=128)
                for index, child in enumerate(children):
                    row['branches'].append(dict(field=label, index=index, present=bool(child),
                        **(read(child, path | {at}, depth+1) if child else {})))
            else:
                row['unread_fields'].append(label)
        return row

    if type(pointer) is not int or pointer <= 0:
        raise ValueError('current_dialogue_action_pointer_required')
    result = read(pointer, set())
    if any(session.reader.read(at, len(raw)) != raw for at, raw in session._guards[start:]):
        raise ValueError('dialogue_operand_changed_during_read')
    if any(session.reader.string(at) != value for at, value in strings):
        raise ValueError('dialogue_operand_text_changed_during_read')
    # Addresses, observation time and evaluation results are not program fields.
    revision = hashlib.sha256(json.dumps(result, sort_keys=True, ensure_ascii=False,
        separators=(',', ':'), allow_nan=False).encode()).hexdigest()
    return dict(result, source='current_declared_dialogue_action_operands',
        source_version={k: source[k] for k in VERSION}, stable=True,
        enclosing_dialogue_roots_required=True, structure_revision=revision,
        root_quest_target_operand_absent=result['type'] in ROOT_ACTIONS and result['decoded'],
        conditions_evaluated=False, definition_id_inferred=False,
        root_effect_target=None, game_inputs=0, memory_writes=0)
