"""Current story/card adoption and guarded actions, separate from quest success."""
from copy import deepcopy
import hashlib,json,math,struct
from .quest_ui_actions import prepare_pointer_proposal,pointer_effect_contract
from .vendor.map_memory import MapReadPending
from .coordinate_geometry import CLIENT_XYWH, safe_rect_xywh_from_frame

SCOPE=('quest_id','instance_id','accept_time','step_id','step_index','objective_index','process_started_at')
SOURCE_REF='data/arbeit-story-ui.json'

def digest(value):
    return hashlib.sha256(json.dumps(value,sort_keys=True,separators=(',',':'),ensure_ascii=False).encode()).hexdigest()

def planner_receipt(frame):
    pending=frame.get('pending_input') or {}
    return pending.get('planner_receipt') or pending.get('receipt') or pending

def current_program(node,frame):
    from .objective_binding import current_objective_occurrence
    from .typed_objective_operands import normalize_objective_operands
    rows=[p for p in frame.get('objective_programs',[]) if current_objective_occurrence(p,frame,node)]
    if len(rows)!=1:raise ValueError('one current typed story/item objective occurrence required')
    normalized=normalize_objective_operands(rows[0],expected=node)
    if not normalized['ready'] or normalized['operation'] not in ('objective.read_story','objective.use_quest_item'):
        raise ValueError('current typed story/item definition incomplete:'+','.join(normalized['missing']))
    return rows[0],normalized

def optional(row,code):
    if not isinstance(row,dict) or row.get('optional') is not True or row.get('interpreted') is not True or type(row.get('has_value')) is not bool:
        raise ValueError('current source Optional presence unavailable')
    if (row.get('optional_argument_type') or {}).get('type_code')!=code:raise ValueError('current Optional argument changed')
    return row['has_value'],row.get('value')

def definition_pages(program):
    """Literal constraints; absent server intervals remain unspecified."""
    values=program['data']['values'];result=[]
    for name in ('ReadingActionName','ReadEndActionName'):optional(values.get(name),14)
    rows=(values.get('StoryBoxList') or {}).get('items')
    if not isinstance(rows,list) or not 1<=len(rows)<=128:raise ValueError('current StoryBoxList unavailable')
    for row in rows:
        if row.get('available') is not True or row.get('type')!='MM.QDBL.CodeGenerated.StoryBoxData':raise ValueError('current StoryBoxData type changed')
        fields=row.get('values') or {};text=fields.get('Text') or {}
        if text.get('interpreted') is not True or text.get('declared_type')!='System.String' or not isinstance(text.get('value'),str):raise ValueError('current story literal text unavailable')
        token_present,token=optional(fields.get('Text_L10nToken'),14)
        if token_present:raise MapReadPending('story_localization_token_to_current_page_resolution_required')
        timed,interval=optional(fields.get('StayInterval'),13)
        if not timed:
            result.append(dict(content=text['value'],stay_interval=None));continue
        if type(interval) not in (float,int) or not math.isfinite(interval) or interval<0:raise ValueError('current story interval invalid')
        interval32=struct.unpack('<f',struct.pack('<f',interval))[0]
        if not math.isfinite(interval32):raise ValueError('current story interval outside Single')
        result.append(dict(content=text['value'],stay_interval=interval32))
    return result

def _lease_and_pending(frame,node):
    from .arbeit_operations import _fresh
    lease=frame.get('input_owner') or {};owner=lease.get('owner') or {}
    if (lease.get('continuous') is not True or not lease.get('epoch') or not _fresh(dict(lease,session_id=frame.get('session_id'),character_id=frame.get('character_id')),frame)
            or any(owner.get(k)!=frame.get(k) for k in ('session_id','character_id'))):
        raise MapReadPending('current_story_input_lease_required')
    pending=frame.get('pending_input') or {};receipt=planner_receipt(frame)
    if pending and pending.get('node_id',receipt.get('node_id'))!=node['id']:
        raise MapReadPending('unknown_or_foreign_pending_input_prevents_story_adoption')
    if pending and any(pending.get(k) not in (None,v) for k,v in (('node_id',node['id']),('session_id',frame['session_id']),('character_id',frame['character_id']))):
        raise MapReadPending('foreign_pending_input_prevents_story_adoption')
    if receipt and receipt.get('node_id') not in (None,node['id']):raise MapReadPending('foreign_planner_receipt_prevents_story_adoption')
    return receipt

