"""Acceptance approach through an exact waiting-instance map card.

This adapter preserves the original move node and uses the existing map-card
planner on a virtual dialogue.accept node. Native autoplay is a request, not
geometric arrival or acceptance. No process/OS access occurs here.

plan_acceptance_movement returns the common operation result (or None when not
applicable). The caller tries it before direct movement only when the original
native route has no input proposal. Existing issued intents are preserved.
acceptance_map_card_effect is shared with the graph's already-accepted prefix
transition; it returns an exact strictly newer typed proof, never arrival.
"""
from copy import deepcopy
from datetime import datetime, timezone

from .map_quest_accept import OWNER, plan_map_quest_accept
from .map_quest_accept_observer import source_definition, source_hash

FLOW = 'map_quest_card.native_autoplay'
READY = 'acceptance_movement.map_card_ready'
NATIVE_UNAVAILABLE = frozenset({
    'current_same_quest_instance_StartAutoPlay_marker_missing',
    'current_quest_autoplay_CTA_disabled',
    'quest_autoplay_evidence_unavailable:native_auto_play_explicitly_not_supported',
})
NULL_OBJECTIVE = ('accept_time', 'step_id', 'step_index', 'objective_index')
VERSION = ('metadata_sha256', 'game_assembly_sha256')
ACCEPTED_BASIS = 'native_IsAccepted_with_current_owned_ActiveQuest_and_verified_waiting_sentinel'
NATIVE_ACCEPTANCE_PHASES = frozenset({
    'quest_map_open_sent', 'quest_marker_select_sent', 'quest_map_pan_sent',
    'quest_map_zoom_sent', 'quest_autoplay_sent',
})


def _stamp(value):
    result = datetime.fromisoformat(value.replace('Z', '+00:00'))
    if result.tzinfo is None:
        raise ValueError('timezone-bearing evidence required')
    return result


def _now(snapshot, now):
    return now or (_stamp(snapshot['now']) if snapshot.get('now') else datetime.now(timezone.utc))


def _fresh(row, now):
    try:
        return -.5 <= (now - _stamp(row['observed_at'])).total_seconds() <= 5
    except (KeyError, TypeError, ValueError, AttributeError):
        return False


def _candidate(node):
    return node.get('op') == 'move.target' and node.get('params', {}).get('role') == 'acceptance'


def _version(row):
    expected = source_definition()
    return all((row.get('source_version') or {}).get(k) == expected[k] for k in VERSION)


def _scope(row):
    return {k: deepcopy(row[k]) for k in OWNER}


def _requested(scope):
    return dict(scope, **{key: None for key in NULL_OBJECTIVE})


def _same_current(scope, node, snapshot):
    return (type(scope.get('instance_id')) is int and scope['instance_id'] > 0
            and scope.get('quest_id') == node.get('params', {}).get('quest_id', node.get('quest_id'))
            and all(scope.get(k) is not None and scope[k] == snapshot.get(k) for k in OWNER[2:]))


def _outer_owned(node, snapshot, receipt):
    return (receipt.get('node_id') == node['id'] and receipt.get('quest_id') == node.get('quest_id')
            and all(receipt.get(k) == snapshot.get(k) for k in ('session_id', 'character_id')))


def _card_scope(node, snapshot, receipt):
    child = receipt.get('child_receipt') or {}
    scope = child.get('offer_scope') or {}
    if (not _outer_owned(node, snapshot, receipt) or child.get('flow') != FLOW
            or child.get('original_node_id') != node['id']
            or child.get('source_revision') != source_hash()
            or receipt.get('phase') != 'map_quest_card.' + str(child.get('stage'))
            or child.get('requested_objective') != _requested(scope)
            or not _same_current(scope, node, snapshot)):
        return None
    return scope


def acceptance_map_card_effect(node, snapshot, receipt, *, now=None):
    """Exact current accepted proof or None; never infer approach completion."""
    if not _candidate(node) or not isinstance(receipt, dict):
        return None
    try:
        scope = _card_scope(node, snapshot, receipt)
        if scope is None:
            return None
        now = _now(snapshot, now)
        lower = max(_stamp(receipt['observed_at']),
                    _stamp(receipt['child_receipt']['observed_at']))
        rows = [p for p in snapshot.get('state_proofs', [])
                if p.get('confirmed') is True and p.get('state') == 'accepted'
                and all(p.get(k) == v for k, v in scope.items())
                and p.get('source') == 'current-root-typed-quest-collections'
                and p.get('basis') == ACCEPTED_BASIS and _version(p)
                and type(p.get('accept_time')) is int and p['accept_time'] > 0
                and _fresh(p, now) and _stamp(p['observed_at']) > lower]
        return deepcopy(rows[0]) if len(rows) == 1 else None
    except (ValueError, KeyError, TypeError, AttributeError):
        return None


