"""Current owned NPC group movement. No first/nearest candidate selection.

This independent module snapshots the r3 generated-target movement policy:
pending keys/occurrence stay bound; original arrival resolves moved targets
before any new plan. Arrival proves approach only, never dialogue/quest effects.
"""
from copy import deepcopy
from datetime import datetime
import hashlib,json,struct
from .group_npc_observer import owned_group_program,source_pins,SOURCE
from .world_target_observer import OWNER,VERSION,KEY_TYPE,channel_space,map_identity

def plan_group_npc_movement(node,snapshot,receipt=None,*,now=None):
    """Current unique spawned target -> existing map input planner.

    Receipt scope is the current quest occurrence AND the full generation key.
    Map opening/view selection and point navigation have separate stages.
    An uncertain prior action is retained; no candidate reselection/re-send.
    Arrival is approach evidence only, never objective or interaction success.
    """
    from .arbeit_operations import _fresh
    from .arbeit_movement import plan_movement
    from .typed_region_movement import SOURCE as PROJECTION_SOURCE,SOURCE_HASH as PROJECTION_HASH
    def wait(reason):
        return dict(status='waiting',action=None,wait_reason=reason,evidence={'objective_arrival_confirmed':False},
            next_receipt=deepcopy(receipt),sent_to_game=False,game_tested=False)
    try:
        program,normalized=owned_group_program(node,snapshot)
        owner={k:program[k] for k in OWNER};group=normalized['operands']['npc_group_tag']['value']
        outer=snapshot.get('map') or {};current=outer.get('snapshot') if outer.get('ok') is True else None
        if not isinstance(current,dict):return wait('current_group_npc_map_observation_required')
        if ((current.get('identity') or {}).get('character_id'),current.get('session_id'),current.get('process_started_at'))!=(program['character_id'],program['session_id'],program['process_started_at']):
            return wait('current_group_npc_map_owner_mismatch')
        if receipt and receipt.get('group_npc_owner')!=owner:return wait('pending_group_npc_objective_occurrence_changed')
        candidates=[r for r in snapshot.get('current_group_npc_targets',[]) if r.get('confirmed') is True and r.get('owner_verified') is True
            and r.get('source')==SOURCE and r.get('source_hash')==source_pins()['source_hash'] and _fresh(r,snapshot)
            and all((r.get('source_version') or {}).get(k)==source_pins()[k] for k in VERSION)
            and all(r.get(k)==v for k,v in owner.items()) and (r.get('group_tag') or {}).get('value')==group]
        if receipt:
            candidates=[r for r in candidates if r['target'].get('entity_key')==receipt.get('group_npc_key')]
        if len(candidates)!=1:return wait('current_spawned_target_missing_or_ambiguous:'+str(len(candidates)))
        row=candidates[0];target=row['target'];key=target.get('entity_key') or {};definition=target.get('definition_id')
        if (row.get('npc_id')!=definition or (target.get('group_membership') or {}).get('group_name')!=group
            or (target.get('group_membership') or {}).get('verified') is not True):return wait('current_group_npc_membership_unverified')
        if (target.get('membership_verified') is not True or target.get('replicated_present') is not True
            or target.get('domain')!='NPC' or key.get('declared_type')!=KEY_TYPE
            or key.get('raw_hex')!=struct.pack('<ii',key['Type'],key['InnerKey']).hex()):
            return wait('current_spawned_target_membership_unverified')
        channel=target['current_channel']['id'];space=channel_space(channel)
        if space is None:return wait('current_spawned_channel_world_definition_missing')
        world=[target['position']['x'],target['position']['y']]
        local=current.get('map_ready') and current.get('map_visible') and (current.get('pose') or {}).get('map_type')!=1 and current.get('world_data_key')==space['world_data_key']
        phase=(receipt or {}).get('phase');prior=receipt
        replanning=bool(receipt and receipt.get('group_npc_stage')=='replan' and phase=='replan_ready')
        if replanning:
            # This receipt records an already-observed effect of the original
            # navigation. Keep its owner/key and evidence; only the next action
            # may use a newly observed position of that same entity.
            resolved=receipt.get('resolved_navigation_receipt') or {}
            arrival=receipt.get('original_goal_arrival') or {}
            if (resolved.get('phase')!='move_sent' or resolved.get('group_npc_owner')!=owner
                or resolved.get('group_npc_key')!=key or arrival.get('confirmed') is not True
                or datetime.fromisoformat(current['captured_at'].replace('Z','+00:00'))<datetime.fromisoformat(arrival['observed_at'].replace('Z','+00:00'))):
                return wait('pending_group_npc_replan_effect_receipt_invalid')
            prior=None
        proof=target.get('projection') or {}
        projection_valid=(proof.get('confirmed') is True and proof.get('source')==PROJECTION_SOURCE and proof.get('source_hash')==PROJECTION_HASH
            and all((proof.get('source_version') or {}).get(k)==source_pins()[k] for k in VERSION)
            and proof.get('world_position')==world and proof.get('world_data_key')==space['world_data_key']
            and proof.get('geo_data_key')==space['geo_data_key'] and target.get('projection_map_identity')==map_identity(current)
            and proof.get('process_started_at')==program['process_started_at'] and proof.get('character_id')==program['character_id']
            and proof.get('session_id')==program['session_id'] and _fresh(proof,snapshot))
        if receipt and receipt.get('group_npc_stage')=='point':
            goal=deepcopy(receipt['group_npc_goal']);stage='point'
            if world!=receipt.get('spawned_world_position'):
                # A moved target is not permission to replace/re-send an
                # uncertain input. Resolve only the original selected-location
                # effect, using a newer current character/channel observation.
                if phase=='move_sent':
                    original=plan_movement(current,goal,receipt,now=now)
                    newer=datetime.fromisoformat(current['captured_at'].replace('Z','+00:00'))>datetime.fromisoformat(receipt['captured_at'].replace('Z','+00:00'))
                    if original.get('status')=='arrived' and newer:
                        arrival=dict(deepcopy(original['evidence']),confirmed=True,observed_at=current['captured_at'])
                        successor=deepcopy(receipt)
                        successor.update(phase='replan_ready',group_npc_stage='replan',captured_at=current['captured_at'],
                            dispatch_state='effect_confirmed',resolved_navigation_receipt=deepcopy(receipt),original_goal_arrival=arrival)
                        return dict(status='transition',action=None,wait_reason='original_group_npc_position_arrived_replan_same_entity',
                            next_receipt=successor,sent_to_game=False,game_tested=False,
                            evidence=dict(group_npc_owner=owner,group_npc_key=deepcopy(key),group_npc_id=definition,group_npc_tag=group,
                                source=SOURCE,source_hash=source_pins()['source_hash'],original_goal_arrival=arrival,
                                current_spawned_world_position=world,objective_arrival_confirmed=False,interaction_completed=False))
                return wait('current_spawned_target_moved_pending_navigation_no_resend')
            if phase!='move_sent':
                if not local or not projection_valid or proof.get('map_position')!=goal['map_position']:
                    return dict(wait('pending_group_npc_point_projection_changed_no_resend'),wait_kind='owned_input_effect',wait_family='group_npc_navigation')
        elif not local:
            token=hashlib.sha256(json.dumps(dict(owner=owner,key=key),sort_keys=True).encode()).hexdigest()
            goal=dict(kind='marker',marker_id='group-npc-view:'+token,world_data_key=space['world_data_key'],region_names=[space['name']])
            stage='view'
        else:
            if receipt and not replanning:
                if (receipt.get('group_npc_stage')!='view' or phase not in ('open_sent','world_sent','region_sent','pan_sent','zoom_sent')
                    or datetime.fromisoformat(current['captured_at'].replace('Z','+00:00'))<=datetime.fromisoformat(receipt['captured_at'].replace('Z','+00:00'))):
                    return dict(wait('pending_group_npc_view_fresh_effect_required'),wait_kind='owned_input_effect',wait_family='group_npc_navigation')
                prior=None
            if not projection_valid:
                return wait('current_spawned_world_projection_required:'+str(target.get('projection_missing','no current projection')))
            goal=dict(kind='map_position',coordinate_space='current-map',world_data_key=space['world_data_key'],map_position=deepcopy(proof['map_position']),
                region_names=[space['name']],channel_id=channel,title='current owned group NPC '+str(definition))
            stage='point'
        selected=current.get('selected') or {};detail=selected.get('detail') or {}
        if stage=='point' and selected.get('long_press') and detail.get('target_channel') is not None and detail['target_channel']&0xffffffff!=channel&0xffffffff:
            return wait('selected_spawned_target_move_channel_mismatch')
        answer=plan_movement(current,goal,prior,now=now)
        if answer.get('next_receipt') is not None:
            answer['next_receipt'].update(group_npc_owner=owner,group_npc_key=deepcopy(key),group_npc_stage=stage,
                group_npc_goal=deepcopy(goal),spawned_world_position=world,group_npc_id=definition,group_npc_tag=group)
            if receipt and prior is None:
                answer['next_receipt']['resolved_replan_receipt' if replanning else 'resolved_view_receipt']=deepcopy(receipt)
        answer.setdefault('evidence',{}).update(group_npc_owner=owner,group_npc_key=deepcopy(key),group_npc_id=definition,group_npc_tag=group,
            source=SOURCE,source_hash=source_pins()['source_hash'],objective_arrival_confirmed=False,interaction_completed=False,
            target_choice='unique current same-objective spawned definition; retained exact generation key after first action')
        return answer
    except (ValueError,RuntimeError,KeyError,TypeError,struct.error) as exc:return wait('group_npc_movement_unavailable:'+str(exc))

