"""Offline reconstruction of frozen Core data. No RPC, requests or trading."""
from pathlib import Path
from decimal import Decimal,localcontext
import csv,hashlib,json,math,statistics,sys
D=Decimal
def proof(bits):
 n=int(bits,16);exp=n>>24;mantissa=n&0x7fffff
 assert not n&0x800000 and mantissa>0
 target=mantissa<<(8*(exp-3)) if exp>=3 else mantissa>>(8*(3-exp))
 assert 0<target<2**256
 return (2**256)//(target+1)
def estimate(headers,end,n):
 rows=headers[end-n:end+1];assert len(rows)==n+1
 times=[r['time'] for r in rows];span=max(times)-min(times);work=int(rows[-1]['chainwork'],16)-int(rows[0]['chainwork'],16)
 assert span>0 and work>0
 with localcontext() as ctx:
  ctx.prec=50;hps=D(work)/D(span)
 return {'window_blocks':n,'header_count':n+1,'start_height':rows[0]['height'],'end_height':rows[-1]['height'],'seconds_max_minus_min':span,'seconds_endpoint_difference':times[-1]-times[0],'work':str(work),'hps':str(hps),'phps':str(hps/D(10**15))}
def analyze(package,output=None):
 package=Path(package);meta=json.loads((package/'capture.json').read_text());headers=json.loads((package/'headers.json').read_text());blocks=json.loads((package/'blocks.json').read_text());checks=0
 def check(value):
  nonlocal checks
  assert value;checks+=1
 for name,digest in meta['input_sha256'].items():check(hashlib.sha256((package/name).read_bytes()).hexdigest()==digest)
 check(meta['chain']=='main');check(len(headers)==601);check(len(blocks)==144);check(headers[-1]['height']==meta['anchor_height']);check(headers[-1]['hash']==meta['anchor_hash'])
 for a,b in zip(headers,headers[1:]):
  check(b['height']==a['height']+1 and b['previousblockhash']==a['hash']);check(int(b['chainwork'],16)-int(a['chainwork'],16)==proof(b['bits']))
 rows=[]
 for b,h in zip(blocks,headers[-144:]):
  s=b['stats'];check((b['height'],b['hash'],b['time'])==(h['height'],h['hash'],h['time']));check(s['height']==b['height'] and s['blockhash']==b['hash'])
  subsidy=s['subsidy'];fees=s['totalfee'];claimed=sum(b['coinbase_output_litoshis']);available=subsidy+fees;mweb=sum(b['mweb_kernel_fee_litoshis'])
  ordinary=s['txs']-1-b['hogex_count'];base=fees-mweb
  check(subsidy==625000000);check(0<=claimed<=available);check(0<=mweb<=fees);check(b['hogex_count'] in [0,1]);check(ordinary>=0 and base>=0)
  rows.append({'height':b['height'],'hash':b['hash'],'header_time':b['time'],'subsidy_litoshis':subsidy,'fees_litoshis':fees,'available_litoshis':available,'claimed_litoshis':claimed,'unclaimed_litoshis':available-claimed,'mweb_kernel_fees_litoshis':mweb,'base_chain_fees_excluding_hogex_litoshis':base,'base_chain_entries_excluding_coinbase_hogex':ordinary,'rpc_txs':s['txs'],'hogex_count':b['hogex_count'],'mweb_kernels':len(b['mweb_kernel_fee_litoshis']),'rpc_maxfee_litoshis':s['maxfee'],'rpc_medianfee_litoshis':s['medianfee']})
 fees=sum(r['fees_litoshis'] for r in rows);subsidy=sum(r['subsidy_litoshis'] for r in rows);mweb=sum(r['mweb_kernel_fees_litoshis'] for r in rows);count=sum(r['base_chain_entries_excluding_coinbase_hogex'] for r in rows);base=fees-mweb
 ordered=sorted(rows,key=lambda r:(-r['fees_litoshis'],r['height']));high=ordered[0];med=D(str(statistics.median([r['fees_litoshis'] for r in rows])))
 fee_summary={'block_range':meta['fee_block_range'],'block_count':144,'header_time_first':rows[0]['header_time'],'header_time_last':rows[-1]['header_time'],'header_time_min':min(r['header_time'] for r in rows),'header_time_max':max(r['header_time'] for r in rows),'header_endpoint_span_seconds':rows[-1]['header_time']-rows[0]['header_time'],'subsidy_ltc':str(D(subsidy)/10**8),'fees_ltc':str(D(fees)/10**8),'available_reward_ltc':str(D(subsidy+fees)/10**8),'claimed_reward_ltc':str(D(sum(r['claimed_litoshis'] for r in rows))/10**8),'unclaimed_ltc':str(D(sum(r['unclaimed_litoshis'] for r in rows))/10**8),'full_claim_blocks':sum(r['unclaimed_litoshis']==0 for r in rows),'fee_share_of_available_pct':str(D(fees)*100/D(subsidy+fees)),'mweb_kernel_fees_ltc':str(D(mweb)/10**8),'mweb_fee_share_pct':str(D(mweb)*100/D(fees)) if fees else None,'mweb_kernel_count':sum(r['mweb_kernels'] for r in rows),'base_chain_entries_excluding_coinbase_hogex':count,'rpc_txs_including_coinbase_hogex':sum(r['rpc_txs'] for r in rows),'hogex_count':sum(r['hogex_count'] for r in rows),'base_chain_fees_excluding_hogex_ltc':str(D(base)/10**8),'mean_base_chain_fee_litoshis':str(D(base)/D(count)) if count else None,'mean_block_fee_litoshis':str(D(fees)/144),'median_block_fee_litoshis':str(med),'largest_fee_block':high,'largest_fee_share_pct':str(D(high['fees_litoshis'])*100/D(fees)) if fees else None,'top_10_block_fee_share_pct':str(D(sum(r['fees_litoshis'] for r in ordered[:10]))*100/D(fees)) if fees else None,'max_block_fee_vs_mean_multiple':str(D(high['fees_litoshis'])*144/D(fees)) if fees else None,'max_block_fee_vs_median_multiple':str(D(high['fees_litoshis'])/med) if med else None,'top_10_blocks':ordered[:10]}
 maximum=high['rpc_maxfee_litoshis'];check(high['mweb_kernel_fees_litoshis']==0 and high['fees_litoshis']<2*maximum);check(maximum==max(r['rpc_maxfee_litoshis'] for r in rows))
 fee_summary['largest_entry_fee_ltc']=str(D(maximum)/10**8)
 fee_summary['largest_entry_fee_share_of_block_pct']=str(D(maximum)*100/D(high['fees_litoshis']))
 fee_summary['largest_entry_fee_share_of_sample_pct']=str(D(maximum)*100/D(fees))
 fee_summary['base_chain_mean_excluding_single_largest_fee_litoshis']=str(D(base-maximum)/D(count-1))
 fee_summary['largest_block_base_chain_mean_litoshis']=str(D(high['fees_litoshis'])/D(high['base_chain_entries_excluding_coinbase_hogex']))
 fee_summary['largest_block_base_chain_mean_excluding_max_litoshis']=str(D(high['fees_litoshis']-maximum)/D(high['base_chain_entries_excluding_coinbase_hogex']-1))
 fee_summary['exclusion_scope']='Sensitivity: remove the one 0.2 LTC maximum-fee entry from the base-chain fee numerator and entry count, not an alternative blockchain. That block has no MWEB fees and total fees below 0.4 LTC, so only one entry can pay that maximum. Sender, intent and transaction ID were not reconstructed.'
 estimates=[estimate(headers,600,n) for n in [30,120,576]]
 for e in estimates:
  observed=D(str(meta['rpc_hashrate_estimates_hps'][str(e['window_blocks'])]));computed=D(e['hps']);check(abs(observed-computed)/computed<D('1e-12'));e['rpc_hps']=str(observed);e['relative_difference']=str(abs(observed-computed)/computed)
 rolling=[];distribution=[]
 for n in [30,120]:
  group=[estimate(headers,j,n) for j in range(n,601)];rolling.extend(group);values=sorted(D(r['phps']) for r in group)
  distribution.append({'window_blocks':n,'overlapping_estimates':len(group),'min_phps':str(values[0]),'max_phps':str(values[-1]),'median_phps':str(statistics.median(values)),'p05_phps':str(values[math.ceil(.05*len(values))-1]),'p95_phps':str(values[math.ceil(.95*len(values))-1]),'zero_or_negative_endpoint_spans':sum(r['seconds_endpoint_difference']<=0 for r in group),'span_differs_from_endpoint':sum(r['seconds_max_minus_min']!=r['seconds_endpoint_difference'] for r in group)})
 rates=[D(e['phps']) for e in estimates];deltas=[b['time']-a['time'] for a,b in zip(headers,headers[1:])]
 hash_summary={'anchor_height':meta['anchor_height'],'anchor_hash':meta['anchor_hash'],'estimates':estimates,'largest_vs_smallest_pct':str((max(rates)/min(rates)-1)*100),'rolling_distributions':distribution,'negative_adjacent_header_deltas':sum(x<0 for x in deltas),'zero_adjacent_header_deltas':sum(x==0 for x in deltas),'distinct_bits':sorted(set(h['bits'] for h in headers)),'endpoint_vs_extreme_times_differ_at_anchor':sum(e['seconds_max_minus_min']!=e['seconds_endpoint_difference'] for e in estimates),'scope':'Observed estimates share one boundary, and rolling windows overlap. Rates are expected proof work divided by miner-declared elapsed header time; they are not measured hardware, independent observations or a causal test of miner exits.'}
 check(sum(r['available_litoshis'] for r in rows)==subsidy+fees);check(sum(r['claimed_litoshis']+r['unclaimed_litoshis'] for r in rows)==subsidy+fees);check(sum(r['base_chain_fees_excluding_hogex_litoshis'] for r in rows)+mweb==fees)
 result={'release':'network-analyses-20261004-v1','capture':meta,'fee_analysis':fee_summary,'hashrate_analysis':hash_summary,'passed_assertions':checks,'offline_only':True}
 if output:
  output=Path(output);output.mkdir(parents=True,exist_ok=True)
  (output/'results.json').write_text(json.dumps(result,indent=2)+'\n')
  for name,data in [('fee-blocks.csv',rows),('hashrate-windows.csv',estimates),('rolling-hashrate.csv',rolling)]:
   with (output/name).open('w',newline='') as f:
    writer=csv.DictWriter(f,fieldnames=data[0].keys());writer.writeheader();writer.writerows(data)
 return result
if __name__=='__main__':
 result=analyze(sys.argv[1],sys.argv[2] if len(sys.argv)>2 else None)
 print(json.dumps(result,indent=2))
