// Shared modal "location picker". Every view's Find button opens this overlay:
// a search box + a Leaflet map. Searching a place jumps the map (and a matching
// result selects it immediately); tapping anywhere on the map drops a pin the user
// can fine-tune before confirming. Picking closes the overlay and hands the chosen
// lat/lon back to the page via the onPick callback.
//
// Leaflet stays a classic script (global L), loaded before the page modules. The
// map itself is created lazily the first time the picker opens (and reused after),
// so pages that never open it pay nothing.
import { uv } from "./account.js";

let overlay, mapEl, map, marker, pin = null, onPickCb = null, pinIcon = null;
let searchForm, searchInput, sugEl, confirmBtn, hintEl;
let sugSeq = 0, sugTimer = 0, sugAbort = null;

export const PIN_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>`;

function build() {
  overlay = document.createElement("div");
  overlay.className = "mp-overlay";
  overlay.hidden = true;
  overlay.innerHTML = `
    <div class="mp-modal" role="dialog" aria-modal="true" aria-label="Pick a location">
      <div class="mp-head">
        <h2>Find a location</h2>
        <button type="button" class="mp-close" aria-label="Close">&times;</button>
      </div>
      <form class="mp-search" autocomplete="off">
        <input type="text" placeholder="Search for a place…" autocomplete="off" />
        <button type="submit">Search</button>
        <ul class="mp-sug" hidden></ul>
      </form>
      <div class="mp-map"></div>
      <div class="mp-foot">
        <span class="mp-hint">Search above, or tap the map to drop a pin.</span>
        <button type="button" class="mp-confirm" disabled>Use this location</button>
      </div>
    </div>`;
  document.body.appendChild(overlay);

  mapEl = overlay.querySelector(".mp-map");
  searchForm = overlay.querySelector(".mp-search");
  searchInput = searchForm.querySelector("input");
  sugEl = overlay.querySelector(".mp-sug");
  confirmBtn = overlay.querySelector(".mp-confirm");
  hintEl = overlay.querySelector(".mp-hint");

  overlay.querySelector(".mp-close").onclick = close;
  // Tapping the dimmed backdrop (outside the modal) closes the picker.
  overlay.addEventListener("pointerdown", (e) => { if (e.target === overlay) close(); });
  confirmBtn.onclick = () => finish(pin);

  // Live suggestions (top 5, single-letter-typo tolerant) as the user types:
  // debounced so a fast typist doesn't fire a request per keystroke, aborted
  // + sequence-guarded so a slow stale response can't overwrite a newer list.
  async function fetchSug(q) {
    const seq = ++sugSeq;
    if (sugAbort) sugAbort.abort();
    sugAbort = new AbortController();
    try {
      const res = await fetch(uv(`suggest?q=${encodeURIComponent(q)}`),
        { signal: sugAbort.signal, credentials: "include" });
      const d = await res.json();
      if (seq === sugSeq) renderSug(d.results || []);
    } catch (err) { /* aborted, or offline — the map stays the fallback way to pick */ }
  }
  searchInput.addEventListener("input", () => {
    clearTimeout(sugTimer);
    // The old list stays visible until fresh results land, but its highlight
    // must go — Enter mid-typing shouldn't pick a suggestion for the shorter
    // query the list still reflects.
    const stale = sugEl.querySelector("li.active");
    if (stale) stale.classList.remove("active");
    const q = searchInput.value.trim();
    if (q.length < 2) { sugEl.hidden = true; return; }
    sugTimer = setTimeout(() => fetchSug(q), 250);
  });
  // Arrow keys walk the list (Enter picks via the submit handler below);
  // Escape dismisses the list before the overlay's Escape-to-close kicks in.
  searchInput.addEventListener("keydown", (e) => {
    if (e.key === "Escape" && !sugEl.hidden) {
      sugEl.hidden = true;
      e.stopPropagation();
      return;
    }
    if (sugEl.hidden || (e.key !== "ArrowDown" && e.key !== "ArrowUp")) return;
    const items = [...sugEl.children];
    if (!items.length) return;
    e.preventDefault();
    const cur = items.findIndex((li) => li.classList.contains("active"));
    const next = e.key === "ArrowDown"
      ? (cur + 1) % items.length
      : (cur <= 0 ? items.length - 1 : cur - 1);
    setActiveSug(items, next);
    items[next].scrollIntoView({ block: "nearest" });
  });
  searchForm.addEventListener("submit", (e) => {
    e.preventDefault();
    clearTimeout(sugTimer);
    // Enter picks the highlighted suggestion when the list is up; otherwise
    // it searches immediately (no debounce wait).
    const active = sugEl.hidden ? null : sugEl.querySelector("li.active");
    if (active) { active.click(); return; }
    const q = searchInput.value.trim();
    if (q) fetchSug(q);
  });
  // Hide the suggestions when tapping elsewhere in the modal (but not the map,
  // which has its own tap handler).
  overlay.addEventListener("pointerdown", (e) => {
    if (!e.target.closest(".mp-search")) sugEl.hidden = true;
  }, true);
  document.addEventListener("keydown", (e) => {
    if (overlay && !overlay.hidden && e.key === "Escape") close();
  });
}

function setActiveSug(items, idx) {
  items.forEach((li, i) => li.classList.toggle("active", i === idx));
}

function renderSug(list) {
  const shown = list;
  sugEl.innerHTML = "";
  if (!shown.length) { sugEl.hidden = true; return; }
  shown.forEach((r) => {
    const li = document.createElement("li");
    li.innerHTML = `${r.name}<span class="sub">, ${[r.admin1, r.country].filter(Boolean).join(", ")}</span>`;
    // A picked search result is an unambiguous choice — drop the pin, recenter,
    // and select it right away (the map is there for manual/fine picks).
    li.onclick = () => {
      sugEl.hidden = true;
      searchInput.value = r.name;
      setPin(r.lat, r.lon, 10);
      finish({ lat: r.lat, lon: r.lon });
    };
    // Keep one highlight: hovering moves it off whatever the arrows chose.
    li.addEventListener("pointerenter", () =>
      setActiveSug([...sugEl.children], [...sugEl.children].indexOf(li)));
    sugEl.appendChild(li);
  });
  // Pre-highlight the top match so Enter picks it straight away.
  setActiveSug([...sugEl.children], 0);
  sugEl.hidden = false;
}

function setPin(lat, lon, zoom) {
  pin = { lat, lon };
  if (marker) marker.setLatLng([lat, lon]);
  else marker = L.marker([lat, lon], pinIcon ? { icon: pinIcon } : undefined).addTo(map);
  map.setView([lat, lon], zoom || map.getZoom());
  confirmBtn.disabled = false;
  hintEl.textContent = "Pin set. Confirm below, or tap elsewhere to move it.";
}

function finish(p) {
  if (!p) return;
  const cb = onPickCb;
  close();
  if (cb) cb(p.lat, p.lon);
}

export function open(cur, onPick) {
  if (!overlay) build();
  onPickCb = onPick;
  pin = null;
  confirmBtn.disabled = true;
  // Cancel any in-flight/pending suggestion fetch from the previous open so
  // a late response can't pop a stale list over the fresh empty box.
  clearTimeout(sugTimer);
  sugSeq++;
  sugEl.hidden = true;
  searchInput.value = "";
  hintEl.textContent = "Search above, or tap the map to drop a pin.";
  overlay.hidden = false;

  if (!map) {
    map = L.map(mapEl, { worldCopyJump: true, zoomControl: true }).setView([25, 0], 2);
    // Colorful CARTO "Voyager" basemap (no key needed), split into two raster
    // layers so road weight and label size can be tuned independently:
    //  • the label-free base is requested one zoom lower and drawn at 512px
    //    (tileSize 512 + zoomOffset -1) so roads read bold and prominent;
    //  • the labels-only layer is drawn on top at its natural size, keeping city
    //    names small and crisp rather than scaled up with the roads.
    // @2x keeps both sharp; a darken/saturate filter (style.css, .mp-base only)
    // deepens the roads while leaving the label text at full contrast.
    const CARTO = "https://{s}.basemaps.cartocdn.com/rastertiles";
    const ATTRIB = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/attributions">CARTO</a>';
    L.tileLayer(`${CARTO}/voyager_nolabels/{z}/{x}/{y}@2x.png`, {
      subdomains: "abcd", maxZoom: 20, tileSize: 512, zoomOffset: -1,
      className: "mp-base", attribution: ATTRIB,
    }).addTo(map);
    L.tileLayer(`${CARTO}/voyager_only_labels/{z}/{x}/{y}@2x.png`, {
      subdomains: "abcd", maxZoom: 20, className: "mp-labels",
    }).addTo(map);
    // Branded accent teardrop pin (the same glyph the Find button uses), styled
    // in style.css — replaces Leaflet's default blue raster marker.
    pinIcon = L.divIcon({
      className: "mp-pin", html: PIN_ICON, iconSize: [32, 32], iconAnchor: [16, 30],
    });
    map.on("click", (e) => setPin(e.latlng.lat, e.latlng.lng));
  }
  // Leaflet measures the container on creation; it's zero-sized while hidden, so
  // recalc once the modal is actually visible and (if reopening on a known spot)
  // drop a pin there as the starting point.
  setTimeout(() => {
    map.invalidateSize();
    if (cur && cur.lat != null && cur.lon != null) setPin(cur.lat, cur.lon, 10);
    searchInput.focus();
  }, 60);
}

function close() { if (overlay) overlay.hidden = true; }

// Wire a page's Find/Add button: pin icon + label, and open the picker on
// click seeded with the page's current spot. `setFindLabel` swaps the label
// (e.g. "Find a location" → "Change location") keeping the icon markup.
export function initFindButton(btn, label, getCurrent, onPick) {
  setFindLabel(btn, label);
  btn.addEventListener("click", () => open(getCurrent(), onPick));
}
export function setFindLabel(btn, label) {
  btn.innerHTML = `${PIN_ICON} <span>${label}</span>`;
}
