import { useCallback, useEffect, useRef, useState } from "react";
import { fetchPosts, fetchSmartFeed, fetchUnifiedSearch, resolveAssetRef } from "../../api/client";
import { categoryCode, matchesSubFilter } from "./mapFilter";
import { getCoords, getViewFromMap, haversineKm } from "./mapGeo";
import { createMarkerForPost, createSimpleMarkerForPost } from "./markerManager";
import { getTemplateKeyForPost } from "./templateSpecs";
import { prioritizePosts } from "./postPriority";
import { trackEvent } from "../../utils/analytics";
import { matchesContentFilter } from "../Filters/ContentFilters";

function getId(p) {
  return p?.id ?? p?._id ?? null;
}

function normalizePostId(post) {
  if (!post || typeof post !== "object") return post;
  const pid = Number(post?.post_id ?? post?.postId ?? post?.postID ?? 0);
  if (Number.isFinite(pid) && pid > 0 && Number(post?.id) !== pid) {
    return { ...post, reco_id: post?.id ?? post?.reco_id, id: pid };
  }
  return post;
}

function getPostKey(p) {
  const id = getId(p);
  if (id != null && id !== 0) return `id:${id}`;
  const urlHash = (p?.url_hash || p?.urlHash || "").toString().trim();
  if (urlHash) return `urlhash:${urlHash}`;
  const url = (p?.url || "").toString().trim();
  if (url) return `url:${url}`;
  const title = (p?.title || p?.Title || "").toString().trim();
  const created = (p?.created_at || p?.CreatedAt || p?.createdAt || "").toString().trim();
  if (title || created) return `t:${title}|${created}`;
  return `fallback:${JSON.stringify(p)}`;
}

function sameIdList(a, b) {
  if (a === b) return true;
  if (!Array.isArray(a) || !Array.isArray(b)) return false;
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (getId(a[i]) !== getId(b[i])) return false;
  }
  return true;
}

function asText(v) {
  if (v == null) return "";
  return String(v);
}

function getTemplateKey(post) {
  return getTemplateKeyForPost(post);
}

function normalizeMediaCandidate(value) {
  const raw = (value || "").toString().trim();
  if (!raw) return "";
  if (raw.startsWith("asset://")) return "";
  if (raw.startsWith("//")) return `https:${raw}`;
  if (raw.startsWith("http://")) return `https://${raw.slice(7)}`;
  return raw;
}

function resolvePostImage(post) {
  const candidates = [
    post?.image_small,
    post?.image_medium,
    post?.image_large,
    post?.image,
    post?.media_url,
    post?.image_url,
  ];
  for (const candidate of candidates) {
    const normalized = normalizeMediaCandidate(candidate);
    if (normalized) return normalized;
  }
  return "";
}

function postHaystack(p) {
  const title = asText(p?.title || p?.text || p?.body || "");
  const snippet = asText(p?.snippet || p?.summary || p?.content || "");
  const author = asText(p?.author || p?.Author || p?.user || p?.username || "");
  const city = asText(p?.city || p?.location_name || p?.place || "");
  const cat = asText(p?.category || p?.Category || "");
  const sub = asText(p?.sub_category || p?.subCategory || "");
  return `${title} ${snippet} ${author} ${city} ${cat} ${sub}`.toLowerCase();
}

function matchesSearch(p, qRaw) {
  const q = (qRaw || "").toString().trim().toLowerCase();
  if (!q) return true;

  const author = asText(p?.author || p?.Author || p?.user || p?.username || "").toLowerCase();
  const hay = postHaystack(p);

  const terms = q.split(/\s+/).map((t) => t.trim()).filter(Boolean);
  for (const t of terms) {
    if (t.startsWith("@")) {
      const want = t.slice(1).trim();
      if (!want) continue;
      if (!author.includes(want)) return false;
      continue;
    }
    if (!hay.includes(t)) return false;
  }
  return true;
}

function hasPostsInRadius(posts, centerLat, centerLon, radiusKm) {
  if (!Array.isArray(posts) || posts.length === 0) return false;
  if (!Number.isFinite(centerLat) || !Number.isFinite(centerLon)) return true;
  const limit = Math.max(radiusKm || 0, 1);
  for (const p of posts) {
    const coords = getCoords(p);
    if (!coords) continue;
    const [lon, lat] = coords;
    const dKm = haversineKm(centerLat, centerLon, lat, lon);
    if (dKm <= limit) return true;
  }
  return false;
}

function filterByBounds(posts, map) {
  if (!map || !Array.isArray(posts) || posts.length === 0) return posts || [];
  let bounds;
  try { bounds = map.getBounds(); } catch {}
  if (!bounds) return posts;
  const sw = bounds.getSouthWest();
  const ne = bounds.getNorthEast();
  if (!sw || !ne) return posts;

  const padLat = Math.abs(ne.lat - sw.lat) * 0.15;
  const padLon = Math.abs(ne.lng - sw.lng) * 0.15;
  const minLat = sw.lat - padLat;
  const maxLat = ne.lat + padLat;
  const minLon = sw.lng - padLon;
  const maxLon = ne.lng + padLon;

  return posts.filter((p) => {
    const coords = getCoords(p);
    if (!coords) return false;
    const [lon, lat] = coords;
    return lat >= minLat && lat <= maxLat && lon >= minLon && lon <= maxLon;
  });
}


function getPostTimeMs(p) {
  const raw =
    p?.published_at ||
    p?.publishedAt ||
    p?.PublishedAt ||
    p?.created_at ||
    p?.CreatedAt ||
    p?.createdAt;
  if (!raw) return 0;
  const d = raw instanceof Date ? raw : new Date(String(raw));
  if (Number.isNaN(d.getTime())) return 0;
  return d.getTime();
}

function isClusterMainPost(p) {
  const clusterPostId = Number(p?.cluster_post_id);
  const postId = Number(p?.post_id || p?.id);
  if (Number.isFinite(clusterPostId) && Number.isFinite(postId) && clusterPostId > 0 && postId > 0) {
    return clusterPostId === postId;
  }
  const key = (p?.cluster_key || p?.clusterKey || "").toString().trim();
  const members = Array.isArray(p?.cluster_members) ? p.cluster_members : [];
  if (key && members.length > 0) return true;
  const url = (p?.url || "").toString();
  return url.includes("/cluster/");
}

function clusterColor(post) {
  const c = (post?.category || "NEWS").toString().toUpperCase();
  if (c === "EVENT" || c === "EVENTS") return "#a855f7";
  if (c === "FRIENDS") return "#22c55e";
  if (c === "MARKET") return "#facc15";
  return "#38bdf8";
}

function clusterLineColor(post) {
  const c = (post?.category || "NEWS").toString().toUpperCase();
  if (c === "EVENT" || c === "EVENTS") return "rgba(168,85,247,0.7)";
  if (c === "FRIENDS") return "rgba(34,197,94,0.7)";
  if (c === "MARKET") return "rgba(250,204,21,0.8)";
  return "rgba(56,189,248,0.75)";
}

