# Python 3.10+, standard library only. GET only; this does not compute historical MRR.
import os, json, hashlib, pathlib, sys
from datetime import datetime, timezone
from urllib.request import Request, urlopen
from urllib.parse import urlencode
from urllib.error import HTTPError

BASE = 'https://api.stripe.com'
PATHS = {'/v1/account', '/v1/subscriptions', '/v1/subscription_items'}

def pages(fetch, path, params):
    cursor = None
    seen_ids = set()
    for _ in range(10000):
        args = {**params, 'limit': 100}
        if cursor is not None:
            args['starting_after'] = cursor
        page = fetch(path, args)
        data = page['data']
        if not isinstance(data, list) or type(page['has_more']) is not bool:
            raise ValueError('invalid list response')
        for obj in data:
            if obj['id'] in seen_ids:
                raise ValueError('duplicate id: extraction may have changed')
            seen_ids.add(obj['id'])
            yield obj
        if not page['has_more']:
            return
        if not data:
            raise ValueError('has_more=true but empty page')
        cursor = data[-1]['id']
    raise ValueError('too many pages')

def collect(fetch, expected_account, expected_live):
    account = fetch('/v1/account', {})
    if account['id'] != expected_account:
        raise ValueError('wrong Stripe account')
    subscriptions = list(pages(fetch, '/v1/subscriptions', {'status': 'all'}))
    for sub in subscriptions:
        if sub['livemode'] is not expected_live:
            raise ValueError('wrong environment')
        items = list(pages(fetch, '/v1/subscription_items', {'subscription': sub['id']}))
        for item in items:
            if item['subscription'] != sub['id']:
                raise ValueError('wrong parent subscription')
        sub['items'] = {'data': items, 'has_more': False, 'object': 'list'}
    return subscriptions

def live_fetch(key, version, request_log):
    def fetch(path, params):
        if path not in PATHS:
            raise ValueError('endpoint not allowed')
        query = urlencode(params)
        req = Request(BASE + path + ('?' + query if query else ''), method='GET',
                      headers={'Authorization': 'Bearer ' + key, 'Stripe-Version': version})
        try:
            with urlopen(req, timeout=30) as response:
                request_log.append({'path': path, 'params': params,
                                    'request_id': response.headers.get('Request-Id'),
                                    'stripe_version': response.headers.get('Stripe-Version')})
                return json.load(response)
        except HTTPError as error:
            raise RuntimeError('Stripe HTTP ' + str(error.code) +
                               '; request_id=' + str(error.headers.get('Request-Id'))) from None
    return fetch

def self_test():
    calls = []
    def fake(path, params):
        calls.append((path, dict(params)))
        if path == '/v1/account':
            return {'id': 'acct_fixture'}
        if path == '/v1/subscriptions':
            if 'starting_after' not in params:
                return {'data': [{'id': 'sub_a', 'livemode': False}], 'has_more': True}
            assert params['starting_after'] == 'sub_a'
            return {'data': [{'id': 'sub_b', 'livemode': False}], 'has_more': False}
        sub = params['subscription']
        suffix = '2' if 'starting_after' in params else '1'
        return {'data': [{'id': sub + '_si_' + suffix, 'subscription': sub}],
                'has_more': suffix == '1'}
    result = collect(fake, 'acct_fixture', False)
    assert len(result) == 2
    assert [len(x['items']['data']) for x in result] == [2, 2]
    assert len(calls) == 7
    assert all(p.get('status') == 'all' for path, p in calls if path == '/v1/subscriptions')
    failures = 0
    for response in [
        {'data': [], 'has_more': True},
        {'data': [{'id': 'same'}], 'has_more': True}]:
        try:
            list(pages(lambda *args: response, '/v1/subscriptions', {}))
        except ValueError:
            failures += 1
    assert failures == 2
    try:
        collect(fake, 'acct_wrong', False)
    except ValueError:
        failures += 1
    try:
        collect(fake, 'acct_fixture', True)
    except ValueError:
        failures += 1
    assert failures == 4
    print('mock PASS: 2 subscription pages, 4 item pages, account/mode and malformed-page checks')

if __name__ == '__main__':
    if sys.argv[1:] == ['--self-test']:
        self_test()
    elif len(sys.argv) == 2:
        os.umask(0o077)
        expected = os.environ['STRIPE_EXPECTED_ACCOUNT_ID']
        mode = os.environ['STRIPE_EXPECTED_MODE']
        if mode not in {'test', 'live'}:
            raise ValueError('STRIPE_EXPECTED_MODE must be test or live')
        version = os.environ['STRIPE_API_VERSION']
        key = os.environ['STRIPE_READ_KEY']
        started = datetime.now(timezone.utc).isoformat()
        log = []
        data = collect(live_fetch(key, version, log), expected, mode == 'live')
        out = pathlib.Path(sys.argv[1])
        out.mkdir(mode=0o700, parents=True, exist_ok=False)
        payload = json.dumps(data, ensure_ascii=False, indent=2).encode()
        (out / 'subscriptions.json').write_bytes(payload)
        manifest = dict(account_id=expected, mode=mode, api_version=version,
                        started_at=started, finished_at=datetime.now(timezone.utc).isoformat(),
                        subscription_count=len(data),
                        item_count=sum(len(s['items']['data']) for s in data),
                        sha256=hashlib.sha256(payload).hexdigest(), requests=log,
                        point_in_time_guaranteed=False)
        (out / 'manifest.json').write_text(json.dumps(manifest, indent=2))
        print(json.dumps({k: v for k, v in manifest.items() if k != 'requests'}))
    else:
        raise SystemExit('usage: python3 read_snapshot.py --self-test | NEW_OUTPUT_DIRECTORY')
