import Constants from 'expo-constants';
import * as Device from 'expo-device';
import * as Notifications from 'expo-notifications';
import * as SecureStore from 'expo-secure-store';
import { Platform } from 'react-native';
import { apiClient } from '@/api/client';
import { logError } from '@/lib/errors';
import { routeNotificationTap } from '@/lib/notification-routing';

const STORED_TOKEN_KEY = 'expo_push_token';

let configured = false;
let tokenListenerSubscription: Notifications.Subscription | null = null;

/**
 * Sets the foreground notification handler and tap routing. Safe to call
 * multiple times. Does NOT subscribe to Expo's token-rotation listener —
 * that's the auth store's job via {@link startPushTokenSync}, so token sync
 * only runs while a session is active. Subscribing here at module scope used
 * to fire `apiClient.registerPushToken` before SecureStore hydration completed
 * and silently invalidated the user's real session.
 */
export function configureNotifications(): void {
  if (configured) return;
  configured = true;

  Notifications.setNotificationHandler({
    handleNotification: async () => ({
      shouldShowBanner: true,
      shouldShowList: true,
      shouldPlaySound: true,
      shouldSetBadge: false,
    }),
  });

  // Warm-state taps (app in background or foreground). Cold-start taps are
  // drained separately via getLastNotificationResponseAsync; see
  // flushPendingNavigation in notification-routing.ts.
  Notifications.addNotificationResponseReceivedListener((response) => {
    routeNotificationTap(response.notification);
  });

  void Notifications.getLastNotificationResponseAsync().then((response) => {
    if (response) routeNotificationTap(response.notification);
  });
}

/**
 * Subscribes to Expo's token-rotation listener. Called by the auth store
 * after a successful sign-in or session restore so rotation events only fire
 * `syncPushToken` while authenticated. Idempotent.
 */
export function startPushTokenSync(): void {
  if (tokenListenerSubscription) return;
  tokenListenerSubscription = Notifications.addPushTokenListener((event) => {
    void syncPushToken(event.data);
  });
}

/**
 * Tears down the rotation listener. Called by the auth store on sign-out and
 * session invalidation. Idempotent.
 */
export function stopPushTokenSync(): void {
  tokenListenerSubscription?.remove();
  tokenListenerSubscription = null;
}

/**
 * Acquires (or returns the cached) Expo push token for this device. Returns
 * null on simulators, when permission is denied, or when any step fails — the
 * caller should treat that as "no push, no further action".
 */
async function acquirePushToken(): Promise<string | null> {
  if (!Device.isDevice) return null;

  const existing = await Notifications.getPermissionsAsync();
  let status = existing.status;
  if (status !== 'granted') {
    const requested = await Notifications.requestPermissionsAsync();
    status = requested.status;
  }
  if (status !== 'granted') return null;

  const projectId =
    Constants.expoConfig?.extra?.eas?.projectId ??
    Constants.easConfig?.projectId;
  if (!projectId) {
    logError(new Error('EAS projectId missing — cannot fetch push token'), {
      scope: 'push.acquire',
    });
    return null;
  }

  const tokenResult = await Notifications.getExpoPushTokenAsync({ projectId });
  return tokenResult.data;
}

async function getStoredToken(): Promise<string | null> {
  try {
    return await SecureStore.getItemAsync(STORED_TOKEN_KEY);
  } catch {
    return null;
  }
}

async function setStoredToken(token: string): Promise<void> {
  try {
    await SecureStore.setItemAsync(STORED_TOKEN_KEY, token);
  } catch (err) {
    logError(err, { scope: 'push.store' });
  }
}

async function clearStoredToken(): Promise<void> {
  try {
    await SecureStore.deleteItemAsync(STORED_TOKEN_KEY);
  } catch {
    // ignore
  }
}

/**
 * Compares the freshly-fetched token to whatever we last sent to Orion and
 * only POSTs on a delta. Never throws — push registration is best-effort and
 * must never block the auth path.
 */
export async function syncPushToken(token?: string | null): Promise<void> {
  try {
    const next = token ?? (await acquirePushToken());
    if (!next) return;

    const stored = await getStoredToken();
    if (stored === next) return;

    const platform = Platform.OS === 'ios' ? 'ios' : 'android';
    const appVersion = Constants.expoConfig?.version ?? '';

    await apiClient.registerPushToken({
      token: next,
      platform,
      app_version: appVersion,
    });
    await setStoredToken(next);
  } catch (err) {
    logError(err, { scope: 'push.sync' });
  }
}

/**
 * Best-effort unregister at sign-out. Wipes the stored token even if the
 * server call fails so the next signed-in user re-registers cleanly.
 */
export async function unregisterPushToken(): Promise<void> {
  try {
    const stored = await getStoredToken();
    if (stored) {
      try {
        await apiClient.unregisterPushToken(stored);
      } catch (err) {
        logError(err, { scope: 'push.unregister' });
      }
    }
  } finally {
    await clearStoredToken();
  }
}

/**
 * Test-only: tears down the module-level token listener. Not normally needed
 * in the app lifecycle — Notifications subscriptions live as long as the JS
 * runtime does.
 */
export function _resetPushNotificationsModule(): void {
  tokenListenerSubscription?.remove();
  tokenListenerSubscription = null;
  configured = false;
}
