/**
 * useLiveGuidance — real-GPS turn-by-turn. This is the DEFAULT drive mode: the
 * puck follows the device's actual position and only advances when the phone
 * physically moves. (The Simulate mode, for desk demos, is the mock's replay
 * loop instead — see GuidanceContainer.)
 *
 * Each GPS fix is map-matched onto the route (see routeProgress) and turned
 * into the same `GuidanceEvent` the rest of the app already consumes, so the
 * guidance UI is identical whether the motion is real or simulated. Bearing is
 * taken from the route geometry at the snapped point (stable) rather than the
 * noisy GPS course.
 */
import { useEffect } from "react";
import * as Location from "expo-location";
import type { GuidanceEvent, LatLng, Route } from "../../../modules/mapmap-react-native/src/MapmapNav.types";
import { guidanceEventFromProgress, prepareRoute, snapToRoute } from "./routeProgress";

/** Lateral distance (m) beyond which a fix counts as off-route. */
const OFF_ROUTE_METRES = 45;
/** Consecutive off-route fixes before we surface "rerouting" + ask for a new
 * route (~3s at 1Hz — debounces momentary GPS jitter near the route edge). */
const OFF_ROUTE_FIXES = 3;

export function useLiveGuidance(
  route: Route | undefined,
  active: boolean,
  onEvent: (event: GuidanceEvent) => void,
  /** Called once per sustained deviation, with the driver's current position,
   * so the owner can recompute a route from here. */
  onReroute?: (from: LatLng) => void
): void {
  useEffect(() => {
    if (!active || !route) return;

    let subscription: Location.LocationSubscription | null = null;
    let cancelled = false;
    let offRouteCount = 0;
    // Fires the reroute request once per deviation, not once per off-route fix.
    let rerouteRequested = false;
    // Compute the route's constants once, not per GPS fix (see PreparedRoute).
    const prepared = prepareRoute(route);

    void (async () => {
      const { status } = await Location.requestForegroundPermissionsAsync();
      if (cancelled || status !== Location.PermissionStatus.GRANTED) return;

      subscription = await Location.watchPositionAsync(
        {
          accuracy: Location.Accuracy.BestForNavigation,
          distanceInterval: 1,
          timeInterval: 1000,
        },
        (fix) => {
          if (cancelled) return;
          const position = {
            latitude: fix.coords.latitude,
            longitude: fix.coords.longitude,
          };
          const snap = snapToRoute(position, route.geometry);
          const speed = Math.max(0, fix.coords.speed ?? 0);
          const offRoute = snap.lateralMetres > OFF_ROUTE_METRES;
          offRouteCount = offRoute ? offRouteCount + 1 : 0;
          const rerouting = offRouteCount >= OFF_ROUTE_FIXES;

          // Back on the route — allow the next deviation to trigger a reroute.
          if (!offRoute) rerouteRequested = false;
          if (rerouting && !rerouteRequested) {
            rerouteRequested = true;
            onReroute?.(position);
          }

          // Off-route: use the real GPS heading for the puck (route bearing is
          // meaningless when we're not on the line); -1 means "unknown".
          const heading = fix.coords.heading;
          const bearingOverride =
            offRoute && typeof heading === "number" && heading >= 0 ? heading : undefined;

          onEvent(
            guidanceEventFromProgress(
              route,
              prepared,
              snap.distanceAlong,
              speed,
              bearingOverride,
              offRoute,
              rerouting,
              Date.now(),
              position
            )
          );
        }
      );
      if (cancelled) {
        subscription.remove();
        subscription = null;
      }
    })();

    return () => {
      cancelled = true;
      subscription?.remove();
    };
  }, [route, active, onEvent, onReroute]);
}
