/* elevation.jsx — real denivelation-aware ETA for the web MVP ($0, keyless).

   Two concerns, both on window:
     • vrSpeedForGrade(grade) / vrRouteTiming(coords, steps, elevs) — a cycling speed model that
       varies the average speed per segment by road GRADE (uphill is slow, downhill fast) and adds
       a small per-maneuver junction penalty. Works WITH or WITHOUT an elevation profile: with no
       elevs it assumes flat ground (still grade-0 base speed + turn penalties, already better than
       a constant 15 km/h), and refines once a DEM profile is fetched.
     • vrElevationProfile(coords) — fetches a real elevation profile from a free DEM API
       (open-elevation, opentopodata fallback), subsampled + interpolated back onto every vertex.
       Cached per-route. Skipped on localhost so local dev / the headless harness stay fast and
       deterministic — callers then just get the flat-ground timing.

   Coords are [lat, lng]. All failures are soft (return null) so ETA never breaks. */

function elHav(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));
}

// average cycling speed (km/h) for a road grade (rise/run fraction; + = uphill).
// Calibrated to a relaxed urban rider: ~16 km/h flat, slowing sharply uphill, faster downhill.
window.vrSpeedForGrade = function (g) {
  if (g <= -0.04) return 23;      // steep descent (braking-limited, not faster)
  if (g <= -0.015) return 20;     // gentle descent
  if (g < 0.015) return 16;       // ~flat
  if (g < 0.035) return 12;       // gentle climb
  if (g < 0.06) return 9;         // moderate climb
  return 7;                        // steep climb
};

// per-segment timing along the route. Returns { totalSec, cumSec[], ascent, descent, hasElev }.
// cumSec[i] = seconds from start to vertex i (parallel to a cumDist array), so the nav engine can
// interpolate remaining time for a live ETA.
window.vrRouteTiming = function (coords, steps, elevs) {
  if (!coords || coords.length < 2) return { totalSec: 0, cumSec: [0], ascent: 0, descent: 0, hasElev: false };
  const hasElev = Array.isArray(elevs) && elevs.length === coords.length;
  let totalSec = 0; const cumSec = [0];
  for (let i = 1; i < coords.length; i++) {
    const d = elHav(coords[i - 1], coords[i]);   // meters
    let grade = 0;
    if (hasElev && elevs[i] != null && elevs[i - 1] != null && d > 1) grade = (elevs[i] - elevs[i - 1]) / d;
    const v = window.vrSpeedForGrade(grade) / 3.6;   // m/s
    totalSec += d / Math.max(0.5, v);
    cumSec.push(totalSec);
  }
  // junction / turn penalty — each maneuver beyond depart+arrive costs a few seconds of slowdown.
  const turns = steps && steps.length ? Math.max(0, steps.length - 2) : 0;
  const penalty = turns * 6;
  if (totalSec > 0 && penalty > 0) {
    const f = (totalSec + penalty) / totalSec;       // scale proportionally → cumSec stays monotonic
    for (let i = 0; i < cumSec.length; i++) cumSec[i] *= f;
    totalSec *= f;
  }
  let ascent = 0, descent = 0;
  if (hasElev) for (let i = 1; i < elevs.length; i++) { const dz = elevs[i] - elevs[i - 1]; if (dz > 0) ascent += dz; else descent -= dz; }
  return { totalSec, cumSec, ascent: Math.round(ascent), descent: Math.round(descent), hasElev };
};

// seconds from start to an arbitrary along-track distance, interpolated from cumDist/cumSec.
window.vrTimeAlong = function (alongM, cumDist, cumSec) {
  if (!cumSec || !cumDist || cumSec.length !== cumDist.length || cumDist.length < 2) return null;
  let i = 0; while (i < cumDist.length - 1 && cumDist[i + 1] < alongM) i++;
  const segLen = cumDist[i + 1] - cumDist[i] || 1;
  const f = Math.max(0, Math.min(1, (alongM - cumDist[i]) / segLen));
  return cumSec[i] + f * ((cumSec[i + 1] != null ? cumSec[i + 1] : cumSec[i]) - cumSec[i]);
};

