import type { RouteProp } from '@react-navigation/native';
import { useNavigation, useRoute } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { ApiError } from '../api';
import * as IntentLauncher from 'expo-intent-launcher';
import React from 'react';
import {
  ActivityIndicator,
  Alert,
  Linking,
  Platform,
  Pressable,
  ScrollView,
  StyleSheet,
  Text,
  useWindowDimensions,
  View,
} from 'react-native';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { useClient } from '../ClientContext';
import { isGetFileUrl, resolveGetFilePage } from '../lib/getfileResolver';
import { resolvePornxpPage } from '../lib/pornxpResolver';
import { resolveSxyprnPage } from '../lib/sxyprnResolver';
import { resolveEpornerPage } from '../lib/epornerResolver';
import { resolveFpoxxxPage } from '../lib/fpoxxxResolver';
import type { RootStackParamList } from '../navigation';
import { theme } from '../theme';
import type { PlaybackSource, SceneOut, StreamLink } from '../types';
import { PlaybackQualityModal } from './PlaybackQualityModal';

export function SceneDetailScreen() {
  const client = useClient();
  const queryClient = useQueryClient();
  const route = useRoute<RouteProp<RootStackParamList, 'SceneDetail'>>();
  const nav = useNavigation<NativeStackNavigationProp<RootStackParamList, 'SceneDetail'>>();
  const { id } = route.params;

  const { data, isLoading, error } = useQuery({
    queryKey: ['scene', id],
    queryFn: () => client.getScene(id),
  });

  const isFav = data?.is_favorite ?? false;
  const favMutation = useMutation({
    mutationFn: () => (isFav ? client.removeSceneFavorite(id) : client.addSceneFavorite(id)),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['scene', id] });
      queryClient.invalidateQueries({ queryKey: ['scenes'], refetchType: 'none' });
    },
  });

  // Auto-enrich tags z tube'a gdy scena ma 0 tagów + jest tube playback source.
  // Mainstream tubes (xhamster/porntrex/eporner/...) mają categories per scene które
  // yt-dlp generic nie wyciąga. Strzelamy raz przy otwarciu sceny — endpoint jest
  // idempotentny, kolejne otwarcia tej samej sceny już nic nie robią (gdy są tagi).
  const enrichMutation = useMutation({
    mutationFn: () => client.enrichSceneTags(id),
    onSuccess: (out) => {
      if (out.added > 0) {
        queryClient.invalidateQueries({ queryKey: ['scene', id] });
      }
    },
  });
  React.useEffect(() => {
    if (!data) return;
    if (data.tags.length > 0) return;
    const hasTubeSource = data.playback_sources.some((s) => s.origin?.startsWith('tube:'));
    if (!hasTubeSource) return;
    if (enrichMutation.isPending || enrichMutation.isSuccess) return;
    enrichMutation.mutate();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data?.id, data?.tags.length]);

  // Auto-enrich duration: youporn/xvideos/xnxx/etc udostępniają og:video:duration
  // lub LD-JSON ISO 8601 — direct scrapery (search-only) ich nie pobierają, więc
  // sceny przychodzą z duration_sec=null. Tu jeden fetch → backfill. Bez tego
  // dedup capuje composite na 0.85 (bez strong signal), co ratuje od false-merge,
  // ale też blokuje legit auto-merges między tymi samymi scenami z różnych tube'ów.
  const enrichDurationMutation = useMutation({
    mutationFn: () => client.enrichSceneDuration(id),
    onSuccess: (out) => {
      if (out.duration_sec) {
        queryClient.invalidateQueries({ queryKey: ['scene', id] });
      }
    },
  });
  React.useEffect(() => {
    if (!data) return;
    if (data.duration_sec) return;
    const hasTubeSource = data.playback_sources.some((s) => s.origin?.startsWith('tube:'));
    if (!hasTubeSource) return;
    if (enrichDurationMutation.isPending || enrichDurationMutation.isSuccess) return;
    enrichDurationMutation.mutate();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data?.id, data?.duration_sec]);

  // Auto-enrich thumbnail: dla scen ingestowanych z search-only scrapery
  // (hdporn92, sxyland, sxyprn itp.) thumbnail_url=None w PlaybackSource.
  // Detail page wystawia og:image — jeden fetch tu napełnia wszystkie źródła
  // dla tej sceny. SceneDetail = naturalny moment (user już patrzy na hero).
  const enrichThumbMutation = useMutation({
    mutationFn: () => client.enrichSceneThumbnail(id),
    onSuccess: (out) => {
      if (out.thumbnail_url) {
        queryClient.invalidateQueries({ queryKey: ['scene', id] });
        queryClient.invalidateQueries({ queryKey: ['scenes'], refetchType: 'none' });
      }
    },
  });
  React.useEffect(() => {
    if (!data) return;
    const hasThumb = data.playback_sources.some((s) => s.thumbnail_url);
    if (hasThumb) return;
    const hasTubeSource = data.playback_sources.some((s) => s.origin?.startsWith('tube:'));
    if (!hasTubeSource) return;
    if (enrichThumbMutation.isPending || enrichThumbMutation.isSuccess) return;
    enrichThumbMutation.mutate();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data?.id]);

  // Ręczny refresh miniatury (force) — zgłoszenie d3376a71: zepsuta/stała miniaturka
  // (np. rotting sxyprn/trafficdeposit). Nadpisuje istniejącą świeżą ze strony tube'a.
  const refreshThumbMutation = useMutation({
    mutationFn: () => client.enrichSceneThumbnail(id, true),
    onSuccess: (out) => {
      queryClient.invalidateQueries({ queryKey: ['scene', id] });
      queryClient.invalidateQueries({ queryKey: ['scenes'], refetchType: 'none' });
      Alert.alert(
        'Thumbnail',
        out.thumbnail_url ? 'Refreshed from the source page.' : 'Could not fetch a fresh thumbnail.',
      );
    },
    onError: (e) => Alert.alert('Thumbnail', e instanceof Error ? e.message : String(e)),
  });

  // Stan ładowania hero-miniatury: 'ok' (załadowała się — domyślnie, brak migotania
  // przycisku), 'none' (brak URL), 'broken' (onError). Steruje widocznością przycisku
  // Refresh. Reset przy zmianie sceny.
  const [thumbState, setThumbState] = React.useState<'none' | 'ok' | 'broken'>('ok');
  React.useEffect(() => {
    setThumbState('ok');
  }, [id]);

  // Auto-enrich tags: search-only scrapery nie pobierają tagów z detail page.
  // Pornhat ma `js-ajax-tag` data-setup JSON; xhamster/xvideos/youporn/inne mają
  // dedicated tag_extract patterny. SceneDetail wywołuje 1 fetch → upsert do DB.
  const enrichTagsMutation = useMutation({
    mutationFn: () => client.enrichSceneTags(id),
    onSuccess: (out) => {
      if (out.added > 0) queryClient.invalidateQueries({ queryKey: ['scene', id] });
    },
  });
  React.useEffect(() => {
    if (!data) return;
    if ((data.tags?.length ?? 0) > 0) return;
    const hasTubeSource = data.playback_sources.some((s) => s.origin?.startsWith('tube:'));
    if (!hasTubeSource) return;
    if (enrichTagsMutation.isPending || enrichTagsMutation.isSuccess) return;
    enrichTagsMutation.mutate();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data?.id]);

  // Auto-enrich studio: pornhat ma `js-ajax-dvd` data-setup (DDF/Adult Time/...).
  // Inne tube'y bez handlera — endpoint zwraca null bez modyfikacji.
  const enrichStudioMutation = useMutation({
    mutationFn: () => client.enrichSceneStudio(id),
    onSuccess: (out) => {
      if (out.studio_id) queryClient.invalidateQueries({ queryKey: ['scene', id] });
    },
  });
  React.useEffect(() => {
    if (!data) return;
    if (data.studio) return;
    const hasPornhat = data.playback_sources.some((s) => s.origin === 'tube:pornhatcom');
    if (!hasPornhat) return;
    if (enrichStudioMutation.isPending || enrichStudioMutation.isSuccess) return;
    enrichStudioMutation.mutate();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data?.id]);

  // Long-press tag → confirm → DELETE relację scene_tag (tag zostaje w słowniku,
  // ale scena go nie ma). User-curated odśmiecanie błędnych tagów.
  const removeTagMutation = useMutation({
    mutationFn: (tagId: string) => client.removeTagFromScene(id, tagId),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['scene', id] }),
    onError: (e) => Alert.alert('Failed', e instanceof Error ? e.message : String(e)),
  });
  // Long-press performer pill → DELETE relację scene_performer. Analogicznie
  // do tagów: false-match fuzzy (np. "anna bella" trafiona pod "Bad Bella" przez
  // alias "Bella") usuwany ręcznie z konkretnej sceny — performerka zostaje w
  // słowniku, tylko ta scena ją odpina (zgłoszenia #03e8044a, #12b8c122).
  const removePerformerMutation = useMutation({
    mutationFn: (perfId: string) => client.removePerformerFromScene(id, perfId),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['scene', id] }),
    onError: (e) => Alert.alert('Failed', e instanceof Error ? e.message : String(e)),
  });
  const onPerformerLongPress = (perfId: string, perfName: string) => {
    Alert.alert(
      'Remove performer',
      `Remove "${perfName}" from this scene?`,
      [
        { text: 'Cancel', style: 'cancel' },
        {
          text: 'Remove',
          style: 'destructive',
          onPress: () => removePerformerMutation.mutate(perfId),
        },
      ],
    );
  };

  const onTagLongPress = (tagId: string, tagName: string) => {
    Alert.alert(
      'Remove tag',
      `Remove tag "${tagName}" from this scene?`,
      [
        { text: 'Cancel', style: 'cancel' },
        {
          text: 'Remove',
          style: 'destructive',
          onPress: () => removeTagMutation.mutate(tagId),
        },
      ],
    );
  };

  React.useLayoutEffect(() => {
    nav.setOptions({
      headerRight: () => (
        <Pressable onPress={() => favMutation.mutate()} hitSlop={12} disabled={favMutation.isPending}>
          <Text style={{ color: isFav ? theme.accent : theme.muted, fontSize: 22 }}>
            {isFav ? '★' : '☆'}
          </Text>
        </Pressable>
      ),
    });
  }, [nav, isFav, favMutation]);

  if (isLoading) return <ActivityIndicator style={{ flex: 1, backgroundColor: theme.bg }} color={theme.fg} />;
  if (error instanceof Error) return <Text style={styles.error}>{error.message}</Text>;
  if (!data) return null;

  return (
    <ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
      <Hero data={data} onThumbState={setThumbState} />

      <View style={styles.body}>
        {/* Refresh pokazujemy gdy wyświetlana miniatura faktycznie się NIE ładuje
            (brak URL lub obraz padł — onError), nie tylko po obecności URL. Łączy
            feedback 26c114ed (nie na każdej scenie z dobrą miniaturą) i ef0c6a5a
            (thumbnail_url bywa obecny, ale obraz 404 → user widzi puste pole). */}
        {data.playback_sources.some((s) => s.origin?.startsWith('tube:')) &&
        thumbState !== 'ok' ? (
          <Pressable
            style={styles.refreshThumbBtn}
            onPress={() => refreshThumbMutation.mutate()}
            disabled={refreshThumbMutation.isPending}
          >
            <Text style={styles.refreshThumbText}>
              {refreshThumbMutation.isPending ? 'Refreshing thumbnail…' : '↻ Refresh thumbnail'}
            </Text>
          </Pressable>
        ) : null}

        {(data.code || data.director) && (
          <View style={styles.metaRow}>
            {data.code ? <Text style={styles.metaDim}>code · {data.code}</Text> : null}
            {data.director ? <Text style={styles.metaDim}>dir · {data.director}</Text> : null}
          </View>
        )}

        {data.performers.length > 0 && (
          <Section title="Performers">
            <View style={styles.pillRow}>
              {data.performers.map((p) => (
                <Pressable
                  key={p.id}
                  style={styles.pillPrimary}
                  onPress={() =>
                    nav.push('PerformerScenes', { id: p.id, name: p.canonical_name })
                  }
                  onLongPress={() => onPerformerLongPress(p.id, p.canonical_name)}
                  delayLongPress={400}
                >
                  <Text style={styles.pillPrimaryText}>
                    {p.canonical_name}
                    {p.as_alias ? ` (as ${p.as_alias})` : ''}
                  </Text>
                </Pressable>
              ))}
            </View>
            <Text style={styles.tagHint}>long-press to remove from this scene</Text>
          </Section>
        )}

        {data.studio && (
          <Section title="Studio">
            <View style={styles.pillRow}>
              <Pressable
                style={styles.pillPrimary}
                onPress={() =>
                  nav.push('StudioScenes', {
                    id: data.studio!.slug,
                    studioId: data.studio!.id,
                    name: data.studio!.name,
                  })
                }
              >
                <Text style={styles.pillPrimaryText}>
                  {data.studio.name}
                  {data.studio.network ? ` · ${data.studio.network}` : ''}
                </Text>
              </Pressable>
            </View>
          </Section>
        )}

        {data.tags.length > 0 && (
          <Section title="Tags">
            <View style={styles.pillRow}>
              {data.tags.map((t) => (
                <Pressable
                  key={t.id}
                  style={styles.pillSecondary}
                  onPress={() => nav.push('TagScenes', { slug: t.slug, name: t.name })}
                  onLongPress={() => onTagLongPress(t.id, t.name)}
                  delayLongPress={500}
                >
                  <Text style={styles.pillSecondaryText}>{t.name}</Text>
                </Pressable>
              ))}
            </View>
            <Text style={styles.tagHint}>long-press to remove from this scene</Text>
          </Section>
        )}

        {data.external_refs.length > 0 && (
          <Section title="Sources">
            <View style={styles.pillRow}>
              {(() => {
                // Sceny popularne mogą mieć dziesiątki refów na ten sam tube (różne URL,
                // np. /search/.../ + /tag/... + /performer/... — wszystkie dla tej samej sceny).
                // Deduplikujemy po source name, pokazujemy count jeśli > 1.
                const counts = new Map<string, number>();
                for (const r of data.external_refs) {
                  counts.set(r.source, (counts.get(r.source) ?? 0) + 1);
                }
                return Array.from(counts.entries()).map(([source, count]) => (
                  <View key={source} style={styles.pillSource}>
                    <Text style={styles.pillSourceText}>
                      {source}{count > 1 ? ` ×${count}` : ''}
                    </Text>
                  </View>
                ));
              })()}
            </View>
          </Section>
        )}

        {data.playback_sources.length > 0 && (
          <Section title="Watch">
            {data.playback_sources.map((p) => (
              <PlaybackButton
                key={p.id}
                sceneId={data.id}
                sceneDurationSec={data.duration_sec ?? null}
                source={p}
              />
            ))}
            <Text style={styles.tagHint}>long-press: open in browser / mark as broken</Text>
          </Section>
        )}

        {data.description ? (
          <Section title="Description">
            <Text style={styles.description}>{data.description}</Text>
          </Section>
        ) : null}
      </View>
    </ScrollView>
  );
}

