// Live mode: real drone poses from the explorer's read-only SQL API.
// The same gated /api/query endpoint a peer drone (or an engineer on the
// bench) would use — the prototype is just another read-only consumer.

import { AREA, LINK_RANGE, POSE_RATE_BYTES, type LinkStats, type World } from "./sim";

export const DEFAULT_EXPLORER_URL =
  (import.meta.env.VITE_EXPLORER_URL as string | undefined) ?? "http://localhost:30088";

export interface LivePose {
  id: string;
  x: number; // meters, mission frame
  y: number;
  yawDeg: number;
  battery: number | null;
  tsNs: number;
}

// Latest pose per drone from its own 'sent' state rows, joined with the
// latest battery reading from telemetry. arg_max keeps it a single scan.
const LIVE_SQL = `
WITH pose AS (
  SELECT drone_id,
         arg_max(pos_x, ts_ns) AS x,
         arg_max(pos_y, ts_ns) AS y,
         arg_max(yaw, ts_ns)   AS yaw,
         max(ts_ns)            AS ts_ns
  FROM state
  WHERE direction = 'sent'
  GROUP BY drone_id
),
batt AS (
  SELECT drone_id, arg_max(level_pct, ts_ns) AS battery
  FROM telemetry
  WHERE sensor = 'battery'
  GROUP BY drone_id
)
SELECT p.drone_id, p.x, p.y, p.yaw, p.ts_ns, b.battery
FROM pose p LEFT JOIN batt b USING (drone_id)
ORDER BY p.drone_id`;

interface QueryResponse {
  columns?: string[];
  rows?: (string | number | null)[][];
  error?: string;
}

export async function fetchLivePoses(baseUrl: string): Promise<LivePose[]> {
  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/api/query`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ sql: LIVE_SQL }),
  });
  if (!res.ok) throw new Error(`explorer responded ${res.status}`);
  const data = (await res.json()) as QueryResponse;
  if (data.error) throw new Error(data.error);
  return (data.rows ?? []).map((r) => ({
    id: String(r[0]),
    x: Number(r[1]),
    y: Number(r[2]),
    yawDeg: Number(r[3]),
    tsNs: Number(r[4]),
    battery: r[5] === null ? null : Number(r[5]),
  }));
}

// Scale mission-frame meters into the prototype's square view. The virtual
// drones patrol a smaller square than the sim world, so fit the observed
// extent (with padding) instead of assuming a size.
function fitScale(poses: LivePose[]): number {
  const extent = Math.max(400, ...poses.map((p) => Math.max(p.x, p.y))) * 1.15;
  return AREA / extent;
}

function linkKey(a: number, b: number): string {
  return a < b ? `${a}-${b}` : `${b}-${a}`;
}

/** Convert live poses into the World shape the renderer already speaks. */
export function toLiveWorld(poses: LivePose[], prev: World | null, dtS: number): World {
  const k = fitScale(poses);
  const drones = poses.map((p, i) => ({
    id: i,
    name: p.id,
    pos: { x: p.x * k, y: p.y * k },
    vel: { x: 0, y: 0 },
    heading: (p.yawDeg * Math.PI) / 180,
    waypoint: 0,
    battery: p.battery ?? 100,
    channel: 0,
  }));

  // Every in-range pair exchanges pose broadcasts; accumulate totals so the
  // panel keeps its meaning between polls.
  const links = new Map<string, LinkStats>();
  let totalBytes = prev?.totalBytes ?? 0;
  for (let i = 0; i < drones.length; i++) {
    for (let j = i + 1; j < drones.length; j++) {
      const dist = Math.hypot(
        drones[i].pos.x - drones[j].pos.x,
        drones[i].pos.y - drones[j].pos.y,
      );
      if (dist > LINK_RANGE) continue;
      const key = linkKey(i, j);
      const old = prev?.links.get(key);
      const bytes = POSE_RATE_BYTES * 2 * dtS;
      totalBytes += bytes;
      links.set(key, {
        key,
        a: i,
        b: j,
        totalUp: (old?.totalUp ?? 0) + bytes / 2,
        totalDown: (old?.totalDown ?? 0) + bytes / 2,
        rate: POSE_RATE_BYTES * 2,
        flash: 0.6,
        bulk: 0,
      });
    }
  }

  return {
    t: (prev?.t ?? 0) + dtS,
    drones,
    obstacles: [],
    links,
    totalBytes,
    broadcasts: (prev?.broadcasts ?? 0) + links.size * 2,
  };
}