def bind_story_target(typed,frame,node):
    from .arbeit_operations import _fresh
    receipt=_lease_and_pending(frame,node);program,normalized=current_program(node,frame)
    if typed.get('stable') is not True or not _fresh(typed,frame) or any(typed.get(k)!=frame.get(k) for k in ('session_id','character_id','process_started_at')):
        raise ValueError('current story source/time/owner unverified')
    if typed.get('present') is not True:raise MapReadPending('current_story_or_item_card_absent')
    sem=typed['pointer_semantics'];op=normalized['operation'];requested={k:deepcopy(program[k]) for k in SCOPE}
    if op=='objective.read_story':
        if sem.get('ui_kind')!='story':raise MapReadPending('current matching StoryBox UI required')
        server_adoption=None
        try:expected=definition_pages(program)
        except MapReadPending:
            from .story_read_source import server_page_binding
            server_adoption=server_page_binding(typed,frame,node,program,receipt);expected=None
        actual=typed.get('pages') or []
        if expected is not None and (len(actual)!=len(expected) or any(a.get('content')!=e['content'] or
            (e['stay_interval'] is not None and a.get('stay_interval')!=e['stay_interval']) for a,e in zip(actual,expected))):
            raise MapReadPending('current_story_pages_do_not_exactly_match_selected_definition')
        kind='current_story_server_pages_adoption' if server_adoption else 'current_story_content_match';fingerprint=digest(actual)
        # This is an explicit selected-goal content adoption, never a server owner read.
        identity=dict(ui_pointer=typed['ui_pointer'],handle=typed['handle'],pages_fingerprint=fingerprint)
        stage=typed['stage']
    else:
        if sem.get('ui_kind') not in ('quest_fake_item_card','quest_fake_item_inventory','inventory_hud_entry'):raise MapReadPending('current exact quest fake item card or inventory entry required')
        item=normalized['operands'].get('quest_fake_item') or normalized['operands'].get('item') or {}
        if item.get('table_domain')!='QuestFakeItemSheet' or item.get('id')!=sem.get('fake_item_id'):
            raise MapReadPending('current_fake_item_card_definition_mismatch')
        unique=(normalized.get('constraints') or {}).get('UniqueName') or {}
        if unique.get('has_value') is True:raise MapReadPending('current_fake_item_card_has_no_unique_name_binding')
        if sem['ui_kind']=='quest_fake_item_card':
            kind='current_fake_item_card_match';identity=dict(ui_pointer=typed['ui_pointer'],fake_item_id=item['id'],card_info_pointer=sem['card_info_pointer']);stage='use'
        else:
            kind='current_fake_item_inventory_entry';identity=dict(ui_pointer=typed['ui_pointer'],fake_item_id=item['id'],entry_kind=sem['ui_kind']);stage=typed['stage']
    saved=(receipt.get('expected_effect') or {}).get('story_binding')
    if saved and (saved.get('binding_kind')!=kind or saved.get('goal_scope')!=requested or saved.get('actual_ui')!=identity):
        raise MapReadPending('pending_story_input_actual_ui_or_goal_changed_no_resend')
    navigation_origin=(receipt.get('expected_effect') or {}).get('inventory_navigation_binding')
    if navigation_origin:
        current=(typed.get('navigation') or {}).get('current_fields') or {}
        if current.get('inventory_ui_pointer')!=navigation_origin.get('inventory_ui_pointer'):
            raise MapReadPending('pending_inventory_navigation_root_changed_no_resend')
    bound=dict({k:program[k] for k in SCOPE},node_id=node['id'],phase='story_read' if op=='objective.read_story' else ('quest_item_ui' if stage.startswith(('open_','inventory_')) else 'quest_item_use'),
        binding_kind=kind,goal_scope=requested,actual_ui=identity,server_quest_owner_read=False,
        adoption_basis='explicit_current_input_lease_and_selected_goal_exact_source_definition',
        current_identity_bound=True,source_bound=True,ownership_verified=True,
        ownership_scope='requested_goal_to_current_UI_content_only',source=typed['source'],source_refs=typed['source_refs'],stage=stage)
    if op=='objective.read_story' and server_adoption:bound['server_page_adoption']=server_adoption
    return bound

