// ===== PARC-EX CARTE — Main App =====
var _React = React;
var useState  = _React.useState;
var useRef    = _React.useRef;
var useEffect = _React.useEffect;
var useMemo   = _React.useMemo;
var useCallback = _React.useCallback;

/* ---- Tweak defaults ---- */
var TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "mapStyle": "light",
  "walkSpeed": "normal"
}/*EDITMODE-END*/;

var TILE_URLS = {
  light: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
  dark:  'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
};

var WALK_SPEEDS = { slow: 60, normal: 80, fast: 100 }; // metres per minute

/* ---- API helper ----
   Shared sync is ON via a same-origin Cloudflare Pages Function at /api/state
   (functions/api/state.js, backed by Workers KV). API_BASE is empty so calls are
   relative to wherever the site is served. If the endpoint is unreachable (e.g.
   plain `npx serve` with no Functions), the app falls back to localStorage. */
var API_BASE = (function () {
  try {
    var meta = document.querySelector('meta[name="api-base"]');
    if (meta && meta.content) return meta.content.replace(/\/$/, '');
  } catch (e) {}
  return '';
})();

function api(endpoint, opts) {
  var url = API_BASE + endpoint;
  var options = Object.assign({ headers: { 'Content-Type': 'application/json' } }, opts || {});
  if (options.body && typeof options.body !== 'string') {
    options.body = JSON.stringify(options.body);
  }
  return fetch(url, options)
    .then(function (r) {
      if (!r.ok) throw new Error('API ' + r.status);
      return r.json();
    });
}

var _syncEnabled = true;