def acceptance_native_effect(node, snapshot, receipt, *, now=None):
    """Resolve only an owned pre-accept native intent with later typed acceptance.

    The waiting offer can disappear after acceptance. Its saved instance and
    goal key remain the comparison basis; a new offer is never substituted.
    This proves the goal state, not that a map gesture caused acceptance.
    """
    from .arbeit_movement import _digest, _owner, MovementEvidenceUnavailable
    from .vendor.map_quest_actions import rules
    if (not isinstance(receipt, dict)
            or not (_candidate(node) or node.get('op') in ('quest.offer', 'dialogue.accept'))):
        return None
    try:
        child = receipt.get('child_receipt') or {}
        requested = child.get('requested_objective') or {}
        phase = child.get('phase')
        if (not _outer_owned(node, snapshot, receipt)
                or receipt.get('dispatch_state') in ('not_sent', 'dry_run')
                or phase not in NATIVE_ACCEPTANCE_PHASES
                or receipt.get('phase') != 'quest_autoplay.' + phase
                or child.get('action_scope') not in (None, 'quest_instance')
                or requested.get('quest_id') != node.get('quest_id')
                or node.get('params', {}).get('quest_id', node.get('quest_id')) != node.get('quest_id')
                or type(requested.get('instance_id')) is not int or requested['instance_id'] <= 0
                or any(key not in requested or requested[key] is not None for key in NULL_OBJECTIVE)):
            return None
        outer = snapshot.get('map') or {}
        raw = outer.get('snapshot', outer)
        now = _now(snapshot, now)
        if (outer.get('ok') is False or raw.get('certainty') != 'confirmed'
                or not raw.get('observation_source')
                or not _fresh({'observed_at': raw.get('captured_at')}, now)):
            return None
        owner = _owner(raw)
        if (type(owner[0]) is not int or owner[0] <= 0
                or child.get('owner') != owner
                or any(owner[i] != snapshot.get(key) for i, key in
                    ((1, 'process_started_at'), (2, 'character_id'), (3, 'session_id')))
                or (snapshot.get('pid') is not None and snapshot['pid'] != owner[0])
                or requested.get('process_started_at') != owner[1]
                or child.get('goal_key') != _digest({key: requested.get(key)
                    for key in ('quest_id', 'instance_id', 'accept_time')})):
            return None
        definition = rules()
        if raw.get('source_version') and any(raw['source_version'].get(key) != definition[key] for key in VERSION):
            return None
        # Original native receipts have no invented source-version field. The
        # current accepted dictionary is source-pinned, independently of the UI.
        lower = max(_stamp(receipt['observed_at']), _stamp(child['captured_at']))
        scope = dict(quest_id=requested['quest_id'], instance_id=requested['instance_id'],
            process_started_at=owner[1], character_id=owner[2], session_id=owner[3])
        rows = [proof for proof in snapshot.get('state_proofs', [])
                if proof.get('confirmed') is True and proof.get('state') == 'accepted'
                and all(proof.get(key) == value for key, value in scope.items())
                and proof.get('source') == 'current-root-typed-quest-collections'
                and proof.get('basis') == ACCEPTED_BASIS and _version(proof)
                and all((proof.get('source_version') or {}).get(key) == definition[key] for key in VERSION)
                and type(proof.get('accept_time')) is int and proof['accept_time'] > 0
                and _fresh(proof, now) and _stamp(proof['observed_at']) > lower]
        return deepcopy(rows[0]) if len(rows) == 1 else None
    except (MovementEvidenceUnavailable, ValueError, KeyError, TypeError, AttributeError):
        return None


