"""Reused study-5 evidence helpers. Completed Pure inputs; no final-graph prefix backfill.

All writes are confined to this report. Run with python analysis/analyze.py.
Existing exploratory entry membership is frozen from study 4, not retrained here.
"""
import bisect
import csv
import hashlib
import json
import math
from collections import Counter, defaultdict
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
REPO = ROOT.parents[1]
OLD = ROOT.parent / 'region-dominator-backtrace-20261107'
FUNCTIONS = {'odkcollect.download', 'activitydiary.add-activity', 'activitydiary.edit-existing-activity'}
WINDOWS = (2, 5, 10, 20)
METHODS = tuple('A'+str(x) for x in WINDOWS) + ('B', 'C', 'B_refresh_split')
READ_FILES = {}

def read(p):
    p = Path(p); data = p.read_bytes(); READ_FILES[str(p)] = hashlib.sha256(data).hexdigest()
    return json.loads(data)

def lines(p):
    p = Path(p); data = p.read_bytes(); READ_FILES[str(p)] = hashlib.sha256(data).hexdigest()
    return [json.loads(l) for l in data.splitlines()]

def write(name, value):
    p = (ROOT/name).resolve(); assert p.is_relative_to(ROOT)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(json.dumps(value, ensure_ascii=False, indent=2)+'\n')

def table(name, rows):
    rows = list(rows); write('analysis/'+name+'.json', rows)
    fields = list(dict.fromkeys(k for r in rows for k in r))
    with (ROOT/'analysis'/f'{name}.csv').open('w', newline='') as f:
        w = csv.DictWriter(f, fields); w.writeheader()
        for r in rows:
            w.writerow({k: json.dumps(v, ensure_ascii=False) if isinstance(v, (dict,list,tuple)) else v for k,v in r.items()})

def key(*v): return json.dumps(v, ensure_ascii=False, separators=(',',':'))
def short(a): return (a or '').split('.')[-1]
def label(e): return e['content_desc'] or e['text'] or e['resource_id'].split('/')[-1] or e['action_type']

def continuity(a,b):
    errors=[]
    for ok, reason in [
        (b['native_transition_order']==a['native_transition_order']+1, 'unconfirmed_transition_between'),
        (b['order']==a['order']+1, 'confirmed_order_gap'),
        (a['monotonic_ns']<b['monotonic_ns'], 'time_not_increasing'),
        (a['target_gui_id']==b['source_gui_id'], 'GUI_ID_gap'),
        (a['target_gui_timestamp']==b['source_gui_timestamp'], 'GUI_timestamp_gap'),
        (a['target_state']==b['state'], 'State_gap'),
        (a['version']==b['version'], 'version_changed')]:
        if not ok: errors.append(reason)
    return errors

def inside(app, activity):
    return short(activity)==('FormDownloadListActivity' if app=='odkcollect' else 'EditActivity') if activity else None

def target_at(e, cutoff):
    opts=[o for o in e['target_activity_evidence'] if o['available_ns']<=cutoff]
    return min(opts,key=lambda x:x['available_ns'])['activity'] if opts else ''

def terminal(e):
    rid=e['resource_id'].split('/')[-1]
    if rid.startswith('quickFix'): return 'object_conflict_or_mode_handoff'
    if rid=='action_edit_done': return 'save_attempt_not_necessarily_exit'
    if rid=='action_edit_delete': return 'delete_attempt_not_necessarily_exit'
    return None

def action_class(e):
    """A reviewed operation family for display, never an identity equivalence rule."""
    r=e['resource_id'].split('/')[-1]
    if r in {'edit_activity_color','hexCode','okColorButton'}: return 'shared_color_UI'
    if r=='edit_activity_name': return 'shared_name_field_UI_data_dependent'
    if terminal(e): return 'object_mode_or_result_sensitive'
    if e['action_type']=='MODEL_BACK': return 'navigation_context_dependent'
    return 'other_or_unknown'

def metric(run, ids, targets=None):
    es=[run['byid'][t] for t in ids]
    filtered=targets is not None
    targets = targets if targets is not None else {t:True for t in ids}
    states={e['target_key'] for e in es if targets.get(e['transition_id'])}
    bare_states=sorted({e['target_state'] for e in es if targets.get(e['transition_id'])})
    con=[e for e in es if e['concrete_key']]
    return dict(events=len(es),target_scope='interior_activity_filtered' if filtered else 'all_observed_including_exits_and_unresolved',
        observed_version_target_states=len({e['target_key'] for e in es}),observed_bare_target_states=len({e['target_state'] for e in es}),
        interior_target_states=len(states) if filtered else None,
        interior_bare_State_count=len(bare_states) if filtered else None,interior_bare_State_IDs=bare_states if filtered else None,
        native_new_state=sum(e['new_state'] for e in es),
        interior_native_new_state=sum(e['new_state'] for e in es if targets.get(e['transition_id'])) if filtered else None,
        first_observed_version_state=sum(e['first_target_state'] for e in es),
        first_model_transition=sum(e['first_model_transition'] for e in es),
        first_ID_only_model_transition=sum(e['first_ID_only_model_transition'] for e in es),
        first_concrete_transition=sum(e['first_concrete_transition'] for e in es),
        concrete_executions=len(con), first_local_concrete=sum(e['first_local_concrete'] for e in con),
        first_activity_concrete=sum(e['first_activity_concrete'] for e in con),
        first_operation_family=sum(e['first_operation_family'] for e in es),
        unique_local_concrete=len({e['concrete_key'] for e in con}),
        unique_activity_concrete=len({e['activity_concrete_key'] for e in con}),
        unique_model_transitions=len({e['model_transition_key'] for e in es}),
        unique_concrete_transitions=len({e['concrete_transition_key'] for e in es}),
        repeated_state_fraction=(sum(not e['first_target_state'] for e in es)/len(es)) if es else None,
        repeated_model_transition_fraction=(sum(not e['first_model_transition'] for e in es)/len(es)) if es else None,
        repeated_local_concrete_fraction=(sum(not e['first_local_concrete'] for e in con)/len(con)) if con else None)

