"""Recheck the exact current dialogue transaction before physical input.

This guards input ownership only. It never supplies acceptance, objective, or
reward completion. The default session remains inert until explicitly opened.
"""
from copy import deepcopy

from .dialogue_binding import DialogueBindingSession
from .quest_ui_actions import QuestUIActionSession
from .vendor.map_memory import MapReadPending


def request_identity(request):
    """Stable transaction identity; observation timestamps remain separate."""
    if not isinstance(request, dict):
        raise ValueError('current direct dialogue request binding required')
    keys = ('node_id', 'quest_id', 'operation', 'target_id', 'session_id', 'character_id',
            'process_started_at', 'source_revision', 'transaction_id',
            'client_source_version', 'transaction', 'before_scope', 'current_dialogue_input_owned',
            'quest_effect_owner_verified')
    result = {key: deepcopy(request.get(key)) for key in keys}
    if (not request.get('transaction_id') or not request.get('transaction')
            or not request.get('before_scope')
            or request.get('current_dialogue_input_owned') is not True
            or request.get('quest_effect_owner_verified') is not False):
        raise ValueError('direct dialogue transaction/source ownership incomplete')
    if any(result[key] is None for key in keys):
        raise ValueError('direct dialogue request identity incomplete')
    result['before_scope'] = {key: deepcopy(request['before_scope'][key]) for key in
        ('quest_id', 'node_id', 'waiting_instance_id', 'accepted_occurrence',
         'instance_id', 'accept_time', 'step_id', 'step_index', 'objective_index',
         'process_started_at') if key in request['before_scope']}
    transaction = request['transaction']
    result['transaction'] = {key: deepcopy(transaction.get(key)) for key in
        ('dialogue_id', 'dialogue_index', 'prompt_index', 'prompt_type', 'npc_id',
         'target_object_id', 'target_entity_key', 'selection')}
    structure = transaction.get('action_structure') or {}
    result['transaction']['action_structure'] = {key: deepcopy(structure.get(key)) for key in
        ('type', 'namespace', 'structure_revision', 'source_version')}
    baseline = request.get('before_objective')
    result['before_objective'] = None if baseline is None else {key: deepcopy(baseline.get(key)) for key in
        ('complete', 'confirmed', 'quest_id', 'instance_id', 'accept_time', 'step_id',
         'step_index', 'objective_index', 'process_started_at', 'session_id', 'character_id',
         'source', 'source_version', 'basis', 'semantics')}
    return result


def bind_direct_request_semantics(snapshot, request):
    """Call before prepare_pointer_proposal so probe and click bind the request."""
    semantics = deepcopy(snapshot.get('pointer_semantics', snapshot.get('details', {})))
    semantics['direct_dialogue_request'] = request_identity(request)
    snapshot['pointer_semantics'] = semantics
    return snapshot


class DirectDialogueInputSession(QuestUIActionSession):
    def current_request_dialogue(self):
        # The two session classes define incompatible `field` methods:
        # DialogueBindingSession returns metadata; QuestUIActionSession a value.
        # Share the existing reader, never use multiple inheritance or open a
        # second process. Keep each read's byte guards in the enclosing session.
        if not hasattr(self, '_guards'):
            self._guards = []
        view = DialogueBindingSession.__new__(DialogueBindingSession)
        view.__dict__ = dict(self.__dict__)
        view._guards = []
        result = view.current_dialogue()
        self._guards.extend(view._guards)
        return result