def _offer(node, snapshot, now):
    qid = node.get('params', {}).get('quest_id', node.get('quest_id'))
    rows = [r for r in snapshot.get('offers', []) if r.get('quest_id') == qid]
    if len(rows) != 1:
        raise ValueError('current unique waiting instance required')
    row = rows[0]
    if (not all(row.get(k) is True for k in ('confirmed', 'eligible', 'eligibility_verified', 'conditions_met'))
            or row.get('source') != 'current-root-typed-quest-collections'
            or row.get('basis') != 'verified_current_waiting_ActiveQuest_status_and_current_QuestStaticData_Sender'
            or not _fresh(row, now) or not _version(row) or not _same_current(_scope(row), node, snapshot)):
        raise ValueError('fresh exact waiting instance unavailable')
    return row


def _active(snapshot, scope, now):
    from .vendor.map_quest_actions import rules
    raw = (snapshot.get('map') or {}).get('snapshot', snapshot.get('map') or {})
    value = snapshot.get('current_autoplay') or raw.get('current_autoplay') or {}
    return (all(value.get(k) is True for k in ('confirmed', 'owner_verified', 'active'))
            and value.get('source') == 'current_root_AutoPlayComponent_server_target_and_active_fields'
            and value.get('source_hash') == rules()['source_hash'] and _version(value)
            and all(value.get(k) == scope[k] for k in OWNER[2:])
            and value.get('target') == {'kind': 'quest_instance', 'instance_id': scope['instance_id']}
            and _fresh(value, now))


def _wait(node, reason, receipt, **extra):
    from .arbeit_operations import _waiting
    return _waiting(node, reason, ['exact_waiting_instance_map_card_or_current_typed_acceptance'],
                    receipt, not_geometric_arrival=True, not_objective_completion=True, **extra)


def _open_successor(node, snapshot, receipt, offer, now):
    """The only issued foreign intent we resolve: observed native map-open."""
    from .quest_autoplay import plan_offer_autoplay
    if receipt.get('phase') != 'quest_autoplay.quest_map_open_sent':
        return None
    child = receipt.get('child_receipt') or {}
    scope = _scope(offer)
    requested = child.get('requested_objective') or {}
    if (any(requested.get(k) != scope[k] for k in ('quest_id', 'instance_id', 'process_started_at'))
            or any(requested.get(k) is not None for k in NULL_OBJECTIVE)
            or child.get('phase') != 'quest_map_open_sent'):
        return None
    outer = snapshot.get('map') or {}
    raw = outer.get('snapshot', outer)
    if (outer.get('ok') is False or outer.get('planner_ready') is False
            or raw.get('map_visible') is not True or raw.get('map_ready') is not True
            or _stamp(raw['captured_at']) <= _stamp(receipt['observed_at'])):
        return None
    planned = plan_offer_autoplay(raw, offer, child, now=now)
    if planned.get('wait_reason') not in NATIVE_UNAVAILABLE:
        return None
    evidence = dict(source=raw['observation_source'], observed_at=raw['captured_at'],
        source_version=deepcopy(offer['source_version']), offer_scope=scope,
        map_open_observed=True, native_autoplay_dispatched=False,
        original_phase=receipt['phase'], native_boundary=planned['wait_reason'])
    next_receipt = deepcopy(receipt)
    next_receipt.update(phase=READY, dispatch_state='effect_confirmed', observed_at=raw['captured_at'],
        child_receipt=dict(original_node_id=node['id'], offer_scope=scope,
            requested_objective=_requested(scope), source_revision=source_hash(),
            resolved_navigation_receipt=deepcopy(receipt), map_open_evidence=evidence))
    return dict(status='transition', reason='owned_map_open_confirmed_map_card_approach_available',
        next_receipt=next_receipt, effect_evidence=[evidence], proposal=None,
        not_geometric_arrival=True, not_objective_completion=True)


def _entry(node, snapshot, receipt, scope):
    child = receipt.get('child_receipt') or {}
    original = child.get('resolved_navigation_receipt') or {}
    evidence = child.get('map_open_evidence') or {}
    requested = (original.get('child_receipt') or {}).get('requested_objective') or {}
    return (receipt.get('phase') == READY and receipt.get('dispatch_state') == 'effect_confirmed'
        and _outer_owned(node, snapshot, receipt) and _outer_owned(node, snapshot, original)
        and child.get('original_node_id') == node['id'] and child.get('offer_scope') == scope
        and child.get('requested_objective') == _requested(scope) and child.get('source_revision') == source_hash()
        and original.get('phase') == 'quest_autoplay.quest_map_open_sent'
        and (original.get('child_receipt') or {}).get('phase') == 'quest_map_open_sent'
        and all(requested.get(k) is None for k in NULL_OBJECTIVE)
        and all(requested.get(k) == scope[k] for k in ('quest_id', 'instance_id', 'process_started_at'))
        and evidence.get('offer_scope') == scope and evidence.get('source')
        and evidence.get('map_open_observed') is True and evidence.get('native_autoplay_dispatched') is False
        and evidence.get('native_boundary') in NATIVE_UNAVAILABLE and _version(evidence)
        and evidence.get('observed_at') == receipt.get('observed_at')
        and _stamp(evidence['observed_at']) > _stamp(original['observed_at']))