function Hero({
  data,
  onThumbState,
}: {
  data: SceneOut;
  onThumbState?: (s: 'none' | 'ok' | 'broken') => void;
}) {
  const { width } = useWindowDimensions();
  const heroHeight = Math.round(width * (9 / 16));

  const animatedUrl = data.playback_sources.find((s) => s.animated_thumbnail_url)
    ?.animated_thumbnail_url;
  const staticUrl = data.playback_sources.find((s) => s.thumbnail_url)?.thumbnail_url;
  const url = animatedUrl ?? staticUrl;

  React.useEffect(() => {
    if (!url) onThumbState?.('none');
  }, [url, onThumbState]);

  const minutes = data.duration_sec ? Math.floor(data.duration_sec / 60) : null;
  const meta = [
    data.release_date ?? null,
    data.studio?.name ?? null,
    minutes !== null ? `${minutes} min` : null,
  ]
    .filter(Boolean)
    .join('  ·  ');

  if (!url) {
    return (
      <View style={[styles.heroFallback, { minHeight: heroHeight * 0.55 }]}>
        <Text style={styles.heroTitle} numberOfLines={3}>
          {data.title}
        </Text>
        {meta ? <Text style={styles.heroMeta}>{meta}</Text> : null}
      </View>
    );
  }

  return (
    <View style={[styles.hero, { height: heroHeight }]}>
      <Image
        source={{ uri: url }}
        style={StyleSheet.absoluteFill}
        contentFit="cover"
        transition={200}
        onLoad={() => onThumbState?.('ok')}
        onError={() => onThumbState?.('broken')}
      />
      <LinearGradient
        colors={['transparent', 'rgba(8,9,15,0.55)', theme.bg]}
        locations={[0, 0.55, 1]}
        style={StyleSheet.absoluteFill}
        pointerEvents="none"
      />
      <View style={styles.heroOverlay}>
        <Text style={styles.heroTitle} numberOfLines={3}>
          {data.title}
        </Text>
        {meta ? <Text style={styles.heroMeta}>{meta}</Text> : null}
      </View>
    </View>
  );
}

