"""Current quest-marker ownership, read through existing map roots only.

The key identifies presentation for a quest instance/step/objective. It does
not turn a region marker into a spawned prop or grant plain movement. Native
StartAutoPlayRequested still carries only the whole QuestInstanceId.
"""
from mini_masukja.native_type_reference import type_definition_index
from functools import lru_cache
import hashlib
import json
import struct

from ..paths import root
from .map_quest_actions import rules as autoplay_rules

SOURCE_REF='data/arbeit-dynamic-map-targets.json'
SOURCE='current_LocationDetailQuestData_QuestMapMarkerKey'


@lru_cache(maxsize=1)
def rules():
    raw=(root()/SOURCE_REF).read_bytes();digest=hashlib.sha256(raw).hexdigest()
    if digest!='2ad175e93c5b56943bc7798b80753f034c6e931e651c270a24eae809dd220712':
        raise ValueError('Dynamic quest marker source changed')
    result=json.loads(raw);result['source_hash']=digest;return result


def read_quest_marker_scope(session,detail,detail_fields,*,builder_instance=None):
    """Read closed ValueTuple<QuestInstanceId,int,int>, with source-proven meaning.

    No cache initialization, native calls, fallback offsets or process opening.
    The caller includes these watched fields in its final stable-root check.
    """
    r=session.reader;definition=rules()
    if any(session.plan.get(k)!=definition[k] for k in ('metadata_sha256','game_assembly_sha256')):
        raise ValueError('Current dynamic marker source version mismatch')
    address=detail+detail_fields['<QuestData>k__BackingField']
    quest=struct.unpack('<Q',session.watch(address,8))[0]
    if not quest:return {'available':False,'missing':['current marker has no quest detail']}
    for row in definition['native_probes']:
        expected=bytes.fromhex(row['raw_hex'])
        if session.watch(session.base+row['rva'],len(expected))!=expected:
            raise ValueError('Current quest marker producer source changed: '+row['name'])
    schema=next(x for x in definition['classes'] if x['name']=='LocationDetailQuestData')
    klass=struct.unpack('<Q',session.watch(quest,8))[0];fields=session.fields(klass,schema)
    field=next(x for x in schema['fields'] if x['name']=='QuestMapMarkerKey')
    if field['type']['name']!='ValueTuple`3' or field['type']['type_code']!=21:
        raise ValueError('Source quest marker key is not the selected tuple')
    table=struct.unpack('<Q',session.watch(klass+0x80,8))[0]
    tp=struct.unpack('<Q',session.watch(table+field['index']*32+8,8))[0]
    def type_data(pointer):
        raw=session.watch(pointer,12)
        return struct.unpack_from('<Q',raw)[0],(struct.unpack_from('<I',raw,8)[0]>>16)&255
    generic,code=type_data(tp)
    if code!=21 or not generic:raise ValueError('Current marker tuple generic type missing')
    generic_raw=session.watch(generic,32)
    inst=struct.unpack_from('<Q',generic_raw,8)[0];closed=struct.unpack_from('<Q',generic_raw,24)[0]
    if not inst or not closed:raise ValueError('Current marker closed tuple class is not initialized')
    raw=session.watch(inst,16);count=struct.unpack_from('<I',raw)[0];argv=struct.unpack_from('<Q',raw,8)[0]
    if count!=3 or not argv:raise ValueError('Current marker tuple arguments unavailable')
    args=struct.unpack('<3Q',session.watch(argv,24));types=[type_data(p) for p in args]
    ident=next(x for x in definition['classes'] if x['name']=='QuestInstanceId')
    if (type_definition_index(session, args[0]),types[0][1])!=(ident['type_index'],17) or [t[1] for t in types[1:]]!=[8,8]:
        raise ValueError('Current marker tuple arguments differ from QuestInstanceId/int/int')
    if r.class_name(closed)!=('ValueTuple`3','System'):
        raise ValueError('Current marker closed tuple class differs')
    layout=r.layout(closed,['Item1','Item2','Item3'])
    if layout['instance_size']!=32 or [x['type_code'] for x in layout['fields']]!=[17,8,8]:
        raise ValueError('Current marker tuple size or field types changed')
    offsets=[x['offset']-16 for x in layout['fields']]
    if offsets!=[0,8,12]:raise ValueError('Current marker tuple offsets differ from pinned constructor')
    if fields['QuestRewardDatas']-fields['QuestMapMarkerKey']!=16:
        raise ValueError('Current marker tuple inline span changed')
    instance,step,objective=struct.unpack('<qii',session.watch(quest+fields['QuestMapMarkerKey'],16))
    if instance<=0 or step < -1 or objective < -1:
        raise ValueError('Current marker key contains invalid quest indices')
    if builder_instance not in (None,0,instance):
        raise ValueError('Current marker key and callback builder quest instances differ')
    return dict(available=True,quest_instance_id=instance,step_index=step,objective_index=objective,
        source=SOURCE,source_ref=SOURCE_REF,source_hash=definition['source_hash'],field_layout_verified=True,
        key_semantics_verified=True,coordinate_semantics='current_presentation_marker',
        spawned_target_position_verified=False,accept_time_carried=False,
        action_scope='quest_instance',objective_index_parameter=False)


def current_objective_marker_matches(marker,expected):
    scope=(marker.get('detail') or {}).get('quest_marker_scope') or {}
    return (scope.get('available') is True and scope.get('field_layout_verified') is True
        and scope.get('key_semantics_verified') is True and scope.get('source')==SOURCE
        and scope.get('source_hash')==rules()['source_hash']
        and all(type(scope.get(key)) is int and type(expected.get(other)) is int and scope[key]==expected[other]
            for key,other in (('quest_instance_id','instance_id'),('step_index','step_index'),('objective_index','objective_index'))))


def native_callback_matches(marker,instance):
    callback=(marker.get('detail') or {}).get('quest_autoplay') or {}
    return (callback.get('available') is True and callback.get('field_layout_verified') is True
        and callback.get('action_scope')=='quest_instance' and callback.get('objective_index_parameter') is False
        and callback.get('quest_instance_id')==instance and callback.get('target_type')=='Quest'
        and callback.get('event_type')=='StartAutoPlayRequested'
        and callback.get('source')=='current_map_MoveButtonEvent_StartAutoPlayRequested_AutoPlayTarget'
        and callback.get('source_hash')==autoplay_rules()['source_hash'])
