#!/usr/bin/env python3
"""Offline synthetic model. Usage: python reproduce.py.txt --output NEW_DIRECTORY
Python standard library only. No network, wallet, trade or payment access.
"""
import argparse
import csv
import io
import json
from decimal import Decimal, localcontext, ROUND_CEILING, ROUND_FLOOR
from pathlib import Path

ATOM, CENT, ZERO = Decimal('0.00000001'), Decimal('0.01'), Decimal('0')

def s(value):
    return format(value, 'f') if isinstance(value, Decimal) else value

def atoms(value):
    amount = Decimal(value)
    if amount < 0 or amount % ATOM:
        raise ValueError('LTC balances must be nonnegative whole litoshi.')
    return amount

def cash_credit(amount, price, rate, fixed):
    gross = amount * price
    fee = gross * rate
    raw = gross - fee - fixed
    return gross, fee, raw, max(ZERO, raw.quantize(CENT, rounding=ROUND_FLOOR))

def run_scenario(*, scenario_id, path_id, returns, initial_ltc, initial_cash,
                 initial_price, bill, fee_rate, fixed_fee):
    with localcontext() as ctx:
        ctx.prec = 80
        ltc, cash, price = atoms(initial_ltc), Decimal(initial_cash), Decimal(initial_price)
        bill, fee_rate, fixed_fee = Decimal(bill), Decimal(fee_rate), Decimal(fixed_fee)
        if cash < 0 or price <= 0 or bill < 0 or not ZERO <= fee_rate < 1 or fixed_fee < 0:
            raise ValueError('Invalid cash, price, bill or fee assumptions.')
        start_ltc, start_cash = ltc, cash
        stopped, first_shortfall, ledger = False, None, []
        for month, percent in enumerate(returns, 1):
            change = Decimal(percent)
            if change <= -100:
                raise ValueError('Every modeled price must remain positive.')
            price *= 1 + change / 100
            before_ltc, before_cash = ltc, cash
            cash_to_bill = min(cash, bill) if not stopped else ZERO
            cash -= cash_to_bill
            due = bill - cash_to_bill
            sold = gross = percentage_fee = applied_fixed = credit = remainder = required = ZERO
            if due > 0 and not stopped:
                gross_needed = (due + fixed_fee) / (1 - fee_rate)
                required = (gross_needed / price).quantize(ATOM, rounding=ROUND_CEILING)
                candidate = min(required, ltc)
                if candidate > 0:
                    g, f, raw, credited = cash_credit(candidate, price, fee_rate, fixed_fee)
                    if credited > 0:
                        sold, gross, percentage_fee, applied_fixed = candidate, g, f, fixed_fee
                        credit, remainder = credited, raw - credited
                        ltc -= sold
                        cash += credit
            sale_to_bill = min(cash, due) if not stopped else ZERO
            cash -= sale_to_bill
            paid, shortfall = cash_to_bill + sale_to_bill, bill - cash_to_bill - sale_to_bill
            if shortfall > 0:
                first_shortfall = first_shortfall or month
                stopped = True
            ledger.append(dict(scenario_id=scenario_id, path_id=path_id, month=month,
                return_pct=change, price_usd=price, opening_ltc=before_ltc,
                opening_cash_usd=before_cash, bill_due_usd=bill,
                cash_used_before_sale_usd=cash_to_bill, required_ltc=required,
                ltc_sold=sold, gross_sale_usd=gross, proportional_fee_usd=percentage_fee,
                fixed_fee_usd=applied_fixed, cash_credited_usd=credit,
                uncredited_rounding_remainder_usd=remainder, bill_paid_usd=paid,
                unpaid_usd=shortfall, closing_ltc=ltc, closing_cash_usd=cash,
                closing_marked_value_usd=ltc * price + cash,
                status='shortfall_sales_stopped' if shortfall > 0 else 'paid'))
        total = lambda field: sum((r[field] for r in ledger), ZERO)
        summary = dict(scenario_id=scenario_id, path_id=path_id, months=len(returns),
            initial_ltc=start_ltc, initial_cash_usd=start_cash,
            initial_wealth_usd=start_ltc * Decimal(initial_price) + start_cash,
            monthly_bill_usd=bill, proportional_fee=fee_rate, fixed_sale_fee_usd=fixed_fee,
            terminal_price_usd=price, total_bill_due_usd=total('bill_due_usd'),
            total_bill_paid_usd=total('bill_paid_usd'), total_unpaid_usd=total('unpaid_usd'),
            total_ltc_sold=total('ltc_sold'), total_proportional_fee_usd=total('proportional_fee_usd'),
            total_fixed_fee_usd=total('fixed_fee_usd'),
            total_uncredited_rounding_remainder_usd=total('uncredited_rounding_remainder_usd'),
            sale_count=sum(r['ltc_sold'] > 0 for r in ledger), first_shortfall_month=first_shortfall,
            terminal_ltc=ltc, terminal_cash_usd=cash, terminal_marked_value_usd=ltc * price + cash)
        return summary, ledger

def build(inputs):
    summaries, ledgers = [], []
    price = Decimal(inputs['initial_price_usd'])
    for cost in inputs['cost_models']:
        for reserve in inputs['initial_cash_reserves_usd']:
            cash = Decimal(reserve)
            initial_ltc = (Decimal(inputs['initial_wealth_usd']) - cash) / price
            for path_id, returns in inputs['returns_pct'].items():
                name = f"main_{cost['id']}_cash_{reserve}_{path_id}"
                summary, ledger = run_scenario(scenario_id=name, path_id=path_id,
                    returns=returns, initial_ltc=initial_ltc, initial_cash=cash,
                    initial_price=price, bill=inputs['monthly_bill_usd'],
                    fee_rate=cost['proportional_fee'], fixed_fee=cost['fixed_sale_fee_usd'])
                summaries.append(summary); ledgers.extend(ledger)
    for kind, initial_ltc in [('small', inputs['small_case_initial_ltc']),
                              ('depletion', inputs['depletion_case_initial_ltc'])]:
        for path_id, returns in inputs['returns_pct'].items():
            summary, ledger = run_scenario(scenario_id=f'{kind}_{path_id}', path_id=path_id,
                returns=returns[:inputs[kind + '_case_months']], initial_ltc=initial_ltc, initial_cash='0',
                initial_price=price, bill=inputs['monthly_bill_usd'], fee_rate='0', fixed_fee='0')
            summaries.append(summary); ledgers.extend(ledger)
    return {'model_version': inputs['model_version'], 'evidence_type': inputs['evidence_type'],
            'scenario_count': len(summaries), 'ledger_row_count': len(ledgers),
            'summaries': summaries, 'monthly_ledger': ledgers}

def json_text(data):
    return json.dumps(data, default=s, ensure_ascii=False, indent=2) + '\n'

def csv_text(rows):
    out = io.StringIO(newline='')
    writer = csv.DictWriter(out, fieldnames=list(rows[0]), lineterminator='\n')
    writer.writeheader()
    writer.writerows({k: s(v) for k, v in row.items()} for row in rows)
    return out.getvalue()

def outputs(inputs):
    data = build(inputs)
    return {'results.json': json_text(data), 'summary.csv': csv_text(data['summaries']),
            'monthly-ledger.csv': csv_text(data['monthly_ledger'])}

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--output', type=Path, required=True,
                        help='Write derived results here; frozen inputs are not edited.')
    args = parser.parse_args()
    here = Path(__file__).resolve().parent
    if args.output.resolve() == here:
        parser.error('Choose an output directory different from the frozen package.')
    inputs = json.loads((here / 'inputs.json').read_text(encoding='utf-8'))
    args.output.mkdir(parents=True, exist_ok=True)
    derived = outputs(inputs)
    for name, text in derived.items():
        (args.output / name).write_text(text, encoding='utf-8', newline='\n')
    report = json.loads(derived['results.json'])
    print(f"Reproduced {report['scenario_count']} declared synthetic scenarios and {report['ledger_row_count']} monthly ledger rows.")
