"""Reconcile an owned native story request with a current rooted Story UI.

This is an intermediate input handoff, never a quest effect or a server-side
quest owner inference. It reads only already supplied snapshots and source pins.
"""
from copy import deepcopy

from .story_read_source import ADOPTION, SCOPE, canonical_native_receipt, interaction_adoptions, native_request_context
from .story_ui_actions import bind_story_target, current_program, planner_receipt
from .vendor.map_memory import MapReadPending


def _explicitly_unsent(row):
    if not isinstance(row, dict):
        return False
    if any(row.get(key) is False for key in ('attempted', 'input_attempted')) or row.get('dry_run') is True:
        return True
    if row.get('status') in ('blocked', 'dry_run', 'not_sent', 'cancelled') or row.get('dispatch_state') in ('not_sent', 'dry_run', 'blocked', 'cancelled'):
        return True
    # Inspect only dispatch ledgers, not static action_requests whose not_sent is
    # a definition field rather than evidence about this actual input.
    return any(_explicitly_unsent(value) for value in row.get('receipts', []))


def _actual_autoplay(snapshot):
    identity = snapshot.get('identity') or {}
    mapping = snapshot.get('map') or {}
    raw_map = mapping.get('snapshot') or mapping
    values = [value for value in (identity.get('current_autoplay'), raw_map.get('current_autoplay'))
              if isinstance(value, dict)]
    if not values:
        return None
    projected = snapshot.get('current_autoplay')
    selected = projected if projected is not None else values[0]
    if not any(selected == value for value in values):
        return None
    # Identity and map can be distinct reads. Their times need not match, but
    # their claims about this target and its owner must not contradict.
    claims = ('target', 'target_quest_step_index', 'target_quest_objective_index',
              'session_id', 'character_id', 'process_started_at', 'active')
    if any(any(value.get(key) != selected.get(key) for key in claims) for value in values):
        return None
    return selected


