// MUST be the first import for react-navigation 7 + reanimated/gesture-handler
// in release builds. Otherwise Hermes-bundled APK boots to a white screen.
import 'react-native-gesture-handler';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import * as Sentry from '@sentry/react-native';
import Constants from 'expo-constants';
import { registerRootComponent } from 'expo';
import { useFonts } from 'expo-font';
import { Text as RNText } from 'react-native';
import * as ScreenCapture from 'expo-screen-capture';
import { StatusBar } from 'expo-status-bar';
import * as Updates from 'expo-updates';
import {
  apkInstallerAvailable,
  canRequestInstall,
  installApk,
  openInstallPermissionSettings,
} from './src/native/apkInstaller';
import React, { useEffect, useRef, useState } from 'react';
import { ActivityIndicator, Alert, AppState, AppStateStatus, Linking, View } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { GoonClient } from './src/api';
import { ClientProvider } from './src/ClientContext';
import { PreferencesProvider } from './src/PreferencesContext';
import { SceneActionsProvider } from './src/SceneActionsContext';
import { OnboardingModal } from './src/components/OnboardingModal';
import { WhatsNewModal } from './src/components/WhatsNewModal';
import { ErrorBoundary } from './src/ErrorBoundary';
import { isAccepted as isAgeGateAccepted } from './src/lib/agegate';
import { APP_VERSION } from './src/lib/appVersion';
import { DEFAULT_API_KEY, DEFAULT_BACKEND_URL } from './src/lib/backend';
import { getSettings as getLockSettings } from './src/lib/applock';
import { AppNavigator } from './src/navigation';
import { AgeGateScreen } from './src/screens/AgeGateScreen';
import { AppLockScreen } from './src/screens/AppLockScreen';
import { LoginScreen } from './src/screens/LoginScreen';
import { clearCredentials, isLegacyAdopted, loadCredentials, markLegacyAdopted } from './src/storage';
import { theme } from './src/theme';

// Sentry: init przed registerRootComponent. Pusty DSN → SDK no-op (devel build bez
// crash reportingu). DSN czytamy z `EXPO_PUBLIC_SENTRY_DSN` (Expo SDK 49+ auto-injectuje
// env var do bundla przy buildzie), z fallback na app.json `extra.sentryDsn` (legacy).
// Lokalnie: ustaw w `mobile/.env` (gitignored). CI: GitHub Secret `SENTRY_DSN` →
// workflow exportuje jako `EXPO_PUBLIC_SENTRY_DSN` przed gradle build.
const SENTRY_DSN =
  process.env.EXPO_PUBLIC_SENTRY_DSN ||
  (Constants.expoConfig?.extra?.sentryDsn as string | undefined);
const SENTRY_ENV =
  process.env.EXPO_PUBLIC_SENTRY_ENVIRONMENT ||
  (Constants.expoConfig?.extra?.sentryEnvironment as string | undefined) ||
  'production';
if (SENTRY_DSN) {
  Sentry.init({
    dsn: SENTRY_DSN,
    environment: SENTRY_ENV,
    // 0.1 = 10% sesji ma full performance trace. Free tier ma 10k transactions/mies,
    // przy 1 użytkowniku to wieczność. Errory są zawsze 100%.
    tracesSampleRate: 0.1,
    // Lokalna wersja appki — dystyngwuje builds w Sentry release filter. expo-constants
    // czyta `version` z `app.json` (a nie z package.json), więc tutaj `expoConfig.version`.
    release: `goon-mobile@${Constants.expoConfig?.version ?? '0.0.0'}`,
    // Filtr szumu: Android Background ANR (łapany przez AppExitInfo) to OS zamrażający
    // apkę w TLE i raportujący "nie odpowiada" — zero ramek JS/app, nic do naprawy
    // (GOON-1D). Dropujemy TYLKO background; foreground ANR zostaje (realny jank).
    beforeSend: (event) => {
      const isAnr = event.exception?.values?.some(
        (v) => v?.type === 'ApplicationNotResponding' || /\bANR\b/.test(v?.value || ''),
      );
      if (isAnr && event.contexts?.app?.in_foreground === false) return null;
      return event;
    },
    // Boot diagnostic: jeden message przy starcie z tagiem `source:boot` pozwala
    // potwierdzić że SDK rzeczywiście wysyła. Jeśli w Sentry nie ma go po starcie
    // appki → init nie startuje albo zaprzeszł blockera (network/DNS/uplink).
    // sendDefaultPii=false (default) — IP / user-agent / cookies nie idą do Sentry.
    // attachScreenshot=false — scena thumbnails / video frames nie wyciekną w error reports.
  });
  // Boot breadcrumb (NIE event). Wcześniej `captureMessage('mobile boot OK', level:info)`
  // tworzył osobny event przy KAŻDYM starcie → 171 eventów / 13 userów szumu w dashboardzie
  // (GOON-Q). Diagnostyk spełnił rolę — wiemy że SDK wysyła. Breadcrumb zostawia kontekst
  // bootu doczepiony do realnych błędów, ale nie zaśmieca listy issue.
  Sentry.addBreadcrumb({
    category: 'boot',
    message: 'mobile boot OK',
    level: 'info',
  });
}

