import type { RouteProp } from '@react-navigation/native';
import { useNavigation, useRoute } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useEvent } from 'expo';
import * as Haptics from 'expo-haptics';
import * as ScreenOrientation from 'expo-screen-orientation';
import { useVideoPlayer, VideoView, type VideoSource } from 'expo-video';
import React from 'react';
import {
  ActivityIndicator,
  Animated,
  LayoutChangeEvent,
  Pressable,
  StatusBar,
  StyleSheet,
  Text,
  View,
} from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { WebView, type WebViewMessageEvent } from 'react-native-webview';
import { useClient } from '../ClientContext';
import type { RootStackParamList } from '../navigation';
import { theme } from '../theme';

interface RouteParams {
  url: string;
  sceneId: string;
  durationSec?: number | null;
  refererHost?: string;
  title?: string;
  mode?: 'video' | 'webview';
  fallbackEmbedUrl?: string;
}

const DEFAULT_UA =
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';

const VIDEO_EXT_RE = /\.(mp4|m3u8|mpd|webm|ts|mov)(?:[?/#]|$)/i;
const PLAYER_PAGE_RE = /(?:\/player\/|\/embed\/|\/e\/|\.php(?:[?/#]|$))/i;

// Ad-network domains — block at subframe load + match w INJECTED_JS adblock.
// Sync z app/extractors/tubes/_embed_iframe.py:AD_DOMAIN_RE.
const AD_HOSTS = [
  'hoirms.com', 'propellerads.com', 'popads.net', 'popcash.net', 'trafficstars.com',
  'exoclick.com', 'adsterra.com', 'happyleafmotion.com', 'adskeeper.com',
  'hilltopads.com', 'juicyads.com', 'trafficjunky.net', 'adblade.com',
  'mavrtracktor.com', 'adtng.com', 'bluetrafficstream.com', 'smartpop.io',
  'mypornclub.com', 'cdntrafficstars.com', 'trafficfactory.biz', 'popcrn.com',
  'popmyads.com', 'adcash.com', 'chaturbate.com', 'stripchat.com', 'streamate.com',
  'willingcease.com', 'doubleclick.net', 'googlesyndication.com',
  'google-analytics.com', 'googletagmanager.com',
  'traffichaus.com', 'plugrush.com', 'clickadu.com',
  'redirectvoluum.com', 'clickaine.com', 'hilltopads.net', 'popshq.com',
];

function detectMode(url: string): 'video' | 'webview' {
  if (VIDEO_EXT_RE.test(url)) return 'video';
  if (PLAYER_PAGE_RE.test(url)) return 'webview';
  if (/\/proxy\//.test(url)) return 'video';
  return 'video';
}

export function PlayerScreen() {
  const route = useRoute<RouteProp<RootStackParamList, 'Player'>>();
  const params = route.params as RouteParams;
  const mode = params.mode ?? detectMode(params.url);

  React.useEffect(() => {
    ScreenOrientation.unlockAsync().catch(() => {});
    return () => {
      ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(() => {});
    };
  }, []);

  if (mode === 'webview') return <EmbedWebViewPlayer params={params} />;
  return <NativeVideoPlayer params={params} />;
}

function NativeVideoPlayer({ params }: { params: RouteParams }) {
  const client = useClient();
  const nav = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Player'>>();
  const { url, sceneId, durationSec, refererHost, title, fallbackEmbedUrl, headers: paramHeaders, fallbackProxyUrl } = params;

  const source: VideoSource = React.useMemo(() => {
    // Backend dostarczone headers (Referer + UA z extractor) mają precedencję —
    // CDN binduje URL do konkretnego Referera embed page'a (np. watchporn.to dla
    // 0dayxx scen). Bez tego CDN zwraca 410.
    if (paramHeaders) {
      return { uri: url, headers: paramHeaders };
    }
    // Legacy fallback: stary path z refererHost (proxy URL nie wymaga headers).
    const hdr: Record<string, string> = { 'User-Agent': DEFAULT_UA };
    if (refererHost) {
      hdr['Referer'] = refererHost.startsWith('http') ? refererHost : `https://${refererHost}/`;
    }
    return { uri: url, headers: hdr };
  }, [url, refererHost, paramHeaders]);

  const player = useVideoPlayer(source, (p) => {
    p.loop = false;
    p.play();
  });

  const statusEvent = useEvent(player, 'statusChange', { status: player.status });
  const status = statusEvent?.status ?? player.status;
  const playerError = statusEvent?.error;
  const playingEvent = useEvent(player, 'playingChange', { isPlaying: player.playing });
  const isPlaying = playingEvent?.isPlaying ?? player.playing;

  // Auto-fallback na WebView gdy native ExoPlayer dostanie błąd, a backend dostarczył
  // embed URL. Najczęstsza przyczyna: IP-bound CDN URL (luluvids/iceyfile/tnmr) —
  // backend extracted z VPS IP, mobile dostaje 403. WebView fetch'uje URL we własnym
  // kontekście strony (cookies, Chrome JA3, IP w session) → działa.
  // Fallback chain (na error):
  //   1. direct CDN URL z headers — preferred (0 VPS bandwidth)
  //   2. → proxy URL (VPS re-fetchuje, streamuje do mobile) — gdy CDN IP-bound
  //   3. → WebView z embed URL — gdy ExoPlayer w ogóle nie radzi sobie z formatem
  // Każdy step ma osobną ref żeby nie loopować.
  const didFallbackProxyRef = React.useRef(false);
  const didFallbackWebViewRef = React.useRef(false);
  React.useEffect(() => {
    if (status !== 'error') return;
    // Step 1 → 2: direct fail (403/410/etc), spróbuj proxy URL.
    if (fallbackProxyUrl && !didFallbackProxyRef.current && url !== fallbackProxyUrl) {
      didFallbackProxyRef.current = true;
      nav.replace('Player', {
        url: fallbackProxyUrl,
        sceneId,
        durationSec,
        refererHost,
        title,
        // Proxy URL nie wymaga headers (proxy sam dodaje Referer przy upstream fetch).
        headers: undefined,
        fallbackProxyUrl: undefined,  // już użyty
        fallbackEmbedUrl,  // zostawiamy do step 3
      });
      return;
    }
    // Step 2 → 3: proxy też failed, spróbuj WebView z embed URL.
    if (fallbackEmbedUrl && !didFallbackWebViewRef.current) {
      didFallbackWebViewRef.current = true;
      nav.replace('Player', {
        url: fallbackEmbedUrl,
        sceneId,
        durationSec,
        refererHost,
        title,
        mode: 'webview',
      });
    }
  }, [status, fallbackProxyUrl, fallbackEmbedUrl, url, nav, sceneId, durationSec, refererHost, title]);

  const lastReportedRef = React.useRef(0);
  // Lokalny tick co 500ms — driver dla custom scrubber + time labels. expo-video
  // ma `timeUpdate` event ale firuje z mniejszą częstotliwością niż chcemy dla UI.
  const [position, setPosition] = React.useState(0);
  const [knownDuration, setKnownDuration] = React.useState<number>(durationSec ?? 0);
  React.useEffect(() => {
    const tick = setInterval(() => {
      try {
        const pos = player.currentTime || 0;
        setPosition(pos);
        const dur = player.duration || 0;
        if (dur > 0 && Math.abs(dur - knownDuration) > 0.5) setKnownDuration(dur);
        const posInt = Math.floor(pos);
        const durInt = Math.floor(dur || durationSec || 0) || null;
        if (posInt > 0 && Math.abs(posInt - lastReportedRef.current) >= 10) {
          lastReportedRef.current = posInt;
          client
            .upsertProgress(sceneId, {
              position_sec: posInt,
              duration_sec: durInt ?? undefined,
              // "Watched" = email-style "read": min. 30s aktywnej odtwarzania (bo 1 sec
              // to przypadkowy klik). Tile z `finished=true` jest dim'owany w listach
              // jako sygnał "to widziałem już". Nie chcemy threshold 95% — to nie Netflix,
              // user przegląda materiał, decyduje po pierwszych sekundach.
              finished: posInt >= 30,
            })
            .catch(() => {});
        }
      } catch {
        // disposed
      }
    }, 500);
    return () => {
      clearInterval(tick);
      try {
        const pos = Math.floor(player.currentTime || 0);
        const dur = Math.floor(player.duration || durationSec || 0) || null;
        if (pos > 0) {
          client
            .upsertProgress(sceneId, {
              position_sec: pos,
              duration_sec: dur ?? undefined,
              finished: pos >= 30,
            })
            .catch(() => {});
        }
      } catch {
        // best-effort
      }
    };
  }, [player, sceneId, durationSec, client, knownDuration]);

  // ----- controls visibility (auto-hide po 3.5s bez interakcji) ----------------
  const [controlsVisible, setControlsVisible] = React.useState(true);
  const fade = React.useRef(new Animated.Value(1)).current;
  const hideTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
  const cancelHide = React.useCallback(() => {
    if (hideTimerRef.current) {
      clearTimeout(hideTimerRef.current);
      hideTimerRef.current = null;
    }
  }, []);
  const scheduleHide = React.useCallback(() => {
    cancelHide();
    hideTimerRef.current = setTimeout(() => setControlsVisible(false), 3500);
  }, [cancelHide]);
  React.useEffect(() => {
    Animated.timing(fade, {
      toValue: controlsVisible ? 1 : 0,
      duration: 180,
      useNativeDriver: true,
    }).start();
    if (controlsVisible) scheduleHide();
    else cancelHide();
  }, [controlsVisible, fade, scheduleHide, cancelHide]);
  React.useEffect(() => () => cancelHide(), [cancelHide]);

  // ----- gesture overlay state (popups for ±10s i 2x) -------------------------
  const [seekHint, setSeekHint] = React.useState<'left' | 'right' | null>(null);
  const [seekDelta, setSeekDelta] = React.useState(0);
  const seekHintTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
  const showSeekHint = React.useCallback((side: 'left' | 'right', delta: number) => {
    setSeekHint(side);
    setSeekDelta((d) => (Math.sign(d) === Math.sign(delta) ? d + delta : delta));
    if (seekHintTimerRef.current) clearTimeout(seekHintTimerRef.current);
    seekHintTimerRef.current = setTimeout(() => {
      setSeekHint(null);
      setSeekDelta(0);
    }, 700);
  }, []);
  React.useEffect(() => () => {
    if (seekHintTimerRef.current) clearTimeout(seekHintTimerRef.current);
  }, []);

  const [speedActive, setSpeedActive] = React.useState(false);

  // ----- pan-to-seek (horizontal swipe scrub) ---------------------------------
  // Activation: 20px threshold po X, więc nie konfliktuje z double-tap (dużo krótszy
  // ruch) ani long-press (statyczny). Mapping: pełna szerokość ekranu = całe wideo,
  // jak YouTube / IG stories. Seek na koniec gestu (nie podczas onUpdate) bo
  // expo-video nie radzi sobie z 60Hz seekingiem — buffer hits + flicker.
  const panStartTimeRef = React.useRef(0);
  const [panSeekTarget, setPanSeekTarget] = React.useState<number | null>(null);

  // Layout szerokości overlaya — żeby double-tap wiedział lewa vs prawa połowa.
  const layoutWidthRef = React.useRef(0);
  const onLayout = React.useCallback((e: LayoutChangeEvent) => {
    layoutWidthRef.current = e.nativeEvent.layout.width;
  }, []);

  // ----- gesture definitions --------------------------------------------------
  // Single-tap → toggle controls. Czeka aż double-tap fail (~250ms by RNGH default).
  // Bez tego pierwszy tap z double-tap-a togglowałby controls.
  const singleTap = React.useMemo(
    () =>
      Gesture.Tap()
        .numberOfTaps(1)
        .maxDuration(250)
        .onEnd((_e, success) => {
          if (!success) return;
          setControlsVisible((v) => !v);
        })
        .runOnJS(true),
    [],
  );

  const doubleTap = React.useMemo(
    () =>
      Gesture.Tap()
        .numberOfTaps(2)
        .maxDelay(280)
        .onEnd((e, success) => {
          if (!success) return;
          const w = layoutWidthRef.current;
          const isRight = w > 0 && e.x > w / 2;
          // +15s/-15s per bug-report 2026-05-16 (#cdff6341) — user preferuje +15
          // bo 10 czuje się jak za krótkie na pomijanie krótkich klipów ad/intro.
          const delta = isRight ? 15 : -15;
          try {
            player.seekBy(delta);
          } catch {
            // ignore
          }
          showSeekHint(isRight ? 'right' : 'left', delta);
          Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
          setControlsVisible(true);
        })
        .runOnJS(true),
    [player, showSeekHint],
  );

  // Long-press → 2× speed dopóki trzymasz. minDuration 220ms żeby nie konfliktowało
  // z double-tap (drugi tap trwa krócej). onTouchesUp finalizuje gest.
  const longPress = React.useMemo(
    () =>
      Gesture.LongPress()
        .minDuration(220)
        .maxDistance(30)
        .onStart(() => {
          try {
            player.playbackRate = 2.0;
          } catch {
            // ignore
          }
          setSpeedActive(true);
          Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
        })
        // onEnd palpi gdy gesture state → END (palec puszczony NORMALNIE).
        // onFinalize to safety-net dla CANCELLED/FAILED — np. gdy systemowy
        // gesture (scroll, swipe back) przejmie palec. Bez tego playbackRate
        // zostawał na 2.0 i user musiał drugi raz tapnąć żeby wyłączyć
        // (zgłoszone 2026-05-10, bug-report #f53e50b9). Wcześniejszy kod
        // używał nieudokumentowanego `.onTouchesUp/Cancelled` które na LongPress
        // gesture nie wywoływały się w wszystkich scenariuszach.
        .onEnd(() => {
          try {
            player.playbackRate = 1.0;
          } catch {
            // ignore
          }
          setSpeedActive(false);
        })
        .onFinalize(() => {
          try {
            if (player.playbackRate !== 1.0) {
              player.playbackRate = 1.0;
            }
          } catch {
            // ignore
          }
          setSpeedActive(false);
        })
        .runOnJS(true),
    [player],
  );

  const dur = knownDuration || durationSec || 0;

  const panSeek = React.useMemo(
    () =>
      Gesture.Pan()
        // Aktywacja dopiero po 20px w bok — niżej myliłoby się z drgnięciem palca
        // przy tap. Pionowy ruch >40px abortuje (rezerwa na future zoom/brightness).
        .activeOffsetX([-20, 20])
        .failOffsetY([-40, 40])
        .onStart(() => {
          cancelHide();
          panStartTimeRef.current = player.currentTime || 0;
          setControlsVisible(true);
          Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
        })
        .onUpdate((e) => {
          const w = layoutWidthRef.current;
          if (w <= 0 || dur <= 0) return;
          const deltaSec = (e.translationX / w) * dur;
          const target = Math.max(0, Math.min(dur, panStartTimeRef.current + deltaSec));
          setPanSeekTarget(target);
        })
        .onEnd((e) => {
          const w = layoutWidthRef.current;
          if (w > 0 && dur > 0) {
            const deltaSec = (e.translationX / w) * dur;
            const target = Math.max(0, Math.min(dur, panStartTimeRef.current + deltaSec));
            try {
              player.currentTime = target;
            } catch {
              // ignore
            }
          }
          setPanSeekTarget(null);
          scheduleHide();
        })
        .onFinalize((_e, success) => {
          if (!success) setPanSeekTarget(null);
        })
        .runOnJS(true),
    [player, dur, cancelHide, scheduleHide],
  );

  // Race: pierwszy aktywny gest wygrywa. Single-tap musi czekać aż double-tap fail.
  // panSeek na początku — gdy palec ruszy >20px, wygrywa nad tap/long-press.
  // longPress PRZED doubleTap: Exclusive priorytetyzuje po kolejności, a doubleTap
  // ma maxDelay=280ms "waiting state" który blokował longPress (minDuration=220ms)
  // — palec trzymany 220ms nigdy nie aktywował 2x speed bo doubleTap wciąż "myślał"
  // czy będzie drugi tap. Bug-report 2026-05-16 #7c13a549/#cdff6341 (eporner/hqporner).
  const composedGesture = React.useMemo(
    () => Gesture.Exclusive(panSeek, longPress, doubleTap, singleTap),
    [panSeek, longPress, doubleTap, singleTap],
  );

  // ----- scrubber pan ---------------------------------------------------------
  const [scrubX, setScrubX] = React.useState<number | null>(null);
  const scrubBarWidthRef = React.useRef(0);
  const scrubBarLayout = React.useCallback((e: LayoutChangeEvent) => {
    scrubBarWidthRef.current = e.nativeEvent.layout.width;
  }, []);
  const scrubGesture = React.useMemo(
    () =>
      Gesture.Pan()
        .onStart((e) => {
          cancelHide();
          setScrubX(Math.max(0, Math.min(scrubBarWidthRef.current, e.x)));
        })
        .onUpdate((e) => {
          setScrubX(Math.max(0, Math.min(scrubBarWidthRef.current, e.x)));
        })
        .onEnd((e) => {
          const w = scrubBarWidthRef.current;
          if (w > 0 && knownDuration > 0) {
            const ratio = Math.max(0, Math.min(1, e.x / w));
            try {
              player.currentTime = ratio * knownDuration;
            } catch {
              // ignore
            }
          }
          setScrubX(null);
          scheduleHide();
        })
        .runOnJS(true),
    [player, knownDuration, cancelHide, scheduleHide],
  );

  // ----- fullscreen toggle ----------------------------------------------------
  const [isLandscape, setIsLandscape] = React.useState(false);
  const toggleFullscreen = React.useCallback(async () => {
    if (isLandscape) {
      await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP).catch(
        () => {},
      );
      setIsLandscape(false);
    } else {
      await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE).catch(
        () => {},
      );
      setIsLandscape(true);
    }
    setControlsVisible(true);
  }, [isLandscape]);

  // ----- helpers --------------------------------------------------------------
  const togglePlay = React.useCallback(() => {
    if (player.playing) player.pause();
    else player.play();
    setControlsVisible(true);
  }, [player]);

  const progressRatio = dur > 0 ? Math.min(1, Math.max(0, position / dur)) : 0;
  const scrubbingRatio =
    scrubX !== null && scrubBarWidthRef.current > 0
      ? Math.max(0, Math.min(1, scrubX / scrubBarWidthRef.current))
      : null;
  // Priorytet: scrubber (palec na pasku) > pan-seek (swipe na video) > playback.
  const panSeekRatio = panSeekTarget !== null && dur > 0 ? panSeekTarget / dur : null;
  const displayRatio = scrubbingRatio ?? panSeekRatio ?? progressRatio;
  const displayedTime =
    scrubbingRatio !== null
      ? scrubbingRatio * dur
      : panSeekTarget !== null
        ? panSeekTarget
        : position;

  return (
    <View style={styles.root} onLayout={onLayout}>
      {/* Hidden status bar — full-bleed video w landscape bez 24px paska systemu. */}
      <StatusBar hidden />

      <VideoView
        style={styles.video}
        player={player}
        allowsFullscreen={false}
        allowsPictureInPicture
        nativeControls={false}
        contentFit="contain"
      />

      {/* Gesture warstwa nad video ale POD controls overlay — controls.pointerEvents=
          'box-none' przepuszcza taps poza buttons na to. */}
      <GestureDetector gesture={composedGesture}>
        <Animated.View style={StyleSheet.absoluteFill} collapsable={false} />
      </GestureDetector>

      {/* Pan-seek preview bubble (swipe horizontal scrub) — pokazuje target time + delta. */}
      {panSeekTarget !== null && (
        <View pointerEvents="none" style={styles.panSeekBubble}>
          <Text style={styles.panSeekTime}>{formatTime(panSeekTarget)}</Text>
          <Text style={styles.panSeekDelta}>
            {(() => {
              const delta = Math.round(panSeekTarget - panStartTimeRef.current);
              return delta >= 0 ? `+${delta}s` : `${delta}s`;
            })()}
          </Text>
        </View>
      )}

      {/* Seek hint popup (−10s / +10s) */}
      {seekHint && (
        <View
          pointerEvents="none"
          style={[
            styles.seekBubble,
            seekHint === 'left' ? styles.seekBubbleLeft : styles.seekBubbleRight,
          ]}
        >
          <Text style={styles.seekBubbleText}>
            {seekHint === 'left' ? '« ' : ''}
            {seekDelta > 0 ? `+${seekDelta}` : seekDelta}s
            {seekHint === 'right' ? ' »' : ''}
          </Text>
        </View>
      )}

      {/* 2× speed pill */}
      {speedActive && (
        <View pointerEvents="none" style={styles.speedPill}>
          <Text style={styles.speedPillText}>▶▶ 2×</Text>
        </View>
      )}

      {/* Controls overlay — pointerEvents box-none żeby gesture overlay pod spodem
          dalej dostawał taps poza interactive elements (back, play, scrubber). */}
      <Animated.View
        pointerEvents={controlsVisible ? 'box-none' : 'none'}
        style={[StyleSheet.absoluteFill, { opacity: fade }]}
      >
        <View style={styles.controlsTop} pointerEvents="box-none">
          <Pressable onPress={() => nav.goBack()} hitSlop={16} style={styles.iconBtn}>
            <Text style={styles.iconText}>←</Text>
          </Pressable>
          {title ? (
            <Text style={styles.titleText} numberOfLines={1}>
              {title}
            </Text>
          ) : (
            <View style={{ flex: 1 }} />
          )}
          <Pressable onPress={toggleFullscreen} hitSlop={16} style={styles.iconBtn}>
            <Text style={styles.iconText}>{isLandscape ? '⤬' : '⛶'}</Text>
          </Pressable>
        </View>

        <View style={styles.controlsCenter} pointerEvents="box-none">
          <Pressable onPress={togglePlay} hitSlop={20} style={styles.playBtn}>
            <Text style={styles.playBtnText}>{isPlaying ? '❚❚' : '▶'}</Text>
          </Pressable>
        </View>

        <View style={styles.controlsBottom} pointerEvents="box-none">
          <Text style={styles.timeText}>{formatTime(displayedTime)}</Text>
          <GestureDetector gesture={scrubGesture}>
            <View style={styles.scrubTouchArea} onLayout={scrubBarLayout} collapsable={false}>
              <View style={styles.scrubTrack} />
              <View
                style={[
                  styles.scrubFill,
                  { width: `${displayRatio * 100}%` },
                ]}
              />
              <View
                style={[
                  styles.scrubThumb,
                  { left: `${displayRatio * 100}%` },
                ]}
              />
            </View>
          </GestureDetector>
          <Text style={styles.timeText}>{formatTime(dur)}</Text>
        </View>
      </Animated.View>

      {status === 'loading' && (
        <View style={styles.overlay} pointerEvents="none">
          <ActivityIndicator color={theme.fg} size="large" />
          <Text style={styles.overlayText}>{title ?? 'Loading…'}</Text>
        </View>
      )}
      {status === 'error' && !fallbackEmbedUrl && (
        <View style={styles.overlay}>
          <Text style={styles.errorTitle}>Playback failed</Text>
          <Text style={styles.errorBody}>
            {playerError?.message ?? 'Stream nie odpalił się.'}
          </Text>
          <Pressable style={styles.btn} onPress={() => nav.goBack()}>
            <Text style={styles.btnText}>Back</Text>
          </Pressable>
        </View>
      )}
      {status === 'error' && fallbackEmbedUrl && (
        <View style={styles.overlay} pointerEvents="none">
          <ActivityIndicator color={theme.fg} size="large" />
          <Text style={styles.overlayText}>Native player failed — switching to embed…</Text>
        </View>
      )}
    </View>
  );
}

function formatTime(sec: number): string {
  if (!isFinite(sec) || sec < 0) sec = 0;
  const total = Math.floor(sec);
  const h = Math.floor(total / 3600);
  const m = Math.floor((total % 3600) / 60);
  const s = total % 60;
  const mm = String(m).padStart(2, '0');
  const ss = String(s).padStart(2, '0');
  return h > 0 ? `${h}:${mm}:${ss}` : `${m}:${ss}`;
}

// JS injected do embed page. Robi 3 rzeczy:
//
//  1. **Adblock**: zatrzymuje `window.open`, klikalne linki z `target=_blank`, dynamiczne
//     iframe ad-overlays. Hostery jak xtremestream/playmogo używają "click-to-play" tricku
//     gdzie pierwszy tap na video idzie do reklamy (popunder), nie do play.
//  2. **Auto-extract**: hookuje `XMLHttpRequest.open` + `fetch()` + sondowanie `<video>.src`
//     żeby wyciągnąć finalny m3u8/mp4 URL. Gdy znajdzie, posyła do RN przez postMessage —
//     RN pokazuje banner "Otwórz w native playerze" który robi nav.replace na ExoPlayer.
//  3. **Auto-tap**: po załadowaniu, klika programowo na <video> + .play() — pomija ekran
//     "tap to play" reklamowego overlaya.
//
// Kontekst: WebView (Chrome engine) jest jedyną drogą dla CF-WAF-blocked hosterów (perverzija
// → xtremestream.xyz, watchporn → playmogo). Apka porn-app robi dokładnie to samo — embed
// JS hostera zna m3u8 URL bo Chrome JA3 pasuje, a ExoPlayer w naszej apce nie.
const INJECTED_JS = `
(function() {
  if (window.__goonPatched) return;
  window.__goonPatched = true;

  // -- 1. Ad-network domain blocklist ----------------------------------------
  // Sync z app/extractors/tubes/_embed_iframe.py:AD_DOMAIN_RE. Match na hostname
  // — jakikolwiek URL którego host KOŃCZY się tym (uwzględnia subdomeny ib.hoirms.com).
  // Hostery aggregator tubes (porn4days, siska/playmogo) wstrzykują ad scripts +
  // popunder iframes z tych domen po CAPTCHA solve. Bez block widać pełnoekranowe
  // reklamy nakładane na player.
  const AD_HOSTS = [
    'hoirms.com','propellerads.com','popads.net','popcash.net','trafficstars.com',
    'exoclick.com','adsterra.com','happyleafmotion.com','adskeeper.com',
    'hilltopads.com','juicyads.com','trafficjunky.net','adblade.com',
    'mavrtracktor.com','adtng.com','bluetrafficstream.com','smartpop.io',
    'mypornclub.com','cdntrafficstars.com','trafficfactory.biz','popcrn.com',
    'popmyads.com','adcash.com','chaturbate.com','stripchat.com','streamate.com',
    'willingcease.com','doubleclick.net','googlesyndication.com',
    'google-analytics.com','googletagmanager.com',
    // Popunder networks często widywane na DoodStream-rebranding hostach
    'ib.hoirms.com','traffichaus.com','plugrush.com','clickadu.com',
    'redirectvoluum.com','clickaine.com','hilltopads.net','popshq.com',
  ];
  const isAdHost = function(url) {
    if (!url) return false;
    try {
      let host = url;
      if (url.indexOf('//') !== -1) host = url.split('//')[1].split('/')[0];
      host = host.toLowerCase();
      for (let i = 0; i < AD_HOSTS.length; i++) {
        const ad = AD_HOSTS[i];
        if (host === ad || host.endsWith('.' + ad)) return true;
      }
    } catch (e) {}
    return false;
  };

  // window.open zwraca fake window (no-op) bo hostery nie wykrywają fail.
  window.open = function() {
    return { closed: false, focus: function() {}, close: function() {} };
  };

  // Block <a target="_blank"> i clicki na overlaye reklamowe.
  document.addEventListener('click', function(e) {
    let el = e.target;
    while (el && el.nodeType === 1) {
      if (el.tagName === 'A') {
        const href = el.getAttribute('href') || '';
        const target = el.getAttribute('target') || '';
        if (target === '_blank' || (href && href.startsWith('http') && !href.includes(location.host))) {
          e.preventDefault();
          e.stopPropagation();
          e.stopImmediatePropagation();
          return false;
        }
      }
      el = el.parentNode;
    }
  }, true);

  // Block dynamic ad-script injection. Hostery (DoodStream variant z siska) ładują
  // adsnet przez \`document.createElement('script').src = ad-url\` + appendChild. Patch
  // tych metod żeby filter URL przed insertem do DOM.
  const origCreateElement = Document.prototype.createElement;
  Document.prototype.createElement = function(tagName) {
    const el = origCreateElement.apply(this, arguments);
    if (typeof tagName === 'string' && /^(script|iframe|img)$/i.test(tagName)) {
      // Hook src setter — gdy URL jest ad-host, set noop URL żeby ad nie ładował się.
      let cachedSrc = '';
      try {
        Object.defineProperty(el, 'src', {
          configurable: true,
          get: function() { return cachedSrc; },
          set: function(v) {
            if (isAdHost(v)) { cachedSrc = ''; return; }
            cachedSrc = v;
            el.setAttribute('src', v);
          },
        });
      } catch (e) {}
    }
    return el;
  };

  // Patch appendChild + insertBefore żeby blokować już-ustawione ad src (przypadki
  // gdy ad-network ustawi src przed appendChild). Dla iframe + script + img.
  const filterAdNode = function(node) {
    if (!node || node.nodeType !== 1) return node;
    const tag = (node.tagName || '').toUpperCase();
    if (!/^(SCRIPT|IFRAME|IMG)$/.test(tag)) return node;
    const src = node.getAttribute && (node.getAttribute('src') || '');
    if (src && isAdHost(src)) {
      // Zwracamy zaślepkę zamiast prawdziwego node — appendChild zakończy się
      // sukcesem ale fizycznie nic do DOM nie idzie. Hostery nie wykrywają fail.
      return document.createComment('blocked-ad');
    }
    return node;
  };
  const origAppendChild = Node.prototype.appendChild;
  Node.prototype.appendChild = function(node) {
    return origAppendChild.call(this, filterAdNode(node));
  };
  const origInsertBefore = Node.prototype.insertBefore;
  Node.prototype.insertBefore = function(node, ref) {
    return origInsertBefore.call(this, filterAdNode(node), ref);
  };

  // Block location.href / location.assign cross-origin nav (popunder via redirect).
  try {
    const origAssign = Location.prototype.assign;
    Location.prototype.assign = function(url) {
      if (isAdHost(url)) return;
      return origAssign.apply(this, arguments);
    };
    const origReplace = Location.prototype.replace;
    Location.prototype.replace = function(url) {
      if (isAdHost(url)) return;
      return origReplace.apply(this, arguments);
    };
  } catch (e) {}

  // -- 1.5. Cookie/consent auto-dismiss --------------------------------------
  // Tube'y typu hqporner mają cookie-consent gate ("Allow All / Allow Essential
  // Only") który blokuje kt_player JS — player nie inicjalizuje się dopóki user
  // nie kliknie. INJECTED_JS scrape \`<source>.src\` odpala się więc za wcześnie
  // (DOM nie ma jeszcze video). Auto-klikamy consent żeby odblokować player.
  //
  // Bezpieczeństwo: klikamy TYLKO element którego tekst pasuje do consent-frazy
  // ORAZ leży w kontenerze z markerem cookie/consent/gdpr (≤6 przodków). To
  // wyklucza przypadkowy klik w reklamę "Continue to site".
  const CONSENT_TEXT_RE = /^(allow all|accept all|accept|accept & continue|accept and continue|i accept|i agree|agree|agree & continue|got it|enable all|consent|continue|ok|akceptuj.*|zgadzam.*|zgoda|rozumiem|wyra(z|ż)am zgod)$/i;
  const CONSENT_CTX_RE = /(cookie|consent|gdpr|privacy|cmp|onetrust|didomi|cookiebar|cookie-?notice)/i;
  const dismissConsent = function() {
    const els = document.querySelectorAll('button, a, [role="button"], div[onclick], span[onclick], input[type="button"], input[type="submit"]');
    for (let i = 0; i < els.length; i++) {
      const el = els[i];
      const txt = ((el.textContent || el.value || '') + '').trim();
      if (!txt || txt.length > 32) continue;
      if (!CONSENT_TEXT_RE.test(txt)) continue;
      // Kontekst: element lub ≤6 przodków ma cookie/consent marker (class/id).
      let ctx = el, depth = 0, inCtx = false;
      while (ctx && depth < 7) {
        const cn = ctx.className;
        const sig = ((typeof cn === 'string' ? cn : (cn && cn.baseVal) || '') + ' ' + (ctx.id || '')).toLowerCase();
        if (CONSENT_CTX_RE.test(sig)) { inCtx = true; break; }
        ctx = ctx.parentElement; depth++;
      }
      if (!inCtx) continue;
      try {
        el.click();
        window.ReactNativeWebView.postMessage(JSON.stringify({type: 'consent_dismissed'}));
      } catch (e) {}
    }
  };

  // Niektóre hostery wstrzykują full-screen <iframe> jako ad — usuwamy periodically.
  // Plus iframe-ad już istniejące przed naszym patchowaniem (race condition).
  const removeAdIframes = function() {
    document.querySelectorAll('iframe').forEach(function(f) {
      const src = f.src || '';
      if (isAdHost(src)) { f.remove(); return; }
      const isPlayer = /\\/(?:e|embed|player|hls)\\//.test(src);
      const isCurrentOrigin = src.includes(location.host);
      if (!isPlayer && !isCurrentOrigin && src.startsWith('http')) {
        f.remove();
      }
    });
    // Również full-screen overlay divs (ads często overlay na video element)
    document.querySelectorAll('div[style*="z-index"], div[style*="position: fixed"], div[style*="position:fixed"]').forEach(function(d) {
      const style = d.getAttribute('style') || '';
      // Heurystyka: full-screen overlay div z high z-index + brak <video> w środku.
      if (/z-index:\\s*(\\d{4,}|2147)/.test(style) && !d.querySelector('video')) {
        const rect = d.getBoundingClientRect();
        if (rect.width > window.innerWidth * 0.7 && rect.height > window.innerHeight * 0.5) {
          d.remove();
        }
      }
    });
  };
  setInterval(function() {
    removeAdIframes();
    dismissConsent();
  }, 1000);
  // Pierwsza próba consent natychmiast (banner bywa w SSR HTML) — bez czekania
  // na pierwszy tick interwału.
  dismissConsent();

  // -- 2. Auto-extract m3u8/mp4 -----------------------------------------------
  const VIDEO_RE = /https?:\\/\\/[^"'\\s<>]+\\.(?:m3u8|mp4|mpd)(?:\\?[^"'\\s<>]*)?/i;
  const seen = new Set();
  function report(url) {
    if (!url || seen.has(url)) return;
    if (!VIDEO_RE.test(url)) return;
    seen.add(url);
    try {
      window.ReactNativeWebView.postMessage(JSON.stringify({type: 'video_url', url: url}));
    } catch (e) {}
  }

  const xhrOpen = XMLHttpRequest.prototype.open;
  XMLHttpRequest.prototype.open = function(method, url) {
    report(url);
    return xhrOpen.apply(this, arguments);
  };
  const origFetch = window.fetch;
  window.fetch = function(input, init) {
    const u = typeof input === 'string' ? input : (input && input.url) || '';
    report(u);
    return origFetch.apply(this, arguments);
  };

  // -- 3. Auto-tap + DOM scan -------------------------------------------------
  let ticks = 0;
  const interval = setInterval(function() {
    ticks++;
    document.querySelectorAll('video, source').forEach(function(el) {
      if (el.src) report(el.src);
      if (el.currentSrc) report(el.currentSrc);
      // Auto-play: niektóre playery wymagają jawnego play() po click. Jeśli paused
      // i ma valid src, próbujemy odpalić.
      if (el.tagName === 'VIDEO' && el.paused && (el.src || el.currentSrc)) {
        try {
          el.muted = false;
          const p = el.play();
          if (p && p.catch) p.catch(function(){});
        } catch (e) {}
      }
    });
    // Jeśli mamy video URL i video się odpaliło, możemy zatrzymać polling.
    // Próg podniesiony 5→15: po auto-dismiss cookie consent kt_player (hqporner)
    // potrzebuje kilku sekund na init — zbyt wczesny stop łapał tylko preroll-ad
    // URL zanim pojawił się prawdziwy <source>. 15 ticków = ~15s retry window.
    if (seen.size > 0 && ticks > 15) clearInterval(interval);
    if (ticks > 90) clearInterval(interval);
  }, 1000);

  true;
})();
`;

function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
  const client = useClient();
  const nav = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Player'>>();
  const { url, sceneId, durationSec, refererHost } = params;
  const [extractedUrl, setExtractedUrl] = React.useState<string | null>(null);
  const [resolveStatus, setResolveStatus] = React.useState<'idle' | 'pending' | 'failed'>('idle');
  const [resolveError, setResolveError] = React.useState<string | null>(null);
  const [skipResolve, setSkipResolve] = React.useState(false);
  const [resolveAttempted, setResolveAttempted] = React.useState(false);

  // Stage 0.8: mobile-side hoster resolvery. Mobile IP usera unika Cloudflare
  // Turnstile / CAPTCHA gate który blokuje Hetzner VPS — embed page renderuje
  // pełny HTML z player config, z którego liczymy direct m3u8/mp4 URL.
  //   - DoodStream-variant (playmogo/dood*) — pass_md5 protokół (doodstream.ts)
  //   - P.A.C.K.E.R.-JWPlayer (luluvid/streamwish) — eval-unpack (packerHoster.ts)
  // Sukces → NativeVideoPlayer (bez reklam/cookie). Fail → fallback do WebView.
  React.useEffect(() => {
    if (resolveAttempted || skipResolve) return;
    (async () => {
      const source = refererHost ? `https://${refererHost.replace(/^https?:\/\//, '')}/` : undefined;
      const { isDoodStream, resolveDoodStream } = await import('../lib/doodstream');
      const { isPackerHoster, resolvePackerHoster } = await import('../lib/packerHoster');

      let result: { url?: string; headers?: Record<string, string>; error?: string } | null = null;
      if (isDoodStream(url)) {
        setResolveStatus('pending');
        result = await resolveDoodStream(url, source);
      } else if (isPackerHoster(url)) {
        setResolveStatus('pending');
        result = await resolvePackerHoster(url, source);
      }

      setResolveAttempted(true);
      if (!result) {
        return; // nie nasz hoster — WebView fallback (INJECTED_JS) zajmie się resztą
      }
      if (result.url) {
        setResolveStatus('idle');
        nav.replace('Player', {
          url: result.url,
          sceneId,
          durationSec,
          refererHost,
          headers: result.headers,
          mode: 'video',
        });
      } else {
        setResolveStatus('failed');
        setResolveError(result.error || 'unknown');
      }
    })();
  }, [url, refererHost, sceneId, durationSec, resolveAttempted, skipResolve, nav]);

  const sourceHeaders = React.useMemo<Record<string, string>>(() => {
    const h: Record<string, string> = {};
    if (refererHost) {
      h['Referer'] = refererHost.startsWith('http') ? refererHost : `https://${refererHost}/`;
    }
    return h;
  }, [refererHost]);

  // WebView blokuje cross-origin nav — initialHostRef trzyma host pierwszego URL,
  // używany do allow-listy redirectów same-eTLD+1. DECLARED HERE (przed useEffectami
  // które go używają) żeby uniknąć ref-before-decl errors w lint.
  const initialHostRef = React.useRef<string | null>(null);
  React.useEffect(() => {
    try {
      initialHostRef.current = new URL(url).hostname;
    } catch {
      initialHostRef.current = null;
    }
  }, [url]);

  // Filter ad / preroll / midroll URLs — player JS hostów (xhamster, KVS, etc.)
  // czasem fetcha ad video PRZED rzeczywistym contentem. INJECTED_JS reportuje
  // każdy mp4/m3u8 — bierzemy pierwszy NIE-ad URL.
  const isLikelyAd = React.useCallback((u: string): boolean => {
    const low = u.toLowerCase();
    if (/\/(preroll|midroll|postroll|adroll|ad-?\d+|advert)/.test(low)) return true;
    if (/(tsyndicate|exoclick|trafficstars|adskeeper|popmyads|magsrv|adcash)\./.test(low)) return true;
    // Trailer / preview thumbnails — często `_preview.mp4` lub thumb `.jpg.mp4`
    if (/_preview\.(mp4|m3u8)/.test(low)) return true;
    if (/\.jpg\.mp4/.test(low)) return true;
    return false;
  }, []);

  const onMessage = React.useCallback((evt: WebViewMessageEvent) => {
    try {
      const data = JSON.parse(evt.nativeEvent.data);
      if (data.type === 'video_url' && typeof data.url === 'string') {
        if (isLikelyAd(data.url)) return;
        setExtractedUrl((curr) => curr ?? data.url);
      }
    } catch {
      // not JSON
    }
  }, [isLikelyAd]);

  // Gdy INJECTED_JS wyłapał czysty video URL z DOM/XHR — przerzucamy na
  // NativeVideoPlayer (expo-video / ExoPlayer) BEZ reklam, cookie consent,
  // related videos. Bez tego user widział tylko stronę hostera w WebView.
  React.useEffect(() => {
    if (!extractedUrl) return;
    nav.replace('Player', {
      url: extractedUrl,
      sceneId,
      durationSec,
      refererHost: refererHost || (initialHostRef.current || undefined),
      mode: 'video',
    });
  }, [extractedUrl, nav, sceneId, durationSec, refererHost]);

  React.useEffect(() => {
    client
      .upsertProgress(sceneId, { position_sec: 0, duration_sec: durationSec ?? undefined })
      .catch(() => {});
  }, [client, sceneId, durationSec]);

  // WebView blokuje cross-origin nav (popunder ad opens new tab/page poza embeddem).
  // Pierwsze ładowanie (`url`) ma `navigationType=undefined` lub `other` — przepuszczamy.
  // Cokolwiek innego (klik, JS redirect) z innego origin → block i log.
  // initialHostRef już declared wyżej (przed useEffect który robi nav.replace).
  const onShouldStartLoad = React.useCallback(
    (req: { url: string; navigationType?: string; isTopFrame?: boolean }) => {
      // Pierwsze załadowanie strony — przepuść.
      if (req.url === url) return true;
      // Subframes (iframe player) — przepuść BIASES wyjątek dla ad-network domen.
      // Player JS rzuca XHR-y + sub-iframe-y do CDN-ów media; ad-network subframe loads
      // (mavrtracktor/exoclick/itp.) blokujemy żeby popundery nie wstrzyknęły się.
      if (req.isTopFrame === false) {
        try {
          const host = new URL(req.url).hostname.toLowerCase();
          for (let i = 0; i < AD_HOSTS.length; i++) {
            if (host === AD_HOSTS[i] || host.endsWith('.' + AD_HOSTS[i])) return false;
          }
        } catch {
          // ignore
        }
        return true;
      }
      // Top-frame nav: pozwól na same-eTLD+1 (paradisehill.cc → en.paradisehill.cc,
      // pervl3.io → pervl5.io); blokuj cross-domain nav (adware popundery).
      try {
        const target = new URL(req.url).hostname.toLowerCase();
        const initial = initialHostRef.current?.toLowerCase();
        if (initial && target === initial) return true;
        if (initial) {
          const tParts = target.split('.');
          const iParts = initial.split('.');
          if (tParts.length >= 2 && iParts.length >= 2 &&
              tParts.slice(-2).join('.') === iParts.slice(-2).join('.')) {
            return true;
          }
        }
      } catch {
        // ignore
      }
      return false;
    },
    [url],
  );

  // DoodStream resolver in-flight — pokaż spinner zamiast WebView
  if (resolveStatus === 'pending') {
    return (
      <View style={[styles.root, styles.overlay]}>
        <ActivityIndicator color={theme.fg} size="large" />
        <Text style={styles.overlayText}>Resolwuję bezpośredni link...</Text>
      </View>
    );
  }

  // Resolver failed — pokaż error + przycisk żeby przejść do WebView
  if (resolveStatus === 'failed' && !skipResolve) {
    return (
      <View style={[styles.root, styles.overlay]}>
        <Text style={[styles.overlayText, { fontSize: 16, marginBottom: 6 }]}>
          DoodStream resolver fail
        </Text>
        <Text style={[styles.overlayText, { fontSize: 12, opacity: 0.7, marginBottom: 20 }]}>
          {resolveError || 'unknown'}
        </Text>
        <Pressable
          onPress={() => setSkipResolve(true)}
          style={{
            backgroundColor: theme.accent,
            paddingHorizontal: 24,
            paddingVertical: 10,
            borderRadius: 8,
          }}
        >
          <Text style={{ color: theme.fg, fontWeight: '600' }}>Otwórz w WebView</Text>
        </Pressable>
      </View>
    );
  }

  return (
    <View style={styles.root}>
      <WebView
        source={{ uri: url, headers: sourceHeaders }}
        style={styles.video}
        userAgent={DEFAULT_UA}
        injectedJavaScriptBeforeContentLoaded={INJECTED_JS}
        onMessage={onMessage}
        onShouldStartLoadWithRequest={onShouldStartLoad}
        setSupportMultipleWindows={false}
        allowsInlineMediaPlayback
        mediaPlaybackRequiresUserAction={false}
        javaScriptEnabled
        domStorageEnabled
        thirdPartyCookiesEnabled
        mixedContentMode="always"
        allowsFullscreenVideo
        startInLoadingState
        renderLoading={() => (
          <View style={styles.overlay} pointerEvents="none">
            <ActivityIndicator color={theme.fg} size="large" />
            <Text style={styles.overlayText}>Loading hoster…</Text>
          </View>
        )}
      />
      {extractedUrl && (
        <Pressable
          style={styles.extractBanner}
          onPress={() =>
            nav.replace('Player', {
              url: extractedUrl,
              sceneId,
              durationSec,
              refererHost,
              mode: 'video',
            })
          }
        >
          <Text style={styles.extractText}>▶ Otwórz w native playerze</Text>
          <Text style={styles.extractSub}>{extractedUrl.slice(0, 80)}</Text>
        </Pressable>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  root: { flex: 1, backgroundColor: '#000' },
  video: { flex: 1, backgroundColor: '#000' },
  overlay: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'rgba(0,0,0,0.6)',
    paddingHorizontal: 32,
  },
  overlayText: { color: theme.fg, marginTop: 12, fontSize: 13, textAlign: 'center' },
  errorTitle: { color: theme.bad, fontSize: 18, fontWeight: '700', marginBottom: 8 },
  errorBody: { color: theme.fg, fontSize: 14, marginBottom: 16, textAlign: 'center' },
  btn: {
    backgroundColor: theme.accent,
    paddingHorizontal: 22,
    paddingVertical: 10,
    borderRadius: 10,
  },
  btnText: { color: theme.fg, fontWeight: '700' },
  extractBanner: {
    position: 'absolute',
    bottom: 24,
    left: 12,
    right: 12,
    backgroundColor: theme.accent,
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderRadius: 12,
  },
  extractText: { color: theme.fg, fontWeight: '700', fontSize: 15 },
  extractSub: { color: theme.fg, opacity: 0.85, fontSize: 11, marginTop: 2 },

  // ----- custom controls overlay --------------------------------------------
  controlsTop: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    paddingTop: 28,
    paddingHorizontal: 14,
    paddingBottom: 12,
    flexDirection: 'row',
    alignItems: 'center',
    gap: 12,
    backgroundColor: 'rgba(0,0,0,0.45)',
  },
  controlsCenter: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    alignItems: 'center',
    justifyContent: 'center',
  },
  controlsBottom: {
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
    paddingTop: 10,
    paddingBottom: 18,
    paddingHorizontal: 14,
    flexDirection: 'row',
    alignItems: 'center',
    gap: 10,
    backgroundColor: 'rgba(0,0,0,0.45)',
  },
  iconBtn: {
    width: 36,
    height: 36,
    alignItems: 'center',
    justifyContent: 'center',
  },
  iconText: { color: theme.fg, fontSize: 22, fontWeight: '600' },
  titleText: {
    flex: 1,
    color: theme.fg,
    fontSize: 14,
    fontWeight: '600',
  },
  playBtn: {
    width: 76,
    height: 76,
    borderRadius: 38,
    backgroundColor: 'rgba(0,0,0,0.55)',
    alignItems: 'center',
    justifyContent: 'center',
  },
  playBtnText: { color: theme.fg, fontSize: 26, fontWeight: '700' },
  timeText: {
    color: theme.fg,
    fontSize: 12,
    fontVariant: ['tabular-nums'],
    minWidth: 44,
    textAlign: 'center',
  },
  // Scrubber: 48px hit-box (≥ Material's 48dp tap target). Track + fill + thumb
  // wszystkie absolute z explicit `top`, żeby były wycentrowane wzgl. siebie
  // i przeżyły zmianę wysokości containera bez rozjazdu.
  scrubTouchArea: {
    flex: 1,
    height: 48,
  },
  scrubTrack: {
    position: 'absolute',
    left: 0,
    right: 0,
    top: 21,
    height: 6,
    backgroundColor: 'rgba(255,255,255,0.28)',
    borderRadius: 3,
  },
  scrubFill: {
    position: 'absolute',
    left: 0,
    top: 21,
    height: 6,
    backgroundColor: theme.accent,
    borderRadius: 3,
  },
  scrubThumb: {
    position: 'absolute',
    width: 20,
    height: 20,
    borderRadius: 10,
    backgroundColor: theme.accent,
    marginLeft: -10,
    top: 14,
    // subtle outer ring żeby thumb był widoczny na jasnym wideo
    borderWidth: 2,
    borderColor: 'rgba(0,0,0,0.45)',
  },
  seekBubble: {
    position: 'absolute',
    top: '50%',
    marginTop: -22,
    paddingVertical: 10,
    paddingHorizontal: 16,
    backgroundColor: 'rgba(0,0,0,0.7)',
    borderRadius: 22,
  },
  seekBubbleLeft: { left: '15%' },
  seekBubbleRight: { right: '15%' },
  seekBubbleText: { color: theme.fg, fontSize: 16, fontWeight: '700' },
  // Pan-seek preview bubble — duży, centered, dwie linie: target time + delta.
  panSeekBubble: {
    position: 'absolute',
    top: '40%',
    alignSelf: 'center',
    left: 0,
    right: 0,
    alignItems: 'center',
    justifyContent: 'center',
  },
  panSeekTime: {
    color: theme.fg,
    fontSize: 28,
    fontWeight: '700',
    fontVariant: ['tabular-nums'],
    backgroundColor: 'rgba(0,0,0,0.7)',
    paddingHorizontal: 18,
    paddingVertical: 8,
    borderRadius: 12,
    overflow: 'hidden',
  },
  panSeekDelta: {
    color: theme.accent,
    fontSize: 14,
    fontWeight: '700',
    marginTop: 6,
    fontVariant: ['tabular-nums'],
  },
  speedPill: {
    position: 'absolute',
    top: 80,
    alignSelf: 'center',
    left: 0,
    right: 0,
    flexDirection: 'row',
    justifyContent: 'center',
  },
  speedPillText: {
    color: theme.fg,
    fontSize: 14,
    fontWeight: '700',
    backgroundColor: 'rgba(0,0,0,0.7)',
    paddingVertical: 6,
    paddingHorizontal: 14,
    borderRadius: 14,
    overflow: 'hidden',
  },
});
