#!/usr/bin/env python3
"""
Put a small, ridable fleet on the map around a given point.

The load generators answer "does it hold up"; this one answers "can I open the app and ride".
It walks the same three steps an operator does in the dashboard, so nothing here is a shortcut
around the real flow:

  1. register the vehicle          POST /api/v1/scooters
  2. sign it off into service      PUT  /api/v1/scooters/{id}/commission
  3. let the device report itself  MQTT beeb/{deviceId}/up

Step 3 is what actually places the vehicle: position lives in Redis and only ever arrives as
telemetry, so a registered vehicle that has never spoken is correctly invisible to riders.

    python3 seed_demo_fleet.py --lat 35.8434 --lng 10.5977 --count 6 --mqtt-port 51883

Clean up afterwards with --remove, which deletes exactly the vehicles this prefix created.
"""

import argparse
import json
import math
import random
import sys
import time
import urllib.error
import urllib.request

from mqtt_load import MqttClient

PROFILE = "generic-json-v1"


def api(method: str, url: str, body: dict | None = None) -> dict | None:
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(url, data=data, method=method,
                                 headers={"Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            raw = resp.read()
            return json.loads(raw) if raw else None
    except urllib.error.HTTPError as e:
        detail = e.read().decode(errors="replace")[:200]
        print(f"  {method} {url.rsplit('/', 1)[-1]} -> {e.code} {detail}")
        return None


def scatter(lat: float, lng: float, radius_m: float) -> tuple[float, float]:
    """A uniform point in the disc, not the square: corners would put scooters out of range."""
    angle = random.uniform(0, 2 * math.pi)
    distance = radius_m * math.sqrt(random.random())
    dlat = (distance * math.cos(angle)) / 111_320.0
    dlng = (distance * math.sin(angle)) / (111_320.0 * math.cos(math.radians(lat)))
    return lat + dlat, lng + dlng


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--fleet-url", default="http://localhost:8082")
    ap.add_argument("--mqtt-host", default="localhost")
    ap.add_argument("--mqtt-port", type=int, default=1883)
    ap.add_argument("--lat", type=float, required=True)
    ap.add_argument("--lng", type=float, required=True)
    ap.add_argument("--radius", type=float, default=400, help="metres from the centre point")
    ap.add_argument("--count", type=int, default=6)
    ap.add_argument("--prefix", default="DEMO")
    ap.add_argument("--vehicle-type", default="SCOOTER")
    ap.add_argument("--remove", action="store_true", help="delete this prefix's vehicles instead")
    ap.add_argument("--zone-url", default="http://localhost:8087")
    ap.add_argument("--with-zone", action="store_true",
                    help="also draw a GREEN operating zone around the point")
    ap.add_argument("--zone-km", type=float, default=3.0, help="operating zone side, km")
    args = ap.parse_args()

    scooters = f"{args.fleet_url}/api/v1/scooters"

    if args.remove:
        existing = api("GET", scooters) or []
        gone = 0
        for s in existing:
            if str(s.get("qrCode", "")).startswith(f"{args.prefix}-"):
                api("DELETE", f"{scooters}/{s['id']}")
                gone += 1
        print(f"removed {gone} vehicles with the {args.prefix}- prefix")
        return 0

    print(f"seeding {args.count} vehicles within {args.radius:.0f}m of "
          f"{args.lat:.5f},{args.lng:.5f}")

    if args.with_zone:
        # Without a green zone here, ending a ride is refused: once any green zone exists the
        # platform only allows ends inside one, and the seeded fleet would be unridable to
        # completion. Drawing the operating area is part of standing a location up.
        half_lat = (args.zone_km * 1000 / 2) / 111_320.0
        half_lng = (args.zone_km * 1000 / 2) / (111_320.0 * math.cos(math.radians(args.lat)))
        corners = [(args.lat - half_lat, args.lng - half_lng),
                   (args.lat - half_lat, args.lng + half_lng),
                   (args.lat + half_lat, args.lng + half_lng),
                   (args.lat + half_lat, args.lng - half_lng)]
        created = api("POST", f"{args.zone_url}/api/v1/zones", {
            "name": f"{args.prefix} operating area",
            "type": "GREEN",
            "polygon": [{"lat": round(la, 6), "lng": round(ln, 6)} for la, ln in corners],
        })
        print(f"  operating zone: {'created' if created else 'FAILED'} "
              f"({args.zone_km:g}km around the point)")

    placed = []
    for i in range(args.count):
        qr = f"{args.prefix}-{i + 1:03d}"
        device = f"{args.prefix}{i + 1:03d}"
        created = api("POST", scooters, {
            "qrCode": qr,
            "lockId": device,          # telemetry is matched on lockId (or IMEI)
            "vehicleType": args.vehicle_type,
            "iotProfile": PROFILE,
        })
        if not created:
            continue
        # Commissioning is the gate that lets a vehicle be offered to riders, so a seeded fleet
        # has to pass through it like any other.
        if api("PUT", f"{scooters}/{created['id']}/commission",
               {"commissionedBy": "demo-seed"}) is None:
            continue
        placed.append((device, qr))

    if not placed:
        print("nothing registered; is fleet-service up?")
        return 1

    client = MqttClient(args.mqtt_host, args.mqtt_port, f"seed-{int(time.time())}")
    for device, qr in placed:
        lat, lng = scatter(args.lat, args.lng, args.radius)
        client.publish(f"beeb/{device}/up", json.dumps({
            "battery": random.randint(45, 99),
            "signal": random.randint(3, 5),
            "lat": round(lat, 6),
            "lng": round(lng, 6),
            "lock": "LOCKED",
            "event": "telemetry",
        }).encode())
        print(f"  {qr:12} {lat:.5f},{lng:.5f}")
    client.close()

    # Telemetry crosses MQTT then Kafka, so give the hop a moment before reading it back.
    time.sleep(3)
    nearby = api("GET", f"{scooters}/nearby?lat={args.lat}&lng={args.lng}&radiusMeters=1500") or []
    print(f"nearby now returns {len(nearby)} vehicles")
    return 0 if nearby else 1


if __name__ == "__main__":
    sys.exit(main())
