/**
 * Small great-circle geometry helpers used by `synthesizeDriveCorpus.ts`.
 *
 * Deliberately self-contained (no cross-import from the
 * `mapmap-react-native` module — see modules/mapmap-react-native/src/geo.ts
 * for the module-side twin) so `src/replay/**` has zero coupling to the
 * native module package boundary, matching CONTRACTS.md's "no two tasks
 * share a file" ownership split.
 */
import type { LatLng } from "../../modules/mapmap-react-native/src/MapmapNav.types";

const EARTH_RADIUS_METERS = 6_371_000;

function toRadians(degrees: number): number {
  return (degrees * Math.PI) / 180;
}

function toDegrees(radians: number): number {
  return (radians * 180) / Math.PI;
}

/** Great-circle distance between two points, in metres (haversine). */
export function distanceMeters(a: LatLng, b: LatLng): number {
  const dLat = toRadians(b.latitude - a.latitude);
  const dLon = toRadians(b.longitude - a.longitude);
  const lat1 = toRadians(a.latitude);
  const lat2 = toRadians(b.latitude);
  const sinDLat = Math.sin(dLat / 2);
  const sinDLon = Math.sin(dLon / 2);
  const h = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon;
  return 2 * EARTH_RADIUS_METERS * Math.asin(Math.min(1, Math.sqrt(h)));
}

/** Initial bearing travelling from `a` to `b`, in degrees [0, 360). */
export function bearingDegrees(a: LatLng, b: LatLng): number {
  const lat1 = toRadians(a.latitude);
  const lat2 = toRadians(b.latitude);
  const dLon = toRadians(b.longitude - a.longitude);
  const y = Math.sin(dLon) * Math.cos(lat2);
  const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);
  return (toDegrees(Math.atan2(y, x)) + 360) % 360;
}

/** Linear interpolation between two points; accurate enough at route scale. */
export function lerpLatLng(a: LatLng, b: LatLng, t: number): LatLng {
  return {
    latitude: a.latitude + (b.latitude - a.latitude) * t,
    longitude: a.longitude + (b.longitude - a.longitude) * t,
  };
}

/** Total length of a polyline, in metres. */
export function pathLengthMeters(points: readonly LatLng[]): number {
  let total = 0;
  for (let i = 1; i < points.length; i++) {
    total += distanceMeters(points[i - 1], points[i]);
  }
  return total;
}

export type PointAtDistanceResult = {
  point: LatLng;
  bearing: number;
};

/**
 * Walks a polyline to the given distance-from-start and returns the
 * interpolated point plus the bearing of the segment it falls on. Clamps to
 * the polyline's endpoints for out-of-range distances.
 */
export function pointAtDistance(points: readonly LatLng[], targetDistance: number): PointAtDistanceResult {
  if (points.length === 0) {
    throw new Error("pointAtDistance: empty polyline");
  }
  if (points.length === 1 || targetDistance <= 0) {
    const bearing = points.length > 1 ? bearingDegrees(points[0], points[1]) : 0;
    return { point: points[0], bearing };
  }

  let travelled = 0;
  for (let i = 1; i < points.length; i++) {
    const segStart = points[i - 1];
    const segEnd = points[i];
    const segLength = distanceMeters(segStart, segEnd);
    const segBearing = bearingDegrees(segStart, segEnd);

    if (travelled + segLength >= targetDistance) {
      const remaining = targetDistance - travelled;
      const t = segLength === 0 ? 0 : Math.min(1, Math.max(0, remaining / segLength));
      return { point: lerpLatLng(segStart, segEnd, t), bearing: segBearing };
    }

    travelled += segLength;

    if (i === points.length - 1) {
      return { point: segEnd, bearing: segBearing };
    }
  }

  const last = points[points.length - 1];
  return { point: last, bearing: 0 };
}

/**
 * Cumulative distance-from-start (metres) at the polyline index nearest to
 * each of `positions`. Used to translate maneuver `position`s into
 * distance-along-route thresholds.
 */
export function cumulativeDistancesAt(geometry: readonly LatLng[], positions: readonly LatLng[]): number[] {
  const cumulative: number[] = [0];
  for (let i = 1; i < geometry.length; i++) {
    cumulative.push(cumulative[i - 1] + distanceMeters(geometry[i - 1], geometry[i]));
  }

  return positions.map((position) => {
    let bestIndex = 0;
    let bestDistance = Infinity;
    for (let i = 0; i < geometry.length; i++) {
      const d = distanceMeters(geometry[i], position);
      if (d < bestDistance) {
        bestDistance = d;
        bestIndex = i;
      }
    }
    return cumulative[bestIndex] ?? 0;
  });
}