function Section({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <View style={styles.section}>
      <Text style={styles.sectionTitle}>{title}</Text>
      {children}
    </View>
  );
}

function guessVideoMime(url: string): string {
  const path = url.split('?')[0].toLowerCase();
  if (path.endsWith('.m3u8')) return 'application/vnd.apple.mpegurl';
  if (path.endsWith('.mpd')) return 'application/dash+xml';
  if (path.endsWith('.webm')) return 'video/webm';
  if (path.endsWith('.mkv')) return 'video/x-matroska';
  if (path.endsWith('.mp4')) return 'video/mp4';
  return 'video/*';
}

function PlaybackButton({
  sceneId,
  sceneDurationSec,
  source,
}: {
  sceneId: string;
  sceneDurationSec: number | null;
  source: PlaybackSource;
}) {
  const client = useClient();
  const queryClient = useQueryClient();
  const nav = useNavigation<NativeStackNavigationProp<RootStackParamList, 'SceneDetail'>>();
  const [resolving, setResolving] = React.useState(false);
  const [qualityLinks, setQualityLinks] = React.useState<StreamLink[] | null>(null);
  // Tube origin format: 'tube:hqpornercom' (nowy) lub 'pornapp:hqpornercom' (legacy
  // sprzed migracji 0011) — backend rozumie oba prefixy.
  const isTube = source.origin.startsWith('tube:') || source.origin.startsWith('pornapp:');

  // Watch tracking: każde uruchomienie playera/browsera oznaczamy jako "started"
  // (position_sec=0 jeśli pierwszy raz; nie ruszamy jeśli już była progres).
  // Continue watching rail i `last_played_at` na home są zasilane stąd.
  const markStarted = React.useCallback(() => {
    client
      .upsertProgress(sceneId, {
        position_sec: 0,
        duration_sec: sceneDurationSec ?? undefined,
      })
      .then(() => {
        queryClient.invalidateQueries({ queryKey: ['watch-recent'] });
      })
      .catch(() => {
        // best-effort, nie blokujemy odtwarzania
      });
  }, [client, sceneId, sceneDurationSec, queryClient]);
  // origin format: 'tube:hqpornercom' → display 'hqpornercom'
  const label = source.origin.includes(':') ? source.origin.split(':').slice(1).join(':') : source.origin;

  const openUrl = async (url: string) => {
    const ok = await Linking.canOpenURL(url);
    if (!ok) {
      Alert.alert('Cannot open', url);
      return;
    }
    await Linking.openURL(url);
  };

  // Inline player (expo-video / ExoPlayer) — odtwarza w tej samej apce, pasuje headery
  // (Referer/UA) potrzebne dla CDN-ów które blokują requesty bez właściwego origin'u
  // (hqporner/iceyfile/itd.).
  // Refererem jest origin z PlaybackSource.page_url (np. "hqporner.com").
  // `fallbackEmbedUrl` jest używany przez Player gdy native ExoPlayer dostanie błąd
  // (typowe dla IP-bound CDN URL'i z luluvids/iceyfile).
  const openAsVideo = async (
    link: StreamLink,
    fallbackEmbedUrl?: string,
  ) => {
    let refererHost: string | undefined;
    try {
      refererHost = new URL(source.page_url).hostname;
    } catch {
      // ignore
    }
    // Preferuj direct CDN URL (0 VPS bandwidth) → fallback proxy URL (jeśli direct
    // fails). Backend dostarcza oba w StreamLink. Headers tylko dla direct path.
    let initialUrl = link.direct_url || link.stream_url!;
    const isDirect = !!link.direct_url && initialUrl === link.direct_url;

    // hdporn.gg/fullmovies.xxx: direct_url to `.../get_file/...` które 302-redirectuje
    // na fpvcdn z IP fetchera. ExoPlayer wywala się na tym cross-domain redirekcie →
    // fallback proxy → „mignięcie". Resolvujemy redirect TU (na telefonie → fpvcdn z IP
    // telefonu) i podajemy ExoPlayerowi finalny URL — bez błędu/migotania/proxy. Fail =
    // gramy oryginał (obecne zachowanie z fallbackiem). ~100-300ms (get_file 302 szybki).
    // hdporn.gg/fullmovies.xxx: backendowy get_file jest zbindowany do IP VPS (page-loader)
    // → telefon dostaje 410 → fallback proxy (mignięcie). Re-fetchujemy STRONĘ na telefonie
    // (phone-bound get_file) → fpvcdn z IP telefonu → ExoPlayer gra direct (bez proxy/migotania).
    if (isDirect && isGetFileUrl(initialUrl)) {
      const ref = link.headers?.Referer || (refererHost ? `https://${refererHost}/` : undefined);
      const resolved = await resolveGetFilePage(source.page_url, link.quality, ref);
      if (resolved) initialUrl = resolved;
    }

    // IP-bound tuby resolwowane phone-side: przekaż page_url, by Player mógł
    // re-resolve'ować świeży token gdy native padnie na initial-load (zmiana IP / TTL).
    const PHONE_RESOLVE_ORIGINS = ['tube:sxyprncom', 'tube:epornercom', 'tube:fpoxxx'];
    nav.navigate('Player', {
      url: initialUrl,
      sceneId,
      origin: source.origin,
      resolvePageUrl: PHONE_RESOLVE_ORIGINS.includes(source.origin) ? source.page_url : undefined,
      playbackId: source.id,
      durationSec: sceneDurationSec,
      refererHost,
      title: source.origin,
      headers: isDirect && link.headers ? link.headers : undefined,
      fallbackProxyUrl: isDirect ? link.stream_url || undefined : undefined,
      fallbackEmbedUrl,
    });
  };

  const onPress = async () => {
    // Non-tube origin: brak własnego extractora (np. paradisehill bez resolve), otwieramy
    // page/embed/stream w przeglądarce.
    if (!isTube) {
      try {
        markStarted();
        await openUrl(source.stream_url || source.embed_url || source.page_url);
      } catch (e) {
        Alert.alert('Failed to open', e instanceof Error ? e.message : String(e));
      }
      return;
    }

    // pornxp.ph: CDN token IP-bound (backend 403 cross-IP) → backend oddaje WebView
    // fallback który czarno-ekranił (bug-report 2026-06-07, fd06cd86). Telefon sam
    // pobiera stronę (phone-IP-bound mp4) → natywne multi-quality, zero WebView/reklam.
    if (source.origin === 'tube:pornxpph') {
      setResolving(true);
      try {
        const links = await resolvePornxpPage(source.page_url);
        if (links.length > 0) {
          markStarted();
          if (links.length === 1) await openAsVideo(links[0], source.page_url);
          else setQualityLinks(links);
          return;
        }
        // pusto → spadnij na backend resolve (WebView) poniżej
      } catch {
        // ignore → backend fallback
      } finally {
        setResolving(false);
      }
    }

    // sxyprn / eporner: CDN token IP-bound do tego KTO POBRAŁ STRONĘ (audit 2026-06-11).
    // Backend resolvuje z VPS → telefon dostaje URL bound do IP VPS → direct daje 403 /
    // 10B placeholder → fallback na proxy → CAŁE wideo przez Hetzner. Telefon sam pobiera
    // stronę (phone IP) → token bound do telefonu → gra direct, zero VPS. [] → backend niżej.
    const phoneResolver =
      source.origin === 'tube:sxyprncom'
        ? resolveSxyprnPage
        : source.origin === 'tube:epornercom'
          ? resolveEpornerPage
          : source.origin === 'tube:fpoxxx'
            ? resolveFpoxxxPage
            : null;
    if (phoneResolver) {
      setResolving(true);
      try {
        const links = await phoneResolver(source.page_url);
        if (links.length > 0) {
          markStarted();
          if (links.length === 1) await openAsVideo(links[0], source.page_url);
          else setQualityLinks(links);
          return;
        }
        // pusto → spadnij na backend resolve poniżej (HosterDead → mark dead dla
        // skasowanego sxyprn posta; backend i tak ma proxy fallback).
      } catch {
        // ignore → backend fallback
      } finally {
        setResolving(false);
      }
    }

    // Tube origin: backend resolve → lista linków: część direct video (stream_url),
    // część hoster embed (embed_url, np. StreamWish/doodporn HTML page).
    //   - direct → MX Player (intent video/*)
    //   - embed-only → browser (HTML page nie odtworzy się w MX, MX wywaliłby błąd)
    setResolving(true);
    try {
      const res = await client.resolvePlayback(sceneId, source.id);
      const all = res.links || [];
      // Dedup po URL — porn-app czasem dubluje ten sam link z różnym quality string.
      const seen = new Set<string>();
      const unique = all.filter((l) => {
        const key = l.stream_url || l.embed_url || '';
        if (!key || seen.has(key)) return false;
        seen.add(key);
        return true;
      });
      const directLinks = unique.filter((l) => !!l.stream_url);
      const embedLinks = unique.filter((l) => !l.stream_url && !!l.embed_url);

      if (directLinks.length === 0 && embedLinks.length === 0) {
        Alert.alert('No stream', 'porn-app did not return any stream URL — falling back to the page.');
        await openUrl(source.page_url);
        return;
      }

      // Brak direct video → otwórz embed w naszym WebView playerze. WebView (Chrome
      // engine) wpuszczany jest przez WAF/JA3-fingerprint hostów (xtremestream,
      // playmogo, streamtape) które blokują direct fetch z native playera.
      // Player JS hostera może też ujawnić finalny m3u8 — auto-extract w PlayerScreen
      // pokaże banner "Otwórz w native playerze".
      if (directLinks.length === 0) {
        const target = res.best?.embed_url || embedLinks[0].embed_url!;
        let refererHost: string | undefined;
        try {
          refererHost = new URL(source.page_url).hostname;
        } catch {
          // ignore
        }
        markStarted();
        nav.navigate('Player', {
          url: target,
          sceneId,
          origin: source.origin,
          playbackId: source.id,
          durationSec: sceneDurationSec,
          refererHost,
          title: source.origin,
          mode: 'webview',
        });
        return;
      }

      // Fallback embed do Playera — gdy ExoPlayer dostanie 403 (IP-bound CDN), apka
      // przełączy się na WebView z tym embed URL'em. Bierzemy pierwszy embed jaki
      // jest, niezależnie od kolejności w `links`.
      const fallbackEmbedUrl = embedLinks[0]?.embed_url || res.best?.embed_url || undefined;

      if (directLinks.length === 1) {
        markStarted();
        await openAsVideo(directLinks[0], fallbackEmbedUrl);
        return;
      }
      setQualityLinks(directLinks);
    } catch (e) {
      // 410 = backend zaznaczył link jako PERMANENT dead (HosterDead exception
      // lub tube page 404/410). Refresh sceny żeby ten button zniknął z listy.
      if (e instanceof ApiError && e.status === 410) {
        Alert.alert(
          'Dead link',
          'The tube removed this video. We marked the source, you won\'t see it again.',
        );
        queryClient.invalidateQueries({ queryKey: ['scene', sceneId] });
        queryClient.invalidateQueries({ queryKey: ['scenes'], refetchType: 'none' });
        return;
      }
      // 503 = transient extraction failure (hoster chwilowo niedostępny, network
      // glitch, ad-iframe race condition). NIE oznaczamy źródła jako dead —
      // następne kliknięcie pewnie zadziała.
      if (e instanceof ApiError && e.status === 503) {
        Alert.alert(
          'Try again',
          'Temporary problem with the host. Tap Play again in a moment.',
        );
        return;
      }
      Alert.alert(
        'Resolve failed',
        (e instanceof Error ? e.message : String(e)) + '\n\nFallback: opening source page.',
      );
      try {
        await openUrl(source.page_url);
      } catch {
        // pomijamy — alert już był
      }
    } finally {
      setResolving(false);
    }
  };

  const onSelectQuality = async (link: StreamLink) => {
    setQualityLinks(null);
    try {
      markStarted();
      if (link.stream_url) {
        // Wybór z dropdownu jakości — fallback na inny embed gdyby ten konkretny
        // direct link dał 403. `qualityLinks` zawiera tylko stream_url-e, więc
        // szukamy embed'a w `links` na resolve response wcześniej; tu już go nie
        // mamy, ale to kompromis (rzadki przypadek multi-quality + dead direct).
        await openAsVideo(link);
      } else if (link.embed_url) {
        let refererHost: string | undefined;
        try {
          refererHost = new URL(source.page_url).hostname;
        } catch {
          // ignore
        }
        nav.navigate('Player', {
          url: link.embed_url,
          sceneId,
          origin: source.origin,
          playbackId: source.id,
          durationSec: sceneDurationSec,
          refererHost,
          title: source.origin,
          mode: 'webview',
        });
      }
    } catch (e) {
      Alert.alert('Failed to open', e instanceof Error ? e.message : String(e));
    }
  };

  const action = isTube ? (resolving ? 'resolving…' : 'pick quality & play') : 'open in browser';

  // Long-press → action sheet z 3 opcjami. Bug-reports 2026-05-12/2026-05-20:
  // (1) user chce móc usunąć złe linki (xhamster wrong-video/eporner wrong-scene),
  // (2) user chce diagnostyki — otworzyć page_url w browser (zobaczyć HTML tube'a
  //     bez WebView playera), żeby rozsądnie ocenić czy hoster faktycznie broken.
  const onLongPress = () => {
    Alert.alert(
      label,
      'What do you want to do with this link?',
      [
        {
          text: 'Open in browser (diagnostics)',
          onPress: async () => {
            try {
              const url = source.page_url || source.embed_url || source.stream_url;
              if (url) {
                await Linking.openURL(url);
              } else {
                Alert.alert('No URL', 'This playback has no page_url to open.');
              }
            } catch (e) {
              Alert.alert('Could not open', e instanceof Error ? e.message : String(e));
            }
          },
        },
        {
          text: 'Mark as invalid',
          style: 'destructive',
          onPress: async () => {
            try {
              await client.markPlaybackDead(sceneId, source.id);
              queryClient.invalidateQueries({ queryKey: ['scene', sceneId] });
              queryClient.invalidateQueries({ queryKey: ['scenes'], refetchType: 'none' });
            } catch (e) {
              Alert.alert('Failed', e instanceof Error ? e.message : String(e));
            }
          },
        },
        { text: 'Cancel', style: 'cancel' },
      ],
    );
  };

  return (
    <>
      <Pressable
        style={({ pressed }) => [
          styles.watchButton,
          pressed && styles.watchButtonPressed,
        ]}
        onPress={onPress}
        onLongPress={onLongPress}
        delayLongPress={500}
        disabled={resolving}
      >
        <Text style={styles.watchLabel}>{label}</Text>
        <Text style={styles.watchMeta}>
          {source.quality ? `${source.quality} · ` : ''}
          {source.duration_sec ? `${Math.floor(source.duration_sec / 60)}m · ` : ''}
          {action}
        </Text>
      </Pressable>
      <PlaybackQualityModal
        visible={qualityLinks !== null}
        links={qualityLinks ?? []}
        onSelect={onSelectQuality}
        onCancel={() => setQualityLinks(null)}
      />
    </>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: theme.bg },
  scrollContent: { paddingBottom: 32 },

  hero: {
    width: '100%',
    backgroundColor: theme.card,
    overflow: 'hidden',
  },
  heroOverlay: {
    position: 'absolute',
    left: 16,
    right: 16,
    bottom: 12,
  },
  heroFallback: {
    paddingHorizontal: 16,
    paddingTop: 24,
    paddingBottom: 16,
    backgroundColor: theme.bgElevated,
    justifyContent: 'flex-end',
  },
  heroTitle: {
    color: theme.fg,
    fontSize: 24,
    fontWeight: '800',
    letterSpacing: -0.3,
    textShadowColor: 'rgba(0,0,0,0.6)',
    textShadowOffset: { width: 0, height: 1 },
    textShadowRadius: 4,
  },
  heroMeta: {
    color: theme.muted,
    fontSize: 13,
    marginTop: 6,
    fontWeight: '500',
  },

  body: { paddingHorizontal: 16, paddingTop: 8 },
  metaRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 14, marginTop: 8 },
  metaDim: { color: theme.mutedDim, fontSize: 12, fontWeight: '500' },

  section: { marginTop: 20 },
  sectionTitle: {
    color: theme.mutedDim,
    fontSize: 11,
    textTransform: 'uppercase',
    marginBottom: 10,
    letterSpacing: 1.2,
    fontWeight: '700',
  },

  pillRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },

  pillPrimary: {
    borderColor: theme.accent,
    borderWidth: 1,
    backgroundColor: `${theme.accent}1A`,
    borderRadius: 16,
    paddingHorizontal: 12,
    paddingVertical: 6,
  },
  pillPrimaryText: {
    color: theme.accentGlow,
    fontSize: 13,
    fontWeight: '600',
  },

  pillSecondary: {
    borderColor: theme.border,
    borderWidth: 1,
    borderRadius: 14,
    paddingHorizontal: 10,
    paddingVertical: 5,
  },
  pillSecondaryText: {
    color: theme.muted,
    fontSize: 13,
  },
  tagHint: { color: theme.mutedDim, fontSize: 11, marginTop: 6 },
  refreshThumbBtn: {
    alignSelf: 'flex-start',
    paddingVertical: 6,
    paddingHorizontal: 12,
    borderRadius: 8,
    borderWidth: 1,
    borderColor: theme.border,
    backgroundColor: theme.card,
    marginBottom: 12,
  },
  refreshThumbText: { color: theme.muted, fontSize: 13, fontWeight: '600' },

  pillSource: {
    borderColor: theme.accent,
    borderWidth: 1,
    borderRadius: 14,
    paddingHorizontal: 10,
    paddingVertical: 4,
  },
  pillSourceText: {
    color: theme.accent,
    fontSize: 11,
    fontWeight: '700',
    textTransform: 'uppercase',
    letterSpacing: 0.6,
  },

  description: { color: theme.fg, fontSize: 14, lineHeight: 21 },
  error: { color: theme.bad, padding: 16 },

  watchButton: {
    backgroundColor: theme.bgElevated,
    borderColor: theme.accent,
    borderWidth: 1.5,
    borderRadius: 12,
    padding: 14,
    marginBottom: 10,
    shadowColor: theme.accent,
    shadowOffset: { width: 0, height: 0 },
    shadowOpacity: 0.25,
    shadowRadius: 8,
    elevation: 3,
  },
  watchButtonPressed: {
    backgroundColor: `${theme.accent}26`,
    borderColor: theme.accentGlow,
  },
  watchLabel: { color: theme.accentGlow, fontWeight: '700', fontSize: 15 },
  watchMeta: { color: theme.muted, fontSize: 12, marginTop: 4 },
});