def plan_acceptance_movement(node, snapshot, receipt=None, *, now=None):
    """Common operation result; call after native pre-input route is unavailable.

Map-card and this helper's transition receipts must be routed here before any
other navigation planner. All other unresolved receipts remain untouched.
"""
    if not _candidate(node):
        return None
    try:
        return _plan(node, snapshot, receipt, _now(snapshot, now))
    except (ValueError, KeyError, TypeError, AttributeError) as exc:
        return _wait(node, 'acceptance_map_card_binding_unavailable:' + str(exc), receipt)


def _plan(node, snapshot, receipt, now):
    from .arbeit_operations import _authored_action
    if receipt and not _outer_owned(node, snapshot, receipt):
        return _wait(node, 'acceptance_map_card_original_receipt_owner_changed', receipt)
    child = None
    entry = None
    if receipt and str(receipt.get('phase', '')).startswith('map_quest_card.'):
        scope = _card_scope(node, snapshot, receipt)
        if scope is None:
            return _wait(node, 'acceptance_map_card_receipt_scope_changed', receipt)
        effect = acceptance_map_card_effect(node, snapshot, receipt, now=now)
        if effect:
            return _wait(node, 'acceptance_observed_graph_prefix_transition_required', receipt, acceptance_evidence=effect)
        child = receipt['child_receipt']
        entry = child.get('entry_transition')
    else:
        if not receipt and (not snapshot.get('map_quest_card_frame')
                or snapshot.get('action_proposals', {}).get(node['id'])):
            return None
        offer = _offer(node, snapshot, now)
        scope = _scope(offer)
        if receipt:
            successor = _open_successor(node, snapshot, receipt, offer, now)
            if successor:
                return successor
            if receipt.get('phase') == 'quest_autoplay.quest_map_open_sent':
                # A current native marker/action or an unconfirmed open stays
                # with the original native planner; do not block its effects.
                return None
            if not _entry(node, snapshot, receipt, scope):
                return _wait(node, 'acceptance_existing_native_or_movement_intent_preserved', receipt)
            entry = receipt
        if _active(snapshot, scope, now):
            return _wait(node, 'quest_native_autoplay_already_active_wait_for_typed_effect', receipt)
    virtual = deepcopy(node)
    virtual['op'] = 'dialogue.accept'
    outer = snapshot.get('map') or {}
    raw = outer.get('snapshot', outer)
    planned = plan_map_quest_accept(virtual, dict(snapshot, map=raw),
        snapshot.get('map_quest_card_frame') or {}, child, now=now)
    if planned.get('status') != 'action':
        from .wait_contract import effect_wait_contract
        contract = (effect_wait_contract(receipt, family='map_quest_card')
            if planned.get('wait_kind') == 'owned_input_effect' and planned.get('wait_family') == 'map_quest_card' else None)
        reason = ('acceptance_map_card_strictly_newer_acceptance_required' if planned.get('status') == 'accepted'
                  else planned.get('wait_reason', 'current_map_card_effect_required'))
        return _wait(node, reason, receipt, map_card_evidence=deepcopy(planned.get('evidence')), wait_contract=contract)
    following = deepcopy(planned['next_receipt'])
    if following.get('flow') != FLOW or following.get('offer_scope') != scope:
        return _wait(node, 'acceptance_map_card_planner_scope_changed', receipt)
    following.update(original_node_id=node['id'], requested_objective=_requested(scope))
    if entry:
        following['entry_transition'] = deepcopy(entry)
    observed = (snapshot.get('current_map_accept') or {}).get('observed_at') or raw.get('captured_at')
    return _authored_action(node, snapshot, [planned['action']], [planned['evidence']],
        'map_quest_card.' + following['stage'], receipt, child=following,
        prior_effect=bool(receipt), observed_at=observed)
