"""Current map quest callback -> physical input proposal, with explicit scope.

The native StartAutoPlayRequested carries a QuestInstanceId. It does not carry
an objective selector. The requested objective remains journal/effect context;
the action scope is the whole current quest instance. No client method is
called, and clicking/arrival never means an objective or quest completed.

API: plan_quest_autoplay(map_snapshot, action_request, receipt=None, now=None).
Returns the same status/action/next_receipt shape as plan_movement. Persist the
receipt before dispatch and retain it on uncertain input. No automatic resend.
"""
from copy import deepcopy
from datetime import datetime, timezone
import math

from .arbeit_movement import (_action, _control, _digest, _number, _owner,
    _pair, _point, _pose, _time, _touch, _wait, MovementEvidenceUnavailable)
from .vendor.ui_layout import Rect
from .vendor.map_quest_actions import rules
from .vendor.map_dynamic_targets import current_objective_marker_matches,native_callback_matches

SCOPE=('quest_id','instance_id','accept_time','step_id','step_index','objective_index')


def _callback(marker,instance):
    return native_callback_matches(marker,instance)


def plan_quest_autoplay(snapshot,request,receipt=None,*,now=None):
    try:return _plan(snapshot,request,receipt,now or datetime.now(timezone.utc))
    except (MovementEvidenceUnavailable,KeyError,ValueError,TypeError,AttributeError,OverflowError) as exc:
        return _wait('quest_autoplay_evidence_unavailable:'+str(exc),
                     {'action_scope':'quest_instance','objective_index_parameter':False},receipt)


def plan_offer_autoplay(snapshot,offer,receipt=None,*,now=None):
    """Use the same current native quest callback for an available candidate.

    The waiting ActiveQuest instance identifies the candidate. Eligibility and
    Sender do not prove that a dialogue is open or an accept button exists.
    Those effects remain the accept handler's responsibility.
    """
    if not isinstance(offer,dict):return _wait('current_available_quest_candidate_required',receipt=receipt)
    giver=offer.get('giver_definition') or {}
    if not (offer.get('confirmed') is True and offer.get('available_candidate') is True
        and offer.get('eligible') is True and offer.get('eligibility_verified') is True
        and offer.get('source')=='current-root-typed-quest-collections'
        and offer.get('basis')=='verified_current_waiting_ActiveQuest_status_and_current_QuestStaticData_Sender'
        and giver.get('declared_type')=='Shared.Symbols.NPCSheetId'
        and giver.get('source')=='current_QuestStaticData.Sender'
        and type(giver.get('npc_id')) is int and giver['npc_id']!=0):
        return _wait('verified_current_waiting_quest_and_Sender_required',receipt=receipt)
    request={k:deepcopy(offer.get(k)) for k in ('quest_id','instance_id','source','observed_at','session_id',
        'character_id','process_started_at','source_version')}
    request.update(kind='quest.offer.autoplay',accept_time=None,step_id=None,step_index=None,objective_index=None,
        supported_objective_indices=[],offer_basis=offer['basis'],giver_definition=deepcopy(giver),
        expected_effect={'state':'current_giver_dialogue_then_separate_accept_evidence'})
    return plan_quest_autoplay(snapshot,request,receipt,now=now)


