/* nav-engine.jsx — REAL turn-by-turn navigation for the web MVP ($0, no new infra).

   Three pieces, all attached to window:
     • vrCompileSteps(legs)   — OSRM route legs → our engine step shape (drops synthetic vias)
     • vrBgInstruction(step)  — Bulgarian instruction text + icon for one maneuver
     • NavEngine              — the live-nav state machine (GPS → step tracking, reroute, voice)

   The engine tracks the rider against the active route polyline: it projects the live GPS fix
   onto the line (cross-track + along-track), advances the current maneuver, counts down distance
   to the next turn, recomputes ETA, detects arrival, and — the differentiator vs. Komoot —
   reroutes when the rider strays. It speaks instructions (Web Speech API) and keeps the screen
   awake (Wake Lock API). All browser-native; no API key, no server.

   Positions arrive from navigator.geolocation.watchPosition in the field, or from
   NavEngine.__injectPosition(...) in the headless harness (no real GPS in CI). */

// ── Bulgarian instruction formatter ───────────────────────────────────────────
const BG_MOD = {
  'right': 'надясно', 'left': 'наляво',
  'slight right': 'леко надясно', 'slight left': 'леко наляво',
  'sharp right': 'рязко надясно', 'sharp left': 'рязко наляво',
  'straight': 'направо', 'uturn': 'в обратна посока',
};

function ordinalBg(n) {
  const map = { 1: '1-ви', 2: '2-ри', 3: '3-ти', 4: '4-ти', 5: '5-и', 6: '6-и' };
  return map[n] || (n + '-и');
}

// closest glyph in the existing icon set (icons.jsx). Gaps (slight-left, roundabout, uturn)
// fall back to the nearest available arrow — noted inline.
function iconFor(type, mod) {
  if (type === 'depart') return 'IconNavArrow';
  if (type === 'arrive') return 'IconPin';
  if (type === 'roundabout' || type === 'rotary') return 'IconStraight';  // no roundabout glyph
  if (mod === 'uturn') return 'IconArrowTurnL';                            // no u-turn glyph
  if (mod === 'slight right' || mod === 'right' || mod === 'sharp right') {
    return mod === 'slight right' ? 'IconArrowUpRight' : 'IconArrowTurnR';
  }
  if (mod === 'slight left' || mod === 'left' || mod === 'sharp left') {
    return 'IconArrowTurnL';                                               // no slight-left glyph
  }
  return 'IconStraight';
}

window.vrBgInstruction = function (step) {
  const man = step.maneuver || {};
  const type = man.type || 'turn';
  const mod = man.modifier || '';
  const name = step.name || '';
  const modPhrase = BG_MOD[mod] || 'направо';
  let text;
  switch (type) {
    case 'depart': text = 'Тръгнете'; break;
    case 'turn': text = `Завийте ${modPhrase}`; break;
    case 'new name': text = 'Продължете'; break;
    case 'continue': text = mod === 'straight' || !mod ? 'Продължете направо' : `Завийте ${modPhrase}`; break;
    case 'merge': text = 'Влейте се'; break;
    case 'on ramp': case 'off ramp': text = `Поемете ${modPhrase}`; break;
    case 'end of road': text = `В края на пътя завийте ${modPhrase}`; break;
    case 'fork': text = `Дръжте се ${modPhrase.includes('дясно') ? 'вдясно' : 'вляво'}`; break;
    case 'roundabout': case 'rotary': text = `В кръговото — ${ordinalBg(man.exit || 1)} изход`; break;
    case 'arrive': text = 'Пристигане'; break;
    default: text = mod ? `Завийте ${modPhrase}` : 'Продължете направо';
  }
  return { text, street: name, icon: iconFor(type, mod) };
};

// ── OSRM legs → engine steps ───────────────────────────────────────────────────
// Each step gets a maneuverPoint ([lat,lng], flipped from OSRM's [lng,lat]) so the engine can
// snap it onto the route polyline. The ±400/±700 m detour corridors carry a synthetic via, so
// OSRM returns multiple legs joined by an artificial arrive→depart pair — we drop those so the
// rider isn't told "You have arrived" at a fake waypoint.
function toEngineStep(st) {
  const man = st.maneuver || {};
  const loc = man.location || [0, 0];
  const bg = window.vrBgInstruction({ maneuver: man, name: st.name });
  return {
    type: man.type, modifier: man.modifier || '', name: st.name || '',
    maneuverPoint: [loc[1], loc[0]],
    distance: st.distance || 0,
    icon: bg.icon, text: bg.text, street: bg.street,
  };
}

