#!/usr/bin/env python3
"""Receive all current public collections, including future compatible additions.

Set ELNINO_API_KEY on your backend, then run:
    python sync-partner-all.py mirror.json
Uses only Python's standard library. Never sends the key to a redirect target.
Keeps the previous file on failure. Replaces it only after a complete, stable sync.
The legacy eight-collection sync-partner.py remains available unchanged.
"""
import datetime
import json
import os
import sys
import tempfile
import urllib.error
import urllib.request

BASE = 'https://elnino.drr.sh'
CORE = {'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


def validate_snapshot(payload):
    if payload.get('complete') is not True or not isinstance(payload.get('snapshot'), str) or not payload['snapshot']:
        raise ValueError('Incomplete public snapshot; previous file retained.')
    inventories = payload.get('manifest')
    collections = payload.get('collections')
    if not isinstance(inventories, dict) or not isinstance(collections, dict):
        raise ValueError('Missing collection inventory; previous file retained.')
    if not CORE.issubset(inventories) or set(collections) != set(inventories):
        raise ValueError('Collection inventory mismatch; previous file retained.')
    for name, rows in collections.items():
        if not isinstance(rows, list) or not isinstance(inventories[name], list):
            raise ValueError('Invalid collection; previous file retained.')
        actual = {r['id']: r['revision'] for r in rows}
        expected = {r['id']: r['revision'] for r in inventories[name]}
        if len(actual) != len(rows) or len(expected) != len(inventories[name]) or actual != expected:
            raise ValueError('Incomplete or duplicate records; previous file retained.')
    return payload


def synchronize(target, key, opener=None):
    opener = opener or urllib.request.build_opener(NoRedirect)
    cached = None
    try:
        with open(target, encoding='utf-8') as source:
            cached = validate_snapshot(json.load(source))
    except (OSError, ValueError, KeyError, TypeError):
        pass  # Do not send a conditional request for an unusable local copy.
    headers = {'Authorization': 'Bearer ' + key, 'Accept': 'application/json'}
    if cached and isinstance(cached.get('_sync'), dict) and isinstance(cached['_sync'].get('etag'), str):
        headers['If-None-Match'] = cached['_sync']['etag']
    request = urllib.request.Request(BASE + '/api/v1/bulk?format=json', headers=headers)
    try:
        with opener.open(request, timeout=90) as response:
            payload = validate_snapshot(json.load(response))
            etag = response.headers.get('ETag')
    except urllib.error.HTTPError as error:
        if error.code == 304 and cached:
            return False
        raise
    request = urllib.request.Request(BASE + '/api/v1/manifest', headers={
        'Authorization': 'Bearer ' + key, 'Accept': 'application/json'})
    with opener.open(request, timeout=90) as response:
        final = json.load(response)
    final_inventory = {**final.get('collections', {}), **final.get('extensions', {})}
    if final.get('complete') is not True or final.get('snapshot') != payload['snapshot'] or final_inventory != payload['manifest']:
        raise ValueError('Public data changed during synchronization; retry. Previous file retained.')
    payload['_sync'] = {'etag': etag, 'retrieved_at_utc': datetime.datetime.now(datetime.timezone.utc).isoformat()}
    temporary = None
    try:
        with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', dir=os.path.dirname(os.path.abspath(target)), delete=False) as out:
            temporary = out.name
            json.dump(payload, out, ensure_ascii=False, indent=2)
            out.flush()
            os.fsync(out.fileno())
        os.replace(temporary, target)
    finally:
        if temporary and os.path.exists(temporary):
            os.unlink(temporary)
    return True


def main():
    key = os.environ.get('ELNINO_API_KEY')
    if not key:
        raise ValueError('Set ELNINO_API_KEY in your backend environment first.')
    target = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else 'elnino-mirror.json')
    changed = synchronize(target, key)
    print('Updated complete public mirror.' if changed else 'No change; previous mirror retained.')


if __name__ == '__main__':
    try:
        main()
    except urllib.error.HTTPError as exc:
        print('API returned HTTP', exc.code, '— previous file retained. For 429, wait',
              exc.headers.get('Retry-After', '60'), 'seconds before retrying.', file=sys.stderr)
        sys.exit(1)
    except Exception:
        # Do not print exception URLs, headers, payloads or credentials.
        print('Synchronization failed; previous file retained. Check the connection, key and API availability.', file=sys.stderr)
        sys.exit(1)
