"""Read the CURRENT map CTA's declared quest target; no game invocation.

StartAutoPlayRequested targets a whole quest instance. Its source struct has
no step/objective parameter. Unknown event types remain unavailable.
"""
from functools import lru_cache
import hashlib
import json
import struct

from ..paths import root
from .map_character_context import _class
from .map_character_context import _fields


@lru_cache(maxsize=1)
def rules():
    raw=(root()/'data/arbeit-quest-autoplay.json').read_bytes()
    result=json.loads(raw)
    result['source_hash']=hashlib.sha256(raw).hexdigest()
    return result


def _shape(session,name):
    rule=rules()
    if any(rule[k]!=session.plan[k] for k in ('metadata_sha256','game_assembly_sha256')):
        raise ValueError('current_quest_map_action_source_version_mismatch')
    rows=[r for r in rule['classes'] if r['name']==name]
    if len(rows)!=1:raise ValueError('quest_map_action_type_not_unique:'+name)
    return rows[0]


def _value_fields(session,name):
    shape=_shape(session,name)
    klass=_class(session.reader,session.base,shape)
    fields=session.fields(klass,shape)
    return shape,fields


def read_builder_identity(session,builder,fields):
    """QuestInstanceId is inline int64, QuestWorldPropSheetId inline int32."""
    result={}
    for field,type_name,part,fmt,output in (
        ('questInstanceId','QuestInstanceId','int64Value','<q','quest_instance_id'),
        ('questWorldPropSheetId','QuestWorldPropSheetId','Id','<i','quest_world_prop_id')):
        shape,offsets=_value_fields(session,type_name)
        width=struct.calcsize(fmt)
        if offsets[part]!=16 or shape['type_definition_size_raw'][1]!=width:
            raise ValueError('quest_map_identity_inline_layout_changed:'+type_name)
        result[output]=struct.unpack(fmt,session.watch(builder+fields[field],width))[0]
    return result


def read_current_autoplay(session,event,*,builder_identity=None):
    """Return the actual event payload only after runtime field validation."""
    unavailable={'available':False,'action_scope':'quest_instance','objective_index_parameter':False}
    r=session.reader
    if not event:return dict(unavailable,missing=['current_map_move_event_missing'])
    if r.class_name(r.u64(event))!=('StartAutoPlayRequested','MM.Client.Presentation.EventDefinitions.Integrations'):
        return dict(unavailable,missing=['current_map_move_event_is_not_StartAutoPlayRequested'])
    declaration=_shape(session,'StartAutoPlayRequested')
    event_fields=session.fields(r.u64(event),declaration)
    field=next(f for f in declaration['fields'] if f['name']=='<AutoPlayTarget>k__BackingField')
    if field['type'].get('name')!='AutoPlayTarget' or field['type']['type_code']!=17:
        raise ValueError('current_autoplay_event_target_type_changed')
    target,tf=_value_fields(session,'AutoPlayTarget')
    enum,ef=_value_fields(session,'AutoPlayTargetType')
    instance,qf=_value_fields(session,'QuestInstanceId')
    if ef['value__']!=16 or qf['int64Value']!=16 or instance['type_definition_size_raw'][1]!=8:
        raise ValueError('current_autoplay_target_nested_value_layout_changed')
    width=target['type_definition_size_raw'][1]
    if not 16<=width<=128:raise ValueError('current_autoplay_target_size_invalid')
    address=event+event_fields['<AutoPlayTarget>k__BackingField']
    raw=session.watch(address,width)
    offsets=[tf['targetType']-16,tf['questInstanceId']-16]
    if offsets[0]<0 or offsets[1]<0 or offsets[0]+4>width or offsets[1]+8>width:
        raise ValueError('current_autoplay_target_field_exceeds_inline_value')
    kind=struct.unpack_from('<i',raw,offsets[0])[0]
    ident=struct.unpack_from('<q',raw,offsets[1])[0]
    if kind!=enum['enum_constants']['Quest']:
        return dict(unavailable,missing=['current_autoplay_target_is_not_Quest'],target_type=kind)
    if ident<=0:return dict(unavailable,missing=['current_autoplay_quest_instance_invalid'])
    builder_id=(builder_identity or {}).get('quest_instance_id')
    if builder_id not in (None,0,ident):
        return dict(unavailable,missing=['current_builder_and_callback_quest_instances_differ'])
    return dict(available=True,action_scope='quest_instance',objective_index_parameter=False,
        quest_instance_id=ident,target_type='Quest',event_type='StartAutoPlayRequested',
        source='current_map_MoveButtonEvent_StartAutoPlayRequested_AutoPlayTarget',
        source_hash=rules()['source_hash'],event_pointer=hex(event),field_layout_verified=True,
        builder_identity=builder_identity,game_inputs=0)