def direct_dialogue_probe_wait(candidate, snapshot, frame, node, request, pending):
    """Prove an unchanged cache after this exact sent, input-owned probe.

    This is observation permission, not an effect, resend, or quest owner. Any
    target/cache/owner change returns None; generic pointer failures cannot
    acquire this contract. The caller must freshly rebuild ``request`` from the
    current rooted dialogue and model before calling.
    """
    from .quest_ui_actions import _digest, _fresh, _pointer_scene, _producer_revision, _stamp
    try:
        identity = request_identity(request)
        receipt = pending['planner_receipt']
        if (pending.get('status') != 'sent_unconfirmed'
                or receipt.get('phase') != 'direct_dialogue.request_probe'
                or receipt.get('dispatch_state') in ('not_sent', 'blocked', 'dry_run')
                or request_identity(receipt.get('request_binding')) != identity
                or node.get('id') != identity['node_id'] or node.get('quest_id') != identity['quest_id']
                or not pending.get('operation_id')):
            return None
        if any(frame.get(key) != identity[key] for key in ('session_id', 'character_id', 'process_started_at')):
            return None
        ledger = pending.get('input_receipt') or {}
        rows = ledger.get('receipts') or []
        if ledger.get('status') != 'sent_unconfirmed' or len(rows) != 1:
            return None
        row = rows[0]
        command = row.get('command') or {}
        binding = command.get('probe_context') or row.get('target_binding') or {}
        if (command.get('kind') != 'pointer_probe' or row.get('status') != 'sent_unconfirmed'
                or row.get('attempted') is not True or not row.get('operation_id')
                or not row.get('dispatch_finished_at')
                or binding.get('source_guard_kind') != 'direct-dialogue-request'
                or request_identity(binding.get('request_binding')) != identity
                or binding.get('target_id') != identity['target_id']):
            return None
        owner = frame.get('input_owner') or {}
        state = candidate.get('pointer_state') or snapshot.get('pointer_state') or {}
        geometry = candidate.get('native_geometry_proof') or {}
        if (owner.get('continuous') is not True or not owner.get('epoch')
                or not _fresh(owner, frame) or not _fresh(state, frame) or not _fresh(request, frame)
                or not state.get('source') or not state.get('source_hashes')
                or any(geometry.get(k) is not True for k in
                       ('confirmed','transform_verified','active_hierarchy_verified','source_revision_verified'))
                or geometry.get('frame_id') != frame.get('frame_id')
                or geometry.get('window_signature') != frame.get('window_signature')
                or binding.get('scene_signature') != _pointer_scene(candidate, snapshot)
                or binding.get('source_revision') != _producer_revision(snapshot)
                or binding.get('input_epoch') != owner['epoch']
                or binding.get('window_signature') != frame.get('window_signature')):
            return None
        if any(_stamp(value) <= _stamp(row['dispatch_finished_at'])
               for value in (state['observed_at'], request['observed_at'])):
            return None
        caches = state.get('caches') or []
        if len(caches) != 1:
            return None
        cache = caches[0]
        if (cache.get('handle') != binding.get('pointer_handle')
                or cache.get('cache_pointer') != binding.get('pointer_cache')
                or cache.get('position') != binding.get('before_position')
                or cache.get('update_time') != binding.get('before_update_time')
                or _digest(cache) != binding.get('before_results_hash')):
            return None
        return dict(kind='direct_dialogue_probe_cache_pending', confirmed=True,
            source='current rooted dialogue transaction and unchanged owned pointer cache',
            source_revision=request['source_revision'], source_refs=deepcopy(state['source_hashes']),
            observed_at=state['observed_at'], node_id=node['id'], quest_id=node['quest_id'],
            session_id=identity['session_id'], character_id=identity['character_id'],
            process_started_at=identity['process_started_at'], request_identity=identity,
            transaction_id=identity['transaction_id'], operation_id=pending['operation_id'],
            probe_operation_id=row['operation_id'], receipt_fingerprint=_digest(receipt),
            scene_signature=binding['scene_signature'], input_epoch=owner['epoch'],
            window_signature=frame['window_signature'], input_effect_confirmed=False,
            quest_effect_owner_verified=False)
    except (KeyError, TypeError, ValueError, OverflowError):
        return None


def direct_dialogue_request_scene(session, binding, frame):
    """Current owned roots + selection + callback + geometry, read twice.

    Required frame keys: direct_dialogue_request_node and
    direct_dialogue_request_ui (the source layout wrapper). No old wrapper
    binding or caption is used as the newly read current selection.
    """
    if binding.get('source_guard_kind') != 'direct-dialogue-request':
        raise ValueError('wrong direct dialogue guard family')
    expected = binding.get('request_binding')
    identity = request_identity(expected)
    node = frame.get('direct_dialogue_request_node') or {}
    if node.get('id') != identity['node_id'] or node.get('quest_id') != identity['quest_id']:
        raise ValueError('current direct dialogue node changed')
    if node.get('op') != identity['operation']:
        raise ValueError('current direct dialogue operation changed')
    if any(frame.get(key) != identity[key] for key in
           ('session_id', 'character_id', 'process_started_at')):
        raise ValueError('current direct dialogue input owner changed')
    wrapper = frame.get('direct_dialogue_request_ui')
    if not isinstance(wrapper, dict):
        raise ValueError('current direct dialogue layout wrapper required')
    target = binding.get('target_id')
    if target not in ('dialogue.accept', 'dialogue.report') or target != identity['operation']:
        raise ValueError('direct dialogue request target unsupported')

    before = session.current_request_dialogue()
    before_guards = list(session._guards)
    layout = session.current_layout(target)  # Existing reader starts a guard set.
    session.attach_current_geometry(layout, frame)
    session._guards = before_guards + list(session._guards)
    current_ui = deepcopy(wrapper)
    current_ui.update(layout_snapshot=layout, bindings={}, frame_id=frame['frame_id'])
    current_ui.pop('current_action_binding', None)
    from .ui_observer import bind_named_dialogue
    from .direct_dialogue_request import build_direct_dialogue_request
    current_ui = bind_named_dialogue(current_ui, node)

    def recheck(dialogue):
        # Dispatch validates this already selected input. It does not plan a
        # second request from a pending receipt or reinterpret an uncertain send.
        rebuilt = build_direct_dialogue_request(dialogue, frame, node, current_ui)
        actual = rebuilt.get('request_binding')
        if (not actual or rebuilt.get('target_id') != target
                or request_identity(actual) != identity):
            raise MapReadPending('current direct dialogue transaction or selection changed before input')
        return actual

    current = recheck(before)
    candidates = [candidate for candidate in layout.get('candidates', [])
                  if (candidate.get('control_pointer'), candidate.get('root_object_pointer'),
                      candidate.get('model_event_id'), candidate.get('index')) ==
                  (binding.get('control_pointer'), binding.get('root_object_pointer'),
                   binding.get('model_event_id'), binding.get('target_index'))]
    if len(candidates) != 1:
        raise MapReadPending('current direct dialogue control/callback is no longer unique')
    candidate = candidates[0]
    selected = (current.get('transaction') or {}).get('selection') or {}
    if candidate.get('model_event_id') != selected.get('selection_index'):
        raise MapReadPending('current dialogue callback differs from exact SelectIndex')
    after = session.current_request_dialogue()
    recheck(after)
    bind_direct_request_semantics(layout, current)
    session.assert_stable()
    return candidate, layout