def _plan(snapshot,request,receipt,now):
    if not isinstance(request,dict) or request.get('kind') not in ('quest.objective.autoplay','quest.offer.autoplay'):
        raise MovementEvidenceUnavailable('current_typed_quest_autoplay_request_required')
    offer=request['kind']=='quest.offer.autoplay'
    if any(request.get(key) is None for key in (('quest_id','instance_id') if offer else SCOPE)):
        raise MovementEvidenceUnavailable('current_quest_occurrence_step_objective_scope_missing')
    if type(request['instance_id']) is not int or request['instance_id']<=0:
        raise MovementEvidenceUnavailable('quest_instance_id_invalid')
    semantic=request.get('semantics') or {}
    if offer and (request.get('source')!='current-root-typed-quest-collections' or
        request.get('offer_basis')!='verified_current_waiting_ActiveQuest_status_and_current_QuestStaticData_Sender'):
        raise MovementEvidenceUnavailable('current_waiting_quest_scope_required')
    if not offer and (not request.get('source') or (semantic.get('discriminant') or {}).get('verified') is not True):
        raise MovementEvidenceUnavailable('current_typed_objective_discriminant_required')
    if not offer and (not (semantic.get('auto_play') or {}).get('available') or not ((semantic.get('auto_play') or {}).get('discriminant') or {}).get('verified')):
        raise MovementEvidenceUnavailable('current_typed_autoplay_descriptor_required')
    if (semantic.get('auto_play') or {}).get('type')=='MM.QDBL.CodeGenerated.NotSupportedAutoPlayDescriptorData':
        raise MovementEvidenceUnavailable('native_auto_play_explicitly_not_supported')
    if snapshot.get('certainty')!='confirmed' or not snapshot.get('observation_source'):
        raise MovementEvidenceUnavailable('confirmed_current_map_source_required')
    owner=_owner(snapshot)
    if [str(request.get(k)) for k in ('process_started_at','character_id','session_id')]!=[str(x) for x in owner[1:]]:
        raise MovementEvidenceUnavailable('map_and_current_quest_owner_mismatch')
    for stamp in (snapshot.get('captured_at'),request.get('observed_at')):
        if not -.5<=(now-_time(stamp)).total_seconds()<=5:
            raise MovementEvidenceUnavailable('current_quest_or_map_observation_stale')
    for key in ('game_assembly_sha256','metadata_sha256'):
        if (request.get('source_version') or {}).get(key)!=rules()[key]:
            raise MovementEvidenceUnavailable('current_quest_callback_source_version_mismatch')
        if snapshot.get('source_version') and snapshot['source_version'].get(key)!=rules()[key]:
            raise MovementEvidenceUnavailable('current_map_callback_source_version_mismatch')
    key=_digest({k:request.get(k) for k in ('quest_id','instance_id','accept_time')})
    if receipt and (receipt.get('owner')!=owner or receipt.get('goal_key')!=key):
        raise MovementEvidenceUnavailable('pending_autoplay_owner_or_quest_changed')
    base=dict(owner=owner,goal_key=key,captured_at=snapshot['captured_at'])
    evidence=dict(base,action_scope='quest_instance',objective_index_parameter=False,
        requested_objective={k:request.get(k) for k in SCOPE},current_step={k:request.get(k) for k in ('step_id','step_index')},
        supported_objective_indices=deepcopy(request.get('supported_objective_indices')),
        native_target={'quest_instance_id':request['instance_id']},
        journal_objective_is_not_single_objective_execution_guarantee=True,
        expected_effect=deepcopy(request.get('expected_effect')),coordinate_space='physical-client-pixels',
        source='current_map_MoveButtonEvent_StartAutoPlayRequested_AutoPlayTarget',static_world_projection=False)
    if offer:evidence.update(giver_definition=deepcopy(request.get('giver_definition')),acceptance_observed=False,
                            dialogue_offer_verified=False,request_kind='quest.offer.autoplay')
    phase=(receipt or {}).get('phase')
    if phase=='quest_autoplay_sent':
        return _wait('quest_native_autoplay_dispatched_wait_for_typed_effect_no_resend',evidence,receipt)
    active=snapshot.get('current_autoplay') or {}
    if (active.get('confirmed') is True and active.get('owner_verified') is True and active.get('active') is True
        and active.get('source')=='current_root_AutoPlayComponent_server_target_and_active_fields'
        and active.get('source_hash')==rules()['source_hash']
        and active.get('target')=={'kind':'quest_instance','instance_id':request['instance_id']}
        and all(str(active.get(k))==str(request.get(k)) for k in ('process_started_at','character_id','session_id'))
        and all((active.get('source_version') or {}).get(k)==rules()[k] for k in ('metadata_sha256','game_assembly_sha256'))
        and -.5<=(now-_time(active.get('observed_at'))).total_seconds()<=5):
        return _wait('quest_native_autoplay_already_active_wait_for_typed_effect',
                     dict(evidence,current_autoplay=deepcopy(active)),receipt)
    if not snapshot.get('map_visible'):
        if phase:return _wait('quest_map_open_or_effect_unconfirmed_no_resend',evidence,receipt)
        return _action('click','map.open',base,evidence,phase='quest_map_open_sent',point=_control(snapshot,'map.open'))
    if not snapshot.get('map_ready'):return _wait('current_quest_map_not_ready',evidence,receipt)
    pose=_pose(snapshot)
    selected=snapshot.get('selected') or {}
    if selected and _callback(selected,request['instance_id']):
        detail=selected['detail']
        if current_objective_marker_matches(selected,request):
            evidence['current_objective_marker_scope']=deepcopy(detail['quest_marker_scope'])
        if detail.get('move_enabled') is not True:
            return _wait('current_quest_autoplay_CTA_disabled',evidence,receipt)
        panel=snapshot.get('panel')
        if not panel:return _wait('current_quest_autoplay_MMML_panel_missing',evidence,receipt)
        scale=_number(panel['scale']);sw,sh=_pair(panel['screen_ui'])
        if scale<=0 or abs(sw*scale-pose['width'])>2 or abs(sh*scale-pose['height'])>2:
            return _wait('current_quest_autoplay_panel_viewport_mismatch',evidence,receipt)
        rect=Rect(*(v*scale for v in panel['rect_ui']))
        if not Rect(0,0,pose['width'],pose['height']).contains(rect) or not panel.get('source'):
            return _wait('current_quest_autoplay_panel_clipped_or_unproven',evidence,receipt)
        if abs(rect.bottom-(pose['height']-32*scale))>2 or abs(rect.height-80*scale)>2:
            return _wait('current_quest_autoplay_panel_animation_unsettled',evidence,receipt)
        evidence.update(current_callback=deepcopy(detail['quest_autoplay']),panel_source=panel['source'])
        return _action('click','quest.native_autoplay',base,evidence,phase='quest_autoplay_sent',
            point=list(rect.center),action_scope='quest_instance',quest_instance_id=request['instance_id'])
    if phase=='quest_marker_select_sent':
        return _wait('quest_marker_selection_effect_unconfirmed_no_resend',evidence,receipt)
    markers=[m for m in snapshot.get('markers',[]) if _callback(m,request['instance_id'])]
    if not markers:
        return _wait('current_same_quest_instance_StartAutoPlay_marker_missing',evidence,receipt)
    exact=[m for m in markers if current_objective_marker_matches(m,request)]
    evidence['matching_current_objective_markers']=len(exact)
    evidence['matching_same_instance_callbacks']=len(markers)
    if exact:markers=exact
    area=_touch(snapshot,pose)
    # Every candidate carries the SAME verified native callback target. Pick
    # only by current map geometry for visibility, never static region centres.
    markers.sort(key=lambda m:(math.dist(_point(pose,m['map_position']),area.center),str(m.get('key'))))
    marker=markers[0];point=_point(pose,marker['map_position'])
    evidence.update(marker_key=marker.get('key'),point=point,
                    pose_signature=pose['signature'])
    if exact:evidence['current_objective_marker_scope']=deepcopy(marker['detail']['quest_marker_scope'])
    if phase in ('quest_map_pan_sent','quest_map_zoom_sent') and receipt.get('pose_signature')==pose['signature']:
        return _wait('quest_map_pan_or_zoom_effect_unconfirmed_no_resend',evidence,receipt)
    low,high,ratio=marker.get('ratio_min'),marker.get('ratio_max'),snapshot.get('marker_zoom_ratio')
    if None in (low,high,ratio):return _wait('current_quest_marker_visibility_missing',evidence,receipt)
    low,high,ratio=map(_number,(low,high,ratio))
    if not low<=ratio<=high:
        return _action('scroll','map.zoom',base,evidence,phase='quest_map_zoom_sent',
            prior={'pose_signature':pose['signature']},point=list(area.center),delta=120 if ratio<low else -120)
    if not area.contains(Rect(point[0]-2,point[1]-2,4,4)):
        ax,ay=area.center;dx,dy=ax-point[0],ay-point[1]
        divisor=max(1,abs(dx)/(area.width*.7),abs(dy)/(area.height*.7));dx/=divisor;dy/=divisor
        start,end=[ax-dx/2,ay-dy/2],[ax+dx/2,ay+dy/2]
        if math.dist(start,end)<=12:return _wait('quest_marker_pan_space_insufficient',evidence,receipt)
        return _action('map_drag','map.pan',base,evidence,phase='quest_map_pan_sent',
            prior={'pose_signature':pose['signature']},start=start,end=end)
    return _action('click','quest.map_marker',base,evidence,phase='quest_marker_select_sent',point=point)
