'use client';

/**
 * AuthProvider — thin React layer over the generated auth store.
 *
 * Architecture rule (auth v2): the generated store (`_api/generated/helpers/auth`)
 * is the ONLY auth engine. It owns tokens, JWT-exp semantics, the reactive
 * session snapshot, 401→refresh→retry recovery (cross-store Web-Locks), and
 * the terminal-401 `onSessionExpired` event. React only:
 *   1. subscribes to the snapshot (`useSyncExternalStore`),
 *   2. loads the user profile when a session exists,
 *   3. routes to login when the store says the session died,
 *   4. exposes login/logout actions.
 *
 * Everything that used to live here — authTick, expiry timers, four parallel
 * 401 handlers, presence-vs-exp token checks — is the store's job now.
 */

import { usePathname } from 'next/navigation';
import React, {
  createContext,
  memo,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

import { auth } from '../../_api/generated/helpers/auth';
import { APIError } from '../../_api/generated/helpers';
import { applyRoleLogPolicy } from '../../log-control';
import { useCfgRouter, useLocalStorage, useQueryParams, useSession } from '../hooks';
import { clearProfileCache } from '../hooks/useProfileCache';
import { useAuthRedirectManager } from '../hooks/useAuthRedirect';
import { Analytics, AnalyticsCategory, AnalyticsEvent } from '../utils/analytics';
import { isDev, isBrowser } from '../utils/env';
import { authLogger } from '../utils/logger';
import { AccountsProvider, useAccountsContext } from './AccountsContext';

import type { AuthContextType, AuthProviderProps, UserProfile } from './types';
import type { OTPRequestResult } from '../types';

// The ONLY place route conventions are declared. '/auth' is the sign-in page
// convention; the framework has no opinion about where an app's "home" is, so
// the callback default is the site root. Apps override via AuthConfig.routes;
// everything downstream reads the RESOLVED values from `useAuth().routes`.
const defaultRoutes = {
  auth: '/auth',
  defaultCallback: '/',
  defaultAuthCallback: '/auth',
};

/** Resolve AuthConfig.routes against the conventions — applied exactly once. */
export const resolveAuthRoutes = (
  routes?: { auth?: string; defaultCallback?: string; defaultAuthCallback?: string },
) => ({
  auth: routes?.auth || defaultRoutes.auth,
  defaultCallback: routes?.defaultCallback || defaultRoutes.defaultCallback,
  defaultAuthCallback: routes?.defaultAuthCallback || routes?.auth || defaultRoutes.defaultAuthCallback,
});

const EMAIL_STORAGE_KEY = 'auth_email';

const AuthContext = createContext<AuthContextType | undefined>(undefined);

/** Extract a user-facing message from an APIError response body. */
const apiErrorMessage = (error: APIError): string => {
  const body = error.response as
    | { error?: string; detail?: string; message?: string }
    | undefined;
  return body?.error || body?.detail || body?.message || error.errorMessage;
};

const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config }) => {
  const accounts = useAccountsContext();
  const session = useSession();
  const isAuthenticated = session.status === 'authenticated';

  const router = useCfgRouter();
  const pathname = usePathname();
  const queryParams = useQueryParams();
  const [storedEmail, setStoredEmail, clearStoredEmail] = useLocalStorage<string | null>(
    EMAIL_STORAGE_KEY,
    null,
  );

  // Resolved once; every consumer below (and, via context, every component)
  // reads these — never the raw config and never a module constant.
  const routes = useMemo(() => resolveAuthRoutes(config?.routes), [config?.routes]);
  const routesRef = useRef(routes);
  useEffect(() => {
    routesRef.current = routes;
  }, [routes]);

  const redirectManager = useAuthRedirectManager({
    fallbackUrl: routes.defaultCallback,
    clearOnUse: true,
  });

  const configRef = useRef(config);
  useEffect(() => {
    configRef.current = config;
  }, [config]);

  const accountsRef = useRef(accounts);
  useEffect(() => {
    accountsRef.current = accounts;
  }, [accounts]);

  // Set when a login flow opts out of automatic navigation (verifyOTP with
  // skipRedirect — the AuthLayout success screen owns the final hardPush,
  // including through a 2FA continuation). While set, the "redirect
  // authenticated users away from /auth" effect must stand down, otherwise it
  // races the success screen and eats the saved back-url.
  const pendingManualNavRef = useRef(false);

  // ── Bootstrap: settle the profile for the current session ─────────────────
  // The session itself needs no async bootstrap — the store answers
  // synchronously from token `exp`. The only async part is the profile fetch,
  // and it re-runs automatically when a session appears later (e.g. iframe
  // token delivery via postMessage → setSession → snapshot flips).
  const [profileSettled, setProfileSettled] = useState(false);
  const hasProfile = Boolean(accounts.profile);
  const profileAttemptedRef = useRef(false);

  useEffect(() => {
    if (!isAuthenticated) {
      profileAttemptedRef.current = false;
      // Dead tokens may still sit in storage (access AND refresh both
      // expired). Purge so the login form shows instantly instead of the app
      // firing doomed API calls — the automated "clear localStorage and the
      // form appears".
      //
      // CRITICAL: ask the STORE synchronously, never trust the React snapshot
      // here. During hydration useSyncExternalStore serves the SERVER snapshot
      // ('anonymous') for one commit even when perfectly live tokens sit in
      // storage — purging on that stale value nuked fresh logins (user typed
      // OTP → tokens saved → hardPush → hydration commit saw 'anonymous' →
      // tokens cleared → bounced back to /auth).
      const hasTokens = Boolean(auth.getToken() || auth.getRefreshToken());
      if (hasTokens && !auth.isAuthenticated()) {
        authLogger.warn('Dead session in storage — clearing');
        auth.clearSession();
        clearProfileCache();
      }
      setProfileSettled(true);
      return;
    }
    if (hasProfile || profileAttemptedRef.current) {
      setProfileSettled(true);
      return;
    }
    profileAttemptedRef.current = true;
    let cancelled = false;
    accountsRef.current
      .refreshProfile({ callerId: 'AuthProvider:bootstrap' })
      .catch((error: unknown) => {
        // A terminal 401 collapses the session via the store's
        // onSessionExpired funnel below. Anything else (network, 5xx) keeps
        // the session — the user can retry.
        authLogger.warn('Profile load failed (session kept):', error);
      })
      .finally(() => {
        if (!cancelled) setProfileSettled(true);
      });
    return () => {
      cancelled = true;
    };
  }, [isAuthenticated, hasProfile]);

  const isLoading = !profileSettled;

  // ── The ONE exit: terminal 401 → login ─────────────────────────────────────
  // The store has already cleared the session before this fires (subscribers
  // flipped to 'anonymous'), so the handler only routes. Registration returns
  // its own unsubscribe — StrictMode double-mounts compose instead of racing.
  const expiredHandledRef = useRef(false);
  useEffect(() => {
    const unsubscribe = auth.onSessionExpired(() => {
      if (expiredHandledRef.current) return;
      expiredHandledRef.current = true;
      authLogger.warn('Session expired (terminal 401) — redirecting to login');
      clearProfileCache();
      Analytics.event(AnalyticsEvent.AUTH_SESSION_EXPIRED, {
        category: AnalyticsCategory.AUTH,
      });
      router.hardReplace(routesRef.current.defaultAuthCallback);
    });
    return unsubscribe;
  }, [router]);

  // ── Redirect authenticated users away from the auth page ──────────────────
  // Stands down while a login flow on this page owns navigation (success
  // screen / 2FA continuation — see pendingManualNavRef). When it does fire
  // (an already-authenticated user landing on /auth), it honors the saved
  // back-url with the same priority rule as every other consumer.
  useEffect(() => {
    if (!isAuthenticated || pendingManualNavRef.current) return;
    if (pathname === routes.auth && !queryParams.get('flow')) {
      const saved = redirectManager.hasRedirect() ? redirectManager.getRedirect() : null;
      if (saved) redirectManager.clearRedirect();
      router.push(saved || routes.defaultCallback);
    }
  }, [isAuthenticated, pathname, queryParams, routes, router, redirectManager]);

  // ── Profile actions (delegated to AccountsContext) ─────────────────────────

  const loadCurrentProfile = useCallback(async (callerId?: string): Promise<void> => {
    if (!auth.isAuthenticated()) {
      throw new Error('No valid authentication token');
    }
    await accountsRef.current.refreshProfile({
      callerId: callerId || 'AuthContext.loadCurrentProfile',
    });
  }, []);

  const checkAuthAndRedirect = useCallback(async (): Promise<void> => {
    if (auth.isAuthenticated()) {
      try {
        await loadCurrentProfile('checkAuthAndRedirect');
      } catch (error) {
        authLogger.warn('checkAuthAndRedirect: profile load failed', error);
      }
      router.push(routesRef.current.defaultCallback);
    } else {
      router.push(routesRef.current.defaultAuthCallback);
    }
  }, [loadCurrentProfile, router]);

  // ── OTP login flow ─────────────────────────────────────────────────────────

  const requestOTP = useCallback(
    async (identifier: string, sourceUrl?: string): Promise<OTPRequestResult> => {
      // A login attempt invalidates whatever session was lying around.
      auth.clearSession();

      try {
        const result = await accountsRef.current.requestOTP({
          identifier,
          source_url: sourceUrl,
        });

        Analytics.event(AnalyticsEvent.AUTH_OTP_REQUEST, {
          category: AnalyticsCategory.AUTH,
          label: 'email',
        });

        return {
          success: true,
          message: result.message || 'OTP code sent to your email address',
          webmail: result.webmail ?? null,
        };
      } catch (error) {
        authLogger.error('Request OTP error:', error);

        if (error instanceof APIError) {
          const body = error.response as
            | { retry_after?: number; retryAfter?: number }
            | undefined;
          const retryAfter = body?.retry_after ?? body?.retryAfter;
          return {
            success: false,
            statusCode: error.statusCode,
            message: apiErrorMessage(error),
            retryAfter: typeof retryAfter === 'number' ? retryAfter : undefined,
          };
        }
        return { success: false, message: 'Failed to send OTP' };
      }
    },
    [],
  );

  const verifyOTP = useCallback(
    async (
      identifier: string,
      otpCode: string,
      sourceUrl?: string,
      redirectUrl?: string,
      skipRedirect?: boolean,
    ): Promise<{
      success: boolean;
      message: string;
      user?: UserProfile;
      requires_2fa?: boolean;
      session_id?: string | null;
      should_prompt_2fa?: boolean;
    }> => {
      try {
        // The caller opting out of redirect commits to handling ALL navigation
        // for this login — including the 2FA continuation and success screen.
        pendingManualNavRef.current = Boolean(skipRedirect);

        const result = await accountsRef.current.verifyOTP({
          identifier,
          otp: otpCode,
          source_url: sourceUrl,
        });

        if (result.requires_2fa && result.session_id) {
          authLogger.info('2FA required, session:', result.session_id);
          return {
            success: true,
            message: 'OTP verified, 2FA required',
            requires_2fa: true,
            session_id: result.session_id,
            should_prompt_2fa: result.should_prompt_2fa,
          };
        }

        if (!result.access || !result.refresh) {
          authLogger.error('Verify OTP returned invalid response:', result);
          return { success: false, message: 'Invalid OTP verification response' };
        }

        if (identifier.includes('@')) {
          setStoredEmail(identifier);
        }

        Analytics.event(AnalyticsEvent.AUTH_LOGIN_SUCCESS, {
          category: AnalyticsCategory.AUTH,
          label: 'email',
        });
        if (result.user?.id) {
          Analytics.setUser(String(result.user.id));
        }

        if (!skipRedirect) {
          // Priority: saved back-url (per-visit intent from the guard) >
          // explicit redirectUrl (static app default) > config default.
          // hardPush = full page load so every context reinitializes cleanly.
          const saved = redirectManager.hasRedirect() ? redirectManager.getRedirect() : null;
          if (saved) redirectManager.clearRedirect();
          const finalRedirectUrl = saved || redirectUrl || routesRef.current.defaultCallback;
          authLogger.info('Redirecting after auth to:', finalRedirectUrl);
          router.hardPush(finalRedirectUrl);
        }

        return {
          success: true,
          message: 'Login successful',
          user: result.user as UserProfile,
          should_prompt_2fa: result.should_prompt_2fa,
        };
      } catch (error) {
        authLogger.error('Verify OTP error:', error);
        Analytics.event(AnalyticsEvent.AUTH_OTP_VERIFY_FAIL, {
          category: AnalyticsCategory.AUTH,
          label: 'email',
        });
        if (error instanceof APIError) {
          return { success: false, message: apiErrorMessage(error) };
        }
        return { success: false, message: 'Failed to verify OTP' };
      }
    },
    [setStoredEmail, redirectManager, router],
  );

  // ── Session actions ────────────────────────────────────────────────────────

  const refreshToken = useCallback(async (): Promise<{ success: boolean; message: string }> => {
    // Same single-flight + Web-Locks path as the 401 interceptor — rotation,
    // dedup and rotated-token persistence come for free.
    const access = await auth.refreshNow();
    if (access) {
      Analytics.event(AnalyticsEvent.AUTH_TOKEN_REFRESH, {
        category: AnalyticsCategory.AUTH,
      });
      return { success: true, message: 'Token refreshed' };
    }
    Analytics.event(AnalyticsEvent.AUTH_TOKEN_REFRESH_FAIL, {
      category: AnalyticsCategory.AUTH,
    });
    return { success: false, message: 'Error refreshing token' };
  }, []);

  const logout = useCallback((): void => {
    Analytics.event(AnalyticsEvent.AUTH_LOGOUT, {
      category: AnalyticsCategory.AUTH,
    });
    accountsRef.current.logout(); // clears session + profile + cache
    // hardReplace = full reload + history replace, so contexts reinitialize
    // and Back doesn't return to a protected page.
    router.hardReplace(routesRef.current.defaultAuthCallback);
  }, [router]);

  // ── Derived state ──────────────────────────────────────────────────────────

  const user = (accounts.profile as UserProfile | undefined) ?? null;
  const isAdminUser = Boolean(user?.is_staff || user?.is_superuser);

  // Role-aware console verbosity: admins (and dev) get debug logs, regular
  // users in production get a clean console.
  useEffect(() => {
    applyRoleLogPolicy({ isAdmin: isAdminUser });
  }, [isAdminUser]);

  const updateProfile = useCallback(
    async (data: {
      first_name?: string;
      last_name?: string;
      username?: string;
    }): Promise<UserProfile> => {
      const result = await accountsRef.current.partialUpdateProfile(data);
      return result as UserProfile;
    },
    [],
  );

  const uploadAvatar = useCallback(async (avatar: File | Blob): Promise<UserProfile> => {
    const result = await accountsRef.current.uploadAvatar(avatar);
    return result as UserProfile;
  }, []);

  const value = useMemo<AuthContextType>(
    () => ({
      user,
      isLoading,
      // Reactive: flips via the store snapshot (token writes, refreshes,
      // expiry timer, cross-tab/cross-store sync) — no ticks, no polling.
      isAuthenticated,
      isAdminUser,
      routes,
      loadCurrentProfile,
      checkAuthAndRedirect,
      getToken: () => auth.getToken(),
      getRefreshToken: () => auth.getRefreshToken(),
      getSavedEmail: () => storedEmail,
      saveEmail: setStoredEmail,
      clearSavedEmail: clearStoredEmail,
      requestOTP,
      verifyOTP,
      refreshToken,
      logout,
      saveRedirectUrl: redirectManager.setRedirect,
      getRedirectUrl: redirectManager.getFinalRedirectUrl,
      clearRedirectUrl: redirectManager.clearRedirect,
      hasRedirectUrl: redirectManager.hasRedirect,
      updateProfile,
      uploadAvatar,
    }),
    [
      user,
      isLoading,
      isAuthenticated,
      isAdminUser,
      routes,
      loadCurrentProfile,
      checkAuthAndRedirect,
      storedEmail,
      setStoredEmail,
      clearStoredEmail,
      requestOTP,
      verifyOTP,
      refreshToken,
      logout,
      redirectManager,
      updateProfile,
      uploadAvatar,
    ],
  );

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

/**
 * AuthProvider — wraps AccountsProvider + AuthProviderInternal.
 *
 * Memoised: re-renders only when `config` reference or `children` change.
 */
function AuthProviderRaw({ children, config, enabled = true }: AuthProviderProps) {
  if (!enabled) return <>{children}</>;
  return (
    <AccountsProvider>
      <AuthProviderInternal config={config}>{children}</AuthProviderInternal>
    </AccountsProvider>
  );
}

export const AuthProvider = memo(AuthProviderRaw);

/**
 * Default auth state for SSR/outside provider.
 * Safe defaults that read as "not authenticated, not loading".
 */
const defaultAuthState: AuthContextType = {
  user: null,
  isLoading: false,
  isAuthenticated: false,
  isAdminUser: false,
  routes: resolveAuthRoutes(),
  loadCurrentProfile: async () => {
    authLogger.warn('useAuth: loadCurrentProfile called outside AuthProvider');
  },
  checkAuthAndRedirect: async () => {
    authLogger.warn('useAuth: checkAuthAndRedirect called outside AuthProvider');
  },
  getToken: () => null,
  getRefreshToken: () => null,
  getSavedEmail: () => null,
  saveEmail: () => {
    authLogger.warn('useAuth: saveEmail called outside AuthProvider');
  },
  clearSavedEmail: () => {},
  requestOTP: async () => {
    authLogger.warn('useAuth: requestOTP called outside AuthProvider');
    return { success: false, message: 'AuthProvider not available' };
  },
  verifyOTP: async () => {
    authLogger.warn('useAuth: verifyOTP called outside AuthProvider');
    return { success: false, message: 'AuthProvider not available' };
  },
  refreshToken: async () => {
    authLogger.warn('useAuth: refreshToken called outside AuthProvider');
    return { success: false, message: 'AuthProvider not available' };
  },
  logout: () => {
    authLogger.warn('useAuth: logout called outside AuthProvider');
  },
  saveRedirectUrl: () => {},
  getRedirectUrl: () => '',
  clearRedirectUrl: () => {},
  hasRedirectUrl: () => false,
  updateProfile: async () => {
    authLogger.warn('useAuth: updateProfile called outside AuthProvider');
    return {} as UserProfile;
  },
  uploadAvatar: async () => {
    authLogger.warn('useAuth: uploadAvatar called outside AuthProvider');
    return {} as UserProfile;
  },
};

/**
 * Hook to access auth context.
 *
 * SSR-safe: returns default unauthenticated state when called outside
 * AuthProvider (e.g. during static page generation).
 */
export const useAuth = (): AuthContextType => {
  const context = useContext(AuthContext);
  if (context === undefined) {
    if (isBrowser && isDev) {
      authLogger.debug('useAuth called outside AuthProvider, returning default state');
    }
    return defaultAuthState;
  }
  return context;
};

export default AuthContext;
