#!/usr/bin/env python3
"""Offline stdlib reproduction of exact ABS extracts and rank tables. No network."""
from html.parser import HTMLParser
from pathlib import Path
from decimal import Decimal
from hashlib import sha256
import csv,io,json,sys
CAPTION='Average loan sizes for owner occupier dwellings, original, by state'
STATES=['NSW','Vic','Qld','SA','WA','Tas']
class Tables(HTMLParser):
 def __init__(self):
  super().__init__();self.tables=[];self.table=None;self.row=None;self.cell=None;self.caption=False;self.pre=None;self.pres=[]
 def handle_starttag(self,tag,attrs):
  attrs=dict(attrs)
  if tag=='table':self.table={'caption':'','rows':[]}
  if self.table is not None:
   if tag=='caption':self.caption=True
   if tag=='tr':self.row=[]
   if tag in ['td','th']:self.cell=''
  if tag=='pre':self.pre={'class':attrs.get('class',''),'text':''}
 def handle_data(self,data):
  if self.caption and self.table is not None:self.table['caption']+=data
  if self.cell is not None:self.cell+=data
  if self.pre is not None:self.pre['text']+=data
 def handle_endtag(self,tag):
  if tag=='caption':self.caption=False
  if tag in ['td','th'] and self.cell is not None:
   self.row.append(self.cell.strip());self.cell=None
  if tag=='tr' and self.row is not None:
   self.table['rows'].append(self.row);self.row=None
  if tag=='table' and self.table is not None:self.tables.append(self.table);self.table=None
  if tag=='pre' and self.pre is not None:self.pres.append(self.pre);self.pre=None
def csvbytes(header,rows):
 s=io.StringIO(newline='');w=csv.writer(s,lineterminator='\n');w.writerow(header);w.writerows(rows);return s.getvalue().encode()
def reconstruct(root):
 capture=json.loads((root/'SOURCE_PROVENANCE.json').read_text())
 for src in capture['sources']:
  assert sha256((root/'raw'/src['file']).read_bytes()).hexdigest()==src['sha256'],'Source capture hash differs'
 parser=Tables();parser.feed((root/'raw/abs-lending-jun-quarter-2026.html').read_text())
 table=next(t for t in parser.tables if t['caption']==CAPTION)
 assert table['rows'][0]==['',"Australia ('000)","NSW ('000)","Vic ('000)","Qld ('000)","SA ('000)","WA ('000)","Tas ('000)","NT ('000)","ACT ('000)"]
 rows=table['rows'][1:];assert len(rows)==41
 ix=next(i for i,p in enumerate(parser.pres) if p['class']=='chart-caption' and p['text']==CAPTION)
 headers=json.loads(parser.pres[ix+1]['text']);embedded=json.loads(parser.pres[ix+2]['text'])
 assert headers==[' ','Australia','NSW','Vic','Qld','SA','WA','Tas','NT','ACT']
 assert [r[0] for r in rows]==embedded[0]
 for i,r in enumerate(rows):
  assert len(r)==10
  for j,v in enumerate(r[1:]):assert Decimal(v)==Decimal(str(embedded[j+1][i][0]))
 month={'Mar':'03','Jun':'06','Sep':'09','Dec':'12'}
 periods=[f"20{r[0][4:]}-{month[r[0][:3]]}" for r in rows]
 assert periods[0]=='2016-06' and periods[-1]=='2026-06'
 assert all((int(b[:4])*12+int(b[5:]))-(int(a[:4])*12+int(a[5:]))==3 for a,b in zip(periods,periods[1:]))
 geos=headers[1:];geo_index={s:geos.index(s)+1 for s in STATES}
 rank=lambda r,s:1+sum(Decimal(r[geo_index[x]])>Decimal(r[geo_index[s]]) for x in STATES)
 assert [rows[0][geo_index['Vic']],rows[-1][geo_index['Vic']]]==['416.0','664.0']
 assert [rank(rows[0],'Vic'),rank(rows[-1],'Vic')]==[2,5]
 assert {s:int(Decimal(rows[-1][geo_index[s]])) for s in STATES}=={'NSW':842,'Vic':664,'Qld':751,'SA':672,'WA':720,'Tas':516}
 all_rows=[[periods[i],g,rows[i][j+1]] for i in range(41) for j,g in enumerate(geos)]
 chart_rows=[[periods[i]]+[rows[i][geo_index[s]] for s in STATES] for i in range(41)]
 ranks=[[periods[i],s,rows[i][geo_index[s]],rank(rows[i],s)] for i in range(41) for s in STATES]
 endpoints=[[s,rows[0][geo_index[s]],rank(rows[0],s),rows[-1][geo_index[s]],rank(rows[-1],s)] for s in STATES]
 files={'source-extract.csv':csvbytes(['quarter','geography','average_loan_aud_thousands'],all_rows),'chart-data.csv':csvbytes(['quarter']+[s.lower()+'_aud_thousands' for s in STATES],chart_rows),'state-ranks.csv':csvbytes(['quarter','state','average_loan_aud_thousands','rank_among_six_states'],ranks),'endpoints.csv':csvbytes(['state','jun_2016_aud_thousands','jun_2016_rank','jun_2026_aud_thousands','jun_2026_rank'],endpoints)}
 return files,periods,rows,geo_index
def main():
 root=Path(__file__).resolve().parent
 checksums=root/'SHA256SUMS.txt'
 if checksums.exists():
  for line in checksums.read_text().splitlines():
   wanted,name=line.split('  ',1);p=Path(name);assert not p.is_absolute() and '..' not in p.parts
   assert sha256((root/p).read_bytes()).hexdigest()==wanted,'Frozen package asset differs: '+name
 files,*_=reconstruct(root);output=root/'reconstructed';output.mkdir(exist_ok=True)
 for name,raw in files.items():
  (output/name).write_bytes(raw)
  assert (root/name).read_bytes()==raw,name+' differs from exact reconstruction'
 print(json.dumps({'status':'PASS','csvFilesReconstructedExactly':list(files),'quarters':41,'allSourceValues':369,'chartedValues':246,'missingChartCells':0,'rankUniverse':'Six states; Australia, NT and ACT excluded','frozenPackageAssetChecksumsVerified':checksums.exists(),'pngRendering':'Frozen exports are hash-verified; not recreated by this stdlib script'}))
if __name__=='__main__':main()
