#!/usr/bin/env python3
"""Independently audit released withdrawal calendars with exact rational arithmetic."""
from fractions import Fraction as F
from datetime import date,timedelta
from calendar import monthrange
from pathlib import Path
from decimal import Decimal
import csv,json,sys,types
ROOT=Path(sys.argv[1]) if len(sys.argv)>1 else Path(__file__).resolve().parent
checks=0
def check(condition,label):
    global checks;checks+=1
    if not condition:raise AssertionError(label)
def rows(name):
    with (ROOT/name).open(encoding='utf-8',newline='') as f:return list(csv.DictReader(f))
def number(value):return F(str(value))
def equal(actual,expected,label):check(number(actual)==expected,label)
def group(items):
    out={}
    for row in items:out.setdefault(row['scenario'],[]).append(row)
    return out
I=json.loads((ROOT/'inputs.json').read_text(encoding='utf-8'))
S=json.loads((ROOT/'results.json').read_text(encoding='utf-8'))['scenarios']
D=group(rows('daily-ledger.csv'));E=group(rows('events.csv'));C=rows('summary.csv')
check(len(I['scenarios'])==27,'Declared scenario count');check(len(S)==len(C)==27,'One summary per scenario')
start=date.fromisoformat(I['start']);end=date.fromisoformat(I['end_exclusive']);window=(end-start).days
check(window==87,'Declared 87-day window');ATOM=F(1,100000000)
byid={r['scenario']:r for r in S};check(len(byid)==27,'Unique result IDs')
purchase_dates=[start+timedelta(days=n*I['purchase_interval_days']) for n in range(len(I['prices']))]
price_by_date=dict(zip(purchase_dates,map(number,I['prices'])))
for conf in I['scenarios']:
    sid=conf['id'];summary=byid[sid];dayrows=D[sid];events=E[sid]
    check(len(dayrows)==window,'One row per measured day: '+sid)
    check([r['date'] for r in dayrows]==[(start+timedelta(days=n)).isoformat() for n in range(window)],'Contiguous dates: '+sid)
    actions={}
    for e in events:actions.setdefault(e['date'],[]).append(e)
    balance=wallet=fees=acquired=spent=remainder=coindays=peak=F(0);success=blocked=attempts=0
    fee=number(conf['fee_ltc']);minimum=number(conf['minimum_ltc']);threshold=number(I['withdrawal_threshold_ltc'])
    def withdrawal(event,why):
        nonlocal_dummy=None
        global checks
        check(event['event']=='withdrawal' and event['reason']==why,'Withdrawal trigger label: '+sid)
        equal(event['custody_before_ltc'],balance,'Withdrawal before: '+sid)
        net=balance-fee;eligible=net>0 and (balance if conf['minimum_basis']=='gross' else net)>=minimum
        expected_status='success' if eligible else 'empty' if balance==0 else 'blocked'
        check(event['status']==expected_status,'Eligibility independently reconstructed: '+sid)
        equal(event['fee_ltc'],fee if eligible else F(0),'Only a successful withdrawal pays: '+sid)
        equal(event['delivered_ltc'],net if eligible else F(0),'Net receipt: '+sid)
        equal(event['custody_after_ltc'],F(0) if eligible else balance,'Withdrawal after: '+sid)
        return eligible,net
    for n,r in enumerate(dayrows):
        day=start+timedelta(days=n);actual=actions.pop(day.isoformat(),[]);price=price_by_date.get(day);pos=0;bought=F(0)
        if price is not None:
            budget=number(I['purchase_cash']);units=(budget/price)/ATOM;bought=F(units.numerator//units.denominator)*ATOM
            event=actual[pos];pos+=1
            check(event['event']=='purchase','Purchase order: '+sid);equal(event['price_usd'],price,'Execution price: '+sid)
            equal(event['custody_before_ltc'],balance,'Purchase before: '+sid);equal(event['bought_ltc'],bought,'Whole-litoshi purchase: '+sid)
            equal(event['cash_remainder_usd'],budget-bought*price,'Unused cash: '+sid)
            balance+=bought;acquired+=bought;spent+=budget;remainder+=budget-bought*price;peak=max(peak,balance)
            equal(event['custody_after_ltc'],balance,'Purchase after: '+sid)
        due=conf['rule']=='weekly' and price is not None or conf['rule']=='monthly' and day.day==monthrange(day.year,day.month)[1] or conf['rule']=='threshold' and price is not None and balance>=threshold
        if due:
            event=actual[pos];pos+=1;attempts+=1;eligible,net=withdrawal(event,conf['rule'])
            if eligible:wallet+=net;fees+=fee;balance=F(0);success+=1
            elif balance>0:blocked+=1
        check(pos==len(actual),'No unscheduled extra event: '+sid)
        coindays+=balance
        equal(r['bought_ltc'],bought,'Daily purchase: '+sid);equal(r['custody_closing_ltc'],balance,'Daily custody: '+sid)
        equal(r['wallet_cumulative_ltc'],wallet,'Daily wallet: '+sid);equal(r['fee_cumulative_ltc'],fees,'Daily fees: '+sid)
        equal(r['acquired_cumulative_ltc'],acquired,'Daily acquisition: '+sid);equal(r['custody_ltc_days_cumulative'],coindays,'Daily custody-time integral: '+sid)
        check(acquired==wallet+fees+balance,'Daily LTC conservation: '+sid)
        check(all((v/ATOM).denominator==1 for v in [balance,wallet,fees,acquired]),'Whole base units: '+sid)
    before_terminal=balance;terminal_status='not-requested'
    terminal=actions.pop(end.isoformat(),[])
    check(len(terminal)==int(conf['terminal_sweep']),'Explicit terminal policy: '+sid)
    if terminal:
        attempts+=1;eligible,net=withdrawal(terminal[0],'terminal-sweep');terminal_status=terminal[0]['status']
        if eligible:wallet+=net;fees+=fee;balance=F(0);success+=1
        elif balance>0:blocked+=1
    check(not actions,'No events outside declared window: '+sid)
    for field,expected in {'cash_contributed_usd':spent,'unused_cash_usd':remainder,'gross_acquired_ltc':acquired,'total_fee_ltc':fees,'wallet_received_ltc':wallet,'custody_before_terminal_ltc':before_terminal,'custody_final_ltc':balance,'total_retained_ltc':wallet+balance,'custody_ltc_days':coindays,'peak_pre_withdrawal_ltc':peak,'horizon_marked_value_usd':(wallet+balance)*number(I['price_at_horizon'])+remainder}.items():equal(summary[field],expected,'Summary '+field+': '+sid)
    for field,expected in {'successful_withdrawals':success,'blocked_nonempty_attempts':blocked,'withdrawal_attempts':attempts,'window_days':window}.items():check(summary[field]==expected,'Summary count '+field+': '+sid)
    check(summary['terminal_status']==terminal_status,'Terminal status: '+sid)
    check(abs(number(summary['mean_custodial_ltc'])-coindays/window)<=F(1,2*10**12),'Rounded mean custody: '+sid)
    equal(summary['gross_acquired_ltc'],F(581,200),'Same acquisitions in every schedule: '+sid)
    check(fees==success*fee,'Fee counter: '+sid)
    csvrow=next(r for r in C if r['scenario']==sid)
    for field,value in summary.items():check(csvrow[field]==str(value),'CSV/JSON agreement '+field+': '+sid)
# Hand-worked anchors are independent of both event parsers and model implementation.
for sid,expected in {'m10-gross-sweep-weekly':('2.84000000','0','13'),'m10-gross-sweep-monthly':('2.89000000','45.5575','3'),'m10-gross-sweep-threshold':('2.88500000','26.8175','4')}.items():
    s=byid[sid];equal(s['wallet_received_ltc'],number(expected[0]),'Hand-worked wallet '+sid);equal(s['custody_ltc_days'],number(expected[1]),'Hand-worked coin-days '+sid);check(s['successful_withdrawals']==int(expected[2]),'Hand-worked count '+sid)
# A visible wallet difference is not all wealth: 0.16 LTC is merely still with the custodian.
a=byid['m20-gross-sweep-weekly'];b=byid['m20-net-sweep-weekly']
equal(number(b['wallet_received_ltc'])-number(a['wallet_received_ltc']),F(33,200),'Wallet location gap 0.165')
equal(number(b['total_retained_ltc'])-number(a['total_retained_ltc']),F(1,200),'Retained coin gap 0.005')
equal(a['custody_final_ltc'],F(4,25),'Unwithdrawable tail 0.16')
check(a['terminal_status']=='blocked','Terminal sweep cannot erase minimum failure')
check(number(byid['m10-gross-sweep-weekly']['peak_pre_withdrawal_ltc'])>0,'Zero closing coin-days does not imply no peak balance')
# Fee-ratio denominators are checked separately from the calendar model.
f=F(1,200);ratio=F(1,100)
equal(f/F(1,2),ratio,'Fee over gross debit is 1 percent')
equal(f/(F(1,2)-f),F(1,99),'Fee over received coins uses a different denominator')
equal(f/ratio,F(1,2),'Gross-debit target threshold')
equal(f*(1+1/ratio),F(101,200),'Recipient-denominator target threshold')
equal(f/(F(101,200)-f),ratio,'Recipient ratio at its correct threshold')
# Boundary cases exercise numeric guards and rounding separately from the released calendar.
module=types.ModuleType('withdrawal_decimal_model');exec(compile((ROOT/'analyze.py.txt').read_text(encoding='utf-8'),'analyze.py.txt','exec'),module.__dict__)
conf=dict(I['scenarios'][0])
for bad in [{'fee_ltc':'-0.00000001'},{'minimum_ltc':'0.000000001'},{'minimum_basis':'unknown'},{'rule':'unknown'}]:
    try:module.model(I,{**conf,**bad})
    except ValueError:check(True,'Invalid policy rejected')
    else:check(False,'Invalid policy accepted')
small={**I,'start':'2027-01-04','end_exclusive':'2027-01-05','prices':['3'],'purchase_cash':'1','price_at_horizon':'3'}
s,_,_=module.model(small,{**conf,'minimum_ltc':'0','fee_ltc':'0','terminal_sweep':True})
equal(s['gross_acquired_ltc'],F(33333333,100000000),'Floor recurring acquisition to base unit')
equal(s['unused_cash_usd'],F(1,100000000),'Separate unused cash remainder')
print(json.dumps({'state':'passed','checks':checks,'scenarios':len(S),'daily_rows':sum(map(len,D.values())),'events':sum(map(len,E.values())),'independent_arithmetic':'fractions.Fraction','network_requests':0}))
