'use client';

import { useEffect, useState, useCallback, useRef } from 'react';
import { useParams } from 'next/navigation';
import { motion, AnimatePresence } from 'motion/react';
import { apiFetch } from '@/lib/api-client';
import { SwipeCardStack, RestaurantCard } from '@/components/kokonutui/swipe-card-stack';
import { SwipeCardSkeleton } from '@/components/kokonutui/swipe-card-skeleton';
import { MatchResult } from '@/components/match-overlay';
import { JokerReveal } from '@/components/joker-reveal';
import { JokerCard } from '@/components/joker-card';
import { evaluateOpeningStatus, resolveRestaurantTimezone } from '@/lib/opening-hours';
import { SessionInfoSheet } from '@/components/session-info-sheet';
import { TutorialOverlay } from '@/components/tutorial-overlay';
import { LanguageSwitch } from '@/components/language-switch';
import { useTranslation } from '@/lib/i18n/context';
import { logClientError } from '@/lib/log-client-error';
import { usePageTitle } from '@/lib/use-page-title';
import { addMatchToHistory } from '@/lib/match-history';
import { getStoredDisplayName, setStoredDisplayName } from '@/lib/storage';
import { celebrateMatch } from '@/lib/feedback';
import { shareViaNativeOrWeb } from '@/lib/share';

const MAX_LOAD_MORE_CLICKS = 2;

interface ToastItem {
  id: number;
  text: string;
  kind: 'join' | 'leave' | 'info';
}

function SessionCodePill({ code, onClick }: { code: string; onClick?: () => void }) {
  const { t } = useTranslation();
  const Tag = onClick ? 'button' : 'div';
  return (
    <Tag
      onClick={onClick}
      className="flex items-center gap-2 rounded-full px-4 py-1.5 bg-neutral-900 active:scale-95 transition-transform"
      style={{
        background: 'linear-gradient(#0a0a0a, #0a0a0a) padding-box, linear-gradient(90deg, #FE3C72, #ec4899, #00C2A8) border-box',
        border: '1.5px solid transparent',
      }}
    >
      <span className="text-xs text-neutral-400 font-medium tracking-wider uppercase">{t.matchHistory.sessionLabel}</span>
      <span className="font-bold tracking-widest text-white">{code}</span>
    </Tag>
  );
}