def current_story_adoption(node, snapshot, receipt, proposal):
    """Return the exact current adoption proof, or None without changing input.

    A source-bound row is rederived from the current typed program, independently
    supplied current Story UI and native target. Explicitly unsent/blocked/dry
    input remains unresolved. A missing ledger is not invented as a sent ledger;
    a prepared planner receipt requires the actual outer sent input ledger.
    The canonical dispatch projection never rewrites the original pending row.
    """
    from .arbeit_operations import _fresh, _stamp
    from .arbeit_movement import _digest
    from .story_ui_observer import story_source

    try:
        if not all(isinstance(value, dict) for value in (node, snapshot, receipt, proposal)):
            return None
        if _explicitly_unsent(receipt) or _explicitly_unsent(planner_receipt(snapshot)):
            return None
        canonical = canonical_native_receipt(snapshot)
        if canonical is None or receipt not in (planner_receipt(snapshot), canonical):
            return None
        receipt = canonical
        child = receipt.get('child_receipt') or {}
        pending = snapshot.get('pending_input') or {}
        if receipt.get('phase') != 'quest_autoplay.quest_autoplay_sent' or child.get('phase') != 'quest_autoplay_sent':
            return None
        if receipt.get('dispatch_state') != 'sent_unconfirmed' or child.get('action_scope') != 'quest_instance':
            return None
        if any(_explicitly_unsent(row) for row in (receipt, child, pending, pending.get('input_receipt'))):
            return None
        if not pending or canonical_native_receipt(snapshot) != receipt:
            return None
        if any(receipt.get(key) != snapshot.get(key) for key in ('session_id', 'character_id')):
            return None
        if receipt.get('node_id') != node.get('id') or receipt.get('quest_id') != node.get('quest_id'):
            return None
        if child.get('owner') != [snapshot.get('pid'), snapshot.get('process_started_at'),
                                  snapshot.get('character_id'), snapshot.get('session_id')]:
            return None
        active = _actual_autoplay(snapshot)
        if active is None:
            return None
        frame = dict(snapshot, current_autoplay=active)
        program, normalized = current_program(node, frame)
        if normalized.get('operation') != 'objective.read_story' or program['data'].get('type') != 'MM.QDBL.CodeGenerated.ReadStoryBoxQuestObjectiveData':
            return None
        scope = {key: program[key] for key in SCOPE}
        if child.get('requested_objective') != scope:
            return None
        if child.get('goal_key') != _digest({key: scope[key] for key in ('quest_id', 'instance_id', 'accept_time')}):
            return None
        # Common wrapper goal_scope deliberately excludes objective_index.
        if receipt.get('goal_scope') != {key: value for key, value in scope.items() if key != 'objective_index'}:
            return None
        saved_binding = receipt.get('operation_binding') or {}
        if ({key: saved_binding.get(key) for key in SCOPE} != scope
                or saved_binding.get('data_type') != program['data']['type']
                or saved_binding.get('source') != program.get('source')
                or saved_binding.get('source_version') != program.get('source_version')):
            return None
        if not _fresh(program, frame) or not native_request_context(frame, node, program):
            return None
        direct_ui = snapshot.get('current_story_ui')
        nested_ui = (snapshot.get('story_read') or {}).get('current_story_ui')
        if direct_ui is not None and nested_ui is not None and direct_ui != nested_ui:
            return None
        typed = nested_ui or direct_ui or {}
        pins = story_source()
        if (typed.get('source') != 'current-rooted-story-item-ui' or typed.get('source_hash') != pins['source_hash']
                or not typed.get('source_refs') or not _fresh(typed, frame)
                or any(type(typed.get(key)) is not int or typed[key] <= 0 for key in ('ui_pointer', 'handle'))):
            return None
        if _stamp(typed['observed_at']) <= max(_stamp(receipt['observed_at']), _stamp(child['captured_at'])):
            return None
        bound = bind_story_target(typed, frame, node)
        if proposal.get('status') not in ('ready', 'action') or proposal.get('current_binding') != bound:
            return None
        context = proposal.get('context') or {}
        if (context.get('source_revision') != typed['source_hash'] or context.get('observed_at') != typed['observed_at']
                or proposal.get('observed_at') != typed['observed_at'] or proposal.get('observation_source') != typed['source']
                or proposal.get('source_refs') != typed['source_refs']
                or any(context.get(key) != frame.get(key) or proposal.get(key) != frame.get(key)
                       for key in ('session_id', 'character_id'))):
            return None
        expected = proposal.get('expected_effect') or {}
        story_binding = {key: deepcopy(bound[key]) for key in ('binding_kind', 'goal_scope', 'actual_ui', 'server_quest_owner_read')}
        if bound.get('server_page_adoption'):
            story_binding['server_page_adoption'] = deepcopy(bound['server_page_adoption'])
        if expected.get('story_binding') != story_binding or expected.get('node_id') != node['id']:
            return None
        action = proposal.get('action') or {}
        probe = action.get('probe_context') or {}
        role = 'story.' + bound['stage']
        if (bound['stage'] not in ('next', 'reveal', 'close') or action.get('kind') not in ('pointer_probe', 'pointer_click')
                or action.get('role') != role or probe.get('target_id') != role
                or probe.get('source_guard_kind') != 'story-ui' or probe.get('node_id') != node['id']
                or probe.get('target_ui_pointer') != typed['ui_pointer']
                or probe.get('input_epoch') != frame['input_owner']['epoch']
                or action.get('input_epoch') != probe.get('input_epoch')
                or context.get('input_owner') != frame.get('input_owner')
                or context.get('frame_id') != probe.get('frame_id')
                or context.get('window_signature') != probe.get('window_signature')):
            return None
        rows = [row for row in snapshot.get('interaction_adoptions', [])
                if isinstance(row, dict) and row.get('kind') == ADOPTION and row.get('node_id') == node['id']]
        rederived = interaction_adoptions(typed, frame, node, bound, program)
        if len(rows) != 1 or len(rederived) != 1 or rows[0] != rederived[0]:
            return None
        return dict(deepcopy(rows[0]), confirmed=True, intermediate_only=True,
                    native_intent_adopted=True, objective_completion_inferred=False,
                    quest_completion_inferred=False)
    except (KeyError, TypeError, ValueError, AttributeError, MapReadPending):
        return None
