"""Retrieve monthly matching event locations for the interactive map.

Set NOSIBLE_API_KEY. Place this script, build-oil-chokepoints.py, and the
monthly counts JSON in the same directory. Pages are checked against the
counts snapshot; a changed archive generation stops collection.
"""
import argparse
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
import importlib.util
import json
import math
import os
from pathlib import Path
import time
import urllib.error
import urllib.request


def contains(x, y, ring):
    inside = False
    for (ax, ay), (bx, by) in zip(ring, ring[1:]):
        if abs((bx-ax)*(y-ay)-(by-ay)*(x-ax)) <= 1e-10 and min(ax,bx)-1e-10 <= x <= max(ax,bx)+1e-10 and min(ay,by)-1e-10 <= y <= max(ay,by)+1e-10:
            return True
        if (ay > y) != (by > y) and x < (bx-ax)*(y-ay)/(by-ay)+ax:
            inside = not inside
    return inside


def points(event):
    def pair(item):
        x, y = item.get('lng'), item.get('lat')
        if isinstance(x, (int, float)) and isinstance(y, (int, float)) and math.isfinite(x) and math.isfinite(y) and -180 <= x <= 180 and -90 <= y <= 90:
            return (x, y)
    valid = {p for item in event.get('locations', []) if (p := pair(item))}
    if not valid:
        p = pair(event) or pair(event.get('coordinate', {}))
        if p: valid.add(p)
    return valid


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--counts', type=Path, default=Path('oil-chokepoints-monthly.json'))
    parser.add_argument('--output', type=Path, default=Path('oil-chokepoints-markers.json'))
    args = parser.parse_args()
    source = json.loads(args.counts.read_text())
    spec = importlib.util.spec_from_file_location('chokepoints', Path(__file__).parent / 'build-oil-chokepoints.py')
    builder = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(builder)
    geometry = {'type': 'MultiPolygon', 'coordinates': [p['geometry']['coordinates'] for p in source['polygons']]}
    key = os.environ['NOSIBLE_API_KEY']
    endpoint = os.environ.get('NOSIBLE_POLYGON_EVENTS_URL', source['endpoint'])
    rings = {p['id']: p['geometry']['coordinates'][0] for p in source['polygons']}
    result = {}

    def collect(month):
        cache = args.output.parent / ('markers-' + month['month'] + '.json')
        if cache.exists():
            cached = json.loads(cache.read_text())
            if cached['archiveBuild'] == source['archiveBuild']:
                return cached
        area_points = {a['id']: Counter() for a in builder.AREAS}
        polygon_counts = Counter()
        seen = set()
        cursor = None
        pages = 0
        while True:
            body = {'polygon': geometry, 'from': month['from'], 'to': month['to'], 'limit': 500, 'order': 'asc', 'view': 'lite'}
            if cursor: body['cursor'] = cursor
            for attempt in range(4):
                req = urllib.request.Request(endpoint, data=json.dumps(body).encode(), headers={'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json'})
                try:
                    with urllib.request.urlopen(req, timeout=90) as response: page = json.load(response)
                    break
                except urllib.error.HTTPError as error:
                    if error.code not in [429, 502, 503, 504] or attempt == 3:
                        raise RuntimeError(f'{month["month"]}: HTTP {error.code}') from None
                    time.sleep(min(15, 2**attempt))
            assert page['total'] == month['combinedUnique'], (month['month'], pages, page['total'], month['combinedUnique'], page['as_of'])
            assert page['as_of']['index_build'] == source['archiveBuild'], 'Archive generation changed'
            assert page.get('hydration_misses', 0) == 0
            for event in page['events']:
                identity = event['event_id']
                assert identity not in seen
                seen.add(identity)
                coords = points(event)
                hits = {pid for pid, ring in rings.items() if any(contains(x, y, ring) for x, y in coords)}
                assert hits, 'Returned event has no matching coordinate'
                polygon_counts.update(hits)
                for area in builder.AREAS:
                    if not any(p['id'] in hits for p in source['polygons'] if p['areaId'] == area['id']): continue
                    west, south, east, north = area['extent']
                    for x, y in coords:
                        if west <= x <= east and south <= y <= north:
                            inside = any(contains(x, y, rings[p['id']]) for p in source['polygons'] if p['areaId'] == area['id'])
                            area_points[area['id']][(x, y, inside)] += 1
            pages += 1
            cursor = page.get('next_cursor')
            if not cursor: break
        assert len(seen) == month['combinedUnique']
        assert dict(polygon_counts) == month['polygons'], (month['month'], dict(polygon_counts), month['polygons'])
        data = {'month': month['month'], 'archiveBuild': source['archiveBuild'], 'eventsChecked': len(seen), 'pages': pages,
                'areas': {aid: [[x, y, count, inside] for (x,y,inside), count in sorted(counter.items())] for aid, counter in area_points.items()}}
        cache.write_text(json.dumps(data, separators=(',', ':')) + '\n')
        return data

    with ThreadPoolExecutor(max_workers=2) as pool:
        for future in as_completed([pool.submit(collect, month) for month in source['months']]):
            data = future.result()
            result[data['month']] = data
            print(data['month'], 'checked', data['eventsChecked'], 'events in', data['pages'], 'pages', flush=True)
    output = {'schema': 'nosible_chokepoint_monthly_markers_v1', 'archiveBuild': source['archiveBuild'],
              'description': 'Grouped stored coordinates from events matching each chokepoint in each month. Each coordinate is counted once per event. All polygon totals independently checked by point-in-polygon scans of returned locations.',
              'months': {month: result[month] for month in sorted(result)}}
    args.output.write_text(json.dumps(output, separators=(',', ':')) + '\n')
    print('Saved', args.output, flush=True)


if __name__ == '__main__': main()
