// 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 * 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 { ErrorBoundary } from './src/ErrorBoundary';
import { isAccepted as isAgeGateAccepted } from './src/lib/agegate';
import { APP_VERSION } from './src/lib/appVersion';
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, loadCredentials } 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'}`,
    // 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.
  });
  // Capture jednorazowo przy bundle load (przed `App` render). Sentry RN buforuje
  // events do flush przy next tick — wystartuje równo z bridge ready.
  Sentry.captureMessage('mobile boot OK', {
    level: 'info',
    tags: { source: 'boot' },
  });
}

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

export default function App() {
  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);
  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));
        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);
      }
    })();
  }, []);

  // FLAG_SECURE — blocks screenshots and hides app preview from app switcher.
  useEffect(() => {
    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;
    (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.
            await Updates.reloadAsync();
            return; // reloadAsync nie wraca
          }
        } catch {
          // OTA fail (brak sieci / 204 / TLS pin mismatch) — leciemy do A.
        }
      }
      // 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 (!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>
    );
  }

  // 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}>
                <AppNavigator
                  client={client}
                  appVersion={APP_VERSION}
                  onLogout={async () => {
                    await clearCredentials();
                    setClient(null);
                    queryClient.clear();
                  }}
                />
              </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);