window.vrCompileSteps = function (legs) {
  if (!legs || !legs.length) return [];
  const out = [];
  legs.forEach((leg, li) => {
    const steps = leg.steps || [];
    const firstLeg = li === 0, lastLeg = li === legs.length - 1;
    steps.forEach((st) => {
      const t = (st.maneuver || {}).type;
      if (t === 'depart' && !firstLeg) return;   // synthetic via re-start
      if (t === 'arrive' && !lastLeg) return;    // synthetic via stop
      out.push(toEngineStep(st));
    });
  });
  return out;
};

// ── geometry helpers ───────────────────────────────────────────────────────────
function hav(a, b) {
  const R = 6371000, toR = (x) => x * Math.PI / 180;
  const dLat = toR(b[0] - a[0]), dLng = toR(b[1] - a[1]);
  const s = Math.sin(dLat / 2) ** 2 + Math.cos(toR(a[0])) * Math.cos(toR(b[0])) * Math.sin(dLng / 2) ** 2;
  return 2 * R * Math.asin(Math.sqrt(s));
}
function bearing(a, b) {
  const toR = (x) => x * Math.PI / 180, toD = (x) => x * 180 / Math.PI;
  const y = Math.sin(toR(b[1] - a[1])) * Math.cos(toR(b[0]));
  const x = Math.cos(toR(a[0])) * Math.sin(toR(b[0])) - Math.sin(toR(a[0])) * Math.cos(toR(b[0])) * Math.cos(toR(b[1] - a[1]));
  return (toD(Math.atan2(y, x)) + 360) % 360;
}
function toXY(p, ref) {
  const mPerLat = 111320, mPerLng = 111320 * Math.cos(ref[0] * Math.PI / 180);
  return [(p[1] - ref[1]) * mPerLng, (p[0] - ref[0]) * mPerLat];
}
function buildGeom(coords) {
  const cumDist = [0]; let total = 0;
  for (let i = 1; i < coords.length; i++) { total += hav(coords[i - 1], coords[i]); cumDist.push(total); }
  return { cumDist, totalLen: total };
}
// project a point onto the polyline → nearest cross-track distance (m) + along-track distance (m)
function projectToPolyline(pt, coords, cumDist) {
  if (!coords || coords.length < 2) return { crossM: 0, alongM: 0 };
  const ref = coords[0];
  const P = toXY(pt, ref);
  let best = { crossM: Infinity, alongM: 0 };
  for (let i = 0; i < coords.length - 1; i++) {
    const A = toXY(coords[i], ref), B = toXY(coords[i + 1], ref);
    const abx = B[0] - A[0], aby = B[1] - A[1];
    const apx = P[0] - A[0], apy = P[1] - A[1];
    const segLen2 = abx * abx + aby * aby;
    let t = segLen2 > 0 ? (apx * abx + apy * aby) / segLen2 : 0;
    t = Math.max(0, Math.min(1, t));
    const cx = A[0] + t * abx, cy = A[1] + t * aby;
    const dist = Math.hypot(P[0] - cx, P[1] - cy);
    if (dist < best.crossM) best = { crossM: dist, alongM: cumDist[i] + t * Math.sqrt(segLen2) };
  }
  return best;
}
const roundM = (m) => Math.max(0, Math.round(m / 10) * 10);
const fmtKmLocal = (m) => (m / 1000).toFixed(1).replace('.', ',') + ' км';
function clockPlus(min) { const d = new Date(Date.now() + min * 60000); return `${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`; }

