"""Reproduce the correction layer. Python 3 standard library only.
Usage: python apply_audit.py INPUT_DIRECTORY OUTPUT_DIRECTORY
INPUT_DIRECTORY contains anage_data.txt and both user-provided ZIP archives.
Raw inputs are never modified. Output is an audit inventory, not a final sample.
"""
import csv, hashlib, io, json, math, re, sys, zipfile
from pathlib import Path

src, out = map(Path, sys.argv[1:3]); out.mkdir(parents=True, exist_ok=True)
def sha(b): return hashlib.sha256(b).hexdigest()
def rows(b, delimiter=','): return list(csv.DictReader(io.StringIO(b.decode('utf-8-sig')), delimiter=delimiter))
def write(name, data):
    with (out/name).open('w', newline='') as f:
        w=csv.DictWriter(f,fieldnames=list(data[0]));w.writeheader();w.writerows(data)
z=zipfile.ZipFile(src/'30556475.zip')
b=z.read('Supplementary files/Haarlem_et_al_cff_dataset_21_10_2025.csv')
assert sha(b)=='bc9bfd6f923c4e58e69e7aafefe02d1793d5a3a62c7d673d835747205afc1f26'
raw=rows(b); a=(src/'anage_data.txt').read_bytes()
assert sha(a)=='98867969fbd4d0bed6bab415c2715bb19079dbd7f92bdc26e5961856aa1c1519'
anage=rows(a,'\t'); assert len(anage)==4645 and len(raw)==280
cz=zipfile.ZipFile(src/'AnAge_Haarlem_2026_Final_Crosswalk_Audit_2026-10-23.zip')
cross=rows(cz.read('237_taxa_anage_crosswalk.csv')); lookup={r['cff_taxon']:r for r in cross}
bird='https://doi.org/10.1371/journal.pone.0151099'
decisions=[]
for i,r in enumerate(raw,1):
    r.update(record_id=f'H2025_{i:03d}', CFF_original=r['CFF'], CFF_corrected=r['CFF'], endpoint_status='not_source_verified', audit_action='retain_pending_source_check', decision_source='')
    sp=r['Species_phylo']
    if sp in ['Ficedula_albicollis','Ficedula_hypoleuca']:
        r.update(CFF_corrected='128.1' if sp=='Ficedula_albicollis' else '138.2',endpoint_status='verified_species_mean_peak_behavioural_CFF',audit_action='correct_species_value_transposition',decision_source=bird)
    elif sp=='Melanophila_acuminata':
        r.update(endpoint_status='nonvisual_infrared_pit_organ_response',audit_action='exclude_visual_CFF',decision_source='https://pubmed.ncbi.nlm.nih.gov/11282323/')
    elif sp=='Ovis_aries' and 'Ezra-Elia' in r['CFF_reference']:
        r.update(endpoint_status='right_censored_CFF_greater_than_80_Hz',audit_action='exclude_exact_threshold_models',decision_source='https://pubmed.ncbi.nlm.nih.gov/25204753/')
    c=lookup[sp]
    r.update(anage_candidate=c['matched_anage_taxon'], longevity_years=c['longevity_years'], anage_quality=c['anage_quality'], original_crosswalk_risk=c['source_risk_flag'], final_analysis_eligible='not_finalized')
    if r['decision_source']:
        decisions.append({k:r[k] for k in ['record_id','Species_phylo','CFF_original','CFF_corrected','endpoint_status','audit_action','decision_source']})
write('measurement_audit.csv',raw);write('applied_decisions.csv',decisions)
for c in cross:
    rr=[r for r in raw if r['Species_phylo']==c['cff_taxon']]
    c['corrected_cff_values']=';'.join(r['CFF_corrected'] for r in rr)
    c['audit_actions']=';'.join(sorted(set(r['audit_action'] for r in rr)))
    c['final_analysis_eligible']='not_finalized'
write('crosswalk_with_corrections.csv',cross)

# Parse this archive's unquoted Newick syntax. Reject unknown grammar.
def parse_tree(s):
    tok=re.findall(r'[(),:;]|[^\s(),:;]+',s); pos=0
    def node():
        nonlocal pos
        kids=[];name=None;length=0.0
        if tok[pos]=='(':
            pos+=1;kids.append(node())
            while tok[pos]==',': pos+=1;kids.append(node())
            assert tok[pos]==')';pos+=1
        if tok[pos] not in [':',',',')',';']: name=tok[pos];pos+=1
        if tok[pos]==':': pos+=1;length=float(tok[pos]);pos+=1
        return name,length,kids
    root=node();assert tok[pos]==';' and pos==len(tok)-1
    return root
treebytes=z.read('Supplementary files/Haarlem_et_al_tree.tre')
trees=[s.strip()+';' for s in treebytes.decode().split(';') if s.strip()]
taxa=set(lookup); quality={r['cff_taxon'] for r in cross if r['longevity_years'] and r['anage_quality'] in ['acceptable','high']}
treechecks=[]
for i,s in enumerate(trees,1):
    root=parse_tree(s);tips=[];depths=[];lengths=[];polytomies=[]
    def walk(n,d,isroot=False):
        name,l,k=n
        if not isroot:lengths.append(l);d+=l
        if not k:tips.append(name);depths.append(d)
        if len(k)>2:polytomies.append(len(k))
        for c in k:walk(c,d)
    walk(root,0,True)
    treechecks.append(dict(tree_index=i,tips=len(tips),unique_tips=len(set(tips)),missing_CFF_labels=len(taxa-set(tips)),quality_candidate_overlap=len(quality&set(tips)),negative_edges=sum(v<0 for v in lengths),zero_edges=sum(v==0 for v in lengths),nonfinite_edges=sum(not math.isfinite(v) for v in lengths),polytomies=len(polytomies),min_root_tip=min(depths),max_root_tip=max(depths),root_tip_spread=max(depths)-min(depths)))
assert len(trees)==100 and all(r['tips']==237 and r['unique_tips']==237 and r['missing_CFF_labels']==0 for r in treechecks)
assert len(decisions)==4
assert sum(r['CFF_original']!=r['CFF_corrected'] for r in raw)==2
write('tree_checks.csv',treechecks)
summary=dict(date='2026-10-23',status='correction_layer_complete_final_sample_pending',anage_rows=len(anage),anage_sha256=sha(a),cff_rows=len(raw),cff_taxa=len(taxa),cff_sha256=sha(b),tree_sha256=sha(treebytes),trees=len(trees),tips_per_tree=237,quality_candidates_not_final=len(quality),applied_decisions=len(decisions),numeric_corrections=2,tree_check_ranges={k:[min(r[k] for r in treechecks),max(r[k] for r in treechecks)] for k in ['negative_edges','zero_edges','nonfinite_edges','polytomies','root_tip_spread']})
(out/'audit_summary.json').write_text(json.dumps(summary,indent=2)+'\n')
print(json.dumps(summary,indent=2))
