#!/usr/bin/env python3
"""A07: pinned ATUS source audit and synthetic matched-pair planning calculation.
No participant records are supplied, transmitted, or analyzed by these tools.
"""
import argparse, collections, csv, hashlib, io, json, math, pathlib, zipfile

def read_zip(path):
    raw=pathlib.Path(path).read_bytes()
    with zipfile.ZipFile(io.BytesIO(raw)) as z:
        names=[n for n in z.namelist() if n.endswith('.dat')]
        if len(names)!=1: raise ValueError('Expected one .dat file')
        payload=z.read(names[0])
    return list(csv.DictReader(io.StringIO(payload.decode('utf-8-sig')))), hashlib.sha256(raw).hexdigest(), hashlib.sha256(payload).hexdigest()

def audit(respondent,activity):
    rr,rsha,rcsv=read_zip(respondent);aa,asha,acsv=read_zip(activity)
    if rsha!='77566d2864f1b96412a46ceeee9ea53709bc191b092f3573af04c17b2d409873' or asha!='664314e9f0c7373534f10bb1450a1dfc2b50d04c7932ecfea090ff4a1c65bca3':raise ValueError('Source SHA256 differs from pinned ATUS 2025 release; obtain and audit a new source version explicitly')
    if not rr or not aa:raise ValueError('Empty source')
    if len({r['TUCASEID'] for r in rr})!=len(rr):raise ValueError('Duplicate respondent ID')
    byid={r['TUCASEID']:r for r in rr};dur=collections.Counter();work=collections.Counter();keys=set()
    for r in aa:
        key=(r['TUCASEID'],r['TUACTIVITY_N'])
        if key in keys:raise ValueError('Duplicate diary episode key')
        keys.add(key)
        if r['TUCASEID'] not in byid:raise ValueError('Orphan diary episode')
        d=int(r['TUACTDUR24'])
        if not 1<=d<=1440:raise ValueError('Invalid truncated duration')
        dur[r['TUCASEID']]+=d
        if r['TRCODE']=='050101':work[r['TUCASEID']]+=d
    if set(dur)!=set(byid):raise ValueError('Respondent missing diary')
    if any(x!=1440 for x in dur.values()):raise ValueError('Diary fails 1440-minute closure')
    if any(not math.isfinite(float(r['TUFINLWGT'])) or float(r['TUFINLWGT'])<=0 for r in rr):raise ValueError('Invalid final weight')
    teacher=[r for r in rr if r['TELFS'] in ('1','2') and r['TEIO1OCD'] in ('2310','2320','2330')]
    def group(rows):
        weight=sum(float(r['TUFINLWGT']) for r in rows)
        return {'n':len(rows),'n_with_main_job_work':sum(work[r['TUCASEID']]>0 for r in rows),
          'weighted_main_job_minutes_per_diary_day':sum(float(r['TUFINLWGT'])*work[r['TUCASEID']] for r in rows)/weight if weight else None,
          'weighted_minutes_on_days_with_main_job_work':sum(float(r['TUFINLWGT'])*work[r['TUCASEID']] for r in rows)/sum(float(r['TUFINLWGT']) for r in rows if work[r['TUCASEID']]>0) if any(work[r['TUCASEID']]>0 for r in rows) else None}
    return {'analysis':'Executed descriptive secondary-data feasibility audit, not a trial result',
      'verifiedAt':'2026-10-15','dataset':'ATUS 2025 single-year respondent/activity files, released June 25 2026',
      'respondent_zip_sha256':rsha,'activity_zip_sha256':asha,'respondent_dat_sha256':rcsv,'activity_dat_sha256':acsv,
      'respondent_rows':len(rr),'activity_rows':len(aa),'unique_respondents':len(byid),'unique_episode_keys':len(keys),
      'all_diaries_close_to_1440_minutes':True,'orphan_episode_count':0,'respondents_without_activity':0,
      'teacher_definition':{'TELFS':['1','2'],'TEIO1OCD':['2310','2320','2330'],'occupation_system':'2018 Census occupation classification'},
      'selected_occupation_counts':dict(sorted(collections.Counter(r['TEIO1OCD'] for r in teacher).items())),
      'selected_teachers_all_days':group(teacher),'selected_teachers_weekdays':group([r for r in teacher if 2<=int(r['TUDIARYDAY'])<=6]),
      'selected_teachers_weekends':{'n':sum(r['TUDIARYDAY'] in ('1','7') for r in teacher),'estimates_suppressed':True,'n_with_main_job_work':sum(r['TUDIARYDAY'] in ('1','7') and work[r['TUCASEID']]>0 for r in teacher),'reason':'Weekend domain retained for sample accounting only; no weekend estimate or precision claim is made'},
      'diary_month_counts':dict(sorted(collections.Counter(r['TUMONTH'] for r in rr).items(),key=lambda x:int(x[0]))),
      'diaries_in_shutdown_gap':sum('20250930'<=r['TUDIARYDATE']<='20251111' for r in rr),
      'limits':['No replicate-weight standard errors or inferential comparisons calculated. Descriptive means are not official BLS published estimates.',
      'One diary per respondent; 050101 cannot separate teaching, AI drafting, verification, contract hours, unpaid after-hours work or protected planning.',
      '2310 combines elementary and middle schools; 2330 does not uniquely identify a grade span. Public file does not identify local schools or districts for trial linkage.',
      '2025 shutdown removed diary days September 30–November 11; quarter weights do not restore unobserved seasonal behavior.']}