def _proof(typed,frame,node,bound,fields):
    return dict(kind='ui_state',phase=bound['phase'],node_id=node['id'],**{k:bound[k] for k in SCOPE},
        fields=fields,source=typed['source'],source_revision=typed['source_hash'],source_refs=typed['source_refs'],
        observed_at=typed['observed_at'],session_id=frame['session_id'],character_id=frame['character_id'],
        binding_kind=bound['binding_kind'],server_quest_owner_read=False)

def build_story_proposal(typed,frame,node):
    bound=bind_story_target(typed,frame,node);sem=typed['pointer_semantics'];candidate=typed['candidate']
    fields=dict(ui_pointer=typed['ui_pointer'],handle=typed.get('handle'),pages_fingerprint=sem.get('pages_fingerprint'),
        page_index=sem.get('page_index'),show_buttons=sem.get('show_buttons'),visible=True)
    if bound['stage']=='use':
        program,_=current_program(node,frame);effect=program.get('typed_effect') or {}
        fields.update(objective_count=effect.get('count') if effect.get('confirmed') is True else None,objective_complete=effect.get('complete') if effect.get('confirmed') is True else None)
    fields.update(inventory_visible=sem.get('ui_kind')=='quest_fake_item_inventory',card_visible=sem.get('ui_kind')=='quest_fake_item_card',fake_item_id=sem.get('fake_item_id'))
    navigation=typed.get('navigation') or {}
    for key,value in (navigation.get('current_fields') or {}).items():
        if key in fields and fields[key]!=value:raise ValueError('inventory navigation overwrites current UI identity')
        fields[key]=deepcopy(value)
    proof=_proof(typed,frame,node,bound,fields)
    entry_proofs=item_entry_effect(typed,frame,node,fields)
    if candidate is None:return dict(action_proposals={},ui_effect_proofs=[proof]+entry_proofs,current_story_ui=typed,missing=typed.get('missing',[]))
    pointer=prepare_pointer_proposal(candidate,typed,frame);role='story.'+bound['stage']
    pointer['probe_context'].update(source_guard_kind='story-ui',target_id=role,node_id=node['id'])
    contract=pointer_effect_contract(pointer,bound,node,frame,typed['observed_at'])
    expected={k:deepcopy(v) for k,v in proof.items() if k not in ('fields','source','source_refs','observed_at','session_id','character_id')}
    expected['story_binding']={k:deepcopy(bound[k]) for k in ('binding_kind','goal_scope','actual_ui','server_quest_owner_read')}
    if bound.get('server_page_adoption'):expected['story_binding']['server_page_adoption']=deepcopy(bound['server_page_adoption'])
    if bound['stage'].startswith('inventory_'):
        exact=navigation.get('expected_fields') or {};changed=navigation.get('changed_fields') or {}
        if not (exact or changed):raise ValueError('current inventory navigation effect criteria required')
        if any(k not in fields or fields[k]!=v for k,v in changed.items()):raise ValueError('inventory navigation changed-field baseline differs')
        expected['fields']=deepcopy(exact);expected['changed_fields']=deepcopy(changed)
        origin=fields.get('inventory_ui_pointer')
        if type(origin) is not int or origin<=0:raise ValueError('current inventory navigation root required')
        if 'inventory_ui_pointer' in exact and exact['inventory_ui_pointer']!=origin:
            raise ValueError('inventory navigation expected root differs from current source')
        expected['fields']['inventory_ui_pointer']=origin
    elif bound['stage']=='open_inventory':expected['fields']=dict(inventory_visible=True,fake_item_id=sem['fake_item_id'])
    elif bound['stage']=='open_card':expected['fields']=dict(card_visible=True,fake_item_id=sem['fake_item_id'])
    elif bound['stage']=='next':expected['fields']=dict(ui_pointer=typed['ui_pointer'],handle=typed['handle'],pages_fingerprint=sem['pages_fingerprint'],page_index=sem['page_index']+1,visible=True)
    elif bound['stage']=='reveal':expected['fields']=dict(ui_pointer=typed['ui_pointer'],handle=typed['handle'],pages_fingerprint=sem['pages_fingerprint'],page_index=sem['page_index'],show_buttons=True,visible=True)
    elif bound['stage']=='close':expected['fields']=dict(ui_pointer=typed['ui_pointer'],handle=typed['handle'],pages_fingerprint=sem['pages_fingerprint'],visible=False)
    elif fields['objective_count'] is not None:expected['changed_fields']={'objective_count':fields['objective_count']}
    else:expected['fields']={'objective_complete':True}
    if bound['phase']=='quest_item_ui':
        expected['item_card_open_binding']=expected.pop('story_binding')
    if pointer['input_kind']=='pointer_probe':
        key='item_card_open_binding' if bound['phase']=='quest_item_ui' else 'story_binding'
        adoption=expected[key];expected=contract['expected_effect'];expected[key]=adoption
    if navigation.get('receipt_state') is not None:
        expected['inventory_navigation']=deepcopy(navigation['receipt_state'])
    if bound['stage'].startswith('inventory_'):
        expected['inventory_navigation_binding']=dict(kind=navigation['kind'],inventory_ui_pointer=origin,
            source_revision=typed['source_hash'])
    x,y,w,h=candidate['rect_client'];safe=safe_rect_xywh_from_frame(frame)
    if not safe:raise ValueError('current story safe rectangle required')
    action=dict(kind=pointer['input_kind'],role=role,point=pointer['point_client'],source_rect=[x,y,x+w,y+h],safe_rect=list(safe),safe_rect_format=CLIENT_XYWH,
        probe_context=pointer['probe_context'],pointer_verified=pointer.get('pointer_verified'),input_epoch=pointer['probe_context']['input_epoch'])
    if pointer['input_kind']=='pointer_scroll':
        action.update(delta=sem.get('delta'),scroll_receiver_proof=deepcopy(typed.get('scroll_receiver_proof') or candidate.get('scroll_receiver_proof')))
    proposal=dict(status='ready',action=action,current_binding=bound,expected_effect=expected,source_refs=typed['source_refs'],
        observation_source=typed['source'],observed_at=typed['observed_at'],session_id=frame['session_id'],character_id=frame['character_id'],
        context=dict(frame_id=frame['frame_id'],window_signature=frame['window_signature'],observed_at=typed['observed_at'],
            source_revision=typed['source_hash'],session_id=frame['session_id'],character_id=frame['character_id'],input_owner=frame['input_owner']))
    from .story_read_source import interaction_adoptions
    adoptions=interaction_adoptions(typed,frame,node,bound,current_program(node,frame)[0]) if bound['phase']=='story_read' else []
    return dict(action_proposals={node['id']:proposal},ui_effect_proofs=[proof]+entry_proofs+contract['effect_proofs'],current_story_ui=typed,interaction_adoptions=adoptions,missing=[])