function convexHull(points) {
  if (!Array.isArray(points) || points.length < 3) return points || [];
  const pts = [...points].sort((a, b) => (a[0] - b[0]) || (a[1] - b[1]));
  const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
  const lower = [];
  for (const p of pts) {
    while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
      lower.pop();
    }
    lower.push(p);
  }
  const upper = [];
  for (let i = pts.length - 1; i >= 0; i--) {
    const p = pts[i];
    while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
      upper.pop();
    }
    upper.push(p);
  }
  upper.pop();
  lower.pop();
  return lower.concat(upper);
}

function chaikinSmooth(points, iterations = 2) {
  let pts = points.slice();
  if (pts.length < 4) return pts;
  for (let i = 0; i < iterations; i++) {
    const next = [];
    for (let j = 0; j < pts.length - 1; j++) {
      const p0 = pts[j];
      const p1 = pts[j + 1];
      const q = [0.75 * p0[0] + 0.25 * p1[0], 0.75 * p0[1] + 0.25 * p1[1]];
      const r = [0.25 * p0[0] + 0.75 * p1[0], 0.25 * p0[1] + 0.75 * p1[1]];
      next.push(q, r);
    }
    next.push(next[0]);
    pts = next;
  }
  return pts;
}

function circlePolygon(center, radiusKm, steps = 24) {
  const [lon, lat] = center;
  const latDeg = radiusKm / 111;
  const lonDeg = radiusKm / (111 * Math.cos((lat * Math.PI) / 180) || 1);
  const coords = [];
  for (let i = 0; i <= steps; i++) {
    const angle = (Math.PI * 2 * i) / steps;
    coords.push([lon + Math.cos(angle) * lonDeg, lat + Math.sin(angle) * latDeg]);
  }
  return coords;
}

function buildClusterAreaFeatures(posts) {
  const features = [];
  for (const p of posts || []) {
    if (!isClusterMainPost(p)) continue;
    const items = Array.isArray(p?.cluster_items) ? p.cluster_items : [];
    const pts = items
      .map((it) => [Number(it?.lon), Number(it?.lat)])
      .filter((v) => Number.isFinite(v[0]) && Number.isFinite(v[1]));
    if (pts.length === 0) continue;

    let ring = [];
    if (pts.length >= 3) {
      ring = convexHull(pts);
      if (ring.length < 3) {
        ring = pts.slice(0, 3);
      }
      ring = [...ring, ring[0]];
      ring = chaikinSmooth(ring, 2);
      // Slight outward nudge so the bubble touches all points
      const center = ring.slice(0, -1).reduce((acc, p) => [acc[0] + p[0], acc[1] + p[1]], [0, 0]);
      const count = Math.max(1, ring.length - 1);
      const centerPt = [center[0] / count, center[1] / count];
      ring = ring.map((p, idx) => {
        if (idx === ring.length - 1) return p;
        const dx = p[0] - centerPt[0];
        const dy = p[1] - centerPt[1];
        return [centerPt[0] + dx * 1.32, centerPt[1] + dy * 1.32];
      });
      ring[ring.length - 1] = ring[0];
    } else {
      const centerLon = pts.reduce((s, v) => s + v[0], 0) / pts.length;
      const centerLat = pts.reduce((s, v) => s + v[1], 0) / pts.length;
      let maxKm = 0.0;
      for (const [lon, lat] of pts) {
        maxKm = Math.max(maxKm, haversineKm(centerLat, centerLon, lat, lon));
      }
      const radius = Math.min(80, Math.max(0.4, maxKm * 1.55 || 0.6));
      ring = circlePolygon([centerLon, centerLat], radius, 28);
    }

    features.push({
      type: "Feature",
      geometry: { type: "Polygon", coordinates: [ring] },
      properties: {
        id: getId(p) || 0,
        color: clusterColor(p),
        line: clusterLineColor(p),
      },
    });
  }
  return features;
}

const MAX_POOL_POSTS = 160;
const MAX_VISIBLE_POSTS = 45;

function expandBounds(bounds, padRatio = 0.18) {
  if (!bounds) return null;
  const spanLat = Math.abs(bounds.maxLat - bounds.minLat);
  const spanLon = Math.abs(bounds.maxLon - bounds.minLon);
  return {
    minLat: bounds.minLat - spanLat * padRatio,
    maxLat: bounds.maxLat + spanLat * padRatio,
    minLon: bounds.minLon - spanLon * padRatio,
    maxLon: bounds.maxLon + spanLon * padRatio,
  };
}

function inBounds(coords, bounds) {
  if (!coords || !bounds) return false;
  const [lon, lat] = coords;
  return (
    Number.isFinite(lat) &&
    Number.isFinite(lon) &&
    lat >= bounds.minLat &&
    lat <= bounds.maxLat &&
    lon >= bounds.minLon &&
    lon <= bounds.maxLon
  );
}

function prunePosts(posts, center, maxCount, bounds) {
  if (!Array.isArray(posts) || posts.length <= maxCount) return posts || [];
  const [centerLon, centerLat] = center || [];
  const now = Date.now();
  const paddedBounds = expandBounds(bounds);
  const inView = [];
  const outView = [];

  for (const p of posts) {
    const coords = getCoords(p);
    if (paddedBounds && inBounds(coords, paddedBounds)) {
      inView.push(p);
    } else {
      outView.push(p);
    }
  }

  const scoreList = (arr) => arr.map((p) => {
    const coords = getCoords(p);
    let dist = 99999;
    if (coords && Number.isFinite(centerLat) && Number.isFinite(centerLon)) {
      dist = haversineKm(centerLat, centerLon, coords[1], coords[0]);
    }
    const ageH = Math.max(0, (now - getPostTimeMs(p)) / 3600000);
    const score = dist + ageH * 0.4;
    return { p, score };
  }).sort((a, b) => a.score - b.score);

  const picked = [];
  const inScored = scoreList(inView);
  for (const it of inScored) {
    if (picked.length >= maxCount) break;
    picked.push(it.p);
  }
  if (picked.length < maxCount) {
    const outScored = scoreList(outView);
    for (const it of outScored) {
      if (picked.length >= maxCount) break;
      picked.push(it.p);
    }
  }
  return picked;
}

