import { useCallback, useEffect, useState } from 'react';
import { AppState, StyleSheet, View } from 'react-native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import * as SplashScreen from 'expo-splash-screen';
import { useFonts } from 'expo-font';
import {
  Inter_400Regular,
  Inter_500Medium,
  Inter_600SemiBold,
  Inter_700Bold,
} from '@expo-google-fonts/inter';
import {
  JetBrainsMono_400Regular,
  JetBrainsMono_500Medium,
} from '@expo-google-fonts/jetbrains-mono';
import { usePlaybackSync } from '@/audio/usePlaybackSync';
import { NowPlayingHost } from '@/components/player/NowPlayingHost';
import { QuickSearchOverlay } from '@/components/search/QuickSearchOverlay';
import { useScopeLifecycle } from '@/scope/useScopeLifecycle';
import { useLibraryStore } from '@/stores/libraryStore';
import { ensureEQRouteSyncStarted } from '@/audio/eqRouteSync';
import { useAudioSettingsStore } from '@/stores/audioSettingsStore';
import { useRemoteSourcesStore } from '@/stores/remoteSourcesStore';
import { useLastFmSettingsStore } from '@/stores/lastFmSettingsStore';
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
import { usePlaybackTargetStore } from '@/stores/playbackTargetStore';
import { useNormalizationSync } from '@/audio/useNormalizationSync';
import { useLastFmScrobbler } from '@/audio/useLastFmScrobbler';
import {
  clearDesktopRemoteMediaSession,
  setDesktopRemoteMediaSession,
  subscribeDesktopRemoteMediaSessionCommands,
} from '@/services/desktopRemoteMediaSession';
import { identityMatchesPinnedConnection } from '@/services/desktopSyncPolicy';
import {
  getDesktopRemoteConnection,
  setDesktopRemoteConnection,
} from '@/services/desktopRemoteCredentials';
import { fetchDesktopRemoteIdentity } from '@/services/desktopRemoteClient';
import { useDesktopSyncStore } from '@/stores/desktopSyncStore';
import { SyncConflictPrompt } from '@/components/sync/SyncConflictPrompt';
import { useThemeStore } from '@/stores/themeStore';
import { useOnboardingStore } from '@/stores/onboardingStore';
import { OnboardingFlow } from '@/components/onboarding/OnboardingFlow';
import { useTheme } from '@/theme/themed';
import { SessionLifecycle } from '@/session/SessionLifecycle';
import { useLyricsSettingsStore } from '@/stores/lyricsSettingsStore';
import { useSleepTimerStore } from '@/stores/sleepTimerStore';
import { Text } from '@/components/Text';

// Anchor the root stack at the tabs so a deep link straight to a top-level route
// (the widget's `recently-played`, the notification-click redirect) builds
// `[(tabs), route]` instead of just `[route]` — backing out of a deep-linked
// route must never land on an empty stack. Only affects deep-link/launch
// ordering; normal nav is unchanged. (Now-playing itself is no longer a route —
// it's the store-gated NowPlayingHost overlay.)
export const unstable_settings = {
  initialRouteName: '(tabs)',
};

SplashScreen.preventAutoHideAsync();

/** Mirrors RNTP state into the player store. Renders nothing. */
function PlaybackSync() {
  usePlaybackSync();
  return null;
}

/**
 * Re-reads system theme inputs (OS scheme + monet wallpaper ramps) on each
 * return to foreground — wallpaper changes can only happen while backgrounded.
 * OS dark/light toggles are covered by the Appearance listener in themeStore.
 */
function ThemeSystemSync() {
  useEffect(() => {
    const subscription = AppState.addEventListener('change', (state) => {
      if (state === 'active') useThemeStore.getState().refreshSystemInputs();
    });
    return () => subscription.remove();
  }, []);
  return null;
}

function SleepTimerLifecycle() {
  useEffect(() => {
    void useSleepTimerStore.getState().hydrate();
    const subscription = AppState.addEventListener('change', (state) => {
      if (state === 'active') void useSleepTimerStore.getState().reconcile();
    });
    return () => subscription.remove();
  }, []);
  return null;
}

/** Owns the visualizer on/off gate (foreground + playing + motion). Renders nothing. */
function ScopeLifecycle() {
  useScopeLifecycle();
  return null;
}

/** Pushes per-track normalization gain to native on track/settings change. */
function NormalizationSync() {
  useNormalizationSync();
  return null;
}

/** Feeds playback snapshots to the Last.fm scrobble service. Renders nothing. */
function LastFmScrobbler() {
  useLastFmScrobbler();
  return null;
}