// ── the engine ─────────────────────────────────────────────────────────────────
const NavEngine = (() => {
  let st = null;
  const subs = new Set();
  let raf = 0;
  const emit = () => {
    if (raf || typeof requestAnimationFrame !== 'function') { subs.forEach((f) => f()); return; }
    raf = requestAnimationFrame(() => { raf = 0; subs.forEach((f) => f()); });
  };

  let watchId = null, orientHandler = null, wakeLock = null;
  let onRouteCb = null, onArriveCb = null, voiceOn = true, followMode = true;
  let lastPos = null, lastT = 0, offCount = 0, goodCount = 0, lastReroute = 0, rerouteCount = 0;
  let firedBands = {};

  function reset() {
    st = {
      status: 'idle', route: null, selectedId: null, dest: null,
      coords: [], cumDist: [], totalLen: 0, cumSec: null, totalSec: 0, steps: [], stepAlong: [],
      stepIndex: 0, distToManeuver: 0, distRemaining: 0, etaMin: 0, arrival: '',
      speedKmh: 0, heading: null, offRoute: false, pos: null,
    };
  }
  reset();

  function applyRoute(route, sel) {
    st.route = route;
    st.coords = (sel && sel.coords) || [];
    st.steps = (sel && sel.steps) || [];
    const g = buildGeom(st.coords);
    st.cumDist = g.cumDist; st.totalLen = g.totalLen;
    // graded-timing model → live ETA reflects hills + turns instead of a flat 15 km/h.
    // Uses the route's own DEM profile (sel.elevs) when it has been enriched; flat ground otherwise.
    if (window.vrRouteTiming) {
      const t = window.vrRouteTiming(st.coords, st.steps, (sel && sel.elevs) || null);
      st.cumSec = t.cumSec; st.totalSec = t.totalSec;
    } else { st.cumSec = null; st.totalSec = 0; }
    // snap each maneuver onto the route; if a step lacks a maneuverPoint (baked fallback),
    // synthesize from its declared `start`/`distance` so tracking still advances.
    let synth = 0;
    st.stepAlong = st.steps.map((s) => {
      if (s.maneuverPoint) return projectToPolyline(s.maneuverPoint, st.coords, st.cumDist).alongM;
      const v = synth; synth += (s.start || s.distance || 0); return v;
    });
    firedBands = {};
  }

  function speakText(step) { return step.text + (step.street ? ' по ' + step.street : ''); }
  function speak(text) {
    if (!voiceOn || !text) return;
    try {
      if (!window.speechSynthesis) return;
      const u = new SpeechSynthesisUtterance(text);
      u.lang = 'bg-BG'; u.rate = 1.0;
      window.speechSynthesis.cancel();
      window.speechSynthesis.speak(u);
    } catch (e) { /* TTS unsupported → silent */ }
  }
  function announce(idx, dist) {
    const step = st.steps[idx]; if (!step) return;
    const k150 = idx + '_150', k25 = idx + '_25';
    if (dist <= 160 && dist > 30 && !firedBands[k150]) { firedBands[k150] = 1; speak('След ' + roundM(dist) + ' метра, ' + speakText(step)); }
    if (dist <= 28 && !firedBands[k25]) { firedBands[k25] = 1; speak(speakText(step)); }
  }

  async function acquireWake() {
    try { if (navigator.wakeLock) wakeLock = await navigator.wakeLock.request('screen'); } catch (e) { /* denied/hidden */ }
  }
  function onVis() { if (document.visibilityState === 'visible' && st.status !== 'idle' && !wakeLock) acquireWake(); }
  // any manual gesture (pan / pinch-zoom / tap-hold) disengages follow so the rider can explore
  function onDrag() { if (followMode) { followMode = false; emit(); } }
  function wireMapGestures(on) {
    const m = window.__vrMap; if (!m) return;
    try {
      m[on ? 'on' : 'off']('dragstart', onDrag);
      m[on ? 'on' : 'off']('zoomstart', onDrag);
      m[on ? 'on' : 'off']('mousedown', onDrag);
      const c = m.getContainer && m.getContainer();
      if (c) c[on ? 'addEventListener' : 'removeEventListener']('touchstart', onDrag, { passive: true });
    } catch (e) { /* map gone */ }
  }

  function arrive() {
    if (st.status === 'arrived') return;
    st.status = 'arrived'; st.distToManeuver = 0; st.distRemaining = 0;
    if (watchId != null && navigator.geolocation) { try { navigator.geolocation.clearWatch(watchId); } catch (e) {} watchId = null; }
    speak('Пристигнахте');
    emit();
    if (onArriveCb) onArriveCb();
  }

  function maybeReroute() {
    st.offRoute = true;
    if (st.status === 'rerouting') return;
    const now = Date.now();
    if (now - lastReroute < 12000) { emit(); return; }     // don't thrash the public OSRM endpoint
    if (rerouteCount >= 4) { emit(); return; }              // GPS clearly broken — stop looping
    if (!window.fetchLiveRoutes || !st.dest || !st.pos) { emit(); return; }
    st.status = 'rerouting'; lastReroute = now; rerouteCount++; emit();
    window.fetchLiveRoutes(st.pos, st.dest).then((sources) => {
      if (st.status !== 'rerouting') return;
      if (!sources || !sources.length || !window.buildRoute) { st.status = 'navigating'; emit(); return; }
      const newRoute = window.buildRoute(sources, st.dest, st.pos);
      const sel = newRoute.routes.find((r) => r.id === st.selectedId) || newRoute.routes[0];
      applyRoute(newRoute, sel);
      offCount = 0; goodCount = 0; st.status = 'navigating';
      if (onRouteCb) onRouteCb(newRoute, st.selectedId);
      emit();
    }).catch(() => { st.status = 'navigating'; emit(); });
  }

  function onFix(lat, lng, gpsHeading, gpsSpeed) {
    if (!st || st.status === 'idle' || st.status === 'arrived') return;
    const pos = [lat, lng];
    const now = Date.now();

    // speed: prefer GPS speed (m/s), else derive from consecutive fixes
    let speedKmh = 0;
    if (gpsSpeed != null && gpsSpeed >= 0) speedKmh = gpsSpeed * 3.6;
    else if (lastPos && lastT) { const dt = (now - lastT) / 1000; if (dt > 0) speedKmh = hav(lastPos, pos) / dt * 3.6; }
    st.speedKmh = Math.round(Math.max(0, Math.min(60, speedKmh)));

    // heading: prefer GPS course-over-ground; else bearing between fixes (only when moving)
    let heading = (gpsHeading != null && !isNaN(gpsHeading)) ? gpsHeading : null;
    if (heading == null && lastPos && hav(lastPos, pos) > 3) heading = bearing(lastPos, pos);
    if (heading != null && !isNaN(heading)) st.heading = heading;

    lastPos = pos; lastT = now; st.pos = pos;

    const { crossM, alongM } = projectToPolyline(pos, st.coords, st.cumDist);
    st.distRemaining = Math.max(0, st.totalLen - alongM);
    // ETA from the graded-timing model (hills + turns); falls back to flat 15 km/h if unavailable.
    let etaSec = null;
    if (st.cumSec && st.totalSec > 0 && window.vrTimeAlong) {
      const tA = window.vrTimeAlong(alongM, st.cumDist, st.cumSec);
      if (tA != null) etaSec = Math.max(0, st.totalSec - tA);
    }
    st.etaMin = etaSec != null ? Math.max(1, Math.round(etaSec / 60)) : Math.max(1, Math.round(st.distRemaining / 1000 / 15 * 60));
    st.arrival = clockPlus(st.etaMin);

    // current step = next maneuver ahead of us along the route
    let idx = st.stepAlong.findIndex((d) => d > alongM + 1);
    if (idx === -1) idx = st.steps.length ? st.steps.length - 1 : 0;
    st.stepIndex = idx;
    st.distToManeuver = Math.max(0, (st.stepAlong[idx] || 0) - alongM);
    announce(idx, st.distToManeuver);

    if (st.distRemaining < 25) { arrive(); return; }

    if (crossM > 35) { offCount++; goodCount = 0; if (offCount >= 4) { maybeReroute(); } }
    else { offCount = 0; st.offRoute = false; goodCount++; if (goodCount > 8) rerouteCount = 0; }

    // ALWAYS move the puck to the live fix — the rider should see their position even while
    // exploring the map. Only the map-recenter (follow) is gated, so panning isn't fought.
    if (window.__vrShowUserPuck) window.__vrShowUserPuck(lat, lng, st.heading);
    if (followMode && window.__vrMap) { try { window.__vrMap.panTo(pos, { animate: true, duration: 0.5 }); } catch (e) {} }
    emit();
  }

  function start(route, selectedId, opts) {
    opts = opts || {};
    reset();
    st.status = 'navigating';
    st.selectedId = selectedId;
    st.dest = opts.dest || (route && route.dest) || null;
    onRouteCb = opts.onRoute || null;
    onArriveCb = opts.onArrive || null;
    voiceOn = opts.voice !== false;
    followMode = true;
    lastPos = null; lastT = 0; offCount = 0; goodCount = 0; lastReroute = 0; rerouteCount = 0;

    const sel = route && route.routes ? (route.routes.find((r) => r.id === selectedId) || route.routes[0]) : route;
    applyRoute(route, sel);

    acquireWake();
    document.addEventListener('visibilitychange', onVis);
    wireMapGestures(true);
    if (st.coords.length && window.__vrShowUserPuck) window.__vrShowUserPuck(st.coords[0][0], st.coords[0][1], null);
    if (st.steps[0]) speak(speakText(st.steps[0]));

    if (navigator.geolocation) {
      try {
        watchId = navigator.geolocation.watchPosition(
          (p) => onFix(p.coords.latitude, p.coords.longitude, p.coords.heading, p.coords.speed),
          () => { /* denied/timeout — keep the planned line on screen */ },
          { enableHighAccuracy: true, maximumAge: 1000, timeout: 12000 }
        );
      } catch (e) { /* geolocation unavailable */ }
    }
    attachOrientation();
    emit();
    return { geo: st.stepAlong.every((x) => x != null) };
  }

  function attachOrientation() {
    orientHandler = (e) => {
      let h = null;
      if (e.webkitCompassHeading != null) h = e.webkitCompassHeading;        // iOS
      else if (e.absolute && e.alpha != null) h = 360 - e.alpha;             // Android (absolute)
      if (h != null && !isNaN(h) && st.speedKmh < 2) st.heading = h;         // compass only when ~stationary
    };
    try {
      window.addEventListener('deviceorientationabsolute', orientHandler, true);
      window.addEventListener('deviceorientation', orientHandler, true);
    } catch (e) { /* unsupported */ }
  }

  function stop() {
    if (watchId != null && navigator.geolocation) { try { navigator.geolocation.clearWatch(watchId); } catch (e) {} }
    watchId = null;
    if (orientHandler) {
      window.removeEventListener('deviceorientationabsolute', orientHandler, true);
      window.removeEventListener('deviceorientation', orientHandler, true);
      orientHandler = null;
    }
    document.removeEventListener('visibilitychange', onVis);
    wireMapGestures(false);
    if (wakeLock) { try { wakeLock.release(); } catch (e) {} wakeLock = null; }
    try { if (window.speechSynthesis) window.speechSynthesis.cancel(); } catch (e) {}
    if (window.__vrClearUserLocation) window.__vrClearUserLocation();
    st.status = 'idle';
    emit();
  }

  function recenter() { followMode = true; if (st.pos && window.__vrMap) { try { window.__vrMap.panTo(st.pos); } catch (e) {} } emit(); }
  function setVoice(v) { voiceOn = !!v; if (!voiceOn) { try { window.speechSynthesis && window.speechSynthesis.cancel(); } catch (e) {} } }

  function getState() {
    const step = st.steps[st.stepIndex] || null;
    const nextStep = st.steps[st.stepIndex + 1] || null;
    return {
      status: st.status, step, nextStep, stepIndex: st.stepIndex,
      distToManeuver: roundM(st.distToManeuver), distRemaining: st.distRemaining,
      distRemainingKm: fmtKmLocal(st.distRemaining), etaMin: st.etaMin, arrival: st.arrival,
      speedKmh: st.speedKmh, heading: st.heading, offRoute: st.offRoute, follow: followMode,
    };
  }

  return {
    start, stop, recenter, setVoice, getState,
    subscribe: (cb) => { subs.add(cb); return () => subs.delete(cb); },
    __injectPosition: (lat, lng, heading, speed) => onFix(lat, lng, heading == null ? null : heading, speed == null ? null : speed),
  };
})();

window.NavEngine = NavEngine;
