#!/usr/bin/env python3
"""Mirror the public partner API, including corrections and removals.
Set ELNINO_API_KEY in the process environment, then run:
    python sync-partner.py mirror.json
No dependencies. Keeps the old file on any failure; never logs the key.
"""
import json
import os
import sys
import tempfile
import urllib.error
import urllib.parse
import urllib.request

BASE = 'https://elnino.drr.sh'
COLLECTIONS = ('countries', 'analyses', 'evidence', 'actions', 'institutions', 'resources', 'media', 'changes')

class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None  # Never forward an API key to a redirect target.

def main():
    key = os.environ.get('ELNINO_API_KEY')
    if not key:
        raise ValueError('Set ELNINO_API_KEY in your environment first.')
    target = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else 'elnino-mirror.json')
    opener = urllib.request.build_opener(NoRedirect)
    def get(path):
        if not path.startswith('/api/v1/') or urllib.parse.urlsplit(path).netloc:
            raise ValueError('Unexpected API path; synchronization stopped.')
        req = urllib.request.Request(BASE + path, headers={'Authorization': 'Bearer ' + key, 'Accept': 'application/json'})
        with opener.open(req, timeout=60) as response:
            return json.load(response)
    manifest = get('/api/v1/manifest')
    if manifest.get('complete') is not True or set(manifest['collections']) != set(COLLECTIONS):
        raise ValueError('Incomplete or unexpected manifest; old file retained.')
    snapshot = manifest['snapshot']
    data = {}
    for collection in COLLECTIONS:
        path = '/api/v1/' + collection + '?' + urllib.parse.urlencode({'limit': 200, 'snapshot': snapshot})
        rows = []
        visited = set()
        while path:
            if path in visited:
                raise ValueError('Repeated pagination URL; old file retained.')
            visited.add(path)
            page = get(path)
            if page['snapshot'] != snapshot or page['collection'] != collection:
                raise ValueError('Snapshot changed; retry the complete sync.')
            rows.extend(page['records'])
            path = page['next']
        expected = {r['id']: r['revision'] for r in manifest['collections'][collection]}
        actual = {r['id']: r['revision'] for r in rows}
        if expected != actual or len(rows) != len(actual):
            raise ValueError('Incomplete collection; old file retained.')
        data[collection] = rows
    if get('/api/v1/manifest')['snapshot'] != snapshot:
        raise ValueError('Snapshot changed; retry the complete sync.')
    # Complete replacement removes records withdrawn since the last successful sync.
    tmp = None
    try:
        with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', dir=os.path.dirname(target), delete=False) as out:
            tmp = out.name
            json.dump({'meta': manifest['meta'], 'snapshot': snapshot, 'collections': data}, out, ensure_ascii=False, indent=2)
            out.flush()
            os.fsync(out.fileno())
        os.replace(tmp, target)
    finally:
        if tmp and os.path.exists(tmp):
            os.unlink(tmp)
    print('Synchronized', sum(map(len, data.values())), 'public records to', target)

if __name__ == '__main__':
    try:
        main()
    except urllib.error.HTTPError as exc:
        print('API returned HTTP', exc.code, '— old file retained. For 429, wait', exc.headers.get('Retry-After', '60'), 'seconds. For 409, restart sync.', file=sys.stderr)
        sys.exit(1)
    except Exception as exc:
        print(str(exc), file=sys.stderr)
        sys.exit(1)
