import { useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import {
  Search,
  Star,
  Play,
  RefreshCw,
  Loader2,
  ChevronDown,
  LayoutGrid,
  List as ListIcon,
  ExternalLink,
  Minimize2,
  Maximize,
  StretchHorizontal,
} from "lucide-react";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";

import PageLayout from "@/components/PageLayout";
import NoServerPrompt from "@/components/NoServerPrompt";
import VideoPlayer from "@/components/VideoPlayer";
import { isProxiedProfile } from "@/lib/proxyUrl";
import InlinePreviewPlayer from "@/components/InlinePreviewPlayer";
import { useInlinePreviewEnabled } from "@/hooks/useInlinePreviewEnabled";
import { useProfiles } from "@/hooks/useProfiles";
import { useLiveChannels, useReloadPlaylist, useShortEpg } from "@/hooks/useXtreamData";
import { useFavorites } from "@/hooks/useFavorites";
import { useHeartbeat } from "@/hooks/useHeartbeat";
import type { Channel } from "@/types/xtream";

import { NativePlayer, isNativeApp, NativeOverlayChannel } from "@/native/nativePlayer";
import { getPlaybackMode } from "@/lib/playbackMode";
import { getExternalApp } from "@/lib/externalApp";
import { getUserAgent } from "@/lib/userAgent";
import {
  tryLaunchExternalFromBrowser,
  buildVlcUrl,
  buildVlcIosUrl,
  openPlayStore,
} from "@/lib/externalLauncher";
import { stalkerApi } from "@/lib/stalker";
import { FocusableSection, FocusableTile, FocusableButton } from "@/components/focusable";
import { isSmartTV } from "@/lib/platform";
import { useChannelSwitchInterstitial } from "@/hooks/useAdMob";
import { useEntitlement } from "@/hooks/useEntitlement";
import InlineAdSlot from "@/components/InlineAdSlot";
import { useAdFreeUnlock } from "@/hooks/useAdFreeUnlock";
import LazyImage from "@/components/LazyImage";
import { sortChannelsByGeo, sortCategoriesByGeo } from "@/lib/geoSort";
import { ensureLoaded as ensureGeo, subscribe as subscribeGeo, getGeo } from "@/lib/geoWeather";

const AD_SLOT_EVERY = 6;

type ViewMode = "list" | "grid";

const LiveTV = () => {
  const { liveProfile, activeProfile } = useProfiles();
  const effectiveProfile = liveProfile || activeProfile;
  const { t } = useTranslation();
  const { data: channels = [], isLoading, error } = useLiveChannels(effectiveProfile);
  const reload = useReloadPlaylist();
  const { isFavorite, toggle } = useFavorites(effectiveProfile?.id, "live");

  const [search, setSearch] = useState("");
  const [selectedCategory, setSelectedCategory] = useState("All");
  const [selectedChannel, setSelectedChannel] = useState<Channel | null>(null);
  const [liveVisibleCount, setLiveVisibleCount] = useState(100); // pagination for 5000+ channels
  // Tell the backend "this device is watching this channel right now"
  // every 60 s so the admin's Live Stats widget reflects reality.
  useHeartbeat(!!selectedChannel, {
    channel: selectedChannel?.name || selectedChannel?.id,
    kind: "live",
  });
  const [showFavOnly, setShowFavOnly] = useState(false);
  const [viewMode, setViewMode] = useState<ViewMode>("grid");
  const [liveScaleMode, setLiveScaleMode] = useState<"contain" | "cover" | "fill">(
    () => {
      try {
        const v = localStorage.getItem("nadiplay_live_scale_mode");
        return v === "cover" || v === "fill" ? v : "contain";
      } catch {
        return "contain";
      }
    },
  );

  useEffect(() => {
    try {
      localStorage.setItem("nadiplay_live_scale_mode", liveScaleMode);
    } catch {
      // ignore
    }
  }, [liveScaleMode]);

  // ─── World-Cup deep-link autoplay (FEATURE 2026-06) ──────────────
  // When the World-Cup page calls
  // `navigate("/live-tv", { state: { autoPlayChannelId } })`,
  // pick up the id once channels are loaded, select that channel
  // (which triggers the player), then strip the location-state so
  // a remount / back-nav doesn't re-trigger autoplay.
  const location = useLocation();
  const navigate = useNavigate();
  const autoPlayDoneRef = useRef(false);
  useEffect(() => {
    if (autoPlayDoneRef.current) return;
    const target = (location.state as { autoPlayChannelId?: string | number } | null)?.autoPlayChannelId;
    if (target == null || channels.length === 0) return;
    const ch = channels.find((c) => String(c.id) === String(target));
    if (!ch) return;
    autoPlayDoneRef.current = true;
    setSelectedChannel(ch);
    // Replace history entry so back-button doesn't re-autoplay.
    navigate("/live-tv", { replace: true, state: null });
  }, [location.state, channels, navigate]);

  // Running count of channel switches — drives the AdMob interstitial.
  // The admin sets `ads_interstitial_every` (default 5) from the Ads
  // Settings page; the hook fires every Nth switch.
  const [channelSwitches, setChannelSwitches] = useState(0);
  // FEATURE (2026-01): banner is mounted GLOBALLY in App.tsx via
  // useGlobalBanner() — no per-page useAdBanner() needed anymore.
  useChannelSwitchInterstitial(channelSwitches);
  const { adsEnabled } = useEntitlement();
  // BUG FIX (2026-06): inline ad slots in the channel list used to
  // route to /subscription on tap (InlineAdSlot's default fallback),
  // which users mistook for "content forcing the subscription page".
  // They now fire a REWARDED ad instead, so each tap counts toward the
  // "3 ads = 1 hour ad-free" unlock — never a dead-end upsell.
  const { watchAd: watchRewardAd } = useAdFreeUnlock({ adsToWatch: 3, hoursPerUnlock: 1 });

  // Geo-prioritisation: bump channels/categories matching the user's
  // detected country to the top so subscribers in Riyadh / Casablanca /
  // Bucharest see local-first lists. Falls back to original order when
  // the IP lookup hasn't returned yet (or no matches found).
  const [geoBump, setGeoBump] = useState(0);
  useEffect(() => {
    void ensureGeo();
    const off = subscribeGeo(() => setGeoBump((n) => n + 1));
    return off;
  }, []);
  const geo = getGeo();
  // Use geoBump to invalidate memoized lists when geo arrives.
  void geoBump;

  const mode = getPlaybackMode();
  const native = isNativeApp();
  const externalApp = getExternalApp();
  const inlinePreviewEnabled = useInlinePreviewEnabled();
  // On web/TV, when the admin enables the Inline Preview, it takes precedence
  // over External Player mode → channels play inside the small in-app player
  // (HLS via VideoPlayer) instead of being launched in VLC/MX/etc. Native
  // (Capacitor Android/iOS) is unaffected — those builds always defer to the
  // system player.
  const effectiveMode: "internal" | "external" =
    !native && inlinePreviewEnabled ? "internal" : mode;

  const categories = useMemo(
    () =>
      sortCategoriesByGeo(
        ["All", ...Array.from(new Set(channels.map((c) => c.category)))],
        geo?.countryCode,
        geo?.country,
      ),
    [channels, geo?.countryCode, geo?.country]
  );
  // Note: display of "All" is localized via t("common.all") in the render

  const filtered = useMemo(
    () => {
      const filteredList = channels.filter((ch) => {
        const matchSearch = ch.name.toLowerCase().includes(search.toLowerCase());
        const matchCat = selectedCategory === "All" || ch.category === selectedCategory;
        const matchFav = !showFavOnly || isFavorite(ch.id);
        return matchSearch && matchCat && matchFav;
      });
      // Only re-order when "All" is selected; once a user picks a single
      // category, respect the playlist's natural order within that category.
      if (selectedCategory === "All") {
        return sortChannelsByGeo(filteredList, geo?.countryCode, geo?.country);
      }
      return filteredList;
    },
    [channels, search, selectedCategory, showFavOnly, isFavorite, geo?.countryCode, geo?.country]
  );

  // Current programme for the selected channel
  const { data: epg = [] } = useShortEpg(effectiveProfile, selectedChannel?.id);
  const nowPlaying = epg.find((e) => e.nowPlaying) ?? epg[0];

  // ───────────── native channel overlay ─────────────
  // Map the Xtream / M3U channel list into the shape the native
  // right-side overlay understands. We do this once per `channels`
  // change (not on every render) because the array is potentially
  // 5000+ entries — copying it on every keystroke in the search box
  // would be wasteful. The native side has its own search + category
  // filter on top of whatever we send.
  //
  // EPG (now / next) is intentionally NOT included here: fetching EPG
  // for every channel up-front would hammer the portal. The native
  // overlay degrades gracefully when those fields are absent.
  const nativeOverlayChannels = useMemo<NativeOverlayChannel[]>(
    () => {
      if (!native) return []; // saves work on web/TV builds
      return channels.map((c, i) => ({
        number: i + 1,                     // 1-based playlist index
        name: c.name,
        category: c.category || "Other",
        streamUrl: c.streamUrl,
        favorite: isFavorite(c.id),
        logoUrl: c.logoUrl,
      }));
    },
    [channels, isFavorite, native]
  );

  const currentChannelNumber = useMemo<number | undefined>(() => {
    if (!selectedChannel || !native) return undefined;
    const idx = channels.findIndex((c) => c.id === selectedChannel.id);
    return idx >= 0 ? idx + 1 : undefined;
  }, [selectedChannel, channels, native]);

  // ───────────── "Resume {channel}" cold-start toast ─────────────
  // IPTV users typically rotate through the same 5–10 channels daily.
  // Reading the last-played channel from native SharedPreferences and
  // offering a one-tap resume cuts time-to-video dramatically.
  // Shown ONCE per LiveTV mount, only when no channel is selected yet,
  // and only on native (the data is only persisted there).
  const resumeShownRef = useRef(false);
  useEffect(() => {
    if (!native) return;
    if (resumeShownRef.current) return;
    if (selectedChannel) return;            // already playing → don't suggest
    if (!channels.length) return;           // playlist not ready yet
    resumeShownRef.current = true;

    let cancelled = false;
    (async () => {
      try {
        const lp = await NativePlayer.getLastPlayed();
        if (cancelled || !lp?.hasLastPlayed || !lp.url) return;
        // Match by streamUrl first (most reliable), fall back to name.
        const match =
          channels.find((c) => c.streamUrl === lp.url) ||
          (lp.title ? channels.find((c) => c.name === lp.title) : undefined);
        if (!match) return;
        toast(`Resume ${lp.title || match.name}?`, {
          description: "Continue watching your last channel",
          action: {
            label: "Resume",
            onClick: () => onChannelClick(match),
          },
          duration: 8000,
        });
      } catch (e) {
        // Ignore — resume is a nice-to-have, never a hard error.
        console.warn("[LiveTV] getLastPlayed failed:", e);
      }
    })();

    return () => {
      cancelled = true;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [native, channels.length]);

  // ───────────── channel-click handler ─────────────
  const onChannelClick = async (channel: Channel) => {
    let ch = channel;
    // Stalker channels use a `stalker-cmd:<cmd>` placeholder. Resolve it to a
    // real playable URL just-in-time (the URL is short-lived per portal).
    if (ch.streamUrl.startsWith("stalker-cmd:") && effectiveProfile?.kind === "stalker") {
      const cmd = ch.streamUrl.slice("stalker-cmd:".length);
      const t1 = toast.loading(`Resolving ${ch.name}…`);
      try {
        const real = await stalkerApi.resolveStream(
          effectiveProfile.portalUrl!,
          effectiveProfile.macAddress!,
          cmd,
        );
        toast.dismiss(t1);
        ch = { ...ch, streamUrl: real };
      } catch (e: any) {
        toast.error(`Could not resolve stream: ${e?.message || "error"}`, { id: t1 });
        return;
      }
    }
    // Direct playback — no server proxy. All streams go straight to the
    // device for maximum speed and minimum latency.
    // External playback mode → launch the external app and do NOT
    // mount the in-app player. The channel list stays on screen.
    if (effectiveMode === "external") {
      try {
        if (native) {
          const res = await NativePlayer.openExternal({
            url: ch.streamUrl,
            title: ch.name,
            userAgent: getUserAgent(),
            package: externalApp.androidPackage,
          });

          if (res?.launched) {
            toast.success(
              t("liveTV.openingInApp", { channel: ch.name, app: externalApp.label }),
            );
          } else if (res?.reason === "not-installed" && res.package) {
            // Let the user install their chosen external app.
            toast.error(`${externalApp.label} is not installed`, {
              action: {
                label: "Install",
                onClick: () =>
                  NativePlayer.openPlayStore({ package: res.package! }),
              },
              duration: 8000,
            });
          } else {
            toast.error(t("liveTV.couldntOpen", { app: externalApp.label }));
          }
        } else {
          const ok = tryLaunchExternalFromBrowser(ch.streamUrl, {
            package: externalApp.androidPackage,
            title: ch.name,
            userAgent: getUserAgent(),
            onNotInstalled: (pkg) => {
              // No app responded to the intent → almost certainly not
              // installed. Let the user install it in one tap.
              if (!pkg) {
                toast.error(`No ${externalApp.label} app handled the stream`);
                return;
              }
              toast.error(`${externalApp.label} is not installed`, {
                action: {
                  label: "Install",
                  onClick: () => openPlayStore(pkg),
                },
                duration: 10000,
              });
            },
          });
          if (ok) {
            toast.message(t("liveTV.sentToApp", { app: externalApp.label }));
          } else {
            // Desktop / unsupported browser — user must click the visible
            // "Open in <App>" button we render below.
            toast.message(`Tap "Open in ${externalApp.label}" to launch`);
          }
        }
      } catch (e: any) {
        toast.error(
          e?.message || t("liveTV.couldntOpen", { app: externalApp.label }),
        );
      }
      // Still mark as selected so EPG / title updates
      setSelectedChannel(ch);
      return;
    }

    // Internal mode → normal in-app player
    setSelectedChannel(ch);
    // Increment switch counter so the interstitial hook can fire every Nth.
    setChannelSwitches((n) => n + 1);
  };

  // URL scheme to use for the visible "Open in <App>" fallback button
  // when the browser can't auto-launch (desktop / iOS Safari).
  const browserLaunchHref = (streamUrl: string): string | undefined => {
    const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
    if (/iPad|iPhone|iPod/.test(ua)) return buildVlcIosUrl(streamUrl);
    if (/Android/i.test(ua)) return undefined; // handled via intent://
    return buildVlcUrl(streamUrl);
  };

  if (!effectiveProfile) {
    return (
      <PageLayout title={t("liveTV.title")}>
        <NoServerPrompt testIdPrefix="livetv" />
      </PageLayout>
    );
  }

  const externalBadge =
    effectiveMode === "external" ? (
      <div className="glass-card rounded-xl px-3 py-1.5 flex items-center gap-1.5">
        <ExternalLink className="w-3 h-3 text-primary" />
        <span className="text-[10px] text-foreground/80">{externalApp.label}</span>
      </div>
    ) : null;

  // ───────────── channel rows / cards ─────────────
  // NOTE: we intentionally render plain `<div>`s (NOT framer-motion) and
  // use `content-visibility: auto` so the browser skips layout/paint
  // for rows outside the viewport. Rendering 300 `motion.div`s with
  // per-item animations while scrolling through real-world flaky IPTV
  // logo URLs was causing Chrome's renderer to OOM + auto-reload the
  // tab (which manifested as a white flash that dropped the user back
  // on the home screen). This is the root-cause fix for that bug.
  //
  // BUG FIX (2026-01) — Tizen channel-list scroll.
  // `content-visibility: auto` is unreliable on Tizen 4-5 (Chromium
  // 56-69): it correctly skips paint but mis-computes the scroll
  // height, so D-pad navigation past the visible rows never triggers
  // a real scroll. We disable the optimisation on Smart TV builds —
  // TVs render fewer rows on screen at once and have plenty of GPU
  // budget, so we can afford to lay out every row. Mobile / web
  // builds keep the optimisation for the OOM-fix benefit.
  const tvBuild = isSmartTV();
  const rowStyle = tvBuild
    ? {}
    : {
        contentVisibility: "auto" as const,
        containIntrinsicSize: "56px",
      };
  const gridItemStyle = tvBuild
    ? {}
    : {
        contentVisibility: "auto" as const,
        containIntrinsicSize: "160px",
      };

  const renderList = () => (
    <FocusableSection autoFocus focusKey="channel-list" className="space-y-1">
      {filtered.slice(0, liveVisibleCount).flatMap((ch, idx) => {
        const tile = (
          <FocusableTile
            key={ch.id}
            focusKey={`ch-${ch.id}`}
            onEnter={() => onChannelClick(ch)}
            onClick={() => onChannelClick(ch)}
            subtle
            style={rowStyle}
            className={`flex items-center gap-3 px-3 py-2.5 rounded-xl cursor-pointer transition-transform active:scale-[0.98] border-transparent ${
              selectedChannel?.id === ch.id
                ? "glass-card border border-primary/30"
                : "hover:bg-white/5"
            }`}
          >
            <div className="w-10 h-7 rounded bg-muted/30 flex items-center justify-center shrink-0 overflow-hidden">
              {ch.logoUrl ? (
                <img
                  src={ch.logoUrl}
                  alt=""
                  loading="lazy"
                  decoding="async"
                  className="w-full h-full object-contain"
                  onError={(e) => (e.currentTarget.style.display = "none")}
                />
              ) : (
                <Play className="w-3 h-3 text-muted-foreground" />
              )}
            </div>
            <div className="flex-1 min-w-0">
              <p className="text-foreground text-xs font-medium truncate">{ch.name}</p>
              <p className="text-muted-foreground text-[10px] truncate">{ch.category}</p>
            </div>
            <button
              onClick={(e) => {
                e.stopPropagation();
                toggle(ch.id);
              }}
            >
              <Star
                className={`w-4 h-4 ${
                  isFavorite(ch.id) ? "text-primary fill-primary" : "text-muted-foreground"
                }`}
              />
            </button>
          </FocusableTile>
        );
        const showAd = adsEnabled && (idx + 1) % AD_SLOT_EVERY === 0;
        if (!showAd) return [tile];
        return [
          tile,
          <InlineAdSlot key={`ad-row-${idx}`} variant="row" testId={`livetv-inline-ad-row-${idx}`} onClick={() => void watchRewardAd()} />,
        ];
      })}
    </FocusableSection>
  );

  const renderGrid = () => (
    <FocusableSection
      autoFocus
      focusKey="channel-grid"
      // The PARENT wrapper at line ~592 already provides:
      //   • `flex-1` (grow inside the flex column)
      //   • `overflow-y-auto` (the actual scroll context)
      //   • `overscroll-contain` + `touch-pan-y` (anti-shake)
      // Adding those a second time on this grid created a nested
      // scroll context that "ate" the wheel/touch events and the
      // outer container could no longer scroll — that's the bug
      // reported on desktop, mobile AND TV. The grid must stay a
      // simple block (natural height = sum of rows) so the parent's
      // overflow can take over, exactly like renderList() does.
      className="grid gap-2"
      style={{
        gridTemplateColumns: "repeat(auto-fill, minmax(130px, 1fr))",
        gridAutoRows: "max-content",
        // Bottom safe-area inset only. PageLayout already reserves
        // the ad-banner height via --ad-banner-bottom-h and the TV
        // house-banner height via --tv-banner-h, so the LAST row
        // sits flush against the banner top with no extra dead-zone.
        paddingBottom: "env(safe-area-inset-bottom, 0px)",
      }}
    >
      {filtered.slice(0, liveVisibleCount).flatMap((ch, idx) => {
        const tile = (
          <FocusableTile
            key={ch.id}
            focusKey={`ch-grid-${ch.id}`}
            onEnter={() => onChannelClick(ch)}
            onClick={() => onChannelClick(ch)}
            style={gridItemStyle}
            className={`relative aspect-[4/3] flex flex-col items-center justify-center gap-1.5 rounded-xl cursor-pointer transition-transform active:scale-[0.96] p-2 border-transparent ${
              selectedChannel?.id === ch.id
                ? "glass-card border border-primary/40"
                : "glass-card hover:border-primary/20"
            }`}
          >
            <div className="w-14 h-10 rounded bg-muted/30 flex items-center justify-center overflow-hidden">
              {ch.logoUrl ? (
                <img
                  src={ch.logoUrl}
                  alt=""
                  loading="lazy"
                  decoding="async"
                  className="w-full h-full object-contain"
                  onError={(e) => (e.currentTarget.style.display = "none")}
                />
              ) : (
                <Play className="w-4 h-4 text-muted-foreground" />
              )}
            </div>
            <p className="text-foreground text-[11px] font-medium text-center line-clamp-2">
              {ch.name}
            </p>
            <button
              onClick={(e) => {
                e.stopPropagation();
                toggle(ch.id);
              }}
              className="absolute top-1.5 right-1.5"
            >
              <Star
                className={`w-3.5 h-3.5 ${
                  isFavorite(ch.id) ? "text-primary fill-primary" : "text-muted-foreground"
                }`}
              />
            </button>
          </FocusableTile>
        );
        const showAd = adsEnabled && (idx + 1) % AD_SLOT_EVERY === 0;
        if (!showAd) return [tile];
        return [
          tile,
          <InlineAdSlot
            key={`ad-grid-${idx}`}
            testId={`livetv-inline-ad-grid-${idx}`}
            aspectRatio="4 / 3"
            onClick={() => void watchRewardAd()}
          />,
        ];
      })}
    </FocusableSection>
  );

  return (
    <PageLayout title={t("liveTV.title")}>
      <div className="h-full min-h-0 flex flex-col gap-2">
        {/* Top bar: search · category · favourites · view · external badge */}
        <div className="flex flex-wrap items-center gap-2 shrink-0">
          <div className="glass-card rounded-xl px-3 py-2 flex items-center gap-2 flex-1 min-w-[180px]">
            <Search className="w-4 h-4 text-muted-foreground" />
            <input
              type="text"
              placeholder={t("liveTV.searchPlaceholder")}
              value={search}
              onChange={(e) => setSearch(e.target.value)}
              className="bg-transparent text-foreground placeholder:text-muted-foreground text-xs outline-none flex-1"
            />
          </div>

          {/* BUG FIX (2026-01) — Tizen / webOS "All category button not
              working". Native HTML `<select>` dropdowns are not
              reliably navigable with the TV remote D-pad on Samsung
              Tizen / LG webOS — the focus arrives at the element but
              pressing OK either does nothing or pops a dropdown that
              the arrows cannot drive. We split the rendering:
                • Mobile / web → keep the compact <select> (works
                                 everywhere, saves horizontal real
                                 estate on phones).
                • Smart TV    → render a horizontally-scrolling strip
                                of focusable category chips so each
                                category (including "All") is a real
                                D-pad target. */}
          {tvBuild ? (
            <FocusableSection
              focusKey="livetv-category-chips"
              className="glass-card rounded-xl px-2 py-1 flex items-center gap-1.5 overflow-x-auto max-w-full"
              style={{ scrollSnapType: "x mandatory" }}
            >
              {categories.map((c) => {
                const active = c === selectedCategory;
                return (
                  <FocusableButton
                    key={c}
                    focusKey={`cat-chip-${c}`}
                    onEnter={() => setSelectedCategory(c)}
                    onClick={() => setSelectedCategory(c)}
                    data-testid={`livetv-cat-chip-${c === "All" ? "all" : c.toLowerCase().replace(/\s+/g, "-")}`}
                    className={`shrink-0 px-3 py-1.5 rounded-lg text-[11px] tracking-wide transition-colors border ${
                      active
                        ? "bg-primary/20 text-primary border-primary/50"
                        : "text-foreground/80 border-transparent hover:border-primary/30"
                    }`}
                    style={{ scrollSnapAlign: "start" }}
                    subtle
                  >
                    {c === "All" ? t("common.all") : c}
                  </FocusableButton>
                );
              })}
            </FocusableSection>
          ) : (
            <div className="glass-card rounded-xl px-3 py-2 flex items-center gap-2">
              <ChevronDown className="w-4 h-4 text-muted-foreground" />
              <select
                value={selectedCategory}
                onChange={(e) => setSelectedCategory(e.target.value)}
                className="bg-transparent text-foreground text-xs outline-none"
                data-testid="livetv-category-select"
              >
                {categories.map((c) => (
                  <option key={c} value={c} className="bg-background">
                    {c === "All" ? t("common.all") : c}
                  </option>
                ))}
              </select>
            </div>
          )}

          <button
            onClick={() => setShowFavOnly((v) => !v)}
            className={`glass-card rounded-xl px-3 py-2 flex items-center gap-1.5 ${
              showFavOnly ? "border border-primary/40" : ""
            }`}
            title={t("liveTV.favs")}
          >
            <Star
              className={`w-3.5 h-3.5 ${
                showFavOnly ? "text-primary fill-primary" : "text-muted-foreground"
              }`}
            />
            <span className="text-[11px] text-foreground/80">{t("liveTV.favs")}</span>
          </button>

          <div className="glass-card rounded-xl p-0.5 flex items-center">
            <button
              onClick={() => setViewMode("list")}
              className={`px-2.5 py-1.5 rounded-lg ${
                viewMode === "list" ? "bg-primary/20 text-primary" : "text-muted-foreground"
              }`}
              title={t("liveTV.listView")}
            >
              <ListIcon className="w-3.5 h-3.5" />
            </button>
            <button
              onClick={() => setViewMode("grid")}
              className={`px-2.5 py-1.5 rounded-lg ${
                viewMode === "grid" ? "bg-primary/20 text-primary" : "text-muted-foreground"
              }`}
              title={t("liveTV.gridView")}
            >
              <LayoutGrid className="w-3.5 h-3.5" />
            </button>
          </div>

          <div className="glass-card rounded-xl p-0.5 flex items-center" data-testid="livetv-scale-toggle">
            <button
              onClick={() => setLiveScaleMode("contain")}
              className={`px-2.5 py-1.5 rounded-lg ${
                liveScaleMode === "contain" ? "bg-primary/20 text-primary" : "text-muted-foreground"
              }`}
              title="Fit (keep full image)"
              data-testid="livetv-scale-contain-btn"
            >
              <Minimize2 className="w-3.5 h-3.5" />
            </button>
            <button
              onClick={() => setLiveScaleMode("cover")}
              className={`px-2.5 py-1.5 rounded-lg ${
                liveScaleMode === "cover" ? "bg-primary/20 text-primary" : "text-muted-foreground"
              }`}
              title="Zoom (fill, crop sides if needed)"
              data-testid="livetv-scale-cover-btn"
            >
              <Maximize className="w-3.5 h-3.5" />
            </button>
            <button
              onClick={() => setLiveScaleMode("fill")}
              className={`px-2.5 py-1.5 rounded-lg ${
                liveScaleMode === "fill" ? "bg-primary/20 text-primary" : "text-muted-foreground"
              }`}
              title="Stretch (fill full screen)"
              data-testid="livetv-scale-fill-btn"
            >
              <StretchHorizontal className="w-3.5 h-3.5" />
            </button>
          </div>

          <button
            onClick={() => reload(effectiveProfile!.id)}
            className="glass-card rounded-xl p-2"
            title={t("common.refresh")}
          >
            <RefreshCw className="w-3.5 h-3.5 text-muted-foreground hover:text-primary" />
          </button>

          {externalBadge}
        </div>

        {/* Main content */}
        {effectiveMode === "external" ? (
          // External mode: just a big channel list/grid + EPG strip.
          // Clicking a channel fires the external app; no in-app player.
          <div className="flex-1 flex flex-col gap-2 min-h-0">
            {selectedChannel && (
              <div className="glass-card rounded-xl px-3 py-2 flex flex-col sm:flex-row sm:items-center gap-2">
                <div className="flex items-center gap-3 flex-1 min-w-0">
                  <div className="w-9 h-7 rounded bg-muted/30 flex items-center justify-center overflow-hidden">
                    {selectedChannel.logoUrl ? (
                      <img
                        src={selectedChannel.logoUrl}
                        alt=""
                        className="w-full h-full object-contain"
                      />
                    ) : (
                      <Play className="w-3 h-3 text-muted-foreground" />
                    )}
                  </div>
                  <div className="flex-1 min-w-0">
                    <p className="text-foreground text-xs font-medium truncate">
                      {selectedChannel.name}
                    </p>
                    <p className="text-muted-foreground text-[10px] truncate">
                      {nowPlaying?.title
                        ? t("liveTV.nowPrefix", { title: nowPlaying.title })
                        : selectedChannel.category}
                    </p>
                  </div>
                </div>

                {/* Prominent "Open in <App>" button — works on Android
                    via intent://, on iOS via vlc-x-callback://, and on
                    desktop via vlc://<url>. */}
                {(() => {
                  const href = browserLaunchHref(selectedChannel.streamUrl);
                  return href ? (
                    <a
                      href={href}
                      className="inline-flex items-center justify-center gap-1.5 bg-primary hover:bg-primary/90 text-primary-foreground text-xs font-medium px-4 py-2 rounded-lg shadow-lg"
                    >
                      <ExternalLink className="w-3.5 h-3.5" />
                      Open in {externalApp.label}
                    </a>
                  ) : (
                    <button
                      onClick={() => onChannelClick(selectedChannel)}
                      className="inline-flex items-center justify-center gap-1.5 bg-primary hover:bg-primary/90 text-primary-foreground text-xs font-medium px-4 py-2 rounded-lg shadow-lg"
                    >
                      <ExternalLink className="w-3.5 h-3.5" />
                      Open in {externalApp.label}
                    </button>
                  );
                })()}
              </div>
            )}

            <div className="flex-1 overflow-y-auto overscroll-contain pr-1 -mr-1 touch-pan-y min-h-0">
              {isLoading && (
                <div className="flex items-center justify-center py-4">
                  <Loader2 className="w-5 h-5 text-primary animate-spin" />
                </div>
              )}
              {error && (
                <p className="text-destructive text-xs px-2">
                  {t("liveTV.failedLoad")}
                </p>
              )}
              {!isLoading && filtered.length === 0 && (
                <p className="text-muted-foreground text-xs text-center py-6">
                  {t("liveTV.noChannels")}
                </p>
              )}
              {viewMode === "list" ? renderList() : renderGrid()}
            </div>

            {/* Mini EPG for selected channel */}
            {selectedChannel && epg.length > 0 && (
              <div className="glass-card rounded-xl p-2 max-h-36 overflow-y-auto">
                <p className="text-[10px] text-muted-foreground mb-1 px-1">
                  {t("liveTV.epgFor", { name: selectedChannel.name })}
                </p>
                {epg.slice(0, 6).map((e, i) => (
                  <div
                    key={e.id + i}
                    className={`flex items-center gap-2 px-2 py-1 rounded-md ${
                      e.nowPlaying ? "bg-primary/10" : ""
                    }`}
                  >
                    <span className="text-[10px] text-muted-foreground w-10 shrink-0">
                      {e.start ? e.start.slice(11, 16) : "--:--"}
                    </span>
                    <span
                      className={`text-[11px] truncate ${
                        e.nowPlaying ? "text-primary font-medium" : "text-foreground/80"
                      }`}
                    >
                      {e.title || "—"}
                    </span>
                  </div>
                ))}
              </div>
            )}
          </div>
        ) : (
          // Internal mode: classic layout (list + player).
          <div className="flex-1 flex flex-col landscape:max-md:flex-row md:flex-row gap-2 min-h-0">
            <div className="flex-1 md:flex-none landscape:max-md:flex-1 md:w-[340px] lg:w-[380px] xl:w-[440px] 2xl:w-[500px] md:shrink-0 flex flex-col gap-1 min-h-0 min-w-0 order-2 md:order-1 landscape:max-md:order-1">
              <div className="flex-1 overflow-y-auto overscroll-contain pr-1 -mr-1 touch-pan-y">
                {isLoading && (
                  <div className="flex items-center justify-center py-4">
                    <Loader2 className="w-5 h-5 text-primary animate-spin" />
                  </div>
                )}
                {error && (
                  <p className="text-destructive text-xs px-2">
                    {t("liveTV.failedLoad")}
                  </p>
                )}
                {!isLoading && filtered.length === 0 && (
                  <p className="text-muted-foreground text-xs text-center py-4">
                    {t("liveTV.noChannels")}
                  </p>
                )}
                {viewMode === "list" ? renderList() : renderGrid()}
              </div>
            </div>

            <div
              className={`${
                selectedChannel ? "flex" : "hidden md:flex landscape:max-md:flex"
              } flex-col items-center justify-start gap-2 min-w-0 min-h-0 shrink-0 md:flex-1 md:shrink landscape:max-md:flex-1 landscape:max-md:shrink order-1 md:order-2 landscape:max-md:order-2`}
            >
              <div className="w-full max-w-[720px] xl:max-w-[960px] 2xl:max-w-[1280px] aspect-video glass-card rounded-2xl overflow-hidden flex items-center justify-center relative mx-auto max-h-[42vh] md:max-h-none landscape:max-md:max-h-none">
                {selectedChannel ? (
                  // Inline preview is a WEB-only feature. On native
                  // (Capacitor Android/iOS), VideoPlayer launches the
                  // system video player (ExoPlayer / AVPlayer) as a
                  // separate fullscreen activity — there's no <video>
                  // element to wrap, and our gold controls would be
                  // useless. Native users always get the classic
                  // VideoPlayer directly regardless of the admin flag.
                  inlinePreviewEnabled && !native ? (
                    (() => {
                      // Walk filtered list to find prev/next siblings of the
                      // currently-selected channel. Wraps around at the edges.
                      const idx = filtered.findIndex((c) => c.id === selectedChannel.id);
                      const prevCh =
                        idx > 0
                          ? filtered[idx - 1]
                          : filtered.length > 1
                            ? filtered[filtered.length - 1]
                            : undefined;
                      const nextCh =
                        idx >= 0 && idx < filtered.length - 1
                          ? filtered[idx + 1]
                          : filtered.length > 1
                            ? filtered[0]
                            : undefined;
                      return (
                        <InlinePreviewPlayer
                          key={selectedChannel.id}
                          src={selectedChannel.streamUrl}
                          poster={selectedChannel.logoUrl}
                          title={selectedChannel.name}
                          isLive
                          liveScaleMode={liveScaleMode}
                          canGoPrev={!!prevCh}
                          canGoNext={!!nextCh}
                          onPrevChannel={prevCh ? () => onChannelClick(prevCh) : undefined}
                          onNextChannel={nextCh ? () => onChannelClick(nextCh) : undefined}
                        />
                      );
                    })()
                  ) : (
                    <VideoPlayer
                      src={selectedChannel.streamUrl}
                      poster={selectedChannel.logoUrl}
                      title={selectedChannel.name}
                      isLive
                      liveScaleMode={liveScaleMode}
                      channels={nativeOverlayChannels}
                      currentChannelNumber={currentChannelNumber}
                    />
                  )
                ) : (
                  <div className="text-center">
                    <Play className="w-12 h-12 text-primary/50 mx-auto mb-2" />
                    <p className="text-muted-foreground text-sm">{t("liveTV.selectChannel")}</p>
                  </div>
                )}
              </div>

              {selectedChannel && (
                <div className="text-center">
                  <p className="text-foreground text-sm font-medium">
                    {selectedChannel.name}
                  </p>
                  <p className="text-muted-foreground text-[10px]">
                    {nowPlaying?.title
                      ? t("liveTV.nowPrefix", { title: nowPlaying.title })
                      : selectedChannel.category}
                  </p>
                </div>
              )}
            </div>
          </div>
        )}
      </div>
    </PageLayout>
  );
};

export default LiveTV;