def absent_story_effect(typed,frame,node):
    """A newer rooted absence can confirm close, never objective completion."""
    from .arbeit_operations import _stamp
    receipt=_lease_and_pending(frame,node);expected=receipt.get('expected_effect') or {};saved=expected.get('story_binding') or {}
    if typed.get('present') is not False or typed.get('absence_confirmed') is not True or expected.get('phase')!='story_read' or (expected.get('fields') or {}).get('visible') is not False:return []
    program,_=current_program(node,frame)
    if saved.get('goal_scope')!={k:program[k] for k in SCOPE} or not receipt.get('observed_at') or _stamp(typed['observed_at'])<=_stamp(receipt['observed_at']):return []
    if expected.get('source_revision')!=typed['source_hash'] or any(typed.get(k)!=frame.get(k) for k in ('session_id','character_id','process_started_at')):return []
    identity=saved.get('actual_ui') or {}
    if identity.get('ui_pointer') in typed.get('active_ui_pointers',[]):return []
    return [_proof(typed,frame,node,dict(program,node_id=node['id'],phase='story_read',binding_kind=saved['binding_kind']),dict(identity,visible=False))]


def item_entry_effect(typed,frame,node,fields):
    """Fresh rooted inventory/card appearance resolves only its saved open intent."""
    from .arbeit_operations import _stamp
    receipt=planner_receipt(frame);expected=receipt.get('expected_effect') or {};saved=expected.get('item_card_open_binding') or {}
    if expected.get('phase')!='quest_item_ui' or not saved or typed.get('present') is not True:return []
    program,_=current_program(node,frame)
    if saved.get('goal_scope')!={k:program[k] for k in SCOPE}:return []
    if expected.get('source_revision')!=typed['source_hash'] or not receipt.get('observed_at') or _stamp(typed['observed_at'])<=_stamp(receipt['observed_at']):return []
    if saved['actual_ui'].get('fake_item_id')!=fields.get('fake_item_id'):return []
    return [_proof(typed,frame,node,dict(program,phase='quest_item_ui',binding_kind=saved['binding_kind']),fields)]
