// App — root component. State, URL-sync, API-fetches, layout.
//
// Drie data-stromen, alle drie gedebounced en cancelbaar:
//   - listFetch   (gepagineerd, 50/pg, met bbox als filters.binnen=true)
//   - pinsFetch   (gecapt op 10k, GEEN bbox — kaart toont alle filter-matches)
//   - facetsFetch (filter-counters per groep)
// Plus `stats` (één keer op mount) en `detail` (per selectedId).

const { useState, useEffect, useMemo, useDeferredValue, useCallback, useRef } = React;

const LIMIT = 50;
const PIN_CAP = 10000;

function App() {
  // ----- initial state from URL -----------------------------------------
  const initial = urlState.parse();

  const [q, setQ] = useState(initial.q);
  // useDeferredValue stelt typing-bursts uit terwijl React rendert; daarna
  // gaat de waarde nog door de 250ms debounce van debouncedFilters.
  const deferredQ = useDeferredValue(q);

  const [filters, setFilters] = useState({
    q: initial.q,
    tb: initial.tb,
    ac: initial.ac,
    bg: initial.bg,
    org: initial.org,
    th: initial.th,
    vanaf: initial.vanaf,
    totd: initial.totd,
    geom: initial.geom,
    ontv: initial.ontv,
    zaak: initial.zaak,
    binnen: initial.binnen,
  });

  useEffect(() => {
    setFilters((f) => (f.q === deferredQ ? f : { ...f, q: deferredQ }));
  }, [deferredQ]);

  // Debounce: 250ms voor de hele filter-state. q heeft hierbovenop al
  // useDeferredValue, dus typen voelt nog steeds responsief maar de API
  // ziet ongeveer 350ms stilte voordat hij geraakt wordt.
  const debouncedFilters = useDebouncedValue(filters, 250);

  const [selectedId, setSelectedId] = useState(initial.id);
  const [viewport, setViewport] = useState(initial.kaart);
  const [bounds, setBounds] = useState(null);
  const [sortBy, setSortBy] = useState('datum');
  const [page, setPage] = useState(0);
  const [fitTrigger, setFitTrigger] = useState(0);
  const [mobileTab, setMobileTab] = useState('map');

  // ----- server-data state ---------------------------------------------
  const [listRecords, setListRecords] = useState([]);
  const [listTotal, setListTotal] = useState(0);
  const [listLoading, setListLoading] = useState(false);
  const [pins, setPins] = useState([]);
  const [pinsTruncated, setPinsTruncated] = useState(false);
  const [pinsTotalMatching, setPinsTotalMatching] = useState(0);
  const [pinsCap, setPinsCap] = useState(PIN_CAP);
  const [facets, setFacets] = useState(null);
  const [facetsLoading, setFacetsLoading] = useState(false);
  const [stats, setStats] = useState(null);
  const [selectedRec, setSelectedRec] = useState(null);
  const [detailLoading, setDetailLoading] = useState(false);
  const [error, setError] = useState(null);

  // ----- bbox-string (alleen voor /vergunningen, niet voor /pins) ------
  // In een memo zodat opnieuw-fetchen alleen bij echte bounds-verandering
  // gebeurt; anders zou elke moveend de string-identity vernieuwen.
  const bboxStr = useMemo(() => {
    if (!debouncedFilters.binnen || !bounds) return null;
    return [
      bounds.getWest().toFixed(4),
      bounds.getSouth().toFixed(4),
      bounds.getEast().toFixed(4),
      bounds.getNorth().toFixed(4),
    ].join(',');
  }, [debouncedFilters.binnen, bounds]);

  // Reset naar pagina 0 wanneer filters of sort wijzigen.
  useEffect(() => {
    setPage(0);
  }, [debouncedFilters, bboxStr, sortBy]);

  // ----- stats (mount-only) --------------------------------------------
  useEffect(() => {
    const ac = new AbortController();
    api.fetchStats({ signal: ac.signal })
      .then(setStats)
      .catch((e) => {
        if (e.name !== 'AbortError') console.warn('stats failed', e);
      });
    return () => ac.abort();
  }, []);

  // ----- list ----------------------------------------------------------
  useEffect(() => {
    const ac = new AbortController();
    setListLoading(true);
    api.fetchList(debouncedFilters, {
      limit: LIMIT,
      offset: page * LIMIT,
      sort: sortBy,
      bbox: bboxStr,
      signal: ac.signal,
    })
      .then((r) => {
        setListRecords(r.records || []);
        setListTotal(r.total || 0);
        setError(null);
      })
      .catch((e) => {
        if (e.name === 'AbortError') return;
        console.warn('list failed', e);
        setError(e.message);
      })
      .finally(() => setListLoading(false));
    return () => ac.abort();
  }, [debouncedFilters, bboxStr, sortBy, page]);

  // ----- pins (no bbox, no page) ---------------------------------------
  useEffect(() => {
    const ac = new AbortController();
    api.fetchPins(debouncedFilters, { cap: PIN_CAP, signal: ac.signal })
      .then((r) => {
        setPins(r.pins || []);
        setPinsTruncated(!!r.truncated);
        setPinsTotalMatching(r.total_matching || 0);
        setPinsCap(r.cap || PIN_CAP);
      })
      .catch((e) => {
        if (e.name !== 'AbortError') console.warn('pins failed', e);
      });
    return () => ac.abort();
  }, [debouncedFilters]);

  // ----- facets --------------------------------------------------------
  useEffect(() => {
    const ac = new AbortController();
    setFacetsLoading(true);
    api.fetchFacets(debouncedFilters, { signal: ac.signal })
      .then(setFacets)
      .catch((e) => {
        if (e.name !== 'AbortError') console.warn('facets failed', e);
      })
      .finally(() => setFacetsLoading(false));
    return () => ac.abort();
  }, [debouncedFilters]);

  // ----- detail --------------------------------------------------------
  useEffect(() => {
    if (!selectedId) { setSelectedRec(null); return; }
    const ac = new AbortController();
    setDetailLoading(true);
    setSelectedRec(null);
    api.fetchDetail(selectedId, { signal: ac.signal })
      .then(setSelectedRec)
      .catch((e) => {
        if (e.name === 'AbortError') return;
        console.warn('detail failed', e);
        if (/^404\b/.test(e.message)) setSelectedId(null);
      })
      .finally(() => setDetailLoading(false));
    return () => ac.abort();
  }, [selectedId]);

  // ----- URL sync ------------------------------------------------------
  useEffect(() => {
    urlState.write({
      ...filters,
      id: selectedId,
      kaart: viewport,
    });
  }, [filters, selectedId, viewport]);

  const handleViewport = useCallback((vp) => {
    setBounds(vp.bounds);
    setViewport({ lat: vp.lat, lon: vp.lon, zoom: vp.zoom });
  }, []);

  const handleReset = () => {
    setFilters({
      q: '', tb: [], ac: [], bg: [], org: [], th: [],
      vanaf: null, totd: null, geom: false, ontv: false, zaak: '', binnen: true,
    });
    setQ('');
  };

  const mapCenter = viewport ? { lat: viewport.lat, lon: viewport.lon } : { lat: 52.15, lon: 5.3 };

  // ---------- Render ----------------------------------------------------
  return (
    <div className="h-screen w-screen flex flex-col bg-[#FAFAF7] text-slate-900 font-sans">
      <Header
        q={q}
        onQ={setQ}
        totaalLandelijk={stats ? stats.total : 0}
        laatsteUpdate={stats ? (stats.last_ingest || stats.last_publicatie) : null}
      />

      {/* Mobile tab bar (hidden ≥ md) */}
      <div className="md:hidden flex border-b border-slate-200 bg-white">
        {[
          { k: 'filters', l: 'Filters' },
          { k: 'map',     l: 'Kaart' },
          { k: 'list',    l: 'Lijst' },
        ].map((t) => (
          <button
            key={t.k}
            onClick={() => setMobileTab(t.k)}
            className={`flex-1 py-2 text-[13px] border-b-2 ${
              mobileTab === t.k
                ? 'border-emerald-900 text-emerald-900 font-medium'
                : 'border-transparent text-slate-600'
            }`}
          >
            {t.l}
          </button>
        ))}
      </div>

      {/* Main 3-column layout */}
      <div className="flex-1 flex min-h-0">
        {/* Filters */}
        <div className={`${mobileTab === 'filters' ? 'flex' : 'hidden'} md:flex`}>
          <FilterPanel
            state={filters}
            setState={setFilters}
            facets={facets}
            facetsLoading={facetsLoading}
            filteredCount={listTotal}
            onReset={handleReset}
          />
        </div>

        {/* Center: Map + List */}
        <main className={`${mobileTab === 'filters' ? 'hidden' : 'flex'} md:flex flex-1 flex-col min-w-0`}>
          <div className={`${mobileTab === 'list' ? 'hidden' : 'block'} md:block flex-1 min-h-0 relative`}>
            <MapView
              pins={pins}
              truncated={pinsTruncated}
              totalMatching={pinsTotalMatching}
              cap={pinsCap}
              selectedId={selectedId}
              selectedRec={selectedRec}
              onSelect={setSelectedId}
              onViewportChange={handleViewport}
              initialViewport={viewport}
              fitTrigger={fitTrigger}
            />
            {/* Floating: "alleen binnen kaartbeeld" status */}
            {filters.binnen && (
              <button
                onClick={() => setFitTrigger((n) => n + 1)}
                className="absolute top-3 left-3 bg-white/95 backdrop-blur-sm border border-slate-200 rounded-md px-3 py-1.5 text-[12px] text-slate-700 hover:bg-white hover:border-slate-300 shadow-sm flex items-center gap-2 focus:outline-none focus:ring-2 focus:ring-emerald-900/20"
              >
                <span className="w-1.5 h-1.5 rounded-full bg-emerald-900"></span>
                <span className="tabular-nums">{fmt.number(listTotal)}</span>
                <span className="text-slate-500">in beeld · pas aan op alle</span>
              </button>
            )}
          </div>
          <div className={`${mobileTab === 'map' ? 'hidden' : 'flex'} md:flex h-[48%] min-h-[300px] shrink-0`}>
            <ResultList
              records={listRecords}
              total={listTotal}
              limit={LIMIT}
              offset={page * LIMIT}
              loading={listLoading}
              onPrev={() => setPage((p) => Math.max(0, p - 1))}
              onNext={() => setPage((p) => p + 1)}
              selectedId={selectedId}
              onSelect={setSelectedId}
              mapCenter={mapCenter}
              sortBy={sortBy}
              setSortBy={setSortBy}
            />
          </div>
        </main>

        {/* Detail panel — slide-in */}
        {selectedId && (
          <div className="hidden lg:flex">
            <DetailPanel rec={selectedRec} loading={detailLoading} onClose={() => setSelectedId(null)} />
          </div>
        )}
        {/* On mobile/tablet: detail as overlay */}
        {selectedId && (
          <div
            className="lg:hidden fixed inset-0 bg-black/30 z-30"
            onClick={() => setSelectedId(null)}
          >
            <div
              className="absolute right-0 top-0 bottom-0 w-[400px] max-w-[92vw] bg-white shadow-xl"
              onClick={(e) => e.stopPropagation()}
            >
              <DetailPanel rec={selectedRec} loading={detailLoading} onClose={() => setSelectedId(null)} />
            </div>
          </div>
        )}
      </div>

      {/* Niet-blokkerende fout-toast rechtsonder */}
      {error && (
        <div className="fixed bottom-3 right-3 z-50 bg-white border border-red-300 text-red-900 text-[12px] px-3 py-2 rounded-md shadow-sm max-w-md">
          <div className="font-medium mb-0.5">API-fout</div>
          <div className="text-red-700">{error}</div>
        </div>
      )}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