def simulate(seed=20261015,reps=10000):
    import numpy as np
    from scipy.stats import t
    if reps<100:raise ValueError('At least 100 replicates required')
    rng=np.random.default_rng(seed);rows=[]
    for pairs in (16,24,32):
      for paired_sd in (30.,45.,60.):
        null=rng.normal(0,paired_sd,(reps,pairs));alt=rng.normal(-20,paired_sd,(reps,pairs));cut=t.ppf(.975,pairs-1)
        def reject(x):return abs(x.mean(1)/(x.std(1,ddof=1)/np.sqrt(pairs)))>cut
        power=float(reject(alt).mean());false=float(reject(null).mean())
        rows.append({'pairs':pairs,'schools':pairs*2,'assumed_sd_of_pair_differences_minutes':paired_sd,'assumed_effect_minutes':-20.,'power':power,'power_mcse':math.sqrt(power*(1-power)/reps),'null_rejection_rate':false,'approximate_mde_minutes':float((t.ppf(.975,pairs-1)+.8416212335729143)*paired_sd/math.sqrt(pairs))})
    return {'analysis':'Executed artificial-data design sensitivity; no actual teacher outcomes','seed':seed,'replicates_per_scenario':reps,'two_sided_alpha':.05,'software':{'numpy':np.__version__,'scipy':__import__('scipy').__version__},'assumptions':['Independent normally distributed school-pair differences','Complete follow-up, equal school weighting and fixed 20-minute weekday reduction','No missingness, interference, unequal cohort penalty or quality-guardrail power included','SD values are planning assumptions, not estimated from ATUS or local teacher records'],'scenarios':rows}

if __name__=='__main__':
    p=argparse.ArgumentParser();s=p.add_subparsers(dest='command',required=True)
    a=s.add_parser('audit');a.add_argument('--respondent',required=True);a.add_argument('--activity',required=True);a.add_argument('--output',required=True)
    a=s.add_parser('simulate');a.add_argument('--seed',type=int,default=20261015);a.add_argument('--replicates',type=int,default=10000);a.add_argument('--output',required=True)
    args=p.parse_args();result=audit(args.respondent,args.activity) if args.command=='audit' else simulate(args.seed,args.replicates)
    pathlib.Path(args.output).write_text(json.dumps(result,indent=2)+'\n')