/** Loads the selected playback target and connects remote only when desktop is selected. */
function PlaybackTargetSync() {
  const target = usePlaybackTargetStore((s) => s.target);
  const loadTarget = usePlaybackTargetStore((s) => s.load);
  const initDesktopRemote = useDesktopRemoteStore((s) => s.init);

  useEffect(() => {
    void loadTarget();
  }, [loadTarget]);

  useEffect(() => {
    if (target === 'desktop') {
      if (useSleepTimerStore.getState().timer) void useSleepTimerStore.getState().cancel();
      void initDesktopRemote();
    }
  }, [target, initDesktopRemote]);

  return null;
}

/** Mirrors Desktop Remote now-playing into a separate Android MediaSession. */
function DesktopRemoteMediaSessionSync() {
  const connection = useDesktopRemoteStore((s) => s.connection);
  const connectionState = useDesktopRemoteStore((s) => s.connectionState);
  const snapshot = useDesktopRemoteStore((s) => s.snapshot);
  const sendControl = useDesktopRemoteStore((s) => s.sendControl);

  useEffect(() => {
    const subscription = subscribeDesktopRemoteMediaSessionCommands((event) => {
      if (event.command === 'toggle-play') {
        const playing = useDesktopRemoteStore.getState().snapshot?.playbackState === 'playing';
        void useDesktopRemoteStore.getState().sendControl(playing ? 'pause' : 'play');
        return;
      }
      if (event.command === 'stop') {
        void useDesktopRemoteStore.getState().sendControl('pause');
        return;
      }
      if (event.command === 'seek') {
        void useDesktopRemoteStore.getState().sendControl('seek', event.position);
        return;
      }
      void useDesktopRemoteStore.getState().sendControl(event.command);
    });
    return () => subscription.remove();
  }, [sendControl]);

  useEffect(() => {
    if (!connection || connectionState === 'unpaired') {
      clearDesktopRemoteMediaSession();
      return;
    }
    setDesktopRemoteMediaSession(snapshot, connection);
  }, [connection, connectionState, snapshot]);

  useEffect(() => clearDesktopRemoteMediaSession, []);

  return null;
}

const DESKTOP_DISCOVERY_BURST_MS = 20_000;
const DESKTOP_SYNC_STARTUP_RETRY_MS = 2_500;
const DESKTOP_SYNC_REQUEST_POLL_MS = 60_000;

/**
 * Auto-syncs favorites/playlists with the paired desktop when it looks
 * reachable: on foreground (probe-guarded, min-interval limited), when mDNS
 * discovery spots the paired desktop, or when the remote screen connects.
 * Deliberately does NOT init the desktop-remote store here — its connect path
 * retries a powered-off desktop every 2 s forever, which we don't want running
 * from app launch.
 */