export function usePostsEngine({
  mapRef,
  viewParams,
  mainFilter,
  mainFilters = [],
  subFilter,
  subFilters = [],
  contentFilter = "all",
  timeHours,
  searchQuery,
  clusterFocus,
  mainNewsOnly,
  clustersEnabled = true,
  heatmapEnabled = true,
  forceFullPostId = null,
  markersRef,
  expandedElRef,
  onAutoWidenTimeFilter,
  onSelectPost,
  username,
  debugEnabled = false,
  theme = "blue",
  weatherEnabled = false,
}) {
  const allPostsRef = useRef([]);
  const lastFetchRef = useRef({ center: null, radiusKm: null, filterKey: "", timeHours: 0, bounds: null });
  const pendingFilterRef = useRef("");
  const viewChangeInitRef = useRef(true);
  const requestSeqRef = useRef(0);
  const delayedFetchRef = useRef(null);
  const moveDebounceRef = useRef(null);
  const analyticsDebounceRef = useRef(0);
  const initialLoadRef = useRef(true);
  const initialFetchKickRef = useRef(false);
  const autoWidenRef = useRef(false);
  const searchResultsRef = useRef(false);
  const replacePoolRef = useRef(false);
  const retryRef = useRef({ key: "", count: 0, ts: 0 });
  const [mapReady, setMapReady] = useState(false);
  const clusterModeRef = useRef(false);

  const CLUSTER_THRESHOLD = 140;
  const SIMPLE_MARKER_LIMIT = 50;
  const CLUSTER_SOURCE_ID = "sw-posts";
  const CLUSTER_LAYER_ID = "sw-clusters";
  const CLUSTER_COUNT_ID = "sw-cluster-count";
  const UNCLUSTERED_ID = "sw-unclustered";
  const CLUSTER_AREA_SOURCE_ID = "sw-cluster-areas";
  const CLUSTER_AREA_FILL_ID = "sw-cluster-area-fill";
  const CLUSTER_AREA_LINE_ID = "sw-cluster-area-line";

  useEffect(() => {
    if (mapReady) return;
    let cancelled = false;
    const timer = setInterval(() => {
      if (cancelled) return;
      if (mapRef.current) {
        mapRef.current.__swForceFetchAt = Date.now();
        setMapReady(true);
        clearInterval(timer);
      }
    }, 150);
    return () => {
      cancelled = true;
      clearInterval(timer);
    };
  }, [mapReady, mapRef]);

  useEffect(() => {
    if (!mapReady || initialFetchKickRef.current) return;
    const map = mapRef.current;
    if (!map) return;
    initialFetchKickRef.current = true;
    map.__swForceFetchAt = Date.now();
    setTimeout(() => {
      try {
        map.fire("moveend");
      } catch {}
    }, 120);
  }, [mapReady, mapRef]);

  const priorityOpts = {
    maxFullCards: 15,
    clusterRadius: 0.002,
    minScoreForCard: 50,
  };

  const ensureClusterLayers = useCallback((map) => {
    if (!map) return;
    if (typeof map.isStyleLoaded === "function" && !map.isStyleLoaded()) return;
    if (map.getSource(CLUSTER_SOURCE_ID)) return;

    map.addSource(CLUSTER_SOURCE_ID, {
      type: "geojson",
      data: { type: "FeatureCollection", features: [] },
      cluster: true,
      clusterMaxZoom: 12,
      clusterRadius: 46,
    });

    map.addLayer({
      id: CLUSTER_LAYER_ID,
      type: "circle",
      source: CLUSTER_SOURCE_ID,
      filter: ["has", "point_count"],
      paint: {
        "circle-color": [
          "step",
          ["get", "point_count"],
          "#22d3ee",
          30,
          "#3b82f6",
          100,
          "#1d4ed8",
        ],
        "circle-radius": [
          "step",
          ["get", "point_count"],
          16,
          30,
          22,
          100,
          28,
        ],
        "circle-opacity": 0.85,
        "circle-stroke-color": "#e2e8f0",
        "circle-stroke-width": 1.2,
      },
    });

    map.addLayer({
      id: CLUSTER_COUNT_ID,
      type: "symbol",
      source: CLUSTER_SOURCE_ID,
      filter: ["has", "point_count"],
      layout: {
        "text-field": "{point_count_abbreviated}",
        "text-size": 12,
        "text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"],
      },
      paint: {
        "text-color": "#0b1220",
      },
    });

    map.addLayer({
      id: UNCLUSTERED_ID,
      type: "circle",
      source: CLUSTER_SOURCE_ID,
      filter: ["!", ["has", "point_count"]],
      paint: {
        "circle-color": "#38bdf8",
        "circle-radius": 6,
        "circle-opacity": 0.9,
        "circle-stroke-color": "#0b1220",
        "circle-stroke-width": 1,
      },
    });

    if (!map.__swClusterHandlers) {
      map.__swClusterHandlers = true;

      map.on("click", CLUSTER_LAYER_ID, (e) => {
        const features = map.queryRenderedFeatures(e.point, { layers: [CLUSTER_LAYER_ID] });
        const clusterFeature = features && features[0];
        if (!clusterFeature) return;
        const clusterId = clusterFeature.properties?.cluster_id;
        const source = map.getSource(CLUSTER_SOURCE_ID);
        if (!source || clusterId == null || !source.getClusterExpansionZoom) return;
        source.getClusterExpansionZoom(clusterId, (err, zoom) => {
          if (err) return;
          map.easeTo({ center: clusterFeature.geometry.coordinates, zoom, duration: 520 });
        });
      });

      map.on("click", UNCLUSTERED_ID, (e) => {
        const f = e.features && e.features[0];
        if (!f || !f.properties) return;
        const raw = f.properties.post_json;
        if (!raw) return;
        let post = null;
        try { post = JSON.parse(raw); } catch {}
        if (post && typeof onSelectPost === "function") onSelectPost(post);
      });

      map.on("mouseenter", CLUSTER_LAYER_ID, () => {
        map.getCanvas().style.cursor = "pointer";
      });
      map.on("mouseleave", CLUSTER_LAYER_ID, () => {
        map.getCanvas().style.cursor = "";
      });
      map.on("mouseenter", UNCLUSTERED_ID, () => {
        map.getCanvas().style.cursor = "pointer";
      });
      map.on("mouseleave", UNCLUSTERED_ID, () => {
        map.getCanvas().style.cursor = "";
      });
    }
  }, [onSelectPost]);

  const ensureClusterAreaLayers = useCallback((map) => {
    if (!map) return;
    if (typeof map.isStyleLoaded === "function" && !map.isStyleLoaded()) return;
    if (map.getSource(CLUSTER_AREA_SOURCE_ID)) return;

    map.addSource(CLUSTER_AREA_SOURCE_ID, {
      type: "geojson",
      data: { type: "FeatureCollection", features: [] },
    });

    map.addLayer({
      id: CLUSTER_AREA_FILL_ID,
      type: "fill",
      source: CLUSTER_AREA_SOURCE_ID,
      paint: {
        "fill-color": ["get", "color"],
        "fill-opacity": 0.42,
      },
    });

    map.addLayer({
      id: CLUSTER_AREA_LINE_ID,
      type: "line",
      source: CLUSTER_AREA_SOURCE_ID,
      paint: {
        "line-color": ["get", "line"],
        "line-width": 2,
        "line-opacity": 0.7,
      },
    });
  }, [CLUSTER_AREA_SOURCE_ID, CLUSTER_AREA_FILL_ID, CLUSTER_AREA_LINE_ID]);

  const setClusterVisibility = useCallback((map, visible) => {
    if (!map) return;
    const v = visible ? "visible" : "none";
    if (map.getLayer(CLUSTER_LAYER_ID)) map.setLayoutProperty(CLUSTER_LAYER_ID, "visibility", v);
    if (map.getLayer(CLUSTER_COUNT_ID)) map.setLayoutProperty(CLUSTER_COUNT_ID, "visibility", v);
    if (map.getLayer(UNCLUSTERED_ID)) map.setLayoutProperty(UNCLUSTERED_ID, "visibility", v);
  }, []);

  const updateClusterAreas = useCallback((map, posts) => {
    if (!map) return;
    const src = map.getSource(CLUSTER_AREA_SOURCE_ID);
    if (!src || !src.setData) return;
    const features = buildClusterAreaFeatures(posts || []);
    src.setData({ type: "FeatureCollection", features });
  }, [CLUSTER_AREA_SOURCE_ID]);

  const updateClusterData = useCallback((map, posts) => {
    if (!map) return;
    const src = map.getSource(CLUSTER_SOURCE_ID);
    if (!src || !src.setData) return;
    const features = [];
    for (const p of posts || []) {
      const coords = getCoords(p);
      if (!coords) continue;
      const [lon, lat] = coords;
      const payload = {
        id: getId(p),
        title: p.title || p.Title || "",
        snippet: p.snippet || "",
        category: p.category || p.Category || "",
        sub_category: p.sub_category || p.subCategory || "",
        lat,
        lon,
        url: p.url || "",
        source: p.source || "",
        created_at: p.created_at || p.CreatedAt || p.createdAt || "",
        published_at: p.published_at || "",
        url_hash: p.url_hash || p.urlHash || "",
      };
      features.push({
        type: "Feature",
        geometry: { type: "Point", coordinates: [lon, lat] },
        properties: {
          id: payload.id || 0,
          post_json: JSON.stringify(payload),
        },
      });
    }
    src.setData({ type: "FeatureCollection", features });
  }, []);

  const splitForClusters = useCallback(
    (visible) => {
      const map = mapRef.current;
      if (!map) return { fullCardPosts: [], clusterPosts: visible || [] };
      const searchActive = (searchQuery || "").trim().length > 0;
      if (searchActive) {
        return { fullCardPosts: [], clusterPosts: visible || [] };
      }
      const center = map.getCenter();
      const viewCenter = center ? { lng: center.lng, lat: center.lat } : null;
      const { fullCardPosts, simpleMarkerPosts } = prioritizePosts(
        visible || [],
        viewCenter,
        priorityOpts
      );
      return { fullCardPosts, clusterPosts: simpleMarkerPosts };
    },
    [mapRef, priorityOpts, searchQuery]
  );

  const syncPriorityMarkers = useCallback(
    (fullList, simpleList) => {
      const map = mapRef.current;
      if (!map) return;
      if (expandedElRef?.current) return;

      const wanted = new Map();
      for (const post of fullList || []) {
        const id = getId(post);
        if (id != null) wanted.set(id, { post, type: "full" });
      }
      for (const post of simpleList || []) {
        const id = getId(post);
        if (id != null && !wanted.has(id)) wanted.set(id, { post, type: "simple" });
      }

      const cur = markersRef.current || [];
      const next = [];

      for (const item of cur) {
        const id = item?.id;
        const want = id != null ? wanted.get(id) : null;
        const haveType = item?.type || "full";
        if (!want || haveType !== want.type) {
          try {
            const el = item?.el;
            if (el) {
              el.classList.add("sw-disappear");
              el.style.pointerEvents = "none";
            }
          } catch {}
          setTimeout(() => {
            try { if (item?.el && item.el.__swUnmount) item.el.__swUnmount(); } catch {}
            try { item?.marker?.remove?.(); } catch {}
          }, 840);
        } else {
          next.push(item);
        }
      }

      markersRef.current = next;
      const have = new Set(next.map((x) => x.id));
      for (const [id, entry] of wanted.entries()) {
        if (have.has(id)) continue;
        if (entry.type === "simple") {
          createSimpleMarkerForPost(entry.post, mapRef, markersRef, expandedElRef, theme);
        } else {
          createMarkerForPost(entry.post, mapRef, markersRef, expandedElRef, theme);
        }
      }
    },
    [mapRef, markersRef, expandedElRef, theme]
  );

  const [status, setStatus] = useState("");
  const [debugStatus, setDebugStatus] = useState("debug active");
  const [visiblePosts, setVisiblePosts] = useState([]);
  const [loadingPosts, setLoadingPosts] = useState(false);
  const [loadError, setLoadError] = useState("");

  const syncMarkers = useCallback(
    (visible) => {
      const map = mapRef.current;
      if (!map) return;
      map.__swMarkersRef = markersRef;

      // If user is reading an expanded overlay, don't touch markers
      if (expandedElRef?.current || map.__swCenteredOverlay) return;

      // Use priority system to decide which posts show full cards vs simple markers
      const center = map.getCenter();
      const viewCenter = center ? { lng: center.lng, lat: center.lat } : null;

      const searchActive = (searchQuery || "").trim().length > 0;
      let fullCardPosts = [];
      let simpleMarkerPosts = [];
      const prioritized = prioritizePosts(visible, viewCenter, {
        maxFullCards: 15,        // 10-15 cartes complètes selon le zoom
        clusterRadius: 0.002,    // ~200m clustering radius (augmenté pour plus de groupement)
        minScoreForCard: 50,     // Score minimum pour carte complète (augmenté de 35 à 50)
      });
      fullCardPosts = prioritized.fullCardPosts;
      simpleMarkerPosts = prioritized.simpleMarkerPosts;

      // Combine both types for "wanted" tracking
      const wanted = new Map();
      for (const post of visible) {
        const id = getId(post);
        if (id != null) {
          // Weather posts: skip completely (shown in top bar widget instead)
          const ct = (post?.content_type || "").toLowerCase();
          if (ct === "weather") {
            continue; // Skip weather markers - they appear in the search bar widget
          }
          // Tag avec le type de marqueur souhaité
          const isFullCard =
            id === forceFullPostId ||
            fullCardPosts.some(p => getId(p) === id);
          wanted.set(id, { post, type: isFullCard ? 'full' : 'simple' });
        }
      }

      const cur = markersRef.current || [];
      const next = [];

      // remove markers not wanted (smooth fade-out)
      for (const item of cur) {
        const id = item?.id;
        if (id == null || !wanted.has(id)) {
          try {
            const el = item?.el;
            if (el) {
              el.classList.add("sw-disappear");
              el.style.pointerEvents = "none";
            }
          } catch {}

          // let CSS transition play, then remove from map
          setTimeout(() => {
            try { if (item?.el && item.el.__swUnmount) item.el.__swUnmount(); } catch {}
            try { item?.marker?.remove?.(); } catch {}
          }, 840);
        } else {
          const entry = wanted.get(id);
          if (entry?.post) {
            item.post = entry.post;
            if (item.el) {
              item.el.__post = entry.post;
              const nextKey = getTemplateKey(entry.post);
              const nextImg = resolvePostImage(entry.post);
              const nextTitle = (entry.post?.title || "").toString();
              if (
                item.el.__lastTemplateKey !== nextKey ||
                item.el.__lastImage !== nextImg ||
                item.el.__lastTitle !== nextTitle
              ) {
                if (item.type === "simple" && item.el.__renderSimple) {
                  try { item.el.__renderSimple(); } catch {}
                } else if (item.el.__renderCompact) {
                  try { item.el.__renderCompact(); } catch {}
                }
              }
            }
          }
          next.push(item);
        }
      }

      markersRef.current = next;

      const have = new Set(next.map((x) => x.id));

      // add missing markers - use appropriate type based on priority
      for (const [id, { post, type }] of wanted.entries()) {
        if (have.has(id)) continue;

        if (type === 'full') {
          createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme);
        } else {
          createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRef, theme);
        }
      }
    },
    [mapRef, markersRef, expandedElRef, theme, searchQuery, forceFullPostId, weatherEnabled]
  );

  useEffect(() => {
    const map = mapRef.current;
    replacePoolRef.current = true;
    pendingFilterRef.current = `${mainFilter}|${subFilter}|${contentFilter}|${timeHours}`;
    allPostsRef.current = [];
    autoWidenRef.current = true; // Prevent auto-widen when user manually changes filter (keep their time choice)
    if (map && clustersEnabled && heatmapEnabled) {
      updateClusterAreas(map, []);
    }
    if (map) {
      map.__swForceFetchAt = Date.now();
      setTimeout(() => {
        try {
          map.fire("moveend");
        } catch {}
      }, 60);
    }
  }, [mainFilter, subFilter, contentFilter, timeHours, mapRef, clustersEnabled, heatmapEnabled, updateClusterAreas]);

  const computeVisible = useCallback(
    (tf) => {
      const basePosts = allPostsRef.current || [];
      const extraPosts = Array.isArray(clusterFocus?.posts) ? clusterFocus.posts : [];
      const posts = extraPosts.length
        ? Array.from(new Map([...extraPosts, ...basePosts].map((p) => {
            const normalized = normalizePostId(p);
            return [getPostKey(normalized), normalized];
          })).values())
        : basePosts;
      const catCode = categoryCode(mainFilter);
      const clusterIDs = clustersEnabled && Array.isArray(clusterFocus?.ids) ? clusterFocus.ids : null;
      const isClusterFocus = !!(clustersEnabled && clusterIDs && clusterIDs.length > 0);

      return posts.filter((p) => {
        // Apply content filter (links, video, image, live, event)
        if (!matchesContentFilter(p, contentFilter)) return false;

        if (isClusterFocus) {
          const id = Number(getId(p));
          const clusterPostId = Number(p?.cluster_post_id);
          if (clusterIDs && !clusterIDs.includes(id) && !clusterIDs.includes(clusterPostId)) {
            return false;
          }
          return true;
        }
        if (clustersEnabled && mainNewsOnly && !isClusterMainPost(p)) return false;
        if (catCode) {
          const pc = (p.category || p.Category || "").toString().toUpperCase();
          if (pc !== catCode) return false;
        }
        if (!matchesSubFilter(p, subFilter)) return false;
        if (!searchResultsRef.current && !matchesSearch(p, searchQuery)) return false;
        return true;
      });
    },
    [mainFilter, subFilter, contentFilter, searchQuery, clusterFocus, mainNewsOnly, clustersEnabled]
  );

  const refreshVisibleAndMarkers = useCallback(
    (tf, force = false) => {
      const vAll = computeVisible(tf);
      const map = mapRef.current;
      const isClusterFocus = !!(clustersEnabled && Array.isArray(clusterFocus?.ids) && clusterFocus.ids.length > 0);
      let v = vAll;
      if (!isClusterFocus) {
        const center = map?.getCenter?.();
        let bounds = null;
        try {
          const b = map?.getBounds?.();
          if (b) {
            const sw = b.getSouthWest();
            const ne = b.getNorthEast();
            bounds = { minLat: sw?.lat, minLon: sw?.lng, maxLat: ne?.lat, maxLon: ne?.lng };
          }
        } catch {}
        if (v.length > MAX_VISIBLE_POSTS) {
          v = prunePosts(v, [center?.lng, center?.lat], MAX_VISIBLE_POSTS, bounds);
        }
      }

      // reduce wall blink: don't update if same ids
      setVisiblePosts((prev) => {
        if (force) return v;
        return sameIdList(prev, v) ? prev : v;
      });

      // keep status silent (no annoying bubbles)
      setStatus("");

      if (map && clustersEnabled && heatmapEnabled) {
        ensureClusterAreaLayers(map);
        updateClusterAreas(map, v);
      }

      if (clustersEnabled && map && v.length >= CLUSTER_THRESHOLD) {
        const { fullCardPosts, clusterPosts } = splitForClusters(v);
        const simpleVisible = clusterPosts.slice(0, SIMPLE_MARKER_LIMIT);
        const clusterRemainder = clusterPosts.slice(SIMPLE_MARKER_LIMIT);
        clusterModeRef.current = true;
        ensureClusterLayers(map);
        syncPriorityMarkers(fullCardPosts, simpleVisible);
        updateClusterData(map, clusterRemainder);
        setClusterVisibility(map, true);
        return;
      }

      if (map && clusterModeRef.current) {
        setClusterVisibility(map, false);
        updateClusterData(map, []);
        clusterModeRef.current = false;
      }

      syncMarkers(v);
    },
    [computeVisible, syncMarkers, mapRef, ensureClusterLayers, ensureClusterAreaLayers, updateClusterAreas, updateClusterData, setClusterVisibility, splitForClusters, syncPriorityMarkers, viewParams, clustersEnabled, heatmapEnabled, clusterFocus, searchQuery]
  );

  // On filter/search changes: recompute + sync (NO clear-all rebuild)
  useEffect(() => {
    if (pendingFilterRef.current) return;
    refreshVisibleAndMarkers(timeHours);
  }, [timeHours, mainFilter, subFilter, contentFilter, searchQuery, clusterFocus, refreshVisibleAndMarkers]);

  useEffect(() => {
    if (!clustersEnabled) return;
    if (!clusterFocus) return;
    if (Array.isArray(clusterFocus.posts) && clusterFocus.posts.length > 0) {
      allPostsRef.current = clusterFocus.posts;
    }
    refreshVisibleAndMarkers(timeHours, true);
  }, [clusterFocus, timeHours, refreshVisibleAndMarkers, clustersEnabled]);

  // On view change: re-filter markers/cards to current bounds even without fetch
  useEffect(() => {
    if (pendingFilterRef.current) return;
    refreshVisibleAndMarkers(timeHours);
  }, [viewParams, refreshVisibleAndMarkers, timeHours]);

  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    if (viewChangeInitRef.current) {
      viewChangeInitRef.current = false;
      return;
    }
    map.__swForceFetchAt = Date.now();
  }, [viewParams]);

  // Fetch on view change (moveend)
  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;

    let cancelled = false;

    async function load() {
      const mapObj = mapRef.current;
      const resolvedView =
        viewParams && viewParams.center ? viewParams : getViewFromMap(mapObj);
      if (!resolvedView?.center) return;
      if ((searchQuery || "").trim()) return;

      // If expanded overlay is open: delay fetch (do NOT disturb UI)
      if (expandedElRef?.current || mapObj?.__swCenteredOverlay) {
        if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
        delayedFetchRef.current = setTimeout(load, 1200);
        return;
      }

      // Respect ignore window if any
      try {
        const until = mapObj?.__swIgnoreFetchUntil || 0;
        if (until && Date.now() < until) {
          const wait = Math.min(2500, until - Date.now());
          if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
          delayedFetchRef.current = setTimeout(load, wait + 25);
          return;
        }
      } catch {}

      const [lng, lat] = resolvedView.center;
      const radiusKm = resolvedView.radiusKm || 750;
      // Include arrays so any toggle triggers refetch
      const mainKey = mainFilters.length > 0 ? [...mainFilters].sort().join(",") : mainFilter;
      const subKey = subFilters.length > 0 ? [...subFilters].sort().join(",") : subFilter;
      const filterKey = `${mainKey}|${subKey}|${contentFilter}`;
      const currentKey = `${filterKey}|${timeHours}`;
      const reqId = ++requestSeqRef.current;
      let bounds = null;
      try {
        const b = mapObj.getBounds();
        if (b) {
          const sw = b.getSouthWest();
          const ne = b.getNorthEast();
          bounds = {
            minLat: sw?.lat,
            minLon: sw?.lng,
            maxLat: ne?.lat,
            maxLon: ne?.lng,
          };
        }
      } catch {}

      const last = lastFetchRef.current;
      const forceFetchAt = mapObj?.__swForceFetchAt || 0;
      const forceFetch = forceFetchAt && Date.now() - forceFetchAt < 15000;
      if (forceFetch) {
        mapObj.__swForceFetchAt = 0;
      }

      if (!forceFetch && last.center && last.filterKey === filterKey && last.timeHours === timeHours) {
        const [lastLng, lastLat] = last.center;
        const distKm = haversineKm(lastLat, lastLng, lat, lng);

        const lastR = last.radiusKm || 0;
        const radiusChanged = !lastR || Math.abs(radiusKm - lastR) / Math.max(lastR, 1) >= 0.1;

        let boundsChanged = false;
        if (last.bounds && bounds) {
          const spanLat = Math.max(0.0001, Math.abs(last.bounds.maxLat - last.bounds.minLat));
          const spanLon = Math.max(0.0001, Math.abs(last.bounds.maxLon - last.bounds.minLon));
          boundsChanged =
            Math.abs(bounds.minLat - last.bounds.minLat) / spanLat > 0.15 ||
            Math.abs(bounds.maxLat - last.bounds.maxLat) / spanLat > 0.15 ||
            Math.abs(bounds.minLon - last.bounds.minLon) / spanLon > 0.15 ||
            Math.abs(bounds.maxLon - last.bounds.maxLon) / spanLon > 0.15;
        }

        const minMoveKm = Math.max(2, radiusKm * 0.05);

        if (!radiusChanged && !boundsChanged && distKm < minMoveKm) return;
      }

      try {
        setLoadingPosts(true);
        setLoadError("");

        const catCode = categoryCode(mainFilter);
        const subCatParam =
          subFilter && subFilter.toUpperCase() !== "ALL" && subFilter !== "All" ? subFilter : "";

        // Use smart feed for personalized, trending posts
        const zoom = typeof mapObj?.getZoom === "function" ? mapObj.getZoom() : undefined;
        let limit = 50;
        if (typeof zoom === "number") {
          if (zoom <= 6) limit = 90;
          else if (zoom <= 8) limit = 70;
          else if (zoom <= 10) limit = 60;
        }
        if (radiusKm > 800) {
          limit = Math.max(limit, 80);
        }
        if (limit > 120) limit = 120;

        // Map contentFilter to content_type for API
        let contentTypeParam = undefined;
        if (contentFilter && contentFilter !== "all") {
          if (contentFilter === "live") contentTypeParam = "camera";
          else if (contentFilter === "video") contentTypeParam = "video,camera";
          else if (contentFilter === "image") contentTypeParam = "image";
          else if (contentFilter === "text") contentTypeParam = "post";
          else if (contentFilter === "links") contentTypeParam = "news";
          else if (contentFilter === "event") contentTypeParam = "event";
        }

        let newPosts = await fetchSmartFeed({
          lat,
          lon: lng,
          radius_km: radiusKm,
          limit,
          zoom,
          bounds,
          username: username || undefined,
          time_hours: timeHours || undefined,
          category: catCode || undefined,
          sub_category: subCatParam || undefined,
          content_type: contentTypeParam || undefined,
        });

        if (cancelled || reqId !== requestSeqRef.current) return;

        const existing = replacePoolRef.current ? [] : allPostsRef.current || [];
        const byId = new Map();
        for (const p of existing) {
          const normalized = normalizePostId(p);
          const key = getPostKey(normalized);
          if (key) byId.set(key, p);
        }
        if (Array.isArray(newPosts)) {
          for (const p of newPosts) {
            const normalized = normalizePostId(p);
            const key = getPostKey(normalized);
            if (!key) continue;
            if (timeHours) normalized._swTimeHours = timeHours;
            const prior = byId.get(key);
            if (prior) {
              byId.set(key, { ...prior, ...normalized });
            } else {
              byId.set(key, normalized);
            }
          }
        }

        allPostsRef.current = prunePosts(Array.from(byId.values()), [lng, lat], MAX_POOL_POSTS, bounds);
        replacePoolRef.current = false;
        lastFetchRef.current = { center: [...resolvedView.center], radiusKm, filterKey, timeHours, bounds };
        if (pendingFilterRef.current === currentKey) {
          pendingFilterRef.current = "";
        }

        // Smooth update: sync markers/clusters + wall without clearing everything
        refreshVisibleAndMarkers(timeHours);
        const visible = computeVisible(timeHours);
        if (debugEnabled) {
          const count = Array.isArray(newPosts) ? newPosts.length : 0;
          const q = (searchQuery || "").trim();
          setDebugStatus(`tf:${timeHours || 0} fetched:${count} visible:${visible.length}${q ? ` q:${q}` : ""}`);
        }
        const now = Date.now();
        if (now - analyticsDebounceRef.current > 4000) {
          analyticsDebounceRef.current = now;
          trackEvent("map_fetch", {
            count: Array.isArray(newPosts) ? newPosts.length : 0,
            radius_km: Math.round(radiusKm || 0),
            zoom: typeof zoom === "number" ? Math.round(zoom * 10) / 10 : undefined,
            filter: filterKey,
            time_hours: timeHours || 0,
          });
        }
        if (
          !autoWidenRef.current &&
          timeHours &&
          timeHours > 0 &&
          Array.isArray(newPosts) &&
          newPosts.length > 0 &&
          visible.length === 0
        ) {
          autoWidenRef.current = true;
          if (typeof onAutoWidenTimeFilter === "function") {
            onAutoWidenTimeFilter(336);
          }
        }

        setLoadingPosts(false);
      } catch (err) {
        setLoadingPosts(false);
        setLoadError("fetch failed");
        if (pendingFilterRef.current === currentKey) {
          pendingFilterRef.current = "";
        }
        const key = `${filterKey}|${Math.round(lat * 100)}|${Math.round(lng * 100)}|${Math.round(radiusKm)}`;
        const now = Date.now();
        if (retryRef.current.key !== key || now - retryRef.current.ts > 5000) {
          retryRef.current = { key, count: 0, ts: now };
        }
        if (retryRef.current.count < 1) {
          retryRef.current.count += 1;
          retryRef.current.ts = now;
          if (delayedFetchRef.current) clearTimeout(delayedFetchRef.current);
          delayedFetchRef.current = setTimeout(load, 650);
        }
      }
    }

    if (moveDebounceRef.current) {
      clearTimeout(moveDebounceRef.current);
      moveDebounceRef.current = null;
    }
    if (initialLoadRef.current) {
      initialLoadRef.current = false;
      load();
    } else {
      moveDebounceRef.current = setTimeout(load, 300);
    }
    return () => {
      cancelled = true;
      if (moveDebounceRef.current) {
        try { clearTimeout(moveDebounceRef.current); } catch {}
        moveDebounceRef.current = null;
      }
      if (delayedFetchRef.current) {
        try { clearTimeout(delayedFetchRef.current); } catch {}
        delayedFetchRef.current = null;
      }
    };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [viewParams, mapReady, mainFilter, mainFilters.join(","), subFilter, subFilters.join(","), contentFilter, mapRef, timeHours, expandedElRef, refreshVisibleAndMarkers, computeVisible, onAutoWidenTimeFilter, syncMarkers, username]);

  useEffect(() => {
    const q = (searchQuery || "").trim();
    if (!q) return;
    let cancelled = false;

    async function runSearch() {
      try {
        const map = mapRef.current;
        let lat;
        let lon;
        let radiusKm;
        try {
          const view = map ? getViewFromMap(map) : null;
          if (view?.center) {
            lon = view.center[0];
            lat = view.center[1];
          } else {
            const center = map?.getCenter?.();
            lat = center?.lat;
            lon = center?.lng;
          }
          if (view?.radiusKm) {
            radiusKm = Math.min(Math.max(view.radiusKm * 1.2, 25), 2500);
          }
        } catch {}
        const data = await fetchUnifiedSearch(q, {
          type: "posts",
          lat,
          lon,
          radius_km: radiusKm,
          limit: 60,
        });
        if (cancelled) return;
        const posts = Array.isArray(data?.posts) ? data.posts : [];
        const normalized = posts.map((p) => normalizePostId(p));
      let bounds = null;
      try {
        const b = map?.getBounds?.();
        if (b) {
          const sw = b.getSouthWest();
          const ne = b.getNorthEast();
          bounds = { minLat: sw?.lat, minLon: sw?.lng, maxLat: ne?.lat, maxLon: ne?.lng };
        }
      } catch {}
      allPostsRef.current = prunePosts(normalized, [lon, lat], MAX_POOL_POSTS, bounds);
        searchResultsRef.current = true;
        refreshVisibleAndMarkers(timeHours);
        trackEvent("search_filter_apply", {
          query_len: q.length,
          results: posts.length,
          context: "map",
        });
      } catch {}
    }

    runSearch();
    return () => {
      cancelled = true;
    };
  }, [searchQuery, viewParams, mapRef, refreshVisibleAndMarkers, timeHours]);

  useEffect(() => {
    if ((searchQuery || "").trim()) return;
    searchResultsRef.current = false;
  }, [searchQuery]);

  useEffect(() => {
    const map = mapRef.current;
    if (!map) return;
    const handleStyle = () => {
      if (!clustersEnabled) return;
      if (!clusterModeRef.current) return;
      ensureClusterLayers(map);
      setClusterVisibility(map, true);
      const { clusterPosts } = splitForClusters(computeVisible(timeHours));
      const clusterRemainder = clusterPosts.slice(SIMPLE_MARKER_LIMIT);
      updateClusterData(map, clusterRemainder);
    };
    map.on("styledata", handleStyle);
    return () => map.off("styledata", handleStyle);
  }, [mapRef, ensureClusterLayers, setClusterVisibility, updateClusterData, computeVisible, timeHours, splitForClusters, clustersEnabled]);

  const handleIncomingPost = useCallback(
    (p) => {
      const incomingId = Number(p?.id ?? p?.post_id ?? p?.postId ?? 0) || null;
      const key = getPostKey(p);
      if (key) {
        const existingIdx = (allPostsRef.current || []).findIndex((x) => getPostKey(x) === key);
        if (existingIdx !== -1) {
          const existing = allPostsRef.current[existingIdx];
          const existingId = Number(existing?.id ?? existing?.post_id ?? existing?.postId ?? 0) || null;
          if (incomingId && existingId && incomingId === existingId) {
            const existingType = String(existing?.content_type || "").toLowerCase();
            const incomingType = String(p?.content_type || "").toLowerCase();
            if (existingType === "video" && incomingType === "live") {
              return;
            }
            const merged = normalizePostId({ ...existing, ...p });
            allPostsRef.current = allPostsRef.current.map((item, idx) =>
              idx === existingIdx ? merged : item
            );
            refreshVisibleAndMarkers(timeHours);
            return;
          }
          return;
        }
      }
      if (incomingId) {
        const existingIdx = (allPostsRef.current || []).findIndex((x) => {
          const id = Number(x?.id ?? x?.post_id ?? x?.postId ?? 0) || null;
          return id && id === incomingId;
        });
        if (existingIdx !== -1) {
          const existing = allPostsRef.current[existingIdx];
          const existingType = String(existing?.content_type || "").toLowerCase();
          const incomingType = String(p?.content_type || "").toLowerCase();
          if (existingType === "video" && incomingType === "live") {
            return;
          }
          const merged = normalizePostId({ ...existing, ...p });
          allPostsRef.current = allPostsRef.current.map((item, idx) =>
            idx === existingIdx ? merged : item
          );
          refreshVisibleAndMarkers(timeHours);
          return;
        }
      }
      const map = mapRef.current;
      const center = map?.getCenter?.();
      const normalized = normalizePostId(p);
      let bounds = null;
      try {
        const b = map?.getBounds?.();
        if (b) {
          const sw = b.getSouthWest();
          const ne = b.getNorthEast();
          bounds = { minLat: sw?.lat, minLon: sw?.lng, maxLat: ne?.lat, maxLon: ne?.lng };
        }
      } catch {}
      allPostsRef.current = prunePosts([normalized, ...allPostsRef.current], [center?.lng, center?.lat], MAX_POOL_POSTS, bounds);

      const catCode = categoryCode(mainFilter);
      if (catCode) {
        const pc = (p.category || p.Category || "").toString().toUpperCase();
        if (pc !== catCode) return;
      }
      if (!matchesSubFilter(p, subFilter)) return;
      if (!matchesSearch(p, searchQuery)) return;

      if (clusterModeRef.current) {
        refreshVisibleAndMarkers(timeHours);
        return;
      }

      // add marker if missing
      const id = getId(normalized);
      const have = new Set((markersRef.current || []).map((x) => x.id));
      if (id != null && !have.has(id)) {
        createMarkerForPost(normalized, mapRef, markersRef, expandedElRef, theme);
      }

      setVisiblePosts((current) => [normalized, ...current]);
    },
    [mainFilter, subFilter, timeHours, searchQuery, mapRef, markersRef, expandedElRef, theme, refreshVisibleAndMarkers]
  );

  const handlePostUpdate = useCallback(
    (payload) => {
      const postId = payload?.post_id || payload?.postId || payload?.id;
      if (!postId) return;

      const counts = payload?.counts || null;
      const truth = payload?.truth || null;
      const postPatch =
        payload?.post ||
        payload?.post_data ||
        payload?.postData ||
        payload?.data ||
        null;
      const truthScore = Number(
        truth?.truth_score ??
          truth?.TruthScore ??
          payload?.truth_score ??
          payload?.truthScore
      );

      const applyPatch = (patch) => {
        const updated = [];
        let changed = false;
        for (const p of allPostsRef.current || []) {
          if ((p?.id || p?.ID) !== postId) {
            updated.push(p);
            continue;
          }
          const next = normalizePostId({ ...p, ...(patch || {}) });
          if (counts) {
            if (Number.isFinite(counts.view_count)) next.view_count = counts.view_count;
            if (Number.isFinite(counts.like_count)) next.like_count = counts.like_count;
            if (Number.isFinite(counts.share_count)) next.share_count = counts.share_count;
            if (Number.isFinite(counts.comment_count)) next.comment_count = counts.comment_count;
          }
          if (Number.isFinite(truthScore)) next.truth_score = truthScore;
          updated.push(next);
          changed = true;
        }
        if (changed) {
          allPostsRef.current = updated;
          // Skip full marker refresh if this post's modal is open - we update UI in-place below
          const map = mapRef.current;
          const ov = map?.__swCenteredOverlay;
          const isModalOpen = ov && ov.postId === postId;
          if (!isModalOpen) {
            refreshVisibleAndMarkers(timeHours, true);
          }
        }
      };

      const needsResolve = (value) =>
        typeof value === "string" && value.trim().startsWith("asset://");

      if (postPatch && typeof postPatch === "object") {
        const assetKeys = ["image_small", "image_medium", "image_large", "image", "media_url", "video_url"];
        const hasAssetRefs =
          assetKeys.some((k) => needsResolve(postPatch[k])) ||
          (Array.isArray(postPatch.media_urls) && postPatch.media_urls.some(needsResolve));

        if (hasAssetRefs) {
          (async () => {
            const resolvedPatch = { ...postPatch };
            await Promise.all(
              assetKeys.map(async (k) => {
                resolvedPatch[k] = await resolveAssetRef(resolvedPatch[k]);
              })
            );
            if (Array.isArray(resolvedPatch.media_urls)) {
              const resolvedMedia = await Promise.all(
                resolvedPatch.media_urls.map((item) => resolveAssetRef(item))
              );
              resolvedPatch.media_urls = resolvedMedia.filter(Boolean);
            }
            applyPatch(resolvedPatch);
          })();
          return;
        }
      }

      applyPatch(postPatch);

      try {
        if (Number.isFinite(truthScore)) {
          const markers = markersRef.current || [];
          for (const item of markers) {
            if (!item || item.id !== postId) continue;
            const el = item.el;
            if (!el) continue;
            const marker = el.querySelector(".sw-truth-mini-marker");
            const scoreEl = el.querySelector(".sw-truth-mini-score");
            if (marker) marker.style.top = `${100 - truthScore}%`;
            if (scoreEl) {
              scoreEl.style.background = `hsl(${Math.round((truthScore / 100) * 120)} 80% 42%)`;
              scoreEl.textContent = `${Math.round(truthScore)}`;
            }
          }
        }
        const map = mapRef.current;
        const ov = map?.__swCenteredOverlay;
        if (ov && ov.postId === postId && ov.modalWrapRef) {
          const wrap = ov.modalWrapRef;
          if (counts) {
            const mapStat = (key, value) => {
              if (!Number.isFinite(value)) return;
              const el = wrap.querySelector(`[data-stat="${key}"] .sw-stat-num`);
              if (el) el.textContent = String(value);
            };
            mapStat("views", counts.view_count);
            mapStat("likes", counts.like_count);
            mapStat("shares", counts.share_count);
            mapStat("comments", counts.comment_count);
          }
          if (Number.isFinite(truthScore)) {
            const marker = wrap.querySelector(".sw-truth-rail-marker");
            const label = wrap.querySelector(".sw-truth-rail-score");
            if (marker) marker.style.top = `${100 - truthScore}%`;
            if (label) label.textContent = `${Math.round(truthScore)}`;
          }
        }
      } catch {}
    },
    [mapRef, refreshVisibleAndMarkers, timeHours]
  );

  const handlePostDelete = useCallback(
    (payload) => {
      const postId = payload?.post_id || payload?.postId || payload?.id;
      if (!postId) return;

      const map = mapRef.current;
      const overlay = map?.__swCenteredOverlay;
      if (overlay && overlay.postId === postId) {
        // Remove event listeners
        try { if (overlay.onKey) window.removeEventListener("keydown", overlay.onKey); } catch {}
        try { if (overlay.onAuth) window.removeEventListener("sociowire:auth-changed", overlay.onAuth); } catch {}
        // Remove overlay element
        try { overlay.el?.remove?.(); } catch {}
        try { overlay.backdrop?.remove?.(); } catch {}
        // Restore map interactions
        const st = overlay.interactionState || {};
        try {
          if (st.dragPan) map.dragPan?.enable?.();
          if (st.scrollZoom) map.scrollZoom?.enable?.();
          if (st.doubleClickZoom) map.doubleClickZoom?.enable?.();
          if (st.touchZoomRotate) map.touchZoomRotate?.enable?.();
        } catch {}
        map.__swCenteredOverlay = null;
      }

      allPostsRef.current = (allPostsRef.current || []).filter((p) => (p?.id || p?.ID) !== postId);
      setVisiblePosts((current) => current.filter((p) => (p?.id || p?.ID) !== postId));

      const markers = markersRef.current || [];
      const nextMarkers = [];
      for (const item of markers) {
        if (!item || item.id !== postId) {
          nextMarkers.push(item);
          continue;
        }
        try { item.marker?.remove?.(); } catch {}
      }
      markersRef.current = nextMarkers;
    },
    [mapRef, markersRef]
  );

  return { status, debugStatus, visiblePosts, loadingPosts, loadError, handleIncomingPost, handlePostUpdate, handlePostDelete };
}
