/* Nocturne Annecy — main App */
const { useState, useEffect, useMemo, useCallback } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "skin": "nuit",
  "accent": "pink",
  "density": "comfortable",
  "showLayout": "grid",
  "liveScrap": true,
  "showFooter": true
}/*EDITMODE-END*/;

const ACCENTS = {
  pink:     { primary: "#FF2D6F", glow: "#FF6FA3", deep: "#B81550", soft: "rgba(255,45,111,0.12)" },
  acid:     { primary: "#C2FF3D", glow: "#DCFF7A", deep: "#88B82A", soft: "rgba(194,255,61,0.14)" },
  electric: { primary: "#5BCFFF", glow: "#9BE3FF", deep: "#2C8FBF", soft: "rgba(91,207,255,0.14)" },
  amber:    { primary: "#FFB13D", glow: "#FFC97A", deep: "#B07F2A", soft: "rgba(255,177,61,0.14)" },
  violet:   { primary: "#B47BFF", glow: "#D2ADFF", deep: "#7A4DBF", soft: "rgba(180,123,255,0.14)" },
};

const AUTH_TOKEN_KEY = "token";
const LEGACY_AUTH_TOKEN_KEY = "urgence.jwt";
const GUEST_PROFILE_KEY = "nightintel_guest_profile";
const LEGACY_GUEST_PROFILE_KEY = "urgence_guest_profile";
const ANNECY_LOCATION = { lat: 45.90155, lng: 6.12195 };

/** Read the current JWT from browser storage. */
function getToken() {
  try {
    return window.localStorage.getItem(AUTH_TOKEN_KEY) || window.localStorage.getItem(LEGACY_AUTH_TOKEN_KEY);
  } catch {
    return null;
  }
}

/** Persist a JWT returned by /api/auth/login or /api/auth/register. */
function setToken(token) {
  try {
    if (token) window.localStorage.setItem(AUTH_TOKEN_KEY, token);
    if (token) window.localStorage.removeItem(LEGACY_AUTH_TOKEN_KEY);
  } catch {}
}

/** Remove the local JWT when auth expires or the user logs out. */
function clearToken() {
  try {
    window.localStorage.removeItem(AUTH_TOKEN_KEY);
    window.localStorage.removeItem(LEGACY_AUTH_TOKEN_KEY);
  } catch {}
}

/** Fetch helper for protected APIs; adds Authorization and handles expired tokens. */
async function fetchWithAuth(input, init = {}) {
  const headers = new Headers(init.headers || {});
  const token = getToken();
  if (token) headers.set("Authorization", `Bearer ${token}`);
  let response = await fetch(input, { ...init, headers });
  if (response.status === 404 && typeof input === "string" && input.startsWith("/auth/")) {
    response = await fetch(`/api${input}`, { ...init, headers });
  }
  if (response.status === 404 && typeof input === "string" && input.startsWith("/events")) {
    response = await fetch(`/api${input}`, { ...init, headers });
  }
  if (response.status === 401) {
    clearToken();
    window.dispatchEvent(new CustomEvent("nightintel:auth-expired"));
  }
  return response;
}