function DesktopSyncAutoTrigger() {
  const connectionState = useDesktopRemoteStore((s) => s.connectionState);
  const discovered = useDesktopRemoteStore((s) => s.discovered);
  const desktopSyncHydrated = useDesktopSyncStore((s) => s.hydrated);
  const desktopSyncEnabled = useDesktopSyncStore((s) => s.desktopSyncEnabled);

  useEffect(() => {
    void useDesktopSyncStore.getState().hydrate();
  }, []);

  // Cold start + each return to foreground: attempt a (probe-guarded) sync and
  // run a short mDNS burst so a desktop that changed LAN address is found.
  useEffect(() => {
    let burstTimer: ReturnType<typeof setTimeout> | null = null;
    let startupRetryTimer: ReturnType<typeof setTimeout> | null = null;
    const onActive = () => {
      void (async () => {
        const sync = useDesktopSyncStore.getState();
        if (!sync.hydrated || !sync.desktopSyncEnabled) return;
        const connection = await getDesktopRemoteConnection();
        if (!connection) return;
        useDesktopSyncStore.getState().maybeAutoSync('foreground');
        const remote = useDesktopRemoteStore.getState();
        if (remote.discoveryAvailable && !remote.discoveryRunning) {
          void remote.startDiscovery();
          burstTimer = setTimeout(() => {
            burstTimer = null;
            void useDesktopRemoteStore.getState().stopDiscovery();
          }, DESKTOP_DISCOVERY_BURST_MS);
        }
      })();
    };
    if (AppState.currentState === 'active') {
      onActive();
    } else {
      startupRetryTimer = setTimeout(() => {
        startupRetryTimer = null;
        if (AppState.currentState === 'active') onActive();
      }, DESKTOP_SYNC_STARTUP_RETRY_MS);
    }
    const subscription = AppState.addEventListener('change', (state) => {
      if (state === 'active') onActive();
    });
    return () => {
      subscription.remove();
      if (burstTimer !== null) clearTimeout(burstTimer);
      if (startupRetryTimer !== null) clearTimeout(startupRetryTimer);
      if (!useDesktopSyncStore.getState().desktopSyncEnabled) {
        void useDesktopRemoteStore.getState().stopDiscovery();
      }
    };
  }, [desktopSyncEnabled, desktopSyncHydrated]);

  // Desktop-initiated "Sync now" pickup: a cheap identity poll while
  // foregrounded (the SSE nudge only reaches us while the remote screen's
  // stream happens to be connected). fetchDesktopRemoteIdentity swallows
  // errors, so a powered-off desktop costs one timed-out request per minute.
  useEffect(() => {
    if (!desktopSyncHydrated || !desktopSyncEnabled) return;
    const timer = setInterval(() => {
      if (AppState.currentState !== 'active') return;
      void (async () => {
        const sync = useDesktopSyncStore.getState();
        if (!sync.hydrated || !sync.desktopSyncEnabled) return;
        const connection = await getDesktopRemoteConnection();
        if (!connection) return;
        const identity = await fetchDesktopRemoteIdentity(
          connection.baseUrl,
          connection.certificateFingerprint
        );
        if (identity?.syncRequestedAt) {
          useDesktopSyncStore.getState().handleSyncRequest();
        }
      })();
    }, DESKTOP_SYNC_REQUEST_POLL_MS);
    return () => clearInterval(timer);
  }, [desktopSyncEnabled, desktopSyncHydrated]);

  // Paired desktop spotted on the LAN: refresh a stale baseUrl (DHCP moves)
  // and trigger a sync.
  useEffect(() => {
    if (!desktopSyncHydrated || !desktopSyncEnabled) return;
    if (discovered.length === 0) return;
    void (async () => {
      const connection = await getDesktopRemoteConnection();
      if (!connection?.endpointUuid) return;
      const match = discovered.find((desktop) => desktop.endpointUuid === connection.endpointUuid);
      if (!match) return;
      if (match.baseUrl && match.baseUrl !== connection.baseUrl) {
        const identity = await fetchDesktopRemoteIdentity(
          match.baseUrl,
          connection.certificateFingerprint
        );
        if (!identity || !identityMatchesPinnedConnection(
          connection.endpointUuid,
          identity.protocolVersion,
          identity.endpointUuid
        )) return;
        const updated = { ...connection, baseUrl: match.baseUrl };
        await setDesktopRemoteConnection(updated);
        if (useDesktopRemoteStore.getState().connection) {
          useDesktopRemoteStore.setState({ connection: updated });
        }
      }
      useDesktopSyncStore.getState().maybeAutoSync('discovery');
    })();
  }, [desktopSyncEnabled, desktopSyncHydrated, discovered]);

  // The remote screen connected — the desktop is definitely reachable.
  useEffect(() => {
    if (desktopSyncHydrated && desktopSyncEnabled && connectionState === 'connected') {
      useDesktopSyncStore.getState().maybeAutoSync('connected');
    }
  }, [desktopSyncEnabled, desktopSyncHydrated, connectionState]);

  return null;
}

