from pathlib import Path
from fractions import Fraction
from functools import lru_cache
from collections import Counter
from datetime import datetime,timezone
import csv,json,hashlib,math,sys
import replay_math as own
own.raw_year=lru_cache(maxsize=None)(own.raw_year)
b=Path('work/storage-test');r=b/'results';o=Path('work/storage-independent-review');plan=json.loads((b/'plan-v2.json').read_text());conditions=json.loads((r/'year-scenario-results.json').read_text());lookup={(x['year'],x['scenario']):x for x in conditions};issues=[];checks=0

def check(ok,label):
 global checks
 checks+=1
 if not ok:issues.append(label)

def equal_table(jsondata,csvfile):
 rows=list(csv.DictReader(csvfile.open(newline='')));check(len(rows)==len(jsondata),'row count '+csvfile.name)
 for i,(js,cr) in enumerate(zip(jsondata,rows)):
  check(set(js)==set(cr),'columns '+csvfile.name+str(i))
  for k,v in js.items():
   expected='' if v is None else str(v)
   check(cr.get(k)==expected,'value '+csvfile.name+str(i)+k)
 return len(rows)
perms=json.loads((r/'permutation-results.json').read_text());equal_table(perms,r/'permutation-results.csv')
scalar=[]
for c in conditions:
 x={k:v for k,v in c.items() if not isinstance(v,(dict,list))};x.setdefault('two_sided_reference_rank_holm_descriptive',None);scalar.append(x)
equal_table(scalar,r/'year-scenario-results.csv');across=json.loads((r/'across-year-summaries.json').read_text());equal_table(across,r/'across-year-summaries.csv')
combined=json.loads((r/'results.json').read_text());check(combined['conditions']==conditions,'combined conditions JSON');check(combined['across_years']==across,'combined across JSON');check(combined['metadata']==json.loads((r/'run-metadata.json').read_text()),'combined metadata JSON')
expected_days={};expected_cumulative={};total_slots=0
for scenario in plan['scenarios']:
 for year in plan['data']['years']:
  key=(year,scenario['id']);d=own.build(year,scenario);c=lookup[key];D=int(c['normalization_denominator']);dd=d['denominator'];scale=Fraction(D,dd);check(scale.denominator==1,'integer residual scale '+str(key));factor=int(scale);cum=0;total_slots+=len(d['rows']);raw=own.raw_year(year);retained=set(d['dates']);excluded=sorted({x[0] for x in raw}-retained);check(c['excluded_dates']==excluded,'excluded dates '+str(key))
  clipped={} if scenario['negative_policy']=='reported_signed' else {'wind_values':sum(x[3]<0 for x in raw if x[0] in retained),'solar_values':sum(x[4]<0 for x in raw if x[0] in retained)}
  check(c['clipped_value_counts']==clipped,'clipped source counts '+str(key))
  scaled_values=[N*factor for day in d['dates'] for N in d['days'][day]]
  digest=hashlib.sha256(('\n'.join(map(str,sorted(scaled_values)))+'\n').encode()).hexdigest();check(c['residual_multiset_sha256']==digest,'residual multiset hash '+str(key))
  for day in d['dates']:
   vals=d['days'][day];acc=0;low=0;high=0
   for N in vals:acc+=N;low=min(low,acc);high=max(high,acc)
   expected_days[(year,scenario['id'],day)]={'month':int(day[5:7]),'slots':len(vals),'net':Fraction(acc,dd),'minimum':Fraction(low,dd),'maximum':Fraction(high,dd)}
   cum+=acc;expected_cumulative[(year,scenario['id'],day)]=Fraction(cum,2*dd)
rows=list(csv.DictReader((r/'day-summaries.csv').open(newline='')));seen=set()
for x in rows:
 key=(int(x['year']),x['scenario'],x['date']);check(key in expected_days and key not in seen,'day key '+str(key));seen.add(key)
 if key not in expected_days:continue
 ex=expected_days[key];den=int(x['residual_denominator']);check(int(x['month'])==ex['month'] and int(x['slots'])==ex['slots'],'day units '+str(key))
 for field,expected in [('net_numerator','net'),('min_prefix_numerator','minimum'),('max_prefix_numerator','maximum')]:check(Fraction(int(x[field]),den)==ex[expected],'day '+field+str(key))
check(seen==set(expected_days),'all day keys complete')
paths=list(csv.DictReader((r/'original-daily-cumulative.csv').open(newline='')));seen=set()
for x in paths:
 key=(int(x['year']),x['scenario'],x['date']);check(key in expected_cumulative and key not in seen,'trajectory key '+str(key));seen.add(key)
 if key in expected_cumulative:check(math.isclose(float(x['cumulative_nominal_MWh_equivalent']),float(expected_cumulative[key]),rel_tol=1e-12,abs_tol=1e-10),'trajectory value '+str(key))
check(seen==set(expected_cumulative),'all trajectory keys complete')
report={'review_type':'internal Codex supplementary output-table parity audit, not external peer review','checked_utc':datetime.now(timezone.utc).isoformat(),'verdict':'PASS' if not issues else 'FAIL','issues':issues,'comparison_count':checks,'day_summary_rows':len(rows),'cumulative_trajectory_rows':len(paths),'original_scenario_slots_checked':total_slots,'permutation_CSV_JSON_parity_rows':len(perms),'condition_CSV_JSON_parity_rows':len(scalar),'across_summary_CSV_JSON_parity_rows':len(across),'all_day_exact_summaries_and_cumulative_paths_match':not issues}
(o/'independent-table-audit-v2.json').write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(report,indent=2));sys.exit(bool(issues))
