"""Partner example: recover missed targets; persist locally, never execute trades.

XP_API_BASE=http://127.0.0.1:8080 XP_PARTNER_KEY=<assigned key>
python sample-consumer.py us-islamic state.json
python sample-consumer.py us-quality state.json --download-format ibkr-rebalance --output-file rebalance.csv
Formats: ibkr-rebalance, ibkr-basket (unsized review template), csv, xlsx.
The authenticated /api/v1/partner/files/csv.zip route downloads all entitled portfolios.
Its manifest identifies decisions, corrections and allocation basis. Platform import
verification and dedicated-account guidance: /guides/ibkr/ and /platforms.json.
For local review, use the approved localhost connection. After DNS/TLS activation,
set XP_API_BASE to https://www.xportfolios.com. Use one state file per portfolio.
"""
import argparse
import json
import os
from pathlib import Path
import tempfile
from urllib.request import Request, urlopen
from urllib.error import HTTPError
from urllib.parse import urlencode

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('slug')
parser.add_argument('state_file', type=Path)
parser.add_argument('--download-format', choices=['ibkr-rebalance', 'ibkr-basket', 'csv', 'xlsx'])
parser.add_argument('--output-file', type=Path)
args = parser.parse_args()
if bool(args.download_format) != bool(args.output_file):
    parser.error('--download-format and --output-file must be supplied together')
slug, path = args.slug, args.state_file
state = json.loads(path.read_text()) if path.exists() else {'decisions': [], 'cursor': None}
state.setdefault('pending_acknowledgements', [])
base = os.environ['XP_API_BASE'].rstrip('/') + '/api/v1/partner/portfolios/' + slug
headers = {'Authorization': 'Bearer ' + os.environ['XP_PARTNER_KEY']}
def save():
    temporary = path.with_suffix('.tmp')
    with temporary.open('w') as stream:
        json.dump(state, stream)
        stream.flush()
        os.fsync(stream.fileno())
    os.replace(temporary, path)
    descriptor = os.open(path.resolve().parent, os.O_RDONLY | os.O_DIRECTORY)
    try:
        os.fsync(descriptor)
    finally:
        os.close(descriptor)


def acknowledge_pending():
    for identifier in list(state['pending_acknowledgements']):
        with urlopen(Request(base + '/acknowledgements/' + identifier, headers=headers, method='POST'), timeout=30):
            pass
        state['pending_acknowledgements'].remove(identifier)
        save()


acknowledge_pending()
while True:
    query = {'limit': 100}
    if state['cursor']:
        query['after'] = state['cursor']
    try:
        with urlopen(Request(base + '/history?' + urlencode(query), headers=headers), timeout=30) as r:
            page = json.load(r)
    except HTTPError as error:
        if error.code == 409 and state['cursor']:
            state['cursor'] = None
            save()
            continue
        raise
    for decision in page['decisions']:
        identifier = decision['rebalance_id']
        replaced = decision.get('supersedes_rebalance_id')
        if replaced and not any(d['rebalance_id'] == replaced for d in state['decisions']):
            raise ValueError('Correction predecessor missing; recover the complete history first')
        if not any(d['rebalance_id'] == identifier for d in state['decisions']):
            state['decisions'].append(decision)
        state['superseded'] = {d['supersedes_rebalance_id']: d['rebalance_id']
                               for d in state['decisions'] if d.get('supersedes_rebalance_id')}
        state['active_decisions'] = [d['rebalance_id'] for d in state['decisions']
                                     if d['rebalance_id'] not in state['superseded']]
        state['cursor'] = identifier
        if identifier not in state['pending_acknowledgements']:
            state['pending_acknowledgements'].append(identifier)
        save()
        acknowledge_pending()
    if not page['has_more']:
        break
print('Saved', len(state['decisions']), 'decisions; no trades executed.')
if args.download_format:
    if not state['decisions']:
        raise ValueError('No saved targets are available for download')
    identifier = state['decisions'][-1]['rebalance_id']
    query = urlencode({'rebalance_id': identifier})
    with urlopen(Request(base + '/files/' + args.download_format + '?' + query,
                         headers=headers), timeout=30) as response:
        if response.headers.get('X-Rebalance-ID') != identifier:
            raise ValueError('Downloaded file does not match the recovered decision')
        if response.headers.get('X-Superseded-By'):
            raise ValueError('Decision was corrected; rerun history recovery before downloading')
        content = response.read()
    with tempfile.NamedTemporaryFile(dir=args.output_file.resolve().parent, delete=False) as stream:
        temporary = Path(stream.name)
        stream.write(content)
        stream.flush()
        os.fsync(stream.fileno())
    try:
        os.replace(temporary, args.output_file)
    finally:
        temporary.unlink(missing_ok=True)
    print('Downloaded', args.download_format, 'for decision', identifier,
          '— receipt only. Review allocation basis, exact instruments and proposed orders.')
