#!/usr/bin/env python3
"""Reproduce a fictional withdrawal calendar. Standard library only; no network."""
from __future__ import annotations
from datetime import date, timedelta
from decimal import Decimal, ROUND_DOWN, localcontext
from pathlib import Path
import argparse, csv, io, json
D=Decimal
ATOM=D('0.00000001')
ZERO=D('0')
def coins(value): return format(value.quantize(ATOM),'f')
def cash(value): return format(value.quantize(D('0.00000001')),'f')
def csv_bytes(rows):
    out=io.StringIO(newline=''); writer=csv.DictWriter(out,fieldnames=list(rows[0]),lineterminator='\n')
    writer.writeheader();writer.writerows(rows);return out.getvalue().encode('utf-8')
def model(inputs, scenario):
    start=date.fromisoformat(inputs['start']);end=date.fromisoformat(inputs['end_exclusive'])
    if end<=start:raise ValueError('Positive study window required')
    fee=D(scenario['fee_ltc']);minimum=D(scenario['minimum_ltc']);threshold=D(inputs['withdrawal_threshold_ltc'])
    if min(fee,minimum,threshold)<0 or fee%ATOM or minimum%ATOM:raise ValueError('Invalid fee/minimum')
    if scenario['minimum_basis'] not in ['gross','net'] or scenario['rule'] not in ['weekly','monthly','threshold']:raise ValueError('Invalid policy')
    prices=[D(p) for p in inputs['prices']]
    if not prices or any(p<=0 for p in prices):raise ValueError('Positive execution prices required')
    interval=int(inputs['purchase_interval_days']);budget=D(inputs['purchase_cash'])
    if interval<1 or budget<=0:raise ValueError('Positive purchase interval/budget required')
    dates=[start+timedelta(days=i*interval) for i in range(len(prices))]
    if dates[-1]>=end:raise ValueError('Purchases must precede end')
    purchases=dict(zip(dates,prices));balance=wallet=fees=acquired=spent=remainder=days=peak=ZERO
    successes=blocked=attempts=0;daily=[];events=[]
    def withdraw(day,reason):
        nonlocal balance,wallet,fees,successes,blocked,attempts
        before=balance;attempts+=1;net=balance-fee
        eligible=net>0 and (balance if scenario['minimum_basis']=='gross' else net)>=minimum
        status='success' if eligible else 'empty' if balance==0 else 'blocked'
        paid=received=ZERO
        if eligible:
            paid=fee;received=net;fees+=paid;wallet+=received;balance=ZERO;successes+=1
        elif before>0:blocked+=1
        events.append({'scenario':scenario['id'],'date':day.isoformat(),'event':'withdrawal','reason':reason,'status':status,'price_usd':'','cash_budget_usd':'','bought_ltc':coins(ZERO),'cash_remainder_usd':'','custody_before_ltc':coins(before),'fee_ltc':coins(paid),'delivered_ltc':coins(received),'custody_after_ltc':coins(balance)})
        return status
    day=start
    while day<end:
        bought=ZERO;price=purchases.get(day)
        if price is not None:
            bought=(budget/price).quantize(ATOM,rounding=ROUND_DOWN);unused=budget-bought*price
            before=balance;balance+=bought;acquired+=bought;spent+=budget;remainder+=unused;peak=max(peak,balance)
            events.append({'scenario':scenario['id'],'date':day.isoformat(),'event':'purchase','reason':'fixed weekly purchase','status':'credited','price_usd':cash(price),'cash_budget_usd':cash(budget),'bought_ltc':coins(bought),'cash_remainder_usd':cash(unused),'custody_before_ltc':coins(before),'fee_ltc':coins(ZERO),'delivered_ltc':coins(ZERO),'custody_after_ltc':coins(balance)})
        due=scenario['rule']=='weekly' and price is not None or scenario['rule']=='monthly' and (day+timedelta(days=1)).month!=day.month or scenario['rule']=='threshold' and price is not None and balance>=threshold
        if due:withdraw(day,scenario['rule'])
        days+=balance
        assert acquired==wallet+fees+balance
        daily.append({'scenario':scenario['id'],'date':day.isoformat(),'bought_ltc':coins(bought),'custody_closing_ltc':coins(balance),'wallet_cumulative_ltc':coins(wallet),'fee_cumulative_ltc':coins(fees),'acquired_cumulative_ltc':coins(acquired),'custody_ltc_days_cumulative':coins(days)})
        day+=timedelta(days=1)
    before_terminal=balance;terminal_status='not-requested'
    if scenario['terminal_sweep']:terminal_status=withdraw(end,'terminal-sweep')
    assert acquired==wallet+fees+balance
    horizon=D(inputs['price_at_horizon']);window=(end-start).days
    result={'scenario':scenario['id'],'group':scenario['group'],'rule':scenario['rule'],'minimum_basis':scenario['minimum_basis'],'minimum_ltc':coins(minimum),'fee_each_ltc':coins(fee),'terminal_sweep':scenario['terminal_sweep'],'cash_contributed_usd':cash(spent),'unused_cash_usd':cash(remainder),'gross_acquired_ltc':coins(acquired),'withdrawal_attempts':attempts,'successful_withdrawals':successes,'blocked_nonempty_attempts':blocked,'total_fee_ltc':coins(fees),'wallet_received_ltc':coins(wallet),'custody_before_terminal_ltc':coins(before_terminal),'custody_final_ltc':coins(balance),'total_retained_ltc':coins(wallet+balance),'custody_ltc_days':coins(days),'mean_custodial_ltc':format(days/D(window),'.12f'),'peak_pre_withdrawal_ltc':coins(peak),'terminal_status':terminal_status,'horizon_marked_value_usd':cash((wallet+balance)*horizon+remainder),'window_days':window}
    return result,daily,events

def outputs(inputs):
    summaries=[];daily=[];events=[]
    with localcontext() as ctx:
        ctx.prec=40
        for scenario in inputs['scenarios']:
            summary,days,actions=model(inputs,scenario);summaries.append(summary);daily.extend(days);events.extend(actions)
    result={'format':inputs['format'],'checked_on':inputs['checked_on'],'scenarios':summaries}
    return {'summary.csv':csv_bytes(summaries),'daily-ledger.csv':csv_bytes(daily),'events.csv':csv_bytes(events),'results.json':(json.dumps(result,indent=2,ensure_ascii=False)+'\n').encode('utf-8')}
def main():
    parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('folder',nargs='?',default=str(Path(__file__).resolve().parent));parser.add_argument('--write',action='store_true',help='Write deterministic CSV/JSON; default checks existing released outputs')
    args=parser.parse_args();folder=Path(args.folder);inputs=json.loads((folder/'inputs.json').read_text(encoding='utf-8'))
    for name,data in outputs(inputs).items():
        target=folder/name
        if args.write:target.write_bytes(data)
        elif target.read_bytes()!=data:raise SystemExit('Reproduction mismatch: '+name)
    print(json.dumps({'state':'written' if args.write else 'reproduced','scenarios':len(inputs['scenarios']),'daily_rows':len(inputs['scenarios'])*(date.fromisoformat(inputs['end_exclusive'])-date.fromisoformat(inputs['start'])).days,'network_requests':0}))
if __name__=='__main__':main()