// ── DEM fetch ────────────────────────────────────────────────────────────────────
const _elevCache = {};
function elSig(coords) {
  const n = coords.length; if (!n) return '';
  const out = [];
  for (let k = 0; k <= 6; k++) { const p = coords[Math.min(n - 1, Math.round(k / 6 * (n - 1)))]; out.push(p[0].toFixed(4) + ',' + p[1].toFixed(4)); }
  return out.join('|') + '#' + n;
}
function isLocal() {
  try { const h = location.hostname; return h === 'localhost' || h === '127.0.0.1' || h === '' || h === '0.0.0.0'; } catch (e) { return false; }
}
async function fetchOpenElevation(pts) {
  const body = JSON.stringify({ locations: pts.map((p) => ({ latitude: p[0], longitude: p[1] })) });
  const res = await fetch('https://api.open-elevation.com/api/v1/lookup', {
    method: 'POST', headers: { 'Content-Type': 'application/json' }, body,
  });
  if (!res.ok) throw new Error('open-elevation ' + res.status);
  const j = await res.json();
  const out = (j.results || []).map((r) => r.elevation);
  if (out.length !== pts.length) throw new Error('open-elevation length');
  return out;
}
async function fetchOpenTopoData(pts) {
  const locs = pts.map((p) => `${p[0].toFixed(5)},${p[1].toFixed(5)}`).join('|');   // <=100 pts/req
  const res = await fetch(`https://api.opentopodata.org/v1/aster30m?locations=${encodeURIComponent(locs)}`);
  if (!res.ok) throw new Error('opentopodata ' + res.status);
  const j = await res.json();
  const out = (j.results || []).map((r) => r.elevation);
  if (out.length !== pts.length) throw new Error('opentopodata length');
  return out;
}

// real elevation for every vertex of `coords`, or null if unavailable. Subsamples to <=100 points,
// fetches, then linearly interpolates the sampled heights back onto every vertex.
window.vrElevationProfile = async function (coords) {
  if (!coords || coords.length < 2) return null;
  if (isLocal()) return null;                       // keep local dev / headless deterministic
  const sig = elSig(coords);
  if (_elevCache[sig] !== undefined) return _elevCache[sig];

  const N = coords.length, cap = 100;
  const idxs = [];
  if (N <= cap) { for (let i = 0; i < N; i++) idxs.push(i); }
  else { for (let k = 0; k < cap; k++) idxs.push(Math.round(k / (cap - 1) * (N - 1))); }
  const uniq = [...new Set(idxs)].sort((a, b) => a - b);
  const sampled = uniq.map((i) => coords[i]);

  let heights = null;
  try { heights = await fetchOpenElevation(sampled); }
  catch (e1) { try { heights = await fetchOpenTopoData(sampled); } catch (e2) { heights = null; } }
  if (!heights) { _elevCache[sig] = null; return null; }

  // scatter sampled heights onto their vertices, then fill the gaps by linear interpolation
  const elevs = new Array(N).fill(null);
  uniq.forEach((vi, s) => { elevs[vi] = heights[s]; });
  for (let s = 0; s < uniq.length - 1; s++) {
    const a = uniq[s], b = uniq[s + 1], ha = elevs[a], hb = elevs[b];
    for (let i = a + 1; i < b; i++) elevs[i] = ha + (hb - ha) * ((i - a) / (b - a));
  }
  if (elevs[0] == null) elevs[0] = heights[0];
  if (elevs[N - 1] == null) elevs[N - 1] = heights[heights.length - 1];

  _elevCache[sig] = elevs;
  return elevs;
};

// Enrich a built route object IN PLACE with real elevation: per route, fetch the DEM profile,
// recompute graded timing, and set elev / time / etaMin / arrival / cumSec / elevs. Returns the
// same object (mutated). No-op on localhost (returns the object unchanged). Callers setRoute()
// afterwards to re-render with the real numbers.
window.vrEnrichRouteElevation = async function (route) {
  if (!route || !route.routes) return route;
  await Promise.all(route.routes.map(async (r) => {
    try {
      const elevs = await window.vrElevationProfile(r.coords);
      if (!elevs) return;
      const t = window.vrRouteTiming(r.coords, r.steps, elevs);
      r.elevs = elevs;
      r.cumSec = t.cumSec; r.totalSec = t.totalSec;
      r.elev = '+' + t.ascent + ' м';
      r.etaMin = Math.max(1, Math.round(t.totalSec / 60));
      r.time = String(r.etaMin);
      if (window.vrArrivalAt) r.arrival = window.vrArrivalAt(r.etaMin);
    } catch (e) { /* leave the flat-model values in place */ }
  }));
  return route;
};
