/**
 * Real road-following routes from MapMap's hosted engine (`POST /route`,
 * Valhalla-shaped), mapped into the bridge contract's RouteResult.
 *
 * Used as the demo/mock route provider so the journey follows actual
 * roads with the engine's own turn instructions. KNOWN PLATFORM BUG
 * (FINDINGS F1): the hosted engine currently pins every origin to a fixed
 * edge in Manchester regardless of the request — so demo destinations are
 * Manchester landmarks, which yields correct, real routes end-to-end.
 */
import type {
  LatLng,
  Maneuver,
  ManeuverType,
  Route,
  RouteRequest,
  RouteResult,
} from "../../modules/mapmap-react-native/src/MapmapNav.types";

const API_HOST = "https://api.mapmap.ai";

/** Valhalla numeric maneuver types → the contract's ManeuverType. */
const MANEUVER_MAP: Record<number, ManeuverType> = {
  1: "depart",
  2: "depart",
  3: "depart",
  4: "arrive",
  5: "arrive",
  6: "arrive",
  8: "straight",
  9: "slight-right",
  10: "turn-right",
  11: "sharp-right",
  12: "uturn",
  13: "uturn",
  14: "sharp-left",
  15: "turn-left",
  16: "slight-left",
  17: "straight",
  18: "on-ramp",
  19: "on-ramp",
  20: "off-ramp",
  21: "off-ramp",
  22: "straight",
  23: "fork-right",
  24: "fork-left",
  25: "merge",
  26: "roundabout",
  27: "roundabout",
  37: "merge",
  38: "merge",
};

/** Decode a Valhalla polyline6 string into LatLng points. */
export function decodePolyline6(encoded: string): LatLng[] {
  const points: LatLng[] = [];
  let index = 0;
  let lat = 0;
  let lng = 0;
  while (index < encoded.length) {
    for (const which of [0, 1] as const) {
      let result = 0;
      let shift = 0;
      let byte = 0x20;
      while (byte >= 0x20) {
        byte = encoded.charCodeAt(index++) - 63;
        result |= (byte & 0x1f) << shift;
        shift += 5;
      }
      const delta = result & 1 ? ~(result >> 1) : result >> 1;
      if (which === 0) lat += delta;
      else lng += delta;
    }
    points.push({ latitude: lat / 1e6, longitude: lng / 1e6 });
  }
  return points;
}

const PROFILE_TO_COSTING: Record<RouteRequest["profile"], string> = {
  car: "auto",
  bicycle: "bicycle",
  walking: "pedestrian",
  truck: "truck",
};

/** Build Valhalla costing_options for a request (truck dims + avoids). */
function costingOptions(request: RouteRequest): Record<string, unknown> | undefined {
  const costing = PROFILE_TO_COSTING[request.profile];
  const opts: Record<string, unknown> = {};
  if (request.profile === "truck" && request.truck) {
    const t = request.truck;
    if (t.heightM != null) opts.height = t.heightM;
    if (t.weightT != null) opts.weight = t.weightT;
    if (t.lengthM != null) opts.length = t.lengthM;
    if (t.widthM != null) opts.width = t.widthM;
    if (t.hazmat) opts.hazmat = true;
  }
  if (request.avoid) {
    if (request.avoid.tolls) opts.use_tolls = 0;
    if (request.avoid.ferries) opts.use_ferry = 0;
    if (request.avoid.motorways) opts.use_highways = 0;
  }
  return Object.keys(opts).length ? { [costing]: opts } : undefined;
}

type ValhallaManeuver = {
  type?: number;
  instruction?: string;
  length?: number; // km
  begin_shape_index?: number;
  roundabout_exit_count?: number;
  /** Road names travelled along during this maneuver's leg. */
  street_names?: string[];
  /** Names at the very start of the leg (e.g. a slip road) — fallback. */
  begin_street_names?: string[];
};

type ValhallaTripObj = {
  legs?: Array<{
    shape?: string;
    maneuvers?: ValhallaManeuver[];
  }>;
  summary?: { length?: number; time?: number };
};

type ValhallaTrip = {
  trip?: ValhallaTripObj;
  /** Alternative routes, each wrapped in its own `trip`. */
  alternates?: Array<{ trip?: ValhallaTripObj }>;
};

/** Map one Valhalla trip (primary or alternate) to our Route, or null if it has
 * no usable geometry. */
function tripToRoute(trip: ValhallaTripObj | undefined, index: number): Route | null {
  const leg = trip?.legs?.[0];
  const summary = trip?.summary;
  if (!leg?.shape) return null;

  const geometry = decodePolyline6(leg.shape);
  if (geometry.length < 2) return null;

  const maneuvers: Maneuver[] = (leg.maneuvers ?? []).map((m) => {
    const names = m.street_names ?? m.begin_street_names;
    const roadName = names && names.length > 0 ? names.join("/") : undefined;
    return {
      type: MANEUVER_MAP[m.type ?? 8] ?? "straight",
      instruction: m.instruction ?? "Continue.",
      distanceMeters: Math.round((m.length ?? 0) * 1000),
      position: geometry[Math.min(m.begin_shape_index ?? 0, geometry.length - 1)],
      ...(m.roundabout_exit_count ? { roundaboutExit: m.roundabout_exit_count } : {}),
      ...(roadName ? { roadName } : {}),
    };
  });

  return {
    index,
    geometry,
    distanceMeters: Math.round((summary?.length ?? 0) * 1000),
    durationSeconds: Math.round(summary?.time ?? 0),
    maneuvers,
    encodedShape: leg.shape,
  };
}

/**
 * Fetch a real route from the hosted engine. Returns null on any failure so
 * callers can fall back (e.g. to the mock's synthetic route).
 */
export async function fetchGatewayRoute(
  apiKey: string,
  request: RouteRequest
): Promise<RouteResult | null> {
  if (!apiKey) return null;
  try {
    const res = await fetch(`${API_HOST}/route`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${apiKey}`,
      },
      body: JSON.stringify({
        costing: PROFILE_TO_COSTING[request.profile],
        costing_options: costingOptions(request),
        // Ask the engine for alternative routes so the preview can offer a
        // choice. Valhalla returns the primary under `trip` and each extra
        // under `alternates[]`; it may return fewer than requested.
        alternates: Math.max(0, Math.min(3, request.alternatives ?? 2)),
        locations: [
          { lat: request.origin.latitude, lon: request.origin.longitude },
          ...(request.waypoints ?? []).map((w) => ({
            lat: w.latitude,
            lon: w.longitude,
            type: "through",
          })),
          { lat: request.destination.latitude, lon: request.destination.longitude },
        ],
      }),
    });
    if (!res.ok) return null;
    const body = (await res.json()) as ValhallaTrip;

    const trips = [body.trip, ...(body.alternates ?? []).map((a) => a?.trip)];
    const routes = trips
      .map((trip, i) => tripToRoute(trip, i))
      .filter((r): r is Route => r !== null);
    // Re-index so route.index is a dense 0..N even if a trip was dropped.
    const reindexed = routes.map((r, i) => ({ ...r, index: i }));
    if (reindexed.length === 0) return null;
    return { routes: reindexed };
  } catch {
    return null;
  }
}