def read_active_autoplay(session,context):
    """Current rooted server target and IsActive, independent of any UI intent."""
    definition=rules();proof=definition['active_root'];r=session.reader
    schema=_shape(session,'AutoPlayComponent')
    if not proof.get('presence_verified'):raise ValueError('active_autoplay_presence_source_missing')
    shape=proof['singular'];entity=context['chain'][3]
    offsets=_fields(r,r.u64(entity),shape)
    # _fields validates the source allowlist. Retain both root aliases so an
    # old component cannot survive a replacement of the current singular root.
    parts={f['name']:f['offset'] for f in shape['fields']}
    pointers={name:struct.unpack('<Q',session.watch(entity+off,8))[0] for name,off in parts.items()}
    component=pointers['IAutoPlayComponent']
    if not component:raise ValueError('current_autoplay_component_absent')
    if pointers['AutoPlayComponent']!=component:raise ValueError('current_autoplay_component_aliases_differ')
    fields=session.fields(r.u64(component),schema)
    for name,prefix in proof['guarded_getter_prefixes'].items():
        probe=next(p for p in proof['native_getter_probes'] if p['name']==name)
        expected=bytes.fromhex(prefix)
        if session.watch(session.base+int(probe['rva'],16),len(expected))!=expected:
            raise ValueError('current_autoplay_native_getter_changed:'+name)
    active_raw=session.watch(component+fields['<IsActive>k__BackingField'],1)[0]
    if active_raw not in (0,1):raise ValueError('current_autoplay_active_bool_invalid')
    step=struct.unpack('<i',session.watch(component+fields['<TargetQuestStepIndex>k__BackingField'],4))[0]
    objective=struct.unpack('<i',session.watch(component+fields['<TargetQuestObjectiveIndex>k__BackingField'],4))[0]
    target,tf=_value_fields(session,'AutoPlayTarget');enum,ef=_value_fields(session,'AutoPlayTargetType')
    ident,qf=_value_fields(session,'QuestInstanceId')
    if ef['value__']!=16 or qf['int64Value']!=16 or ident['type_definition_size_raw'][1]!=8:
        raise ValueError('current_active_autoplay_nested_value_layout_changed')
    field=next(f for f in schema['fields'] if f['name']=='autoplayTargetFromServer')
    if field['type'].get('name')!='AutoPlayTarget' or field['type']['type_code']!=17:
        raise ValueError('current_autoplay_server_target_type_changed')
    raw=session.watch(component+fields['autoplayTargetFromServer'],target['type_definition_size_raw'][1])
    kind=struct.unpack_from('<i',raw,tf['targetType']-16)[0]
    instance=struct.unpack_from('<q',raw,tf['questInstanceId']-16)[0]
    names=[name for name,value in enum['enum_constants'].items() if value==kind]
    if len(names)!=1:raise ValueError('current_autoplay_target_kind_unknown')
    is_quest=kind==enum['enum_constants']['Quest']
    if active_raw and is_quest and instance<=0:
        raise ValueError('current_active_quest_instance_invalid')
    return dict(confirmed=True,owner_verified=True,active=bool(active_raw),
        source='current_root_AutoPlayComponent_server_target_and_active_fields',source_hash=definition['source_hash'],
        target={'kind':'quest_instance' if is_quest else names[0],'instance_id':instance if is_quest else None},
        target_quest_step_index=step,target_quest_objective_index=objective,
        basis=proof['target_basis'],desired_ui_callback_is_not_active_evidence=True,
        source_version={k:definition[k] for k in ('metadata_sha256','game_assembly_sha256')},
        component_pointer=hex(component),game_inputs=0)