export function SessionClient() {
  const { code } = useParams<{ code: string }>();
  const { t, locale } = useTranslation();
  usePageTitle(t.session.pageTitle);
  const [cards, setCards] = useState<RestaurantCard[]>([]);
  const [participantId, setParticipantId] = useState<number | null>(null);
  const [participants, setParticipants] = useState<any[]>([]);
  const [vetosUsed, setVetosUsed] = useState(0);
  const [activeMatch, setActiveMatch] = useState<any | null>(null);
  const [loading, setLoading] = useState(true);
  const [stackEmpty, setStackEmpty] = useState(false);
  const [jokerLoading, setJokerLoading] = useState(false);
  const [jokerAlreadyDrawn, setJokerAlreadyDrawn] = useState(false);
  const [loadingMore, setLoadingMore] = useState(false);
  const [showInfoSheet, setShowInfoSheet] = useState(false);
  const [toasts, setToasts] = useState<ToastItem[]>([]);
  const isLeavingRef = useRef(false);
  const toastIdRef = useRef(0);

  const knownParticipantIdsRef = useRef<Set<number> | null>(null);
  const seenMatchIdsRef = useRef<Set<number>>(new Set());
  const celebratedRef = useRef<Set<number>>(new Set());
  const swipeHistoryRef = useRef<{ restaurantId: number | string; direction: 'yes' | 'no' | 'veto' }[]>([]);
  const prevExpandRef = useRef(0);

  const [jokerReveal, setJokerReveal] = useState<{ pendingMatch: any } | null>(null);

  const [ranking, setRanking] = useState<any[]>([]);
  const [rankingActiveCount, setRankingActiveCount] = useState(0);

  const [needsJoin, setNeedsJoin] = useState(false);
  const [joinName, setJoinName] = useState(getStoredDisplayName() ?? '');
  const [joining, setJoining] = useState(false);
  const [sessionInfo, setSessionInfo] = useState<any | null>(null);
  const [showTutorial, setShowTutorial] = useState(false);
  const [hasMore, setHasMore] = useState(false);

  function pushToast(text: string, kind: 'join' | 'leave' | 'info') {
    const id = ++toastIdRef.current;
    setToasts((prev) => [...prev, { id, text, kind }]);
    setTimeout(() => {
      setToasts((prev) => prev.filter((t2) => t2.id !== id));
    }, 3200);
  }

  useEffect(() => {
    const pid = localStorage.getItem('yumder_participant_id');
    const storedCode = localStorage.getItem('yumder_session_code');
    if (pid && storedCode === code) {
      setParticipantId(Number(pid));
    } else {
      setNeedsJoin(true);
    }
  }, [code]);

  useEffect(() => {
    if (!needsJoin) return;
    apiFetch(`/api/session/${code}/poll`)
      .then((data) => setSessionInfo(data.session))
      .catch(() => {});
  }, [needsJoin, code]);

  // Tutorial nur bei der ERSTEN Session automatisch oeffnen: einmaliges
  // LocalStorage-Flag wird beim ersten Anzeigen gesetzt und bleibt dann
  // bestehen. Manuell bleibt das Tutorial ueber "So geht's" auf der
  // Startseite erreichbar.
  useEffect(() => {
    if (!participantId || needsJoin) return;
    if (typeof window === 'undefined') return;
    if (localStorage.getItem('yumder_tutorial_seen')) return;
    localStorage.setItem('yumder_tutorial_seen', '1');
    setShowTutorial(true);
  }, [participantId, needsJoin]);

  async function handleInlineJoin() {
    setJoining(true);
    try {
      const existingAnonymousId = localStorage.getItem('yumder_anonymous_id');
      const data = await apiFetch(`/api/session/${code}/join`, {
        method: 'POST',
        body: JSON.stringify({
          displayName: joinName,
          anonymousId: existingAnonymousId || undefined,
        }),
      });
      localStorage.setItem('yumder_anonymous_id', data.anonymousId);
      localStorage.setItem('yumder_participant_id', String(data.participantId));
      localStorage.setItem('yumder_session_code', code);
      setStoredDisplayName(joinName.trim());
      setParticipantId(data.participantId);
      setNeedsJoin(false);
      loadRestaurants();
    } catch (e: any) {
      alert(e.message || t.common.errorJoinDefault);
    } finally {
      setJoining(false);
    }
  }

  function handleInlineJoinKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
    if (e.key === 'Enter' && !joining) {
      e.preventDefault();
      handleInlineJoin();
    }
  }

  function mapRestaurants(raw: any[]): RestaurantCard[] {
    return raw.map((r: any) => {
      const tz = resolveRestaurantTimezone(r.lat, r.lon);
      const opening = r.opening_hours_raw
        ? evaluateOpeningStatus(r.opening_hours_raw, new Date(), tz, t.openingHours)
        : null;
      return {
        id: r.id,
        name: r.name,
        imageUrl: r.osm_image_url || r.admin_image_url || r.fallback_image_url || 'https://placehold.co/600x800?text=Kein+Bild',
        isSymbolImage: !r.osm_image_url && !r.admin_image_url,
        cuisine: r.cuisine,
        openingStatus: opening?.label,
        isOpen: opening?.isOpen ?? null,
        sponsored: !!r.sponsored,
        discountText: r.discount_text || undefined,
        disclaimerText: r.disclaimer_text || undefined,
        address: r.address,
        city: r.city || undefined,
        postalCode: r.postal_code || undefined,
        suburb: r.suburb || undefined,
        phone: r.phone,
        website: r.website,
        openingHoursRaw: r.opening_hours_raw,
        lat: r.lat,
        lon: r.lon,
        reservationUrl: r.reservation_url || undefined,
        lieferandoUrl: r.lieferando_url || undefined,
      };
    });
  }

  const loadRestaurants = useCallback(async () => {
    setLoading(true);
    const seed = localStorage.getItem('yumder_participant_id') ?? localStorage.getItem('yumder_anonymous_id') ?? '';
    const data = await apiFetch(`/api/session/${code}/restaurants?seed=${encodeURIComponent(seed)}`);
    const mapped = mapRestaurants(data.restaurants);
    setCards(mapped);
    setStackEmpty(mapped.length === 0);
    setHasMore(!!data.hasMore);
    setLoading(false);
  }, [code, t]);

  useEffect(() => { loadRestaurants(); }, [loadRestaurants]);

  // Laedt die Karten einer hoheren Expand-Stufe nach (session-weit synchron
  // ueber den Host ausgeloest) und haengt nur die neuen an den Stapel an.
  async function loadMoreFromExpand(level: number): Promise<boolean> {
    const seed = localStorage.getItem('yumder_participant_id') ?? localStorage.getItem('yumder_anonymous_id') ?? '';
    try {
      const data = await apiFetch(`/api/session/${code}/restaurants?expand=${level}&seed=${encodeURIComponent(seed)}`);
      const mapped = mapRestaurants(data.restaurants);
      const existingIds = new Set(cards.map((c) => c.id));
      const onlyNew = mapped.filter((c) => !existingIds.has(c.id));
      setCards((prev) => {
        const prevIds = new Set(prev.map((c) => c.id));
        return [...prev, ...onlyNew.filter((c) => !prevIds.has(c.id))];
      });
      setHasMore(!!data.hasMore);
      return onlyNew.length > 0;
    } catch (e) {
      console.error('Mehr-Karten-Fehler:', e);
      logClientError('Mehr-Karten-Fehler', { error: String(e) }, code);
      return false;
    } finally {
      setLoadingMore(false);
    }
  }

  // "Weitere Karten anfordern" (Host-Aktion): erhoeht die Expand-Stufe
  // serverseitig. Alle Teilnehmer erkennen das ueber den Poll und laden nach.
  async function handleRequestMoreCards() {
    if (!participantId || loadingMore || jokerAlreadyDrawn) return;
    setLoadingMore(true);
    // Joker-Bereich sofort ausblenden und Skeleton zeigen, damit der Host
    // nicht versehentlich den Joker zieht, bevor die neuen Karten da sind.
    setStackEmpty(false);
    try {
      await apiFetch(`/api/session/${code}/expand`, {
        method: 'POST',
        body: JSON.stringify({ participantId }),
      });
      // loadingMore bleibt true, bis die neuen Karten via Poll geladen sind.
    } catch (e: any) {
      setLoadingMore(false);
      setStackEmpty(true);
      alert(e.message || t.session.errorJokerFailed);
    }
  }

  // Reagiert auf serverseitige Expand-Level-Aenderungen: laedt die neuen
  // Karten nach und schiebt den Joker damit wieder unter den Stapel.
  useEffect(() => {
    const level = Number(sessionInfo?.expand_level || 0);
    if (level > prevExpandRef.current) {
      prevExpandRef.current = level;
      loadMoreFromExpand(level).then((hasNew) => {
        setStackEmpty(!hasNew);
      });
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [sessionInfo?.expand_level]);

  useEffect(() => {
    const interval = setInterval(async () => {
      try {
        const data = await apiFetch(`/api/session/${code}/poll${participantId ? `?participantId=${participantId}` : ''}`);
        const incoming: any[] = data.participants;
        const activeNow = incoming.filter((p) => p.is_active === 1);
        const activeIdsNow = new Set<number>(activeNow.map((p) => p.id));

        if (knownParticipantIdsRef.current) {
          const prevIds = knownParticipantIdsRef.current;
          for (const p of activeNow) {
            if (!prevIds.has(p.id) && p.id !== participantId) {
              pushToast(t.session.toastJoined(p.display_name || t.session.defaultParticipantName), 'join');
            }
          }
          for (const prevId of prevIds) {
            if (!activeIdsNow.has(prevId) && prevId !== participantId) {
              const wasParticipant = incoming.find((p: any) => p.id === prevId);
              pushToast(t.session.toastLeft(wasParticipant?.display_name || t.session.defaultParticipantName), 'leave');
            }
          }
        }
        knownParticipantIdsRef.current = activeIdsNow;

        setParticipants(incoming);

        if (data.session) {
          setSessionInfo(data.session);
        }

        const me = incoming.find((p: any) => p.id === participantId);
        if (me) {
          setVetosUsed(me.vetos_used);
          if (me.is_active === 0 && !isLeavingRef.current) {
            alert(t.session.errorRemovedFromSession);
            localStorage.removeItem('yumder_participant_id');
            localStorage.removeItem('yumder_session_code');
            window.location.href = '/';
            return;
          }
        }

        if (data.matches?.some((m: any) => m.is_joker_result === 1)) {
          setJokerAlreadyDrawn(true);
        }

        const newMatch = data.matches.find((m: any) => !seenMatchIdsRef.current.has(m.id));
        if (newMatch) {
          seenMatchIdsRef.current.add(newMatch.id);

          if (!celebratedRef.current.has(newMatch.restaurant_id)) {
            celebratedRef.current.add(newMatch.restaurant_id);
            celebrateMatch();
          }

          // UPDATE (Match-Verlauf auf dem Start-Screen): genau HIER und nur
          // hier in den LocalStorage-Verlauf schreiben - dieser Zweig ist
          // durch seenMatchIds bereits pro Match-ID entdoppelt und laeuft
          // fuer ALLE Teilnehmer inkl. Joker-Ergebnisse (is_joker_result).
          // UPDATE: aktive Teilnehmer-Namen (aus "incoming", nicht dem
          // ggf. noch alten "participants"-State) werden jetzt zusaetzlich
          // mitgespeichert - fuer die neue "Wer war dabei?"-Avatar-Reihe
          // im Verlauf auf dem Start-Screen.
          const activeNamesAtMatchTime = activeNow.map(
            (p: any) => p.display_name || t.sessionInfo.guestFallbackName
          );
          addMatchToHistory(
            newMatch.restaurant_name,
            newMatch.is_joker_result === 1,
            newMatch.restaurant_address,
            activeNamesAtMatchTime,
            code
          );

          if (newMatch.is_joker_result === 1) {
            // Joker: Flip-Animation auch fuer alle Nicht-Hosts abspielen,
            // damit sie dem Aufdecken live zusehen koennen. Der Host setzt
            // jokerReveal bereits in handleJoker - hier greift der Poll nur
            // fuer die uebrigen Teilnehmer.
            if (!jokerReveal) {
              const imageUrl =
                newMatch.restaurant_sponsor_image_url ||
                newMatch.restaurant_osm_image_url ||
                newMatch.restaurant_admin_image_url ||
                newMatch.restaurant_fallback_image_url;
              setJokerReveal({
                pendingMatch: {
                  restaurant_id: newMatch.restaurant_id,
                  restaurant_name: newMatch.restaurant_name,
                  restaurant_image_url: imageUrl,
                  restaurant_is_symbol_image:
                    !newMatch.restaurant_sponsor_image_url &&
                    !newMatch.restaurant_osm_image_url &&
                    !newMatch.restaurant_admin_image_url,
                  restaurant_address: newMatch.restaurant_address,
                  restaurant_city: newMatch.restaurant_city ?? data.session?.city,
                  restaurant_postal_code: newMatch.restaurant_postal_code,
                  restaurant_suburb: newMatch.restaurant_suburb,
                  restaurant_phone: newMatch.restaurant_phone,
                  restaurant_website: newMatch.restaurant_website,
                  restaurant_opening_hours_raw: newMatch.restaurant_opening_hours_raw,
                  restaurant_cuisine: newMatch.restaurant_cuisine,
                  restaurant_lat: newMatch.restaurant_lat,
                  restaurant_lon: newMatch.restaurant_lon,
                  restaurant_reservation_url: newMatch.restaurant_reservation_url,
                  restaurant_lieferando_url: newMatch.restaurant_lieferando_url,
                  match_percentage: newMatch.match_percentage,
                  pool_count: newMatch.pool_count,
                  isJokerResult: true,
                  isForcedChoice: newMatch.yes_count === 0,
                },
              });
            }
          } else {
            // BUGFIX (13.08.): Wenn das Match zuerst lokal aus dem eigenen
            // Swipe gesetzt wurde (siehe handleSwipe), landet dessen ID nie
            // in seenMatchIds - der naechste Poll haelt es dadurch faelschlich
            // fuer "neu" und ueberschrieb bisher ALLE Felder hart, auch wenn
            // die Poll-Antwort z.B. lieferando_url nicht mitlieferte. Jetzt:
            // bei gleichem Restaurant bereits bekannte Werte als Fallback
            // behalten statt sie mit undefined zu ueberschreiben.
            setActiveMatch((prev: any) => {
              const sameRestaurant = prev && prev.restaurant_id === newMatch.restaurant_id;
              return {
                ...newMatch,
                restaurant_image_url:
                  newMatch.restaurant_sponsor_image_url ||
                  newMatch.restaurant_osm_image_url ||
                  newMatch.restaurant_admin_image_url ||
                  newMatch.restaurant_fallback_image_url ||
                  (sameRestaurant ? prev!.restaurant_image_url : undefined),
                restaurant_is_symbol_image:
                  !newMatch.restaurant_sponsor_image_url &&
                  !newMatch.restaurant_osm_image_url &&
                  !newMatch.restaurant_admin_image_url,
                restaurant_lieferando_url:
                  newMatch.restaurant_lieferando_url ?? (sameRestaurant ? prev!.restaurant_lieferando_url : undefined),
                restaurant_reservation_url:
                  newMatch.restaurant_reservation_url ?? (sameRestaurant ? prev!.restaurant_reservation_url : undefined),
                restaurant_city:
                  newMatch.restaurant_city ?? (data.session?.city) ?? (sameRestaurant ? prev!.restaurant_city : undefined),
                restaurant_suburb:
                  newMatch.restaurant_suburb ?? (sameRestaurant ? prev!.restaurant_suburb : undefined),
                isJokerResult: false,
                isForcedChoice: false,
                match_percentage: newMatch.match_percentage,
              };
            });
          }
        }
      } catch (e) {
        console.error('Poll-Fehler:', e);
        logClientError('Poll-Fehler', { error: String(e) }, code);
      }
    }, 2000);
    return () => clearInterval(interval);
  }, [code, participantId, jokerReveal, t]);

  // Laedt das aktuelle Zwischen-Ranking (Platz 2, 3, ...) sobald ein echtes
  // Match im Overlay gezeigt wird. Beim Joker gibt es bewusst keine Liste.
  useEffect(() => {
    if (!activeMatch || activeMatch.isJokerResult) {
      setRanking([]);
      setRankingActiveCount(0);
      return;
    }
    let cancelled = false;
    apiFetch(`/api/session/${code}/ranking?exclude=${encodeURIComponent(String(activeMatch.restaurant_id))}`)
      .then((data) => {
        if (cancelled) return;
        setRanking(data.ranking ?? []);
        setRankingActiveCount(data.activeParticipantCount ?? 0);
      })
      .catch((e) => {
        if (cancelled) return;
        console.error('Ranking-Fehler:', e);
        setRanking([]);
        setRankingActiveCount(0);
      });
    return () => { cancelled = true; };
  }, [activeMatch, code]);

  async function handleSwipe(cardId: number | string, direction: 'yes' | 'no' | 'veto') {
    if (!participantId) return;
    try {
      const result = await apiFetch(`/api/session/${code}/swipe`, {
        method: 'POST',
        body: JSON.stringify({ participantId, restaurantId: cardId, direction }),
      });
      swipeHistoryRef.current.push({ restaurantId: cardId, direction });
      if (direction === 'veto') setVetosUsed((v) => v + 1);
      if (result.matchResult?.isMatch) {
        const card = cards.find((c) => c.id === cardId);
        const matchedId = result.matchResult.restaurantId;
        if (!celebratedRef.current.has(matchedId)) {
          celebratedRef.current.add(matchedId);
          celebrateMatch();
        }
        setActiveMatch({
          restaurant_id: cardId,
          restaurant_name: card?.name,
          restaurant_image_url: card?.imageUrl,
          restaurant_is_symbol_image: card?.isSymbolImage,
          restaurant_address: card?.address,
          restaurant_city: card?.city,
          restaurant_postal_code: card?.postalCode,
          restaurant_suburb: card?.suburb,
          restaurant_phone: card?.phone,
          restaurant_website: card?.website,
          restaurant_opening_hours_raw: card?.openingHoursRaw,
          restaurant_cuisine: card?.cuisine,
          restaurant_lat: card?.lat,
          restaurant_lon: card?.lon,
          restaurant_reservation_url: card?.reservationUrl,
          restaurant_lieferando_url: card?.lieferandoUrl,
          match_percentage: result.matchResult.matchPercentage,
          yes_count: result.matchResult.yesCount,
          active_participant_count: result.matchResult.activeParticipantCount,
          isJokerResult: false,
          isForcedChoice: false,
        });
      }
    } catch (e: any) {
      if (direction === 'veto') {
        alert(e.message || t.session.errorVetoLimit);
      }
      console.error('Swipe-Fehler:', e);
      logClientError('Swipe-Fehler', { error: String(e), direction }, code);
    }
  }

  async function handleUndo(): Promise<boolean> {
    if (!participantId) return false;
    const last = swipeHistoryRef.current[swipeHistoryRef.current.length - 1];
    if (!last) return false;
    try {
      await apiFetch(`/api/session/${code}/swipe/undo`, {
        method: 'POST',
        body: JSON.stringify({
          participantId,
          restaurantId: last.restaurantId,
          direction: last.direction,
        }),
      });
      swipeHistoryRef.current.pop();
      if (last.direction === 'veto') {
        setVetosUsed((v) => Math.max(0, v - 1));
      }
      return true;
    } catch (e: any) {
      alert(e.message || t.session.errorUndoFailed);
      return false;
    }
  }

  async function handleJoker() {
    if (jokerAlreadyDrawn || jokerLoading) return;
    setJokerLoading(true);
    try {
      const result = await apiFetch(`/api/session/${code}/joker`, {
        method: 'POST',
        body: JSON.stringify({ participantId }),
      });
      setJokerAlreadyDrawn(true);
      const r = result.restaurant;
      const imageUrl =
        r.sponsor_image_url ||
        r.osm_image_url ||
        r.admin_image_url ||
        r.fallback_image_url ||
        'https://placehold.co/600x800?text=Kein+Bild';

      setJokerReveal({
        pendingMatch: {
          restaurant_id: r.id,
          restaurant_name: r.name,
          restaurant_image_url: imageUrl,
          restaurant_is_symbol_image: !r.sponsor_image_url && !r.osm_image_url && !r.admin_image_url,
          restaurant_address: r.address,
          restaurant_city: r.city,
          restaurant_postal_code: r.postal_code,
          restaurant_suburb: r.suburb,
          restaurant_phone: r.phone,
          restaurant_website: r.website,
          restaurant_opening_hours_raw: r.opening_hours_raw,
          restaurant_cuisine: r.cuisine,
          restaurant_lat: r.lat,
          restaurant_lon: r.lon,
          restaurant_reservation_url: r.reservation_url,
          restaurant_lieferando_url: r.lieferando_url,
          match_percentage: result.matchPercentage,
          pool_count: result.poolCount,
          isJokerResult: true,
          isForcedChoice: !!result.isForcedChoice,
        },
      });
    } catch (e: any) {
      if (e.unfinished && e.unfinished.length > 0) {
        pushToast(t.session.jokerWaitForOthers(e.unfinished), 'info');
      } else {
        alert(e.message || t.session.errorJokerFailed);
      }
    } finally {
      setJokerLoading(false);
    }
  }

  async function handleShare() {
    const url = `${window.location.origin}/session/${code}`;
    const handled = await shareViaNativeOrWeb({
      title: t.session.shareTitle,
      text: t.session.shareText(code),
      url,
    });
    if (handled) return;
    await navigator.clipboard.writeText(url);
    alert(t.session.linkCopied(url));
  }

  async function handleCopyCode() {
    try {
      await navigator.clipboard.writeText(code);
      pushToast(t.sessionInfo.codeCopiedToast, 'info');
    } catch {
      // Clipboard-API kann in seltenen Faellen fehlschlagen - dann einfach
      // stillschweigend nichts tun, das darf den restlichen Nutzerfluss
      // nicht blockieren.
    }
  }

  async function handleLeave() {
    if (!participantId) return;
    if (!confirm(t.session.confirmLeaveSession)) return;
    isLeavingRef.current = true;
    try {
      await apiFetch(`/api/session/${code}/leave`, {
        method: 'POST',
        body: JSON.stringify({ participantId }),
      });
      localStorage.removeItem('yumder_participant_id');
      localStorage.removeItem('yumder_session_code');
      window.location.href = '/';
    } catch (e: any) {
      isLeavingRef.current = false;
      alert(e.message || t.session.errorLeaveFailed);
    }
  }

  // "Neuen Tisch starten": legt eine neue Session mit denselben Filtern des
  // aktuellen Tischs an und wechselt dorthin. Kein Leave der alten Session
  // noetig - die laeuft ueber den 48h-Cleanup aus.
  async function handleNewTable() {
    const s = sessionInfo;
    if (!s || !s.city) {
      alert(t.home.errorCreateDefault);
      return;
    }
    try {
      const data = await apiFetch('/api/session', {
        method: 'POST',
        body: JSON.stringify({
          city: s.city,
          radiusKm: s.radius_km || 3,
          priceMin: s.price_min ?? 1,
          priceMax: s.price_max ?? 4,
          dietFilter: s.diet_filter ?? undefined,
          displayName: getStoredDisplayName() ?? undefined,
          excludeFastFood: !!s.exclude_fast_food,
          excludeChains: !!s.exclude_chains,
          openNowOnly: !!s.open_now_only,
          cuisineTags: s.cuisine_tags ? JSON.parse(s.cuisine_tags) : [],
        }),
      });
      localStorage.setItem('yumder_anonymous_id', data.anonymousId);
      localStorage.setItem('yumder_participant_id', String(data.participantId));
      localStorage.setItem('yumder_session_code', data.code);
      window.location.href = `/session/${data.code}`;
    } catch (e: any) {
      alert(e.message || t.home.errorCreateDefault);
    }
  }

  async function handleKick(targetId: number) {
    if (!confirm(t.session.confirmKick)) return;
    try {
      await apiFetch(`/api/session/${code}/kick`, {
        method: 'POST',
        body: JSON.stringify({ requesterId: participantId, targetParticipantId: targetId }),
      });
    } catch (e: any) {
      alert(e.message || t.session.errorKickFailed);
    }
  }

  if (needsJoin) {
    return (
      <main className="relative flex flex-col items-center justify-center min-h-dvh-safe p-6 gap-5 overflow-x-hidden overflow-y-auto bg-neutral-950 safe-top safe-bottom">
        <div
          aria-hidden
          className="pointer-events-none absolute -top-1/3 left-1/2 -translate-x-1/2 h-[600px] w-[600px] rounded-full opacity-30 blur-3xl"
          style={{
            background:
              'radial-gradient(circle, rgba(254,60,114,0.5) 0%, rgba(0,194,168,0.25) 45%, transparent 75%)',
          }}
        />

        <div className="absolute top-4 right-4 z-20 safe-top">
          <LanguageSwitch />
        </div>

        <motion.h2
          initial={{ opacity: 0, y: 8 }}
          animate={{ opacity: 1, y: 0 }}
          className="relative z-10 text-2xl font-bold bg-gradient-to-r from-yumder-pink to-yumder-teal bg-clip-text text-transparent"
        >
          {t.session.joinTitle(code)}
        </motion.h2>

        <motion.div
          layout
          initial={{ opacity: 0, y: 8 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ delay: 0.05 }}
          className="relative z-10 w-full max-w-sm flex flex-col gap-3 rounded-3xl border border-white/10 bg-neutral-900 p-5 shadow-2xl"
        >
          <input
            placeholder={t.common.namePlaceholderOptional}
            value={joinName}
            onChange={(e) => setJoinName(e.target.value)}
            onKeyDown={handleInlineJoinKeyDown}
            className="bg-neutral-800 rounded-xl px-4 py-3 border border-white/5 focus:border-yumder-pink/60 outline-none transition-colors"
          />
          <motion.button
            onClick={handleInlineJoin}
            disabled={joining}
            whileTap={{ scale: 0.97 }}
            className="relative overflow-hidden bg-gradient-to-r from-yumder-pink to-pink-500 font-semibold rounded-xl px-6 py-3 disabled:opacity-50 shadow-[0_0_25px_rgba(254,60,114,0.4)]"
          >
            {joining ? t.common.joiningButton : t.common.joinButton}
          </motion.button>
        </motion.div>
      </main>
    );
  }

  if (loading) {
    return (
      <main className="relative flex flex-col items-center min-h-dvh-safe p-4 sm:p-6 gap-3 sm:gap-4 overflow-x-hidden overflow-y-auto bg-neutral-950 safe-top safe-bottom">
        <div
          aria-hidden
          className="pointer-events-none absolute -top-1/3 left-1/2 -translate-x-1/2 h-[500px] w-[500px] rounded-full opacity-20 blur-3xl"
          style={{
            background:
              'radial-gradient(circle, rgba(254,60,114,0.5) 0%, rgba(0,194,168,0.25) 45%, transparent 75%)',
          }}
        />

        <div className="relative z-10 flex items-center justify-between w-full max-w-sm shrink-0">
          <SessionCodePill code={code} />
        </div>

        <div className="relative z-10 w-full flex justify-center flex-1 min-h-0">
          <SwipeCardSkeleton />
        </div>
      </main>
    );
  }

  const me = participants.find((x) => x.id === participantId);
  const iAmCreator = me?.is_creator === 1;
  const creatorStillActive = participants.some((p) => p.is_creator === 1 && p.is_active === 1);
  const iAmAllowedToDrawJoker = iAmCreator || (!creatorStillActive && me?.is_active === 1);
  const vetosRemaining = Math.max(0, 3 - vetosUsed);
  const vetosExhausted = vetosRemaining === 0;

  const expandLevel = Number(sessionInfo?.expand_level || 0);
  const canExpandMore = expandLevel < MAX_LOAD_MORE_CLICKS && hasMore;

  // Name des Joker-Berechtigten (Creator bzw. aktiver Ersatz) fuer den
  // Warte-Status der uebrigen Teilnehmer.
  const oracleName = (() => {
    const activeCreator = participants.find((p) => p.is_creator === 1 && p.is_active === 1);
    if (activeCreator) return activeCreator.display_name || t.session.defaultParticipantName;
    return t.session.defaultParticipantName;
  })();

  const showJokerArea = stackEmpty && !jokerAlreadyDrawn && cards.length > 0;

  // Joker-Peek-Karte im Stapel: leicht animiert (sanftes Schweben) und etwas
  // kleiner als die normalen Swipe-Karten, wie im Mockup. Fuer den Berechtigten
  // tappbar (deckt auf), fuer alle anderen deaktiviert (Warte-Status darunter).
  const jokerCardElement = (
    <div className="w-full h-full flex items-center justify-center">
      <motion.button
        type="button"
        onClick={handleJoker}
        disabled={!showJokerArea || !iAmAllowedToDrawJoker || jokerLoading || loadingMore}
        className="w-[70%] max-w-[280px] aspect-[256/360] cursor-pointer disabled:cursor-default"
        animate={{ y: [0, -4, 2, -2, 0], rotate: [0, -1, 0.6, 1, 0] }}
        transition={{ duration: 5, repeat: Infinity, ease: 'easeInOut' }}
        whileTap={{ scale: 0.97 }}
        aria-label={t.session.jokerButton}
      >
        <JokerCard />
      </motion.button>
    </div>
  );

  if (activeMatch) {
    const mapsUrl = `https://www.google.com/maps/search/${encodeURIComponent(
      (activeMatch.restaurant_name || '') + ' ' + (activeMatch.restaurant_address || '')
    )}`;
    return (
      <MatchResult
        restaurantId={activeMatch.restaurant_id}
        restaurantName={activeMatch.restaurant_name || ''}
        matchPercentage={activeMatch.match_percentage || 0}
        imageUrl={activeMatch.restaurant_image_url}
        isSymbolImage={activeMatch.restaurant_is_symbol_image}
        address={activeMatch.restaurant_address}
        city={activeMatch.restaurant_city}
        postalCode={activeMatch.restaurant_postal_code}
        suburb={activeMatch.restaurant_suburb}
        phone={activeMatch.restaurant_phone}
        website={activeMatch.restaurant_website}
        openingHoursRaw={activeMatch.restaurant_opening_hours_raw}
        cuisine={activeMatch.restaurant_cuisine}
        lat={activeMatch.restaurant_lat}
        lon={activeMatch.restaurant_lon}
        isJokerResult={activeMatch.isJokerResult}
        isForcedChoice={activeMatch.isForcedChoice}
        reservationUrl={activeMatch.restaurant_reservation_url}
        lieferandoUrl={activeMatch.restaurant_lieferando_url}
        mapsUrl={mapsUrl}
        runnerUps={ranking}
        activeParticipantCount={activeMatch.active_participant_count ?? rankingActiveCount}
        yesCount={activeMatch.yes_count}
        poolCount={activeMatch.pool_count}
        onLeaveSession={handleLeave}
        onNewTable={handleNewTable}
        sessionCode={code}
      />
    );
  }

  return (
    <main className="relative flex flex-col items-center h-dvh p-4 sm:p-6 gap-3 sm:gap-4 overflow-x-hidden overflow-hidden bg-neutral-950 safe-top safe-bottom">
      <div
        aria-hidden
        className="pointer-events-none absolute -top-1/3 left-1/2 -translate-x-1/2 h-[500px] w-[500px] rounded-full opacity-20 blur-3xl"
        style={{
          background:
            'radial-gradient(circle, rgba(254,60,114,0.5) 0%, rgba(0,194,168,0.25) 45%, transparent 75%)',
        }}
      />

      <div className="fixed top-4 inset-x-0 z-[60] flex flex-col items-center gap-2 pointer-events-none px-4 safe-top">
        <AnimatePresence>
          {toasts.map((toast) => (
            <motion.div
              key={toast.id}
              initial={{ opacity: 0, y: -12, scale: 0.95 }}
              animate={{ opacity: 1, y: 0, scale: 1 }}
              exit={{ opacity: 0, y: -8, scale: 0.95 }}
              className={`text-sm font-medium px-4 py-2 rounded-full border shadow-lg ${
                toast.kind === 'leave'
                  ? 'bg-neutral-800 border-white/10 text-neutral-300'
                  : 'bg-yumder-teal border-yumder-teal text-neutral-900'
              }`}
            >
              {toast.text}
            </motion.div>
          ))}
        </AnimatePresence>
      </div>

      <div className="relative z-10 flex items-center justify-between w-full max-w-sm shrink-0">
        <SessionCodePill code={code} onClick={handleCopyCode} />
        <button
          onClick={() => setShowInfoSheet(true)}
          className="h-9 w-9 rounded-full bg-neutral-900 border border-white/10 text-neutral-300 flex items-center justify-center hover:border-yumder-pink/50 hover:text-yumder-pink transition-colors"
          aria-label={t.session.infoAriaLabel}
          title={t.session.infoTitle}
        >
          <svg viewBox="0 0 24 24" className="w-4 h-4" fill="currentColor">
            <circle cx="5" cy="12" r="1.8" />
            <circle cx="12" cy="12" r="1.8" />
            <circle cx="19" cy="12" r="1.8" />
          </svg>
        </button>
      </div>

      {showJokerArea && (
        <div className="relative z-10 flex flex-col items-center gap-1.5 shrink-0 w-full max-w-sm pt-2">
          <h2 className="text-lg font-black tracking-tight text-white uppercase">
            {t.session.stackEmptyPrompt}
          </h2>
          <div className="w-full flex items-center justify-center gap-3 px-6 mt-1">
            <span className="h-px flex-1 bg-neutral-700" />
            <span className="text-xs font-black uppercase tracking-widest text-neutral-200">
              {canExpandMore ? t.session.eitherJokerLabel : t.session.jokerLabel}
            </span>
            <span className="h-px flex-1 bg-neutral-700" />
          </div>
        </div>
      )}

      <div className="relative z-10 w-full flex justify-center flex-1 min-h-0">
        {loadingMore ? (
          <SwipeCardSkeleton />
        ) : cards.length > 0 ? (
          <SwipeCardStack
            cards={cards}
            onSwipe={handleSwipe}
            onStackEmpty={() => setStackEmpty(true)}
            onUndo={handleUndo}
            vetosExhausted={vetosExhausted}
            vetosRemaining={vetosRemaining}
            resetKey={code}
            sessionCode={code}
            jokerCard={!jokerAlreadyDrawn ? jokerCardElement : undefined}
          />
        ) : (
          <motion.div
            initial={{ opacity: 0, y: 8 }}
            animate={{ opacity: 1, y: 0 }}
            className="flex flex-col items-center justify-center gap-4 text-center px-6 h-full"
          >
            <svg viewBox="0 0 24 24" className="w-14 h-14 text-neutral-600" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <circle cx="12" cy="12" r="9" />
              <path d="M8.5 15.5c1-1.4 2.2-2.1 3.5-2.1s2.5.7 3.5 2.1" />
              <line x1="9" y1="9" x2="9.01" y2="9" strokeWidth="2" />
              <line x1="15" y1="9" x2="15.01" y2="9" strokeWidth="2" />
            </svg>
            <p className="text-neutral-400 text-center max-w-xs">{t.session.noRestaurantsFound}</p>
            <button
              onClick={handleLeave}
              className="rounded-xl px-6 py-3 text-sm font-semibold bg-white hover:bg-neutral-200 text-black active:scale-[0.98] transition-all"
            >
              {t.session.leaveSessionButton}
            </button>
          </motion.div>
        )}
      </div>

      {showJokerArea && (
        <motion.div
          initial={{ opacity: 0, y: 8 }}
          animate={{ opacity: 1, y: 0 }}
          className="relative z-10 flex flex-col items-center gap-3 mt-2 shrink-0 w-full max-w-sm pb-5"
        >
          {iAmAllowedToDrawJoker ? (
            <>
              <p className="text-xs font-bold text-amber-300 tracking-wide">
                {t.session.tapJokerHint}
              </p>
              {canExpandMore && (
                <>
                  <div className="w-full flex items-center justify-center gap-3 px-6">
                    <span className="h-px flex-1 bg-neutral-700" />
                    <span className="text-xs font-black uppercase tracking-widest text-neutral-200">
                      {t.session.orLabel}
                    </span>
                    <span className="h-px flex-1 bg-neutral-700" />
                  </div>
                  <motion.button
                    onClick={handleRequestMoreCards}
                    disabled={loadingMore}
                    whileTap={{ scale: 0.97 }}
                    className="w-full py-3.5 px-6 bg-yumder-teal hover:bg-teal-400 text-neutral-900 font-bold text-sm rounded-2xl flex items-center justify-center disabled:opacity-50 active:scale-[0.98] transition-all shadow-md"
                  >
                    {loadingMore ? t.session.requestingMoreCardsButton : t.session.requestMoreCardsButton}
                  </motion.button>
                </>
              )}
            </>
          ) : (
            <p className="text-sm text-neutral-300 text-center px-4">
              {t.session.waitingForOracle(oracleName)}
            </p>
          )}
        </motion.div>
      )}

      <TutorialOverlay visible={showTutorial} onClose={() => setShowTutorial(false)} />

      <SessionInfoSheet
        visible={showInfoSheet}
        onClose={() => setShowInfoSheet(false)}
        code={code}
        session={sessionInfo}
        participants={participants}
        myParticipantId={participantId}
        iAmCreator={iAmCreator}
        onKick={handleKick}
        onInvite={handleShare}
        onLeave={handleLeave}
      />

      <JokerReveal
        visible={!!jokerReveal}
        match={jokerReveal?.pendingMatch}
        mapsUrl={
          jokerReveal?.pendingMatch
            ? `https://www.google.com/maps/search/${encodeURIComponent(
                (jokerReveal.pendingMatch.restaurant_name || '') + ' ' + (jokerReveal.pendingMatch.restaurant_address || '')
              )}`
            : ''
        }
        sessionCode={code}
        onNewTable={handleNewTable}
        onLeave={handleLeave}
      />
    </main>
  );
}
