#!/usr/bin/env python3
"""Forward local readsb/dump1090 aircraft.json positions through SkyGrid's existing ingest API."""
import argparse
import datetime
import hashlib
import json
import math
import os
import re
import shutil
import stat
import subprocess
import sys
import time
from pathlib import Path
import urllib.error
import urllib.parse
import urllib.request


def number(value, low, high):
    return value if isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) and low <= value <= high else None


def positions(snapshot):
    now = number(snapshot.get("now"), 0, 4102444800)
    if now is None or not isinstance(snapshot.get("aircraft"), list):
        raise ValueError("Invalid aircraft.json snapshot: now and aircraft are required")
    reports, rejected = [], 0
    for aircraft in snapshot["aircraft"]:
        if not isinstance(aircraft, dict):
            rejected += 1
            continue
        hex_code = aircraft.get("hex", "")
        lat, lon = number(aircraft.get("lat"), -90, 90), number(aircraft.get("lon"), -180, 180)
        age = number(aircraft.get("seen_pos"), 0, 120)
        if not isinstance(hex_code, str) or not re.fullmatch(r"[a-fA-F0-9]{6}", hex_code) or lat is None or lon is None or age is None:
            rejected += 1
            continue
        reported_altitude = aircraft.get("alt_baro", aircraft.get("altitude"))
        altitude = number(reported_altitude, -1640, 98425)
        altitude_reference = "barometric"
        if altitude is None:
            altitude = number(aircraft.get("alt_geom"), -1640, 98425)
            altitude_reference = "geometric"
        rate = number(aircraft.get("baro_rate", aircraft.get("vert_rate")), -30000, 30000)
        rate_reference = "barometric"
        if rate is None:
            rate, rate_reference = number(aircraft.get("geom_rate"), -30000, 30000), "geometric"
        speed = number(aircraft.get("gs", aircraft.get("speed")), 0, 2915)
        heading = number(aircraft.get("track"), 0, 360)
        callsign = aircraft.get("flight")
        squawk = aircraft.get("squawk")
        reports.append({
            "icao": hex_code.lower(),
            "observedAt": datetime.datetime.fromtimestamp(now - age, datetime.timezone.utc).isoformat(),
            "lat": lat, "lon": lon,
            "altitudeM": None if altitude is None else altitude * 0.3048,
            "sourceAltitudeFt": altitude,
            "altitudeReference": None if altitude is None else altitude_reference,
            "verticalRateFpm": rate, "verticalRateReference": None if rate is None else rate_reference,
            "groundSpeedMps": None if speed is None else speed * 0.5144444444444445,
            "heading": None if heading is None else heading % 360,
            "callsign": callsign.strip() if isinstance(callsign, str) and len(callsign.strip()) <= 16 else None,
            "squawk": squawk if isinstance(squawk, str) and re.fullmatch(r"[0-7]{4}", squawk) else None,
            "onGround": reported_altitude == "ground",
        })
    return reports, rejected


def read_key(path):
    with open(path, encoding="utf-8") as source:
        if stat.S_IMODE(os.fstat(source.fileno()).st_mode) & 0o077:
            raise ValueError("Key file must be owner-only; use chmod 600")
        token = source.read(1024).strip()
    if not re.fullmatch(r"sg_[A-Za-z0-9_-]{20,200}", token):
        raise ValueError("Key file does not contain a valid feeder key")
    return token


def read_snapshot(path):
    with open(path, encoding="utf-8") as source:
        text = source.read(10_000_001)
    if len(text) > 10_000_000:
        raise ValueError("Decoder snapshot exceeds 10 MB")
    snapshot = json.loads(text)
    if not isinstance(snapshot, dict):
        raise ValueError("Decoder snapshot must be an object")
    now = number(snapshot.get("now"), 0, 4102444800)
    if now is None or not -10 <= time.time() - now <= 120:
        raise ValueError("Decoder snapshot is stale or ahead of the clock; check decoder and NTP")
    return snapshot