function App() {
  var _tw = useTweaks(TWEAK_DEFAULTS);
  var tweaks = _tw[0], setTweak = _tw[1];
  var device = useDevice();

  /* ---- persisted state helpers ---- */
  function loadJSON(key, fallback) {
    try { var s = localStorage.getItem(key); return s ? JSON.parse(s) : fallback; }
    catch (e) { return fallback; }
  }

  /* ---- state ---- */
  var [places, setPlaces]       = useState(function () { return loadJSON('parcex-places', JSON.parse(JSON.stringify(INITIAL_PLACES))); });
  var [customCats, setCustCats] = useState(function () { return loadJSON('parcex-cats', []); });
  var [visited, setVisited]     = useState(function () { return new Set(loadJSON('parcex-visited', [])); });
  var [lang, setLang]           = useState(function () { return localStorage.getItem('parcex-lang') || 'fr'; });
  var [radius, setRadius]       = useState(function () { return parseInt(localStorage.getItem('parcex-radius')) || 2000; });
  var [filter, setFilter]       = useState('all');
  var [search, setSearch]       = useState('');
  var [editMode, setEditMode]   = useState(false);
  var [formOpen, setFormOpen]   = useState(false);
  var [editingId, setEditingId] = useState(null);
  var [catForm, setCatForm]     = useState(false);
  var [mapVis, setMapVis]       = useState(true);
  var [activeId, setActiveId]   = useState(null);
  var [geocoding, setGeocoding] = useState(false);
  var [synced, setSynced]       = useState(false);
  var [syncErr, setSyncErr]     = useState(false);
  var [sortMode, setSortMode]   = useState('cat'); // 'cat' or 'dist'
  var [darkMode, setDarkMode]   = useState(function () { return localStorage.getItem('parcex-dark') === 'true'; });
  var [panelOpen, setPanelOpen] = useState(true);

  /* ---- Load from API on mount ---- */
  useEffect(function () {
    if (!_syncEnabled) { setSynced(true); return; }
    api('/api/state')
      .then(function (data) {
        if (data.places && data.places.length) setPlaces(data.places);
        if (data.categories) setCustCats(data.categories);
        if (data.visited) setVisited(new Set(data.visited));
        if (data.settings) {
          if (data.settings.lang) setLang(data.settings.lang);
          if (data.settings.radius) setRadius(data.settings.radius);
        }
        setSynced(true);
        setSyncErr(false);
      })
      .catch(function () {
        setSynced(true);
        setSyncErr(true);
      });
  }, []);

  /* ---- Sync to API on changes (debounced) ---- */
  var syncTimer = useRef(null);
  useEffect(function () {
    if (!_syncEnabled || !synced) return;
    if (syncTimer.current) clearTimeout(syncTimer.current);
    syncTimer.current = setTimeout(function () {
      api('/api/state', {
        method: 'PUT',
        body: {
          places: places,
          categories: customCats,
          visited: Array.from(visited),
          settings: { lang: lang, radius: radius }
        }
      }).then(function () { setSyncErr(false); })
        .catch(function () { setSyncErr(true); });
    }, 800);
    return function () { if (syncTimer.current) clearTimeout(syncTimer.current); };
  }, [places, customCats, visited, lang, radius, synced]);

  /* ---- map refs ---- */
  var mapElRef   = useRef(null);
  var mapRef     = useRef(null);
  var markersRef = useRef({});
  var circleRef  = useRef(null);
  var tileRef    = useRef(null);
  var tempMkRef  = useRef(null);
  var formRef    = useRef(false); // tracks formOpen for map click

  /* ---- derived ---- */
  var allCats = useMemo(function () { return DEFAULT_CATS.concat(customCats); }, [customCats]);
  var catMap  = useMemo(function () { var m = {}; allCats.forEach(function (c) { m[c.id] = c; }); return m; }, [allCats]);
  var t = UI_TEXT[lang];

  var searchFiltered = useMemo(function () {
    if (!search) return places;
    var q = normalizeStr(search);
    return places.filter(function (p) {
      return normalizeStr(p.name).indexOf(q) !== -1 ||
             normalizeStr(p.note || '').indexOf(q) !== -1 ||
             normalizeStr(p.addr).indexOf(q) !== -1;
    });
  }, [places, search]);

  var visible = useMemo(function () {
    if (filter === 'all') return searchFiltered;
    return searchFiltered.filter(function (p) { return p.cat === filter; });
  }, [searchFiltered, filter]);

  var counts = useMemo(function () {
    var c = {};
    searchFiltered.forEach(function (p) { c[p.cat] = (c[p.cat] || 0) + 1; });
    return c;
  }, [searchFiltered]);

  var walkSpeedVal = WALK_SPEEDS[tweaks.walkSpeed] || 80;

  function getWalkMin(p) {
    if (!p.lat || !p.lng) return null;
    return Math.max(1, Math.round(haversine(HOME_COORDS.lat, HOME_COORDS.lng, p.lat, p.lng) / walkSpeedVal));
  }
  function getDist(p) {
    if (!p.lat || !p.lng) return Infinity;
    return haversine(HOME_COORDS.lat, HOME_COORDS.lng, p.lat, p.lng);
  }

  /* ---- persistence ---- */
  useEffect(function () { try { localStorage.setItem('parcex-places', JSON.stringify(places)); } catch(e){} }, [places]);
  useEffect(function () { try { localStorage.setItem('parcex-cats', JSON.stringify(customCats)); } catch(e){} }, [customCats]);
  useEffect(function () { try { localStorage.setItem('parcex-visited', JSON.stringify(Array.from(visited))); } catch(e){} }, [visited]);
  useEffect(function () { try { localStorage.setItem('parcex-lang', lang); } catch(e){} }, [lang]);
  useEffect(function () { try { localStorage.setItem('parcex-radius', String(radius)); } catch(e){} }, [radius]);
  useEffect(function () { try { localStorage.setItem('parcex-dark', darkMode ? 'true' : 'false'); } catch(e){} }, [darkMode]);

  /* Apply dark mode class to html */
  useEffect(function () {
    document.documentElement.classList.toggle('dark', darkMode);
  }, [darkMode]);

  /* ---- map init ---- */
  useEffect(function () {
    if (!mapElRef.current || mapRef.current) return;
    var map = L.map(mapElRef.current, { zoomControl: false, attributionControl: false })
               .setView([HOME_COORDS.lat, HOME_COORDS.lng], 15);
    L.control.zoom({ position: 'topright' }).addTo(map);

    tileRef.current = L.tileLayer(TILE_URLS[tweaks.mapStyle] || TILE_URLS.light, {
      maxZoom: 20, subdomains: 'abcd'
    }).addTo(map);

    L.marker([HOME_COORDS.lat, HOME_COORDS.lng], {
      icon: L.divIcon({ className: '', html: '<div class="home-mk"></div>', iconSize: [20, 20], iconAnchor: [10, 10] }),
      zIndexOffset: 1000
    }).addTo(map).bindPopup('<b>' + UI_TEXT.fr.yourApt + ' / ' + UI_TEXT.en.yourApt + '</b><br>' + HOME_ADDR);

    map.on('click', function (e) {
      if (!formRef.current) return;
      window.dispatchEvent(new CustomEvent('map-coord-pick', { detail: { lat: e.latlng.lat, lng: e.latlng.lng } }));
      if (tempMkRef.current) map.removeLayer(tempMkRef.current);
      tempMkRef.current = L.marker(e.latlng, {
        icon: L.divIcon({
          className: '',
          html: '<div style="width:14px;height:14px;background:#3b82f6;border-radius:50%;border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.3);opacity:.7"></div>',
          iconSize: [14, 14], iconAnchor: [7, 7]
        })
      }).addTo(map);
    });

    mapRef.current = map;
  }, []);

  /* keep formRef in sync */
  useEffect(function () { formRef.current = formOpen; }, [formOpen]);

  /* pick-mode cursor */
  useEffect(function () {
    if (!mapElRef.current) return;
    if (formOpen) mapElRef.current.classList.add('pick-mode');
    else mapElRef.current.classList.remove('pick-mode');
  }, [formOpen]);

  /* tile swap on tweak change */
  useEffect(function () {
    if (!mapRef.current || !tileRef.current) return;
    var url = TILE_URLS[tweaks.mapStyle] || TILE_URLS.light;
    tileRef.current.setUrl(url);
  }, [tweaks.mapStyle]);

  /* radius circle */
  useEffect(function () {
    if (!mapRef.current) return;
    if (circleRef.current) mapRef.current.removeLayer(circleRef.current);
    circleRef.current = L.circle([HOME_COORDS.lat, HOME_COORDS.lng], {
      radius: radius,
      color: 'rgba(59,130,246,.25)',
      fillColor: 'rgba(59,130,246,.04)',
      fillOpacity: 1, weight: 1.5, dashArray: '6 4'
    }).addTo(mapRef.current);
  }, [radius]);

  /* markers */
  useEffect(function () {
    if (!mapRef.current) return;
    Object.values(markersRef.current).forEach(function (m) { mapRef.current.removeLayer(m); });
    markersRef.current = {};
    visible.forEach(function (p) {
      if (!p.lat || !p.lng) return;
      var c = catMap[p.cat] || { color: '#999' };
      var m = L.marker([p.lat, p.lng], {
        icon: L.divIcon({
          className: '',
          html: '<div style="width:22px;height:22px;background:' + c.color + ';border-radius:50%;border:2.5px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,.25);box-sizing:border-box"></div>',
          iconSize: [22, 22], iconAnchor: [11, 11]
        })
      }).addTo(mapRef.current);
      var sa = p.addr.replace(', Montréal', '');
      var gl = 'https://www.google.com/maps/dir/?api=1&destination=' + encodeURIComponent(p.addr);
      m.bindPopup(
        '<div class="pu-name">' + esc(p.name) + '</div>' +
        '<div class="pu-cat" style="color:' + c.color + '">' + (c[lang] || c.fr) + '</div>' +
        '<div class="pu-addr">' + esc(sa) + '</div>' +
        (p.hours ? '<div class="pu-hours">' + esc(p.hours) + '</div>' : '') +
        (p.note ? '<div class="pu-note">' + esc(p.note) + '</div>' : '') +
        '<a class="pu-dir" href="' + gl + '" target="_blank">' + t.directions + '</a>'
      );
      m.on('click', function () { setActiveId(p.id); });
      markersRef.current[p.id] = m;
    });
  }, [visible, lang, catMap]);

  /* map resize */
  useEffect(function () {
    if (mapVis && mapRef.current) setTimeout(function () { mapRef.current.invalidateSize(); }, 120);
  }, [mapVis]);

  /* fit bounds on filter change */
  useEffect(function () {
    if (!mapRef.current) return;
    var pts = visible.filter(function (p) { return p.lat && p.lng; });
    if (!pts.length) { mapRef.current.setView([HOME_COORDS.lat, HOME_COORDS.lng], 15); return; }
    var bounds = L.latLngBounds(pts.map(function (p) { return [p.lat, p.lng]; }));
    bounds.extend([HOME_COORDS.lat, HOME_COORDS.lng]);
    mapRef.current.fitBounds(bounds, { padding: [30, 30], maxZoom: 16 });
  }, [filter, search]);

  function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }

  function flyTo(p) {
    if (!p.lat || !p.lng) return;
    if (!mapVis) setMapVis(true);
    if (mapRef.current) mapRef.current.flyTo([p.lat, p.lng], 17, { duration: 0.4 });
    if (markersRef.current[p.id]) setTimeout(function () { markersRef.current[p.id] && markersRef.current[p.id].openPopup(); }, 450);
  }

  /* ---- actions ---- */
  function toggleVisit(id) {
    setVisited(function (prev) {
      var next = new Set(prev);
      if (next.has(id)) next.delete(id); else next.add(id);
      return next;
    });
  }

  function handleCard(id) {
    var nid = activeId === id ? null : id;
    setActiveId(nid);
    if (nid) {
      if (!panelOpen) setPanelOpen(true);
      var p = places.find(function (x) { return x.id === id; }); if (p) flyTo(p);
    }
  }

  function handleSave(data) {
    var doSave = function (lat, lng) {
      if (data.id) {
        setPlaces(function (prev) {
          return prev.map(function (p) {
            return p.id === data.id ? Object.assign({}, p, data, { lat: lat, lng: lng }) : p;
          });
        });
      } else {
        var np = Object.assign({}, data, {
          lat: lat, lng: lng,
          id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
        });
        setPlaces(function (prev) { return prev.concat([np]); });
      }
      setFormOpen(false); setEditingId(null);
      if (tempMkRef.current && mapRef.current) { mapRef.current.removeLayer(tempMkRef.current); tempMkRef.current = null; }
    };

    if (data.lat !== null && data.lng !== null) { doSave(data.lat, data.lng); return; }

    /* geocode */
    setGeocoding(true);
    fetch('https://nominatim.openstreetmap.org/search?format=json&q=' + encodeURIComponent(data.addr) + '&limit=1')
      .then(function (r) { return r.json(); })
      .then(function (d) {
        setGeocoding(false);
        if (d.length > 0) doSave(parseFloat(d[0].lat), parseFloat(d[0].lon));
        else doSave(null, null);
      })
      .catch(function () { setGeocoding(false); doSave(null, null); });
  }

  function handleDel(id) {
    if (!confirm(t.confirmDel)) return;
    setPlaces(function (prev) { return prev.filter(function (p) { return p.id !== id; }); });
    if (activeId === id) setActiveId(null);
  }

  function handleReset() {
    if (!confirm(t.confirmReset)) return;
    setPlaces(JSON.parse(JSON.stringify(INITIAL_PLACES)));
    setCustCats([]);
    setVisited(new Set());
  }

  function handleSaveCat(d) {
    if (allCats.find(function (c) { return c.id === d.id; })) d.id = d.id + '-' + Date.now().toString(36).slice(-4);
    setCustCats(function (prev) { return prev.concat([d]); });
    setCatForm(false);
  }

  function handleFilter(f) {
    setFilter(function (prev) { return prev === f && f !== 'all' ? 'all' : f; });
    setActiveId(null);
  }

  /* group visible by category or flat by distance */
  var grouped = useMemo(function () {
    if (sortMode === 'dist') {
      var sorted = visible.slice().sort(function (a, b) { return getDist(a) - getDist(b); });
      return [{ cat: { id: '_all', fr: '', en: '', color: 'transparent', icon: 'pin' }, items: sorted, flat: true }];
    }
    var g = [];
    allCats.forEach(function (cat) {
      var items = visible.filter(function (p) { return p.cat === cat.id; })
        .sort(function (a, b) { return getDist(a) - getDist(b); });
      if (items.length) g.push({ cat: cat, items: items });
    });
    return g;
  }, [visible, allCats, tweaks.walkSpeed, sortMode]);

  /* Escape key */
  useEffect(function () {
    var handler = function (e) { if (e.key === 'Escape') { setFormOpen(false); setCatForm(false); } };
    document.addEventListener('keydown', handler);
    return function () { document.removeEventListener('keydown', handler); };
  }, []);

  /* ---- render ---- */
  return (
    <React.Fragment>
      <header className="hdr">
        <div className="hdr-top">
          <div>
            <div className="hdr-title">{t.appTitle}</div>
            <div className="hdr-sub">{t.subtitle}</div>
          </div>
          <div className="hdr-actions">
            {_syncEnabled && (
              <div className={'sync-badge' + (syncErr ? ' err' : '')} title={syncErr ? 'Hors ligne' : 'Synchronisé'}>
                {syncErr ? 'Hors ligne' : 'Sync'}
              </div>
            )}
            <DeviceBadge device={device} lang={lang} />
            <LangToggle lang={lang} onChange={setLang} />
            <button className={'edit-btn' + (editMode ? ' on' : '')}
                    onClick={function () { setEditMode(!editMode); setActiveId(null); }}>
              {editMode ? t.done : t.edit}
            </button>
          </div>
        </div>
        <div className="search-row">
          <input className="search-in" type="text" placeholder={t.search} value={search}
                 onChange={function (e) { setSearch(e.target.value); setActiveId(null); }} autoComplete="off" />
        </div>
        <FilterBar cats={allCats} filter={filter} onFilter={handleFilter} counts={counts}
                   editMode={editMode} onAddCat={function () { setCatForm(true); }} lang={lang} />
      </header>

      <div className={'main' + (!mapVis ? ' no-map' : '') + ' device-' + device + (!panelOpen && device !== 'phone' ? ' panel-col' : '')}>
        {mapVis && (
          <div className="map-panel">
            <div className="map-ct" ref={mapElRef}></div>
          </div>
        )}
        <div className="list-panel">
          <div className="ctrl-strip">
            {device !== 'phone' && (
              <button className="collapse-btn" onClick={function () { setPanelOpen(!panelOpen); }}>
                {panelOpen ? '▶' : '◀'}
              </button>
            )}
            <div className="rad-ctl">
              <span className="rad-lbl">{t.radius}</span>
              <input className="rad-slider" type="range" min="500" max="5000" step="100" value={radius}
                     onChange={function (e) { setRadius(parseInt(e.target.value)); }} />
              <span className="rad-val">{(radius / 1000).toFixed(1)} km</span>
            </div>
            <SortToggle sort={sortMode} onToggle={setSortMode} lang={lang} />
            <button className="map-tog" onClick={function () { setMapVis(!mapVis); }}>
              {mapVis ? t.hideMap : t.showMap}
            </button>
            <button className="dark-tog" onClick={function () { setDarkMode(!darkMode); }}
                    title={darkMode ? t.lightMode : t.darkMode}>
              {darkMode ? t.lightMode : t.darkMode}
            </button>
          </div>
          <div className="list-inner">
            {editMode && (
              <button className="add-btn" onClick={function () { setFormOpen(true); setEditingId(null); }}>
                + {t.addPlace}
              </button>
            )}
            {!visible.length ? (
              <div className="empty-st">{search ? t.noResults : t.empty}</div>
            ) : grouped.map(function (g) {
              return (
                <div key={g.cat.id}>
                  {!g.flat && (
                    <div className="cat-hdr">
                      <span className="cat-icon" style={{ background: g.cat.color + '18' }}>
                        <CatIcon icon={g.cat.icon || 'pin'} color={g.cat.color} size={14} />
                      </span>
                      {g.cat[lang] || g.cat.fr}
                    </div>
                  )}
                  {g.items.map(function (p) {
                    return (
                      <PlaceCard key={p.id} place={p}
                        cat={catMap[p.cat] || { color: '#999', fr: 'Autre', en: 'Other' }}
                        active={activeId === p.id}
                        visited={visited.has(p.id)}
                        editMode={editMode} lang={lang}
                        walkMin={getWalkMin(p)}
                        onClick={function () { if (!editMode) handleCard(p.id); }}
                        onVisit={function () { toggleVisit(p.id); }}
                        onEdit={function () { setEditingId(p.id); setFormOpen(true); }}
                        onDel={function () { handleDel(p.id); }}
                      />
                    );
                  })}
                </div>
              );
            })}
            {editMode && <button className="reset-btn" onClick={handleReset}>{t.reset}</button>}
            <div className="footer">{t.footer}</div>
          </div>
        </div>
      </div>

      <PlaceForm open={formOpen}
        editing={editingId ? places.find(function (p) { return p.id === editingId; }) : null}
        cats={allCats} lang={lang} onSave={handleSave}
        onCancel={function () {
          setFormOpen(false); setEditingId(null);
          if (tempMkRef.current && mapRef.current) { mapRef.current.removeLayer(tempMkRef.current); tempMkRef.current = null; }
        }} />

      <CategoryForm open={catForm} lang={lang} onSave={handleSaveCat}
        onCancel={function () { setCatForm(false); }}
        usedColors={allCats.map(function (c) { return c.color; })} />

      {geocoding && <div className="geo-ov">{t.geocoding}</div>}

      <TweaksPanel>
        <TweakSection label="Map" />
        <TweakRadio label={lang === 'fr' ? 'Style de carte' : 'Map style'}
          value={tweaks.mapStyle}
          options={['light', 'dark']}
          optionLabels={[t.mapLight, t.mapDark]}
          onChange={function (v) { setTweak('mapStyle', v); }} />
        <TweakSection label={lang === 'fr' ? 'Marche' : 'Walking'} />
        <TweakRadio label={lang === 'fr' ? 'Vitesse' : 'Speed'}
          value={tweaks.walkSpeed}
          options={['slow', 'normal', 'fast']}
          optionLabels={[t.walkSlow, t.walkNormal, t.walkFast]}
          onChange={function (v) { setTweak('walkSpeed', v); }} />
      </TweaksPanel>
    </React.Fragment>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App));