export default function RootLayout() {
  const [fontsLoaded] = useFonts({
    Inter_400Regular,
    Inter_500Medium,
    Inter_600SemiBold,
    Inter_700Bold,
    JetBrainsMono_400Regular,
    JetBrainsMono_500Medium,
  });

  // Failsafe so the splash can never hang the UI blank. `preventAutoHideAsync` runs at
  // module scope — including in the headless JS context Android Auto spins up — so when
  // the process is started from the car first and the app is opened later, the normal
  // "hide once fonts load" path can get stuck. Render (and hide the splash) anyway after
  // a short timeout even if fonts haven't reported in.
  const [splashTimedOut, setSplashTimedOut] = useState(false);
  useEffect(() => {
    const timer = setTimeout(() => setSplashTimedOut(true), 2000);
    return () => clearTimeout(timer);
  }, []);
  // Theme joins the gate so the first painted frame is already in the
  // persisted theme (no flash). The failsafe path paints the default theme
  // and snaps once the SQLite read lands — accepted degradation.
  const themeLoaded = useThemeStore((s) => s.loaded);
  const onboardingLoaded = useOnboardingStore((s) => s.loaded);
  const onboardingComplete = useOnboardingStore((s) => s.onboardingComplete);
  const libraryStatus = useLibraryStore((s) => s.status);
  const fatalUserData = libraryStatus === 'fatalUserData';
  const theme = useTheme();
  const renderReady = (fontsLoaded && themeLoaded && onboardingLoaded) || splashTimedOut;
  const [sessionReady, setSessionReady] = useState(false);
  const handleSessionReady = useCallback(() => setSessionReady(true), []);
  const splashReady = renderReady
    && (!onboardingComplete || sessionReady || splashTimedOut);

  useEffect(() => {
    if (splashReady) {
      void SplashScreen.hideAsync().catch(() => {});
    }
  }, [splashReady]);

  // Eager library init: SQLite open + initial reads are tens of ms, and the
  // Library tab + playback adapters get data immediately. EQ + audio settings load
  // alongside so the native EQ/gain reflect persisted prefs from the first play.
  useEffect(() => {
    useThemeStore
      .getState()
      .load()
      .catch((err) => console.error('[theme] load failed', err));
    useOnboardingStore
      .getState()
      .load()
      .catch((err) => console.error('[onboarding] load failed', err));
    useLibraryStore
      .getState()
      .initialize()
      .catch((err) => console.error('[library] init failed', err));
    ensureEQRouteSyncStarted().catch((err) => console.error('[eq-route] init failed', err));
    useAudioSettingsStore
      .getState()
      .load()
      .catch((err) => console.error('[audioSettings] load failed', err));
    useLyricsSettingsStore
      .getState()
      .load()
      .catch((err) => console.error('[lyricsSettings] load failed', err));
    // Remote sources: load server rows + hydrate the URL registry from cached
    // config/token (no network on launch). Runs after library init reads first.
    useRemoteSourcesStore
      .getState()
      .init()
      .catch((err) => console.error('[remoteSources] init failed', err));
    // Last.fm: construct the scrobble service + drain any persisted offline queue,
    // even if the user never opens the settings screen this session.
    useLastFmSettingsStore
      .getState()
      .init()
      .catch((err) => console.error('[lastfm] init failed', err));
  }, []);

  if (!renderReady) return null;

  return (
    <GestureHandlerRootView style={{ flex: 1, backgroundColor: theme.colors.bgPrimary }}>
      <SafeAreaProvider>
        <StatusBar style={theme.statusBarStyle} />
        {/* The navigator stays mounted whatever the onboarding state (expo-router
            needs a root navigator), but the playback/sync/desktop side-effects and
            overlays are gated off during the wizard — no LAN-discovery bursts or
            scrobbler running mid-onboarding. */}
        {onboardingComplete && !fatalUserData ? (
          <>
            <ThemeSystemSync />
            <SleepTimerLifecycle />
            <PlaybackSync />
            <ScopeLifecycle />
            <NormalizationSync />
            <LastFmScrobbler />
            <PlaybackTargetSync />
            <DesktopRemoteMediaSessionSync />
            <DesktopSyncAutoTrigger />
          </>
        ) : null}
        <Stack
          screenOptions={{
            headerShown: false,
            contentStyle: { backgroundColor: theme.colors.bgPrimary },
          }}
        >
          <Stack.Screen name="(tabs)" />
        </Stack>
        {onboardingComplete && !fatalUserData ? <SessionLifecycle onReady={handleSessionReady} /> : null}
        {fatalUserData ? (
          <View
            style={[
              StyleSheet.absoluteFill,
              {
                backgroundColor: theme.colors.bgPrimary,
                alignItems: 'center',
                justifyContent: 'center',
                padding: 32,
                gap: 12,
              },
            ]}
          >
            <Text variant="title" style={{ textAlign: 'center' }}>
              Library data unavailable
            </Text>
            <Text
              variant="body"
              color={theme.colors.textSecondary}
              style={{ maxWidth: 420, textAlign: 'center' }}
            >
              Astra could not restore your playlists, favorites, and settings from either safety
              snapshot. Nothing was silently reset, and your music files were not changed.
            </Text>
          </View>
        ) : onboardingComplete ? (
          <>
            {/* Always-mounted player overlay (store-gated); open/close is a pure
                UI-thread slide with zero mount cost after the first mount. */}
            <NowPlayingHost />
            <QuickSearchOverlay />
            <SyncConflictPrompt />
          </>
        ) : (
          // First-run gate: opaque full-screen wizard over the (hidden) navigator.
          // markComplete flips the flag → this unmounts, revealing the app.
          <View style={StyleSheet.absoluteFill}>
            <OnboardingFlow
              onDone={() => {
                void useOnboardingStore.getState().markComplete();
              }}
            />
          </View>
        )}
      </SafeAreaProvider>
    </GestureHandlerRootView>
  );
}