const queryClient = new QueryClient({
  defaultOptions: {
    queries: { retry: 1, refetchOnWindowFocus: false, staleTime: 30_000 },
  },
});

// Globalny default fontu dla całego <Text> — General Sans Regular jako body.
// Komponenty które chcą display/mono nadpisują fontFamily jawnie (bo RN nie
// syntezuje weightów dla custom fontów). Bold-ale-bez-fontFamily tekst zostanie
// Regular weightem General Sans — wciąż distinctive face, akceptowalne dla
// nietkniętych ekranów; high-traffic komponenty mają jawny Semibold.
let _textDefaultApplied = false;
function applyDefaultFont() {
  if (_textDefaultApplied) return;
  _textDefaultApplied = true;
  const T = RNText as unknown as { defaultProps?: { style?: unknown } };
  T.defaultProps = T.defaultProps || {};
  const prev = T.defaultProps.style;
  T.defaultProps.style = [{ fontFamily: 'GeneralSans-Regular' }, prev].filter(Boolean);
}

export default function App() {
  const [fontsLoaded] = useFonts({
    'GeneralSans-Regular': require('./assets/fonts/GeneralSans-Regular.ttf'),
    'GeneralSans-Medium': require('./assets/fonts/GeneralSans-Medium.ttf'),
    'GeneralSans-Semibold': require('./assets/fonts/GeneralSans-Semibold.ttf'),
    'GeistMono-Regular': require('./assets/fonts/GeistMono-Regular.ttf'),
  });
  if (fontsLoaded) applyDefaultFont();

  const [hydrated, setHydrated] = useState(false);
  const [ageAccepted, setAgeAccepted] = useState(false);
  const [client, setClient] = useState<GoonClient | null>(null);
  const [bootError, setBootError] = useState<string | null>(null);
  const [locked, setLocked] = useState(false);
  const [lockReady, setLockReady] = useState(false);
  // OTA-check settled — gate'uje ekran PIN, żeby ewentualny reloadAsync (silent OTA)
  // wydarzył się PRZED pytaniem o PIN, nie po (inaczej "PIN ×2", user-report mobilism).
  const [updateSettled, setUpdateSettled] = useState(false);
  const backgroundedAt = useRef<number | null>(null);

  useEffect(() => {
    (async () => {
      try {
        const accepted = await isAgeGateAccepted();
        setAgeAccepted(accepted);
        const creds = await loadCredentials();
        if (creds) {
          setClient(new GoonClient(creds.baseUrl, creds.apiKey));
        } else {
          // No stored credentials → auto-connect to the public instance.
          // LoginScreen only appears after an explicit "Sign out".
          setClient(new GoonClient(DEFAULT_BACKEND_URL, DEFAULT_API_KEY));
        }
        const lockSettings = await getLockSettings();
        if (lockSettings.enabled && lockSettings.hasPin) {
          setLocked(true);
        }
      } catch (e) {
        // SecureStore may fail on devices without lockscreen / keychain quirks —
        // surface the error instead of silently white-screening.
        setBootError(e instanceof Error ? e.message : String(e));
      } finally {
        setHydrated(true);
        setLockReady(true);
      }
    })();
  }, []);

  // Jednorazowe przejęcie legacy stanu usera (favorites/progress/blacklisty sprzed
  // device-scopingu, bug 2026-06-08). Po pierwszym launchu z nowym bundlem przepinamy
  // legacy-shared rows na to urządzenie. Flaga w SecureStore → tylko raz. (Właściciel
  // instancji powinien zrelaunchować apkę jako pierwszy, żeby przejąć swoją historię.)
  useEffect(() => {
    if (!client) return;
    (async () => {
      try {
        if (await isLegacyAdopted()) return;
        await client.adoptLegacy();
        await markLegacyAdopted();
      } catch {
        // brak sieci / już przejęte — spróbuje przy następnym launchu (flaga nieustawiona)
      }
    })();
  }, [client]);

  // FLAG_SECURE — blocks screenshots and hides app preview from app switcher.
  // WŁĄCZONE 2026-06-16: realni userzy (mobilism) — NSFW treść nie ma wyciekać do
  // Recents/screenshotów (user feedback). Bundle OTA jest wspólny, więc to dotyczy
  // też emulatora — na czas debugu (adb screencap) flipnij tymczasowo na false
  // lokalnie i NIE publikuj tej zmiany.
  const SCREEN_CAPTURE_PROTECTION = true;
  useEffect(() => {
    if (!SCREEN_CAPTURE_PROTECTION) return;
    ScreenCapture.preventScreenCaptureAsync('goon-applock').catch(() => {});
    return () => {
      ScreenCapture.allowScreenCaptureAsync('goon-applock').catch(() => {});
    };
  }, []);

  // Update flow — DWUSTOPNIOWY:
  //   B) Expo Updates (silent JS bundle): jeśli backend ma nowszy bundle
  //      manifest dla naszego runtimeVersion, fetch + reload bez ANY dialogu.
  //      Większość naszych zmian (api.ts, screens, components) to JS — silent.
  //   A) APK install (PackageInstaller native): jeśli backend `/version` ma
  //      wersję ZNACZNIE wyższą niż embedded (native bump z nowym Kotlin/manifest),
  //      pokaż dialog → native installer (1-click "Install update?").
  // Fallback: gdy expo-updates SDK niedostępne (dev build, web), używamy starego
  // flow z Linking.openURL.
  const updateChecked = useRef(false);
  useEffect(() => {
    if (!client || updateChecked.current) return;
    updateChecked.current = true;
    // Safety: nie trzymaj "Checking for updates" w nieskończoność gdy OTA call wisi
    // (zła sieć). Po 15s odblokuj PIN — normalny check+fetch JS-bundla to ~1-3s.
    const safety = setTimeout(() => setUpdateSettled(true), 15000);
    (async () => {
      // Etap B: silent JS update
      if (Updates.isEnabled) {
        try {
          const upd = await Updates.checkForUpdateAsync();
          if (upd.isAvailable) {
            await Updates.fetchUpdateAsync();
            // Cichy restart — user zobaczy splash na 200ms i jest w nowej wersji.
            // Restart następuje TU (przed PIN-em), więc PIN pyta tylko raz po reloadzie.
            await Updates.reloadAsync();
            return; // reloadAsync nie wraca
          }
        } catch {
          // OTA fail (brak sieci / 204 / TLS pin mismatch) — leciemy do A.
        }
      }
      // Brak OTA (lub disabled/fail) → odblokuj ekran PIN/login/browse.
      clearTimeout(safety);
      setUpdateSettled(true);
      // Etap A: APK update prompt (native bump albo Expo Updates disabled)
      try {
        const out = await client.getServerVersion();
        const bundled = APP_VERSION;
        if (out.version && out.version !== bundled && out.apk_url) {
          // In-app installer dostępny → "Install" zamiast "Download"
          if (apkInstallerAvailable) {
            const pendingApkUrl = out.apk_url!;
            const runInstall = async () => {
              try {
                await installApk(pendingApkUrl);
                // PackageInstaller pokazuje natywny dialog — JS już nie ma co robić.
              } catch (e) {
                Alert.alert('Install failed', e instanceof Error ? e.message : String(e));
              }
            };
            const tryInstall = async () => {
              const granted = await canRequestInstall();
              if (granted) {
                await runInstall();
                return;
              }
              // Brak permission → wystaw Settings + zarejestruj AppState listener
              // który po powrocie do appki (granted=true) automatycznie odpali
              // installApk. Bez tego user musi zamknąć i otworzyć dialog Update
              // jeszcze raz po toggling permission — zgłaszane w bug-report 97adff93.
              const sub = AppState.addEventListener('change', async (next) => {
                if (next !== 'active') return;
                const now = await canRequestInstall();
                if (now) {
                  sub.remove();
                  await runInstall();
                }
              });
              // Safety timeout: zdejmij listener po 5 minutach jeśli user nie
              // grantnął (uniknie wycieku w przypadku Settings-canceled).
              setTimeout(() => sub.remove(), 5 * 60 * 1000);
              Alert.alert(
                'Install permission',
                'Allow installing updates from Goon? Toggle "Allow from this source", then tap back — install starts automatically.',
                [
                  { text: 'Cancel', style: 'cancel', onPress: () => sub.remove() },
                  { text: 'Open Settings', onPress: () => openInstallPermissionSettings() },
                ],
              );
            };
            Alert.alert(
              'Update available',
              `Server: ${out.version}\nApp: ${bundled}\n\nInstall update now?`,
              [
                { text: 'Later', style: 'cancel' },
                { text: 'Install', onPress: tryInstall },
              ],
            );
          } else {
            // Fallback: Chrome download
            Alert.alert(
              'Update available',
              `Server: ${out.version}\nApp: ${bundled}\n\nTap "Download" to install the latest APK.`,
              [
                { text: 'Later', style: 'cancel' },
                {
                  text: 'Download',
                  onPress: () => Linking.openURL(out.apk_url!).catch(() => {}),
                },
              ],
            );
          }
        }
      } catch {
        // best-effort
      }
    })();
  }, [client]);

  // AppState lock-on-background — re-lock if user backgrounded longer than timeout.
  useEffect(() => {
    const sub = AppState.addEventListener('change', async (next: AppStateStatus) => {
      if (next === 'background' || next === 'inactive') {
        backgroundedAt.current = Date.now();
        return;
      }
      if (next === 'active') {
        const at = backgroundedAt.current;
        backgroundedAt.current = null;
        if (at === null) return;
        const settings = await getLockSettings();
        if (!(settings.enabled && settings.hasPin)) return;
        const elapsed = Math.floor((Date.now() - at) / 1000);
        if (elapsed >= settings.timeoutSeconds) {
          setLocked(true);
        }
      }
    });
    return () => sub.remove();
  }, []);

  if (!fontsLoaded || !hydrated || !lockReady) {
    return (
      <View style={{ flex: 1, backgroundColor: theme.bg, justifyContent: 'center' }}>
        <ActivityIndicator color={theme.fg} />
      </View>
    );
  }

  // Age gate takes precedence over everything else — must be accepted at the
  // current ToS version before any UI (lock, login, browse) is reachable.
  if (!ageAccepted) {
    return (
      <GestureHandlerRootView style={{ flex: 1 }}>
        <SafeAreaProvider>
          <StatusBar style="light" />
          <AgeGateScreen onAccept={() => setAgeAccepted(true)} />
        </SafeAreaProvider>
      </GestureHandlerRootView>
    );
  }

  // PIN dopiero PO sprawdzeniu OTA — inaczej user wpisuje PIN, w tle ściąga się
  // update, reloadAsync restartuje i PIN pyta znowu ("PIN ×2", user-report mobilism).
  // Krótki "Checking for updates" zamiast od razu PIN; jeśli jest OTA, restart
  // nastąpi tu, przed PIN-em. Dotyczy tylko zablokowanych (bez locka → prosto do browse).
  if (locked && !updateSettled) {
    return (
      <GestureHandlerRootView style={{ flex: 1 }}>
        <SafeAreaProvider>
          <StatusBar style="light" />
          <View
            style={{
              flex: 1,
              backgroundColor: theme.bg,
              justifyContent: 'center',
              alignItems: 'center',
            }}
          >
            <ActivityIndicator color={theme.fg} />
            <RNText style={{ color: theme.muted, marginTop: 14, fontSize: 13 }}>
              Checking for updates…
            </RNText>
          </View>
        </SafeAreaProvider>
      </GestureHandlerRootView>
    );
  }

  // App lock takes precedence over everything (even login). Refresh settings every
  // unlock so a freshly-disabled lock doesn't bounce back next background.
  if (locked) {
    return (
      <GestureHandlerRootView style={{ flex: 1 }}>
        <SafeAreaProvider>
          <StatusBar style="light" />
          <AppLockScreen
            onUnlock={() => setLocked(false)}
            onLogout={async () => {
              await clearCredentials();
              setClient(null);
              queryClient.clear();
              setLocked(false);
            }}
          />
        </SafeAreaProvider>
      </GestureHandlerRootView>
    );
  }

  return (
    <ErrorBoundary>
      <GestureHandlerRootView style={{ flex: 1 }}>
        <SafeAreaProvider>
          <QueryClientProvider client={queryClient}>
            <StatusBar style="light" />
            {bootError ? (
              <View style={{ flex: 1, backgroundColor: theme.bg, padding: 20 }}>
                <View style={{ marginTop: 80 }}>
                  <ActivityIndicator color={theme.bad} />
                </View>
              </View>
            ) : null}
            {client ? (
              <ClientProvider client={client}>
                <PreferencesProvider>
                  <SceneActionsProvider>
                    <AppNavigator
                      client={client}
                      appVersion={APP_VERSION}
                      onLogout={async () => {
                        await clearCredentials();
                        setClient(null);
                        queryClient.clear();
                      }}
                    />
                    <WhatsNewModal />
                    <OnboardingModal />
                  </SceneActionsProvider>
                </PreferencesProvider>
              </ClientProvider>
            ) : (
              <LoginScreen
                onAuthenticated={(baseUrl, apiKey) => setClient(new GoonClient(baseUrl, apiKey))}
              />
            )}
          </QueryClientProvider>
        </SafeAreaProvider>
      </GestureHandlerRootView>
    </ErrorBoundary>
  );
}

// `package.json:main` points to this file directly, so we must register the
// root component ourselves (the standard `expo/AppEntry` boilerplate is bypassed).
// Without this line release builds load the bundle but throw
// `Invariant Violation: "main" has not been registered` and white-screen.
//
// Sentry.wrap() owija App w error boundary + adds touch event tracking — standardowy
// pattern z docs Sentry RN. No-op gdy DSN pusty.
registerRootComponent(SENTRY_DSN ? Sentry.wrap(App) : App);