async function authRequest(path, credentials) {
  const response = await fetchWithAuth(`/auth/${path}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(credentials || {}),
  });
  const payload = await response.json().catch(() => ({}));
  if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
  if (payload.token) setToken(payload.token);
  window.dispatchEvent(new CustomEvent("nightintel:auth-updated"));
  return payload;
}

async function login(credentials) {
  return authRequest("login", credentials);
}

async function register(credentials) {
  return authRequest("register", credentials);
}

async function apiGet(path) {
  const response = await fetchWithAuth(path);
  const payload = await response.json().catch(() => ({}));
  if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
  return payload;
}

async function oauthStart(provider) {
  const response = await fetchWithAuth(`/auth/oauth?provider=${encodeURIComponent(provider)}`);
  const payload = await response.json().catch(() => ({}));
  if (!response.ok) {
    const missing = payload.missing_env ? `Missing ${payload.missing_env}` : payload.error;
    throw new Error(missing || `OAuth ${provider} unavailable`);
  }
  if (payload.auth_url) window.location.href = payload.auth_url;
  return payload;
}

window.NightIntelAuth = { getToken, setToken, clearToken, login, register, oauthStart, fetchWithAuth, apiGet };

function encodeGuestProfile(profile) {
  try {
    return btoa(unescape(encodeURIComponent(JSON.stringify(profile))));
  } catch {
    return "";
  }
}

function decodeGuestProfile(value) {
  try {
    return JSON.parse(decodeURIComponent(escape(atob(value))));
  } catch {
    return null;
  }
}

function readGuestProfile() {
  const fallback = { points: 0, badges: [], scans: 0 };
  try {
    const local = window.localStorage.getItem(GUEST_PROFILE_KEY);
    const cookieMatch = document.cookie.match(new RegExp(`${GUEST_PROFILE_KEY}=([^;]+)`));
    const current = decodeGuestProfile(local || decodeURIComponent(cookieMatch?.[1] || ""));
    if (current) return current;
    const legacyLocal = window.localStorage.getItem(LEGACY_GUEST_PROFILE_KEY);
    const legacyCookie = document.cookie.match(new RegExp(`${LEGACY_GUEST_PROFILE_KEY}=([^;]+)`));
    const legacy = decodeGuestProfile(legacyLocal || decodeURIComponent(legacyCookie?.[1] || ""));
    if (legacy) {
      const encoded = encodeGuestProfile(legacy);
      window.localStorage.setItem(GUEST_PROFILE_KEY, encoded);
      document.cookie = `${GUEST_PROFILE_KEY}=${encodeURIComponent(encoded)}; Max-Age=31536000; SameSite=Lax; Path=/`;
      return legacy;
    }
    return fallback;
  } catch {
    return fallback;
  }
}

function writeGuestProfile(profile) {
  const safeProfile = {
    points: Number(profile.points) || 0,
    badges: Array.from(new Set(profile.badges || [])),
    scans: Number(profile.scans) || 0,
  };
  const encoded = encodeGuestProfile(safeProfile);
  try {
    window.localStorage.setItem(GUEST_PROFILE_KEY, encoded);
    document.cookie = `${GUEST_PROFILE_KEY}=${encodeURIComponent(encoded)}; Max-Age=31536000; SameSite=Lax; Path=/`;
  } catch {}
  return safeProfile;
}

function catFromApi(event) {
  const raw = String(event.cat || event.vibe || event.rawCategory || event.type || "soiree").toLowerCase();
  if (raw.includes("after")) return "afterwork";
  if (raw.includes("danse") || raw.includes("dance") || raw.includes("bachata") || raw.includes("salsa")) return "danse";
  if (raw.includes("rencontre") || raw.includes("dating")) return "rencontres";
  if (raw.includes("sport")) return "sport";
  if (raw.includes("social")) return "social";
  if (raw.includes("karaoke")) return "karaoke";
  return "soiree";
}

function catLabel(cat) {
  return ({
    all: "Toutes",
    afterwork: "Afterwork",
    danse: "Danse",
    rencontres: "Rencontres",
    sport: "Sport",
    social: "Social",
    karaoke: "Karaoké",
    soiree: "Soirée",
  })[cat] || "Soirée";
}

function formatEventDate(value) {
  if (!value) return "Date inconnue";
  const date = new Date(`${value}T00:00:00`);
  if (Number.isNaN(date.getTime())) return String(value);
  return new Intl.DateTimeFormat("fr-FR", { weekday: "short", day: "2-digit", month: "short" }).format(date);
}

function isRealDistance(value) {
  const distance = Number(value);
  return Number.isFinite(distance) && distance >= 0 && distance < 200000;
}

function normalizeApiEvent(event, index = 0) {
  const cat = catFromApi(event);
  const placeName = event.placeName || event.venue || event.venue_name || "Lieu à confirmer";
  const date = event.date || event.date_start || event.start_time || event.iso;
  const time = event.time || (event.start_time ? String(event.start_time).slice(11, 16) : "20:00");
  return {
    id: event.id || event.event_id || `api-${index}`,
    cat,
    catLabel: event.catLabel || catLabel(cat),
    date: event.dateLabel || formatEventDate(date),
    time,
    iso: event.iso || event.start_time || `${date || new Date().toISOString().slice(0, 10)}T${time || "20:00"}`,
    mix: Number(event.mix || event.mixityScore || event.mixity_score || 50),
    name: event.name || event.title || "Event sans titre",
    venue: placeName,
    placeName,
    address: event.address || event.location || event.venue_address || placeName,
    placeUrl: event.placeUrl || event.venue_url || event.sourceUrl || event.source_url || "#",
    source: event.source || event.source_name || "NightIntel API",
    sourceUrl: event.sourceUrl || event.source_url || event.url || "#",
    tier: event.tier || event.djTier || "C",
    verified: Boolean(event.verified),
    desc: event.desc || event.description || "",
    age: event.age || event.ageRange || event.age_min || "18+",
    distance_m: isRealDistance(event.distance_m) ? Number(event.distance_m) : null,
  };
}

function buildNoctDataFromEvents(base, events) {
  const counts = { all: events.length };
  for (const event of events) counts[event.cat] = (counts[event.cat] || 0) + 1;
  const ambiance = events.length
    ? Math.round(events.reduce((sum, event) => sum + Number(event.mix || 0), 0) / events.length)
    : 0;
  return {
    ...base,
    liveCount: events.length,
    totalRadars: Math.max(events.length, 1),
    activeRadars: events.length,
    fillPct: events.length ? 100 : 0,
    ambiance,
    signaux: base.signaux.map((signal) => ({ ...signal, count: counts[signal.id] || 0 })),
    upcomingNext: events.slice(0, 4),
    upcomingAll: events,
  };
}

function applyAccent(name) {
  const a = ACCENTS[name] || ACCENTS.pink;
  const r = document.documentElement;
  r.style.setProperty("--neon-pink", a.primary);
  r.style.setProperty("--neon-pink-glow", a.glow);
  r.style.setProperty("--neon-pink-deep", a.deep);
  r.style.setProperty("--neon-pink-soft", a.soft);
  r.style.setProperty("--accent", a.primary);
  r.style.setProperty("--accent-glow", a.glow);
  r.style.setProperty("--accent-soft", a.soft);
}

function applyDensity(d) {
  const r = document.documentElement;
  if (d === "compact") {
    r.style.setProperty("--gap-events", "8px");
    r.style.setProperty("--pad-card", "12px");
  } else {
    r.style.setProperty("--gap-events", "12px");
    r.style.setProperty("--pad-card", "16px");
  }
}

function App() {
  const [data, setData]                 = useState(() => window.NOCT_DATA);
  const [apiState, setApiState]         = useState({ loading: true, error: null });
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const isMobile = useIsMobile();

  const [screen, setScreen]             = useState("home");
  const [authMode, setAuthMode]         = useState("login");
  const [authForm, setAuthForm]         = useState({ email: "", password: "", confirm: "", name: "" });
  const [authError, setAuthError]       = useState(null);
  const [authBusy, setAuthBusy]         = useState(false);
  const [user, setUser]                 = useState(null);
  const [mode, setMode]                 = useState({ veille: true, party: true });
  const [size, setSize]                 = useState("M");
  const [lang, setLang]                 = useState("FR");
  const [search, setSearch]             = useState("");
  const [activeFilter, setActiveFilter] = useState("all");
  const [activeDistrict, setActiveDist] = useState("all");
  const [toggles, setToggles]           = useState({ verified: false, hidePast: false });
  const [mood, setMood]                 = useState(null);
  const [selected, setSelected]         = useState(new Set());
  const [homeResult, setHomeResult]     = useState(null);
  const [homeBusy, setHomeBusy]         = useState(null);
  const [auth, setAuth]                 = useState({ ready: true, isAuthenticated: false });
  const [userLocation, setUserLocation] = useState(ANNECY_LOCATION);
  const [guestProfile, setGuestProfile] = useState(() => readGuestProfile());

  const awardGuest = useCallback((points, badge) => {
    if (auth.isAuthenticated) return;
    setGuestProfile((current) => {
      const next = writeGuestProfile({
        points: (current.points || 0) + points,
        badges: badge ? [...(current.badges || []), badge] : (current.badges || []),
        scans: (current.scans || 0) + 1,
      });
      return next;
    });
  }, [auth.isAuthenticated]);

  useEffect(() => {
    const syncAuth = async () => {
      const token = getToken();
      if (!token) {
        setUser(null);
        setAuth({ ready: true, isAuthenticated: false });
        return;
      }
      try {
        const payload = await apiGet("/auth/me");
        setUser(payload.user || payload);
        setAuth({ ready: true, isAuthenticated: true });
        setScreen((current) => current === "login" || current === "register" ? "home" : current);
      } catch {
        clearToken();
        setUser(null);
        setAuth({ ready: true, isAuthenticated: false });
      }
    };
    syncAuth();
    window.addEventListener("nightintel:auth-expired", syncAuth);
    window.addEventListener("nightintel:auth-updated", syncAuth);
    return () => {
      window.removeEventListener("nightintel:auth-expired", syncAuth);
      window.removeEventListener("nightintel:auth-updated", syncAuth);
    };
  }, []);

  useEffect(() => {
    if (!navigator.geolocation) return;
    navigator.geolocation.getCurrentPosition(
      ({ coords }) => setUserLocation({ lat: coords.latitude, lng: coords.longitude }),
      () => setUserLocation(ANNECY_LOCATION),
      { enableHighAccuracy: false, timeout: 7000, maximumAge: 10 * 60 * 1000 }
    );
  }, []);

  const loadEvents = useCallback(async () => {
    setApiState({ loading: true, error: null });
    try {
      const params = new URLSearchParams({
        city: "annecy",
        hidePast: String(toggles.hidePast),
      });
      if (activeFilter !== "all") params.set("category", activeFilter);
      if (activeFilter !== "all") params.set("vibe", activeFilter);
      if (search.trim()) params.set("search", search.trim());
      if (toggles.verified) params.set("verifiedOnly", "true");
      const payload = await apiGet(`/events?${params.toString()}`);
      const rawEvents = Array.isArray(payload) ? payload : (payload.items || payload.events || []);
      const events = rawEvents.map(normalizeApiEvent);
      setData(buildNoctDataFromEvents(window.NOCT_DATA, events));
      setApiState({ loading: false, error: null });
    } catch (error) {
      setApiState({ loading: false, error: error.message || "API unavailable" });
      setData(window.NOCT_DATA);
    }
  }, [activeFilter, search, toggles.hidePast, toggles.verified]);

  useEffect(() => {
    if (auth.ready) loadEvents();
  }, [auth.ready, loadEvents]);

  /* Live clock */
  const [now, setNow] = useState(() => fmtClock(new Date()));
  useEffect(() => {
    const i = setInterval(() => setNow(fmtClock(new Date())), 30 * 1000);
    return () => clearInterval(i);
  }, []);

  /* Apply tweaks to CSS vars */
  useEffect(() => { applyAccent(t.accent); }, [t.accent]);
  useEffect(() => { applyDensity(t.density); }, [t.density]);

  /* Measure the bottom rail height into --rail-height so the main area's
     bottom padding always clears it. Re-measures on resize. */
  useEffect(() => {
    const measure = () => {
      const r = document.querySelector(".bottom-rail");
      const h = r ? Math.ceil(r.getBoundingClientRect().height) : 0;
      document.documentElement.style.setProperty("--rail-height", h + "px");
    };
    measure();
    window.addEventListener("resize", measure);
    const t1 = setTimeout(measure, 100);
    const t2 = setTimeout(measure, 500);
    return () => {
      window.removeEventListener("resize", measure);
      clearTimeout(t1); clearTimeout(t2);
    };
  }, [t.showFooter]);

  /* Reset accent overrides + apply skin to <html> (so all descendants repaint) */
  useEffect(() => {
    const r = document.documentElement;
    ["--neon-pink", "--neon-pink-glow", "--neon-pink-deep", "--neon-pink-soft",
     "--accent", "--accent-glow", "--accent-soft"]
      .forEach(k => r.style.removeProperty(k));
    r.setAttribute("data-skin", t.skin);
    /* Sync html/body bg so over-scroll matches the active skin */
    const bg = {
      nuit:  "#07070B",
      paper: "#EFE2C8",
      girly: "#FFF0F7",
    }[t.skin] || "#07070B";
    document.documentElement.style.background = bg;
    document.body.style.background = bg;
    /* Force a repaint on .app — Chromium fails to invalidate descendants
       when a high-up custom-property override changes the resolved value of
       `background: var(--ink-2)` on already-painted boxes. Toggling display
       evicts the stale paint and lets the new tokens take effect. */
    const app = document.querySelector(".app");
    if (app) {
      app.style.display = "none";
      // eslint-disable-next-line no-unused-expressions
      app.offsetHeight; // trigger reflow
      app.style.display = "";
    }
  }, [t.skin]);

  const toggleSelect = useCallback((id) => {
    setSelected(prev => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id); else next.add(id);
      return next;
    });
  }, []);

  /* Filter events */
  const filteredFull = useMemo(() => {
    return data.upcomingAll.filter(ev => {
      if (activeFilter !== "all" && ev.cat !== activeFilter) return false;
      if (toggles.verified && !ev.verified) return false;
      if (search.trim()) {
        const q = search.toLowerCase();
        if (!ev.name.toLowerCase().includes(q) && !ev.venue.toLowerCase().includes(q)) return false;
      }
      return true;
    });
  }, [data.upcomingAll, activeFilter, toggles.verified, search]);

  /* Live-count the signaux from real data instead of the hardcoded data.signaux[].count.
     Keeps the order from data.signaux. */
  const liveData = useMemo(() => {
    const counts = { all: data.upcomingAll.length };
    for (const ev of data.upcomingAll) counts[ev.cat] = (counts[ev.cat] || 0) + 1;
    return {
      ...data,
      signaux: data.signaux.map(s => ({ ...s, count: counts[s.id] ?? 0 })),
    };
  }, [data]);

  const eventCount = filteredFull.length;
  const visibleEvents = useMemo(() => filteredFull.slice(0, 120), [filteredFull]);
  const selCount = selected.size;

  const submitAuth = useCallback(async () => {
    setAuthBusy(true);
    setAuthError(null);
    try {
      if (!authForm.email.trim()) throw new Error("Email required");
      if (!authForm.password) throw new Error("Password required");
      if (authMode === "register" && authForm.password !== authForm.confirm) {
        throw new Error("Passwords do not match");
      }
      const payload = await (authMode === "register" ? register : login)({
        email: authForm.email.trim(),
        password: authForm.password,
        name: authForm.name.trim() || authForm.email.trim(),
      });
      setUser(payload.user || null);
      setAuth({ ready: true, isAuthenticated: true });
      setScreen("home");
      await loadEvents();
    } catch (error) {
      setAuthError(error.message || "Authentication failed");
    } finally {
      setAuthBusy(false);
    }
  }, [authForm, authMode, loadEvents]);

  const logoutUser = useCallback(async () => {
    if (!auth.isAuthenticated) {
      setAuthMode("login");
      setScreen("login");
      return;
    }
    await fetchWithAuth("/auth/logout", { method: "POST" }).catch(() => {});
    clearToken();
    setUser(null);
    setAuth({ ready: true, isAuthenticated: false });
    setScreen("home");
  }, [auth.isAuthenticated]);

  const authButtonLabel = auth.isAuthenticated && user?.name ? `Logout · ${user.name}` : "Connexion";

  const runZoneScan = useCallback(async ({ limit = "24", auto = false } = {}) => {
    const params = new URLSearchParams({
      lat: String(userLocation.lat),
      lng: String(userLocation.lng),
      radius: "3000",
      city: "annecy",
      limit,
    });
    const response = await fetchWithAuth(`/api/zone-scan?${params.toString()}`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const payload = await response.json();
    awardGuest(auto ? 15 : 10, "Explorateur");
    return {
      kind: auto ? "zone-auto-scan" : "zone-scan",
      title: auto ? "Zone scannée automatiquement" : "Scanner Zone",
      items: payload.items || [],
      fallback: `WC ${payload.counts?.toilets || 0} · Pharma ${payload.counts?.pharmacies || 0} · Capotes ${payload.counts?.condoms || 0} · Lieux ${payload.counts?.venues || 0}`,
      badge: "Explorateur",
      points: auto ? 15 : 10,
      autoScan: auto,
    };
  }, [awardGuest, userLocation.lat, userLocation.lng]);

  const runHomeReco = useCallback(async (kind) => {
    setHomeBusy(kind);
    setHomeResult(null);
    try {
      if (kind === "pipi") {
        const params = new URLSearchParams({
          lat: String(userLocation.lat),
          lng: String(userLocation.lng),
          radius: "3000",
          limit: "8",
        });
        const response = await fetchWithAuth(`/api/nightintel-pipi?${params.toString()}`);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const payload = await response.json();
        const items = (payload.items || [])
          .sort((a, b) => (Number(a.distance_m) || 9999999) - (Number(b.distance_m) || 9999999));
        if (!items.length) {
          setHomeResult(await runZoneScan({ limit: "24", auto: true }));
          return;
        }
        setHomeResult({
          kind,
          title: "Urgence Pipi",
          items,
          fallback: payload.fallback_kind || "openstreetmap",
        });
        return;
      }
      if (kind === "capote") {
        const params = new URLSearchParams({
          lat: String(userLocation.lat),
          lng: String(userLocation.lng),
          radius: "3000",
          limit: "8",
        });
        const response = await fetchWithAuth(`/api/nightintel-capote?${params.toString()}`);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const payload = await response.json();
        const items = (payload.items || [])
          .sort((a, b) => (Number(a.distance_m) || 9999999) - (Number(b.distance_m) || 9999999));
        if (!items.length) {
          setHomeResult(await runZoneScan({ limit: "24", auto: true }));
          return;
        }
        setHomeResult({
          kind,
          title: "Urgence Capotes",
          items,
          fallback: payload.fallback_kind || "openstreetmap",
        });
        return;
      }
      if (kind === "zone-scan") {
        setHomeResult(await runZoneScan({ limit: "24" }));
        return;
      }
      const params = new URLSearchParams({
        city: "annecy",
        lat: String(userLocation.lat),
        lng: String(userLocation.lng),
        limit: kind === "party" ? "20" : "8",
      });
      const endpoint = kind === "nightintel" ? "/api/recos/nightintel" : "/api/recos/party";
      const response = await fetchWithAuth(`${endpoint}?${params.toString()}`);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const payload = await response.json();
      const items = (payload.items || payload.events || [])
        .map((item) => kind === "party" ? normalizeApiEvent(item) : item)
        .sort((a, b) => {
          if (kind === "party") return new Date(a.iso || a.date || 0) - new Date(b.iso || b.date || 0);
          return (Number(a.distance_m) || 9999999) - (Number(b.distance_m) || 9999999);
        })
        .slice(0, kind === "party" ? 20 : 4);
      if (!items.length) {
        setHomeResult(await runZoneScan({ limit: "24", auto: true }));
        return;
      }
      setHomeResult({
        kind,
        title: kind === "nightintel" ? "Urgence GlouGlou" : kind === "pipi" ? "Urgence Pipi" : kind === "capote" ? "Urgence Capotes" : "Party",
        items,
        fallback: "events_endpoint",
      });
    } catch (error) {
      setHomeResult({
        kind,
        title: kind === "nightintel" ? "Urgence GlouGlou" : kind === "pipi" ? "Urgence Pipi" : kind === "capote" ? "Urgence Capotes" : "Party",
        error: "Recommendations unavailable. The dashboard is still available.",
        detail: error.message,
        items: [],
      });
    } finally {
      setHomeBusy(null);
    }
  }, [runZoneScan, userLocation.lat, userLocation.lng]);

  if (screen === "login" || screen === "register") {
    return (
      <AuthScreen
        mode={authMode}
        setMode={setAuthMode}
        form={authForm}
        setForm={setAuthForm}
        busy={authBusy}
        error={authError}
        onSubmit={submitAuth}
      />
    );
  }

  if (screen === "home") {
    const homeServiceClass = homeResult ? `home-service-active service-${homeResult.kind || "result"}` : "";
    return (
      <>
        <div
          className={`app home-app ${homeServiceClass}`}
          data-auth-ready={auth.ready ? "true" : "false"}
          data-authenticated={auth.isAuthenticated ? "true" : "false"}
        >
          <HomeLanding
            data={data}
            user={user}
            guestProfile={guestProfile}
            homeBusy={homeBusy}
            homeResult={homeResult}
            onReco={runHomeReco}
            onDashboard={() => setScreen("dashboard")}
            onWorldMap={() => { window.location.href = "/world-radar"; }}
            onRegister={() => {
              setAuthMode("register");
              setScreen("register");
            }}
            onLogout={logoutUser}
            onClearResult={() => {
              setHomeResult(null);
              setHomeBusy(null);
            }}
          />
          {t.showFooter ? <Footer/> : null}
        </div>
        <BottomRail authLabel={authButtonLabel} onAuthClick={logoutUser}/>
        <NightIntelVault/>
        <NocturneTweaks t={t} setTweak={setTweak}/>
      </>
    );
  }

  return (
    <>
      <div
        className="app"
        data-auth-ready={auth.ready ? "true" : "false"}
        data-authenticated={auth.isAuthenticated ? "true" : "false"}
      >
        <TopBar
          mode={mode} setMode={setMode}
          size={size} setSize={setSize}
          search={search} setSearch={setSearch}
          lang={lang} setLang={setLang}
          eventCount={eventCount}
          time={now}
          skin={t.skin}
          setSkin={(v) => setTweak("skin", v)}
          authLabel={authButtonLabel}
          onAuthClick={logoutUser}
        />
        <Sidebar
          data={liveData}
          activeFilter={activeFilter} setActiveFilter={setActiveFilter}
          activeDistrict={activeDistrict} setActiveDistrict={setActiveDist}
          toggles={toggles} setToggles={setToggles}
        />
        <main className="main">
          <div className="main-inner">
            <div className="trio-strip">
              <Hero data={data} mood={mood} setMood={setMood} compact/>
              <ScannerStrip compact/>
              <ToolsSection compact/>
            </div>

            <section className="section">
              <div className="sec-head">
                <h2>
                  Événements à venir
                  <span className="count-pill">{eventCount}</span>
                </h2>
                <div className="sec-actions">
                  <a>Toutes les villes <Ico name="chev-right" size={11} style={{ verticalAlign: -1 }}/></a>
                  <button className={`sec-export ${selCount ? "has-sel" : ""}`} title="Exporter vers l'agenda">
                    <Ico name="download" size={13}/>
                    Export agenda
                    {selCount > 0 ? <span className="sec-export-count">· {selCount}</span> : null}
                  </button>
                </div>
              </div>
              {apiState.error ? (
                <div className="home-error" style={{ marginBottom: 12 }}>
                  API error: {apiState.error}
                  <small>Retry, change city, or use local fallback data.</small>
                </div>
              ) : null}
              {apiState.loading ? <EmptyState label="Loading events from /events..." /> : null}
              <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
                {visibleEvents.map(ev => (
                  <EventCardFull
                    key={ev.id}
                    ev={ev}
                    selected={selected.has(ev.id)}
                    onToggle={() => toggleSelect(ev.id)}
                    onOpen={() => {}}
                  />
                ))}
                {filteredFull.length > visibleEvents.length ? (
                  <EmptyState label={`Affichage optimisé : ${visibleEvents.length}/${filteredFull.length} événements. Affine les filtres pour réduire la liste.`} />
                ) : null}
                {filteredFull.length === 0 ? (
                  <EmptyState label="Aucun résultat. Élargis les filtres ou re-scrape la zone." />
                ) : null}
              </div>
            </section>
          </div>
        </main>
        {t.showFooter ? <Footer/> : null}
      </div>
      <BottomRail authLabel={authButtonLabel} onAuthClick={logoutUser}/>
      <NightIntelVault/>
      <NocturneTweaks t={t} setTweak={setTweak}/>
    </>
  );
}

function AuthScreen({ mode, setMode, form, setForm, busy, error, onSubmit }) {
  const isRegister = mode === "register";
  const [oauthError, setOauthError] = useState(null);
  const submitForm = (event) => {
    event.preventDefault();
    onSubmit();
  };
  const externalProviders = [
    ["google", "Google"],
    ["apple", "Apple"],
    ["instagram", "Instagram"],
    ["discord", "Discord"],
  ];
  const startExternal = async (provider) => {
    setOauthError(null);
    try {
      await oauthStart(provider);
    } catch (error) {
      setOauthError(error.message || "OAuth provider unavailable");
    }
  };
  return (
    <main className="auth-screen">
      <section className="auth-card">
        <span className="eyebrow">Nocturne v6</span>
        <h1>{isRegister ? "Create Night Agent" : "Login Night Agent"}</h1>
        <p>Connecte-toi avec un compte externe ou garde l'email en fallback.</p>
        <div className="oauth-grid" aria-label="External login providers">
          {externalProviders.map(([id, label]) => (
            <button key={id} type="button" className={`oauth-btn ${id}`} onClick={() => startExternal(id)}>
              <span>{label.slice(0, 1)}</span>
              {label}
            </button>
          ))}
        </div>
        <form onSubmit={submitForm}>
          <label>
            <span>Email</span>
            <input value={form.email} onChange={(event) => setForm((current) => ({ ...current, email: event.target.value }))} placeholder="agent@nightintel.local" autoComplete="email" />
          </label>
          {isRegister ? (
            <label>
              <span>Name</span>
              <input value={form.name} onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))} placeholder="Night Agent" autoComplete="name" />
            </label>
          ) : null}
          <label>
            <span>Password</span>
            <input type="password" value={form.password} onChange={(event) => setForm((current) => ({ ...current, password: event.target.value }))} placeholder="••••••••" autoComplete={isRegister ? "new-password" : "current-password"} />
          </label>
          {isRegister ? (
            <label>
              <span>Confirm</span>
              <input type="password" value={form.confirm} onChange={(event) => setForm((current) => ({ ...current, confirm: event.target.value }))} placeholder="••••••••" autoComplete="new-password" />
            </label>
          ) : null}
          {error ? <p className="auth-error">{error}</p> : null}
          {oauthError ? <p className="auth-error">{oauthError}</p> : null}
          <button className="auth-primary" disabled={busy} type="submit">
            {busy ? "Loading..." : isRegister ? "Register" : "Login"}
          </button>
          <button className="auth-secondary" type="button" onClick={() => setMode(isRegister ? "login" : "register")}>
            {isRegister ? "I already have an account" : "Create account"}
          </button>
        </form>
      </section>
    </main>
  );
}

function HomeLanding({ data, user, guestProfile, homeBusy, homeResult, onReco, onDashboard, onWorldMap, onRegister, onLogout, onClearResult }) {
  const [focusedItem, setFocusedItem] = useState(null);
  const [validatedCards, setValidatedCards] = useState({});
  const cities = [
    ["ibiza", "Ibiza", "🏝️"],
    ["geneve", "Genève", "🇨🇭"],
    ["annecy", "Annecy", "🏔️"],
    ["barcelona", "Barcelona", "🇪🇸"],
    ["seoul", "Seoul", "🇰🇷"],
  ];
  const categories = [
    ["Bars", "🍺"], ["Pub", "🍻"], ["Clubs", "🪩"], ["Restos", "🍽️"],
    ["Rooftops", "🌇"], ["Plages", "🏖️"], ["Cafés", "☕"], ["Hôtels", "🏨"], ["Secrets", "🔮"],
  ];
  const resultItems = (homeResult?.items || [])
    .slice()
    .sort((a, b) => (Number(a.distance_m) || 9999999) - (Number(b.distance_m) || 9999999));
  const hotSpots = useMemo(() => {
    const byPlace = new Map();
    (data.spotsChauds || []).forEach((spot, index) => {
      const key = spot.name || spot.address || `spot-${index}`;
      if (!key || byPlace.has(key)) return;
      byPlace.set(key, {
        id: spot.id || `spot-manual-${index}`,
        name: key,
        category: spot.category || "Spot",
        address: spot.address || "Adresse à confirmer",
        score: Number(spot.score) || 0,
        source: spot.source || "Night Intel",
        source_url: spot.source_url || spot.sourceUrl,
        placeUrl: spot.placeUrl || spot.source_url || spot.sourceUrl,
        google_maps_url: spot.google_maps_url || (spot.address ? `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${key} ${spot.address}`)}` : null),
      });
    });
    (data.upcomingAll || []).forEach((event) => {
      const key = event.placeName || event.venue || event.address;
      const normalizedKey = String(key || "").toLowerCase().trim();
      if (normalizedKey === "annecy" || normalizedKey.includes("lieu privatisé")) return;
      if (!key || byPlace.has(key)) return;
      byPlace.set(key, {
        id: `spot-${event.id}`,
        name: key,
        category: event.catLabel || event.cat || "Signal",
        address: event.address || event.venue || "Annecy",
        score: Number(event.mix) || 0,
        source: event.source || "Night Intel",
        source_url: event.sourceUrl,
        placeUrl: event.placeUrl || event.sourceUrl,
        google_maps_url: event.address ? `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(`${key} ${event.address}`)}` : null,
      });
    });
    return Array.from(byPlace.values())
      .sort((a, b) => b.score - a.score)
      .slice(0, 12);
  }, [data.spotsChauds, data.upcomingAll]);
  const isFocused = Boolean(focusedItem);
  const closeService = () => {
    setFocusedItem(null);
    onClearResult?.();
  };
  const markCard = (item, value) => {
    const key = item.id || item.name || item.title || item.address;
    setValidatedCards((current) => ({ ...current, [key]: value }));
  };

  return (
    <main className="home-main">
      <section className="home-hero">
        {user ? (
          <div className="home-boost">
            <span>Boost du jour</span>
            <strong>Love fever</strong>
            <em>Chaque reco doit donner une action claire.</em>
            <button className="home-session-btn" onClick={onLogout}>{`Logout · ${user.name || "Night Agent"}`}</button>
          </div>
        ) : null}

        {isFocused ? (
          <section className={`home-focus-card ${homeResult?.kind || "result"}`}>
            <div className="home-focus-topline">
              <button className="home-focus-back" onClick={() => setFocusedItem(null)}>← Retour</button>
              {homeResult ? <button className="home-service-close" onClick={closeService} aria-label="Fermer le service">×</button> : null}
            </div>
            <span className="eyebrow">{homeResult?.title || "Détail"}</span>
            <h1>{focusedItem.name || focusedItem.title}</h1>
            <p>{focusedItem.address || focusedItem.venue || focusedItem.venue_name || "Adresse à confirmer"}</p>
          </section>
        ) : (
          <button className="home-dashboard" onClick={onDashboard}>
            <span>🔥 Night Intel — Dashboard</span>
          </button>
        )}

        {!isFocused && !homeResult ? (
          <div className="home-entry-actions" aria-label="Entrée NightIntel">
            <button className="home-world-map" onClick={onWorldMap}>
              <span>🌍 Plan du monde</span>
              <strong>NightIntel Network</strong>
              <em>Plan full screen · tickers · signaux live</em>
            </button>
            <button
              className={`home-entry-scan ${homeBusy === "zone-scan" ? "loading" : ""}`}
              onClick={() => onReco("zone-scan")}
            >
              <span>📡 Scanner autour de moi</span>
              <strong>Zone phone scan</strong>
              <em>{homeBusy === "zone-scan" ? "Scan autour de toi..." : "WC · pharmacies · capotes · lieux · events"}</em>
            </button>
          </div>
        ) : null}

        {!isFocused && !homeResult ? <div className="home-actions">
          <button
            className={`home-big-card nightintel ${homeBusy === "nightintel" ? "loading" : ""}`}
            onClick={() => onReco("nightintel")}
          >
            <span className="home-card-icon">🍻</span>
            <strong>Urgence GlouGlou</strong>
            <em>4 bars proches, lisibles, actionnables</em>
            <small>{homeBusy === "nightintel" ? "Scan en cours..." : "Friends & Party : What Else ?"}</small>
          </button>
          <button
            className={`home-big-card party ${homeBusy === "party" ? "loading" : ""}`}
            onClick={() => onReco("party")}
          >
            <span className="home-card-icon">🎉</span>
            <strong>Party !</strong>
            <em>Les soirées actives autour de toi</em>
            <small>{homeBusy === "party" ? "Radar en cours..." : `${data.upcomingAll.length} signaux Annecy`}</small>
          </button>
          <button
            className={`home-big-card utility pipi ${homeBusy === "pipi" ? "loading" : ""}`}
            onClick={() => onReco("pipi")}
          >
            <span className="home-card-icon">🚻</span>
            <strong>Urgence Pipi</strong>
            <em>Toilettes proches + bars partenaires</em>
            <small>{homeBusy === "pipi" ? "Recherche WC..." : "Check validé = points"}</small>
          </button>
          <button
            className={`home-big-card utility capote ${homeBusy === "capote" ? "loading" : ""}`}
            onClick={() => onReco("capote")}
          >
            <span className="home-card-icon">🛡️</span>
            <strong>Urgence Capotes</strong>
            <em>Pharmacies, distributeurs, points safe</em>
            <small>{homeBusy === "capote" ? "Recherche safe..." : "Pharmacies + distributeurs · 3 km"}</small>
          </button>
        </div> : null}

        {!isFocused && !homeResult && hotSpots.length ? (
          <section className="home-spots-list" aria-label="Spots Night Intel">
            <div className="home-spots-head">
              <span>📍 Spots</span>
              <strong>Liste des spots</strong>
              <em>Base locale + signaux actifs dédupliqués</em>
              <a href="/spots">Voir tous</a>
            </div>
            <div className="home-spots-grid">
              {hotSpots.map((spot, index) => (
                <article
                  className="home-spot-card"
                  key={spot.id || spot.name || index}
                  onClick={() => setFocusedItem(spot)}
                  role="button"
                  tabIndex={0}
                  onKeyDown={(event) => {
                    if (event.key === "Enter" || event.key === " ") setFocusedItem(spot);
                  }}
                >
                  <span>{String(index + 1).padStart(2, "0")}</span>
                  <strong>
                    <a
                      href={spot.placeUrl || spot.source_url || spot.google_maps_url || "#"}
                      target="_blank"
                      rel="noreferrer noopener"
                      onClick={(event) => event.stopPropagation()}
                    >
                      {spot.name}
                    </a>
                  </strong>
                  <em>{spot.address}</em>
                  <small>{spot.category} · score {spot.score}</small>
                </article>
              ))}
            </div>
          </section>
        ) : null}

        {isFocused ? (
          <section className="home-detail-panel">
            <dl>
              <div><dt>Distance</dt><dd>{isRealDistance(focusedItem.distance_m) ? `${focusedItem.distance_m} m` : "À confirmer"}</dd></div>
              <div><dt>Adresse</dt><dd>{focusedItem.address || "Adresse à confirmer"}</dd></div>
              <div><dt>Ouverture</dt><dd>{focusedItem.opening_hours || "Horaires à vérifier"}</dd></div>
              <div><dt>Accessibilité</dt><dd>{focusedItem.wheelchair || focusedItem.access || "À vérifier"}</dd></div>
              <div><dt>Source</dt><dd>{focusedItem.source || "Open Data"}</dd></div>
            </dl>
            <div className="home-detail-actions">
              {focusedItem.google_maps_url ? <a href={focusedItem.google_maps_url} target="_blank" rel="noreferrer">Ouvrir Google Maps</a> : null}
              {focusedItem.source_url ? <a href={focusedItem.source_url} target="_blank" rel="noreferrer">Voir source</a> : null}
              {!user ? <button onClick={onRegister}>S'enregistrer</button> : null}
            </div>
          </section>
        ) : homeResult ? (
          <section className={`home-results service-${homeResult.kind || "result"}`}>
            <div className="home-results-head">
              <div>
                <span className="eyebrow">{homeResult.title}</span>
                <h2>{homeResult.error ? "Erreur actionnable" : `${resultItems.length} résultats prêts`}</h2>
                {!homeResult.error && homeResult.fallback ? <small>{homeResult.fallback}</small> : null}
              </div>
              <div className="home-results-actions">
                {!user ? <button onClick={onRegister}>S'enregistrer</button> : null}
                <button className="home-service-close" onClick={closeService} aria-label="Fermer le service">×</button>
              </div>
            </div>
            {homeResult.error ? (
              <p className="home-error">{homeResult.error} <small>{homeResult.detail}</small></p>
            ) : (
              <div className="home-result-grid">
                {resultItems.map((item, index) => (
                  <article
                    className={`home-result-card service-${homeResult.kind || "result"}`}
                    key={item.id || item.name || index}
                    onClick={() => setFocusedItem({ ...item, serviceKind: homeResult.kind })}
                    role="button"
                    tabIndex={0}
                    onKeyDown={(event) => {
                      if (event.key === "Enter" || event.key === " ") setFocusedItem({ ...item, serviceKind: homeResult.kind });
                    }}
                  >
                    <span>{String(index + 1).padStart(2, "0")}</span>
                    <strong>
                      <a
                        href={item.sourceUrl || item.source_url || item.url || item.presaleUrl || item.placeUrl || item.google_maps_url || "#"}
                        target="_blank"
                        rel="noreferrer noopener"
                        onClick={(event) => event.stopPropagation()}
                      >
                        {item.name || item.title}
                      </a>
                    </strong>
                    <em>{item.venue || item.address || item.venue_name || item.type || "Annecy"}</em>
                    <small>{isRealDistance(item.distance_m) ? `${item.distance_m} m` : item.date || item.hours || homeResult.fallback}</small>
                    {user ? (
                      <div className="home-validation-actions" onClick={(event) => event.stopPropagation()}>
                        <button
                          className={validatedCards[item.id || item.name || item.title || item.address] === "valid" ? "active valid" : ""}
                          onClick={() => markCard(item, "valid")}
                        >
                          Valider
                        </button>
                        <button
                          className={validatedCards[item.id || item.name || item.title || item.address] === "reject" ? "active reject" : ""}
                          onClick={() => markCard(item, "reject")}
                        >
                          Non
                        </button>
                      </div>
                    ) : null}
                    {item.google_maps_url ? (
                      <a className="home-map-link" href={item.google_maps_url} target="_blank" rel="noreferrer" onClick={(event) => event.stopPropagation()}>Google Maps</a>
                    ) : null}
                  </article>
                ))}
              </div>
            )}
          </section>
        ) : null}

        {!isFocused && !homeResult ? <nav className="home-chip-cloud" aria-label="Villes">
          {cities.map(([id, label, icon]) => (
            <button key={id} onClick={onDashboard}>{icon} {label}</button>
          ))}
        </nav> : null}
        {!isFocused && !homeResult ? <nav className="home-chip-cloud categories" aria-label="Catégories">
          {categories.map(([label, icon]) => (
            <button key={label} onClick={onDashboard}>{icon} {label}</button>
          ))}
        </nav> : null}
        {!isFocused && !homeResult ? <button className="home-myplaces" onClick={onDashboard}>📍 Mes Lieux · Badges · Top 25</button> : null}
      </section>
    </main>
  );
}

function EmptyState({ label }) {
  return (
    <div style={{
      gridColumn: "1 / -1",
      padding: "32px 20px",
      border: "1px dashed var(--ink-4)",
      borderRadius: 10,
      textAlign: "center",
      color: "var(--bone-2)",
      fontFamily: "var(--font-mono)",
      fontSize: 12,
      letterSpacing: "0.04em",
    }}>{label}</div>
  );
}

function fmtClock(d) {
  const h = String(d.getHours()).padStart(2, "0");
  const m = String(d.getMinutes()).padStart(2, "0");
  return `${h}:${m}`;
}

/* ===== Tweaks panel ===== */
const ACCENT_HEX_BY_NAME = {
  pink: "#FF2D6F", acid: "#C2FF3D", electric: "#5BCFFF", amber: "#FFB13D", violet: "#B47BFF",
};
const NAME_BY_HEX = Object.fromEntries(
  Object.entries(ACCENT_HEX_BY_NAME).map(([k, v]) => [v.toLowerCase(), k])
);

function NocturneTweaks({ t, setTweak }) {
  const currentHex = ACCENT_HEX_BY_NAME[t.accent] || "#FF2D6F";
  return (
    <TweaksPanel title="Tweaks">
      <TweakSection label="Skin"/>
      <TweakRadio
        label="Theme"
        value={t.skin}
        onChange={v => setTweak("skin", v)}
        options={[
          { value: "nuit",  label: "Nuit" },
          { value: "paper", label: "Paper" },
          { value: "girly", label: "Girly" },
        ]}
      />
      <TweakSection label="Accent override"/>
      <TweakColor
        label="Couleur"
        value={currentHex}
        onChange={hex => {
          const name = NAME_BY_HEX[String(hex).toLowerCase()] || "pink";
          setTweak("accent", name);
        }}
        options={Object.values(ACCENT_HEX_BY_NAME)}
      />
      <TweakSection label="Mise en page"/>
      <TweakRadio
        label="Densité"
        value={t.density}
        onChange={v => setTweak("density", v)}
        options={[
          { value: "comfortable", label: "Confort" },
          { value: "compact",     label: "Dense" },
        ]}
      />
      <TweakSection label="Éléments"/>
      <TweakToggle
        label="Pulse live"
        value={t.liveScrap}
        onChange={v => setTweak("liveScrap", v)}
      />
      <TweakToggle
        label="Pied de page"
        value={t.showFooter}
        onChange={v => setTweak("showFooter", v)}
      />
    </TweaksPanel>
  );
}

class AppErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
  }
  static getDerivedStateFromError(error) {
    return { error };
  }
  render() {
    if (this.state.error) {
      return (
        <main className="auth-screen">
          <section className="auth-card">
            <span className="eyebrow">Nocturne error</span>
            <h1>Interface fallback</h1>
            <p>{this.state.error.message || "React rendering failed."}</p>
            <button className="auth-primary" onClick={() => window.location.reload()}>Reload</button>
          </section>
        </main>
      );
    }
    return this.props.children;
  }
}

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