#!/usr/bin/env python3
"""Verify the frozen package offline without writing files or fetching data."""
import sys
sys.dont_write_bytecode = True
import csv
import hashlib
import io
import json
import re
import runpy
import zipfile
from collections import Counter
from decimal import Decimal, localcontext
from fractions import Fraction
from pathlib import Path

HERE = Path(__file__).resolve().parent
CHECKS = 0

def check(condition, message):
    global CHECKS
    CHECKS += 1
    if not condition:
        raise AssertionError(message)

def d(value):
    return Decimal(str(value))

def f(value):
    return Fraction(str(value))

def verify():
    inputs = json.loads((HERE / 'inputs.json').read_text(encoding='utf-8'))
    result = json.loads((HERE / 'results.json').read_text(encoding='utf-8'))
    model = runpy.run_path(str(HERE / 'reproduce.py.txt'), run_name='spending_model')
    for name, text in model['outputs'](inputs).items():
        check((HERE / name).read_bytes() == text.encode('utf-8'), 'Regenerated bytes differ: ' + name)
    check(result['scenario_count'] == 16, 'Scenario count')
    check(result['ledger_row_count'] == 154, 'Ledger row count')
    check(Counter(inputs['returns_pct']['fall_then_rebound']) == Counter(inputs['returns_pct']['rise_then_fall']), 'Same return multiset')
    summaries = {x['scenario_id']: x for x in result['summaries']}
    check(len(summaries) == 16, 'Unique scenario IDs')
    atom = Fraction(1, 100000000)
    for sid, summary in summaries.items():
        rows = [r for r in result['monthly_ledger'] if r['scenario_id'] == sid]
        check(len(rows) == summary['months'], 'Rows match months: ' + sid)
        rate, fixed = f(summary['proportional_fee']), f(summary['fixed_sale_fee_usd'])
        prior_ltc, prior_cash = f(summary['initial_ltc']), f(summary['initial_cash_usd'])
        price, stopped, first_shortfall = f(inputs['initial_price_usd']), False, None
        if sid.startswith('main_'):
            check(f(summary['initial_wealth_usd']) == 2000, 'Equal initial wealth')
            check(f(summary['terminal_price_usd']) == 100, 'Same terminal price')
        for month, row in enumerate(rows, 1):
            price *= 1 + f(row['return_pct']) / 100
            sold, gross = f(row['ltc_sold']), f(row['gross_sale_usd'])
            proportional, fixed_paid = f(row['proportional_fee_usd']), f(row['fixed_fee_usd'])
            credited, remainder = f(row['cash_credited_usd']), f(row['uncredited_rounding_remainder_usd'])
            due, paid, unpaid = f(row['bill_due_usd']), f(row['bill_paid_usd']), f(row['unpaid_usd'])
            closing_ltc, closing_cash = f(row['closing_ltc']), f(row['closing_cash_usd'])
            check(row['month'] == month and f(row['price_usd']) == price, 'Month/price progression')
            check(f(row['opening_ltc']) == prior_ltc and f(row['opening_cash_usd']) == prior_cash, 'Opening continuity')
            check(sold >= 0 and closing_ltc >= 0 and closing_cash >= 0, 'No borrowing or negative balance')
            check((sold / atom).denominator == 1 and (closing_ltc / atom).denominator == 1, 'Whole-litoshi amounts')
            check(closing_ltc == prior_ltc - sold, 'Coin conservation')
            check(prior_cash + credited - paid == closing_cash, 'Credited cash conservation')
            check(paid + unpaid == due, 'Every bill accounted for')
            check(gross == sold * price, 'Gross sale value')
            check(proportional == gross * rate, 'Exact proportional fee')
            check(fixed_paid == (fixed if sold else 0), 'Fixed fee only on actual sales')
            if sold:
                raw = gross - proportional - fixed_paid
                check(credited == Fraction((raw * 100).__floor__(), 100), 'Cash credit floored to cents')
                check(remainder == raw - credited and 0 <= remainder < Fraction(1, 100), 'Uncredited remainder accounted for')
                check(credited > 0, 'Do not sell for no positive cash credit')
                needed = due - min(prior_cash, due)
                required_atoms = ((needed + fixed) / (1 - rate) / price / atom).__ceil__()
                check(f(row['required_ltc']) == required_atoms * atom, 'Independent exact-rational minimum sale')
                if paid == due:
                    check(sold == required_atoms * atom, 'No excess coin sale beyond atom rounding')
                else:
                    check(sold == prior_ltc, 'Insufficient case uses available coins only')
            else:
                check(gross == credited == proportional == fixed_paid == remainder == 0, 'No phantom sale cash or fee')
            if stopped:
                check(sold == paid == 0 and unpaid == due, 'Later bills explicitly unpaid after stop')
            if unpaid:
                first_shortfall = first_shortfall or month
                stopped = True
            check(f(row['closing_marked_value_usd']) == closing_ltc * price + closing_cash, 'Marked value includes cash')
            prior_ltc, prior_cash = closing_ltc, closing_cash
        check(summary['first_shortfall_month'] == first_shortfall, 'First shortfall recorded')
        check(f(summary['terminal_ltc']) == prior_ltc and f(summary['terminal_cash_usd']) == prior_cash, 'Terminal balances')
        check(f(summary['terminal_marked_value_usd']) == prior_ltc * price + prior_cash, 'Terminal value')
        for output, field in [('total_bill_paid_usd', 'bill_paid_usd'), ('total_unpaid_usd', 'unpaid_usd'),
            ('total_ltc_sold', 'ltc_sold'), ('total_proportional_fee_usd', 'proportional_fee_usd'),
            ('total_fixed_fee_usd', 'fixed_fee_usd'), ('total_uncredited_rounding_remainder_usd', 'uncredited_rounding_remainder_usd')]:
            check(f(summary[output]) == sum((f(r[field]) for r in rows), Fraction()), 'Summary sum: ' + output)
    expected = {
      'main_no_costs_cash_0_fall_then_rebound': '7', 'main_no_costs_cash_0_rise_then_fall': '8.5',
      'main_no_costs_cash_200_fall_then_rebound': '8', 'main_no_costs_cash_200_rise_then_fall': '8',
      'main_one_percent_plus_one_dollar_cash_0_fall_then_rebound': '6.73737362',
      'main_one_percent_plus_one_dollar_cash_0_rise_then_fall': '8.26767665',
      'main_one_percent_plus_one_dollar_cash_200_fall_then_rebound': '7.79797970',
      'main_one_percent_plus_one_dollar_cash_200_rise_then_fall': '7.79797970',
      'small_fall_then_rebound': '0', 'small_rise_then_fall': '1.5'}
    for sid, balance in expected.items():
        check(f(summaries[sid]['terminal_ltc']) == f(balance), 'Hand/published balance: ' + sid)
    check(f(summaries['depletion_fall_then_rebound']['total_bill_paid_usd']) == 150, 'Partial payment stays partial')
    check(f(summaries['depletion_fall_then_rebound']['total_unpaid_usd']) == 150, 'Later unpaid bill not hidden')
    check(f(summaries['depletion_rise_then_fall']['total_bill_paid_usd']) == 300, 'Other path pays all three bills')
    base = dict(scenario_id='boundary', path_id='fictional', returns=['0', '0', '0'],
        initial_ltc='1', initial_cash='0', initial_price='100', bill='100', fee_rate='0', fixed_fee='0')
    z, rows = model['run_scenario'](**base)
    check(z['total_bill_paid_usd'] == 100 and z['total_unpaid_usd'] == 200, 'Depletion followed by unpaid bills')
    z, rows = model['run_scenario'](**{**base, 'bill': '0', 'fee_rate': '.01', 'fixed_fee': '1'})
    check(z['sale_count'] == 0 and z['terminal_ltc'] == 1, 'No bill means no sale or fee')
    z, rows = model['run_scenario'](**{**base, 'initial_ltc': '.00000001', 'initial_price': '1', 'fixed_fee': '1'})
    check(z['sale_count'] == 0 and z['terminal_ltc'] == d('.00000001'), 'Do not destroy coins for nonpositive net cash')
    z, rows = model['run_scenario'](**{**base, 'returns': ['0', '0'], 'initial_price': '2000000', 'bill': '.01'})
    check(z['sale_count'] == 1 and rows[0]['closing_cash_usd'] == d('.01') and rows[1]['ltc_sold'] == 0, 'Credited surplus carries to next bill')
    for patch in [{'initial_ltc': '.000000001'}, {'initial_ltc': '-1'}, {'initial_cash': '-1'},
                  {'fee_rate': '1'}, {'fee_rate': '-.1'}, {'fixed_fee': '-1'},
                  {'bill': '-1'}, {'initial_price': '0'}, {'returns': ['-100']}]:
        try:
            model['run_scenario'](**{**base, **patch})
        except ValueError:
            check(True, 'Reject invalid model input')
        else:
            check(False, 'Accepted invalid model input: ' + repr(patch))
    # Manifest integrity is an identity check, not a forecast validation.
    manifest = HERE / 'SHA256SUMS.txt'
    check(manifest.is_file(), 'Checksum manifest exists')
    seen = set()
    for line in manifest.read_text(encoding='utf-8').splitlines():
        match = re.fullmatch(r'([0-9a-f]{64})  ([A-Za-z0-9_.-]+)', line)
        check(bool(match), 'Safe manifest entry')
        digest, name = match.groups()
        check(name not in seen and name not in ['SHA256SUMS.txt', 'research-package.zip'], 'Unique nonrecursive checksum entry')
        seen.add(name)
        check((HERE / name).is_file() and hashlib.sha256((HERE / name).read_bytes()).hexdigest() == digest, 'SHA-256: ' + name)
    for name in ['inputs.json', 'results.json', 'summary.csv', 'monthly-ledger.csv', 'reproduce.py.txt', 'verify.py.txt', 'README.txt', 'bill-planning-checklist.txt', 'sources.json', 'same-returns-different-balances.svg', 'cash-buffer-tradeoffs.svg']:
        check(name in seen, 'Required file is covered by manifest: ' + name)
    archive = HERE / 'research-package.zip'
    if archive.is_file():
        with zipfile.ZipFile(archive) as bundle:
            info = bundle.infolist()
            check(len(info) == len(seen) + 1, 'ZIP has no duplicate or unexpected entries')
            check({item.filename for item in info} == seen | {'SHA256SUMS.txt'}, 'ZIP content matches public manifest')
            check(sum(item.file_size for item in info) <= 10000000, 'Bounded total ZIP data')
            for item in info:
                check(item.file_size <= 2000000, 'Bounded ZIP member')
                check(bundle.read(item.filename) == (HERE / item.filename).read_bytes(), 'ZIP bytes: ' + item.filename)
    print(f'PASS: {CHECKS} offline checks; 16 synthetic scenarios, 154 ledger rows. No network requests or files written.')

if __name__ == '__main__':
    with localcontext() as context:
        context.prec = 80
        verify()
