// qiaopi-store.jsx — Persistence layer for community-submitted letters.
//
// Dual-mode, by design:
//   • Deployed on Cloudflare Pages with the D1-backed Function bound
//     (/api/community-letters) → public letters are shared across all visitors.
//   • No backend reachable (local `python -m http.server`, or before the D1
//     binding is set up) → falls back to this browser's localStorage so the
//     site keeps working.
//
// "private" (kept-on-my-device) letters are NEVER sent to the server; they
// live only in localStorage on the device that wrote them.

const COMMUNITY_KEY = 'qiaopi.community.v1';
const API = '/api/community-letters';

// ── localStorage helpers (fallback / private store) ──
function _localAll() {
  try {
    const raw = window.localStorage.getItem(COMMUNITY_KEY);
    const arr = raw ? JSON.parse(raw) : [];
    return Array.isArray(arr) ? arr : [];
  } catch (e) {
    console.warn('[qiaopi-store] local load failed', e);
    return [];
  }
}
function _localSave(letter) {
  try {
    window.localStorage.setItem(COMMUNITY_KEY, JSON.stringify([letter, ..._localAll()]));
  } catch (e) {
    console.error('[qiaopi-store] local save failed', e);
  }
}

// ── backend helpers ──
async function _backendGet() {
  try {
    const res = await fetch(API, { headers: { 'Accept': 'application/json' } });
    if (!res.ok) return null;            // 404 (no Function) / 503 (no binding)
    const arr = await res.json();
    return Array.isArray(arr) ? arr : null;
  } catch (e) {
    return null;                          // offline / not deployed
  }
}
async function _backendPost(letter) {
  try {
    const res = await fetch(API, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(letter),
    });
    return res.ok;
  } catch (e) {
    return false;
  }
}

// Public, shared letters. Tries the backend first; falls back to localStorage.
// Async — callers await it.
async function loadPublicCommunityLetters() {
  const backend = await _backendGet();
  if (backend) return backend.filter((l) => !l.private);
  return _localAll().filter((l) => !l.private);
}

// Persist one letter. Public letters go to the backend (with a localStorage
// mirror as offline cache); private letters stay on this device only.
// Async — returns { ok, where }.
async function saveCommunityLetter(letter) {
  if (letter.private) {
    _localSave(letter);
    return { ok: true, where: 'local-private' };
  }
  const ok = await _backendPost(letter);
  _localSave(letter);   // mirror locally so this device sees it even if backend read lags / is absent
  return { ok: true, where: ok ? 'backend' : 'local-fallback' };
}

// Stable-ish unique id for a community letter.
function makeCommunityId() {
  return 'qp-net-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
}

// ─── Geocoding ─────────────────────────────────────────────────────────
// Community letters carry free-text city names. To plot them on the same
// 信路 map as the historical archive we need {lng,lat}. Strategy:
//   1) reuse coords already present in the historical QIAOPI dataset
//      (covers the canonical 南洋↔闽粤 places, zero maintenance);
//   2) fall back to OpenStreetMap's free Nominatim geocoder for anything
//      not in the archive. Letters that resolve to nothing stay in the
//      index list but are simply not drawn on the map.

let _cityCoordIndex = null;
function buildCityCoordIndex() {
  if (_cityCoordIndex) return _cityCoordIndex;
  const idx = {};
  const add = (name, lng, lat) => {
    const k = (name || '').trim();
    if (!k || lng == null || lat == null) return;
    if (!idx[k]) idx[k] = { lng, lat };
  };
  const src = (typeof QIAOPI !== 'undefined' && Array.isArray(QIAOPI)) ? QIAOPI : [];
  for (const l of src) {
    if (l.from) { add(l.from.city, l.from.lng, l.from.lat); add(l.from.cityEn, l.from.lng, l.from.lat); add(l.from.country, l.from.lng, l.from.lat); }
    if (l.to) { add(l.to.city, l.to.lng, l.to.lat); add(l.to.region, l.to.lng, l.to.lat); add(l.to.cityEn, l.to.lng, l.to.lat); add(l.to.regionEn, l.to.lng, l.to.lat); }
  }
  _cityCoordIndex = idx;
  return idx;
}

// Distinct Chinese place names from the archive — used to seed a <datalist>.
function knownCityNames() {
  const idx = buildCityCoordIndex();
  return Object.keys(idx).filter((k) => /[一-鿿]/.test(k)).sort();
}

// Look a name up in the archive-derived index: exact match first, then a
// substring match — but only on keys ≥3 chars (so province-level keys like
// "广东"/"福建" don't shadow a specific county) and preferring the LONGEST
// matching key. Anything coarser is left for the precise online geocoder.
function lookupCityCoords(name) {
  const k = (name || '').trim();
  if (!k) return null;
  const idx = buildCityCoordIndex();
  if (idx[k]) return idx[k];
  let best = null, bestLen = 0;
  for (const key of Object.keys(idx)) {
    if (key.length < 3) continue;
    if (key.includes(k) || k.includes(key)) {
      if (key.length > bestLen) { best = idx[key]; bestLen = key.length; }
    }
  }
  return best;
}

// Online fallback via OpenStreetMap Nominatim. Global (no region bias — the
// community map is a world map). Returns {lng,lat} | null; never throws.
async function geocodeCity(name) {
  const q = (name || '').trim();
  if (!q) return null;
  try {
    const ctrl = new AbortController();
    const t = setTimeout(() => ctrl.abort(), 6000);
    const url = 'https://nominatim.openstreetmap.org/search?format=json&limit=1'
      + '&accept-language=zh&q=' + encodeURIComponent(q);
    const res = await fetch(url, { signal: ctrl.signal, headers: { 'Accept': 'application/json' } });
    clearTimeout(t);
    if (!res.ok) return null;
    const arr = await res.json();
    if (Array.isArray(arr) && arr[0]) {
      return { lng: parseFloat(arr[0].lon), lat: parseFloat(arr[0].lat) };
    }
  } catch (e) {
    console.warn('[qiaopi-store] geocode failed for', name, e);
  }
  return null;
}

// Fill a letter's from/to coords in place (archive lookup → online fallback).
// Returns the same letter. Safe to await; failures leave coords null.
async function resolveLetterCoords(letter) {
  const sides = [
    { obj: letter.from, names: [letter.from && letter.from.city, letter.from && letter.from.country] },
    { obj: letter.to, names: [letter.to && letter.to.city, letter.to && letter.to.region] },
  ];
  for (const { obj, names } of sides) {
    if (!obj || (obj.lng != null && obj.lat != null)) continue;
    let hit = null;
    // 1) archive table (exact then substring)
    for (const n of names) { hit = lookupCityCoords(n); if (hit) break; }
    // 2) online: try the most specific query first ("城市 国家"), then each part
    if (!hit) {
      const present = names.filter(Boolean);
      const queries = present.length > 1 ? [present.join(' '), ...present] : present;
      for (const q of queries) { hit = await geocodeCity(q); if (hit) break; }
    }
    if (hit) { obj.lng = hit.lng; obj.lat = hit.lat; }
  }
  return letter;
}
