#!/usr/bin/env python3
"""Server-side example, standard library only. Normal credits/limits apply.
OSRM_API_KEY=... python3 delivery.py --addresses addresses.json > review.json
  addresses.json: ["Depot address", "Customer address", ...] (2–4 strings).
Review candidates and prepare confirmed.json, then:
OSRM_API_KEY=... python3 delivery.py --confirmed confirmed.json
  {"depot":[55.2708,25.2048],"stops":[[55.2796,25.1972]],"vehicles":1}
Coordinates: longitude,latitude. No time windows, capacity, truck or live traffic.
"""
import argparse
import json
import math
import os
import sys
import urllib.error
import urllib.parse
import urllib.request


def valid_point(p):
    return (isinstance(p, list) and len(p) == 2
            and all(type(x) in (int, float) and math.isfinite(x) for x in p)
            and abs(p[0]) <= 180 and abs(p[1]) <= 90)


def api(path, key, body=None):
    request = urllib.request.Request('https://osrmroute.com' + path,
        data=json.dumps(body).encode() if body is not None else None,
        headers={'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json',
                 'User-Agent': 'osrmroute-python/1.0'})
    try:
        with urllib.request.urlopen(request, timeout=35) as response:
            return json.load(response)
    except urllib.error.HTTPError as error:
        raise RuntimeError(f'HTTP {error.code}; request ID {error.headers.get("X-Request-Id", "unavailable")}. No automatic retry.') from None


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    mode = parser.add_mutually_exclusive_group(required=True)
    mode.add_argument('--addresses')
    mode.add_argument('--confirmed')
    args = parser.parse_args()
    key = os.environ.get('OSRM_API_KEY')
    if not key:
        raise ValueError('Set OSRM_API_KEY in the server environment.')
    with open(args.addresses or args.confirmed, encoding='utf-8') as handle:
        data = json.load(handle)
    if args.addresses:
        if not isinstance(data, list) or not 2 <= len(data) <= 4 or any(not isinstance(q, str) or not 2 <= len(q) <= 200 for q in data):
            raise ValueError('Use 2–4 address strings.')
        review = [{'address': q, 'candidates': api('/api/1/geocode?' + urllib.parse.urlencode({'q': q, 'limit': 3}), key).get('hits', []),
                   'requires_human_review': True} for q in data]
        print(json.dumps({'stage': 'review_coordinates', 'review': review}, indent=2, ensure_ascii=False))
        return
    if not isinstance(data, dict) or not valid_point(data.get('depot')) or not isinstance(data.get('stops'), list):
        raise ValueError('Use a confirmed depot and confirmed stops.')
    stops, vehicles = data['stops'], data.get('vehicles')
    if not 1 <= len(stops) <= 3 or not all(valid_point(p) for p in stops) or type(vehicles) is not int or not 1 <= vehicles <= 2:
        raise ValueError('Use 1–3 stops and 1–2 vehicles (subject to plan).')
    coords = ';'.join(','.join(str(x) for x in p) for p in [data['depot'], *stops])
    matrix = api('/api/v1/osrm/table/v1/driving/' + coords + '?annotations=duration,distance', key)
    if matrix.get('code') != 'Ok' or not matrix.get('durations') or any(x is None for row in matrix['durations'] for x in row):
        raise ValueError('Disconnected stops. Review coordinates before optimization.')
    result = api('/api/1/vrp', key, {
        'vehicles': [{'vehicle_id': f'vehicle-{i + 1}', 'start': data['depot']} for i in range(vehicles)],
        'services': [{'id': f'stop-{i + 1}', 'location': p} for i, p in enumerate(stops)]})
    if result.get('status') != 'finished' or not result.get('solution', {}).get('routes'):
        raise ValueError('No completed route solution.')
    print(json.dumps({'stage': 'vehicle_routes', 'matrix_seconds': matrix['durations'], 'result': result}, indent=2))


if __name__ == '__main__':
    try:
        main()
    except (ValueError, OSError, RuntimeError) as error:
        print(str(error), file=sys.stderr)
        sys.exit(1)