def install_service(args):
    if os.geteuid() != 0:
        raise ValueError("Run --install-systemd with sudo")
    key_path = str(Path(args.key_file).resolve())
    aircraft_path = str(Path(args.aircraft_json).resolve())
    for value in (key_path, aircraft_path, args.endpoint):
        if any(character in value for character in "\r\n%\\"):
            raise ValueError("Service paths and endpoint cannot contain newlines, percent signs or backslashes")
    read_key(key_path)
    read_snapshot(aircraft_path)
    target = Path("/usr/local/lib/skygrid-bridge")
    target.mkdir(mode=0o755, parents=True, exist_ok=True)
    script = target / "simple-bridge.py"
    if Path(__file__).resolve() != script:
        shutil.copyfile(__file__, script)
    script.chmod(0o755)
    unit = f"""[Unit]
Description=SkyGrid community ADS-B outbound bridge
Wants=network-online.target
After=network-online.target

[Service]
DynamicUser=yes
LoadCredential={json.dumps("feeder.key:" + key_path)}
ExecStart=/usr/bin/python3 {script} --aircraft-json {json.dumps(aircraft_path)} --key-file %d/feeder.key --endpoint {json.dumps(args.endpoint)}
Restart=on-failure
RestartSec=15
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
UMask=0077

[Install]
WantedBy=multi-user.target
"""
    Path("/etc/systemd/system/skygrid-bridge.service").write_text(unit, encoding="utf-8")
    subprocess.run(["systemctl", "daemon-reload"], check=True)
    subprocess.run(["systemctl", "enable", "--now", "skygrid-bridge.service"], check=True)
    subprocess.run(["systemctl", "is-active", "--quiet", "skygrid-bridge.service"], check=True)
    print("Bridge service started. Check journalctl -u skygrid-bridge and your SkyGrid account for committed reports.")


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        raise ValueError("Ingest redirects are refused")


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--aircraft-json", required=True, help="Local decoder aircraft.json path")
    parser.add_argument("--key-file", required=True, help="Owner-only file containing your feeder key")
    parser.add_argument("--endpoint", default="https://skygrid.live/api/ingest/v1/positions")
    parser.add_argument("--once", action="store_true", help="Send one snapshot, otherwise poll every five seconds")
    parser.add_argument("--install-systemd", action="store_true", help="Install and start a background service on stock Pi OS (sudo)")
    args = parser.parse_args()
    endpoint = urllib.parse.urlsplit(args.endpoint)
    if endpoint.scheme != "https" or not endpoint.hostname or endpoint.username or endpoint.password or endpoint.query or endpoint.fragment:
        parser.error("Endpoint must be HTTPS, without credentials, query or fragment")
    if args.install_systemd:
        try:
            install_service(args)
            return 0
        except (OSError, ValueError, subprocess.CalledProcessError) as error:
            print(f"Service installation failed: {error}", file=sys.stderr)
            return 1
    opener = urllib.request.build_opener(NoRedirect())
    while True:
        try:
            token = read_key(args.key_file)
            snapshot = read_snapshot(args.aircraft_json)
            reports, rejected = positions(snapshot)
            if rejected:
                print(f"Skipped {rejected} stale, non-position or invalid records", file=sys.stderr)
            for offset in range(0, len(reports), 500):
                payload = json.dumps({"reports": reports[offset:offset + 500]}, allow_nan=False).encode()
                request = urllib.request.Request(args.endpoint, data=payload, method="POST", headers={
                    "Authorization": "Bearer " + token, "Content-Type": "application/json",
                    "Idempotency-Key": hashlib.sha256(payload).hexdigest(),
                })
                with opener.open(request, timeout=10) as response:
                    if response.status not in (200, 202):
                        raise ValueError("Unexpected ingest response")
            print(f"Submitted {len(reports)} positions; confirm database receipt in your account" if reports else
                  "No fresh aircraft positions; nothing sent. First-report activation is still waiting.", flush=True)
        except urllib.error.HTTPError as error:
            print(f"Ingest HTTP {error.code}; check account receiver status", file=sys.stderr)
            if args.once or error.code in (401, 403):
                return 1
        except (OSError, ValueError, urllib.error.URLError) as error:
            print(f"Bridge failed: {error}", file=sys.stderr)
            if args.once:
                return 1
        if args.once:
            return 0
        time.sleep(5)


if __name__ == "__main__":
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        sys.exit(0)
