"""Virtual drone entry point: fly, sense, write, broadcast, listen, seal."""

from __future__ import annotations

import random
import select
import time
from pathlib import Path

from .broadcast import decode_state, encode_state, open_socket
from .config import Config
from .flight import initial_pose, make_waypoints, step
from .sensors import (
    baro_row,
    battery_row,
    detection_row,
    imu_row,
    state_row,
    temp_row,
)
from .writer import make_writers

TICK_HZ = 10.0
SENSOR_NAMES = ["imu", "baro", "temp", "battery", "rssi"]


def run(cfg: Config) -> None:
    rng = random.Random(f"{cfg.seed}:{cfg.drone_id}")
    root = Path(cfg.data_dir)
    writers = make_writers(root, cfg.flight_id, cfg.drone_id, SENSOR_NAMES)
    sock = open_socket(cfg.udp_port)

    now_ns = time.time_ns()
    pose = initial_pose(cfg, rng, now_ns)
    waypoints = make_waypoints(cfg, rng)
    wp_index = 1

    dt = 1.0 / TICK_HZ
    ticks_total = int(cfg.duration_s * TICK_HZ)
    # Emission accumulators: sensor name -> carry-over fraction of a sample
    acc = {name: 0.0 for name in ["imu", "baro", "temp", "battery", "state", "detection"]}
    rates = {
        "imu": cfg.imu_hz, "baro": cfg.baro_hz, "temp": cfg.temp_hz,
        "battery": cfg.battery_hz, "state": cfg.state_hz, "detection": cfg.detection_rate_hz,
    }
    peers_seen: dict[str, int] = {}
    frames_sent = 0

    print(f"[{cfg.drone_id}] flight={cfg.flight_id} uuid={cfg.flight_uuid} "
          f"duration={cfg.duration_s}s speedup={cfg.speedup}x data={root}")

    for tick in range(ticks_total):
        sim_elapsed = tick * dt
        ts_ns = now_ns + int(sim_elapsed * 1e9)

        pose, reached = step(pose, waypoints[wp_index], cfg, rng, dt, ts_ns)
        if reached:
            wp_index = (wp_index + 1) % len(waypoints)

        def due(name: str) -> int:
            acc[name] += rates[name] * dt
            n = int(acc[name])
            acc[name] -= n
            return n

        for i in range(due("imu")):
            writers["imu"].append(imu_row(pose, rng, ts_ns + i * int(1e9 / max(cfg.imu_hz, 1))))
        for _ in range(due("baro")):
            writers["baro"].append(baro_row(pose, rng, ts_ns))
        for _ in range(due("temp")):
            writers["temp"].append(temp_row(pose, rng, ts_ns))
        for _ in range(due("battery")):
            writers["battery"].append(battery_row(pose, rng, ts_ns, sim_elapsed, cfg.duration_s))
        for _ in range(due("detection")):
            writers["detections"].append(detection_row(pose, rng, ts_ns))

        for _ in range(due("state")):
            frame = encode_state(cfg.drone_id, pose)
            try:
                sock.sendto(frame, (cfg.broadcast_addr, cfg.udp_port))
                frames_sent += 1
            except OSError:
                pass  # lossy link is part of the model
            writers["state"].append({**state_row(pose, ts_ns), "peer_id": cfg.drone_id, "direction": "sent"})

        # Drain incoming peer frames; record them and derive RSSI-like signal
        while True:
            readable, _, _ = select.select([sock], [], [], 0)
            if not readable:
                break
            payload, _addr = sock.recvfrom(256)
            decoded = decode_state(payload)
            if decoded is None or decoded["peer_id"] == cfg.drone_id:
                continue
            peers_seen[decoded["peer_id"]] = decoded["ts_ns"]
            writers["state"].append({**decoded, "direction": "received"})
            dist = max(1.0, ((decoded["pos_x"] - pose.x) ** 2 + (decoded["pos_y"] - pose.y) ** 2) ** 0.5)
            writers["rssi"].append({
                "ts_ns": ts_ns,
                "peer_id": decoded["peer_id"],
                "rssi_dbm": -40.0 - 20.0 * (dist ** 0.5) / 10.0 + rng.gauss(0.0, 2.0),
                "distance_m": dist,
            })

        time.sleep(dt / max(cfg.speedup, 0.01))

    for writer in writers.values():
        writer.seal()
    print(f"[{cfg.drone_id}] done: {frames_sent} state frames sent, "
          f"{len(peers_seen)} peers seen {sorted(peers_seen)}; sealed to {root}")


if __name__ == "__main__":
    run(Config())
