import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
import { useNavigation } from "../context/NavigationContext";
import { useAppConfig } from "../context/AppConfigContext";
import AuthService from "../services/auth.service";
const AuthContext = createContext(undefined);
export function AuthProvider({ children, onAfterLogin, onAfterRegister, onBeforeLogout, onBeforeCheckAuth, loginWithPasskey: providedLoginWithPasskey, unlockWithPassword: providedUnlockWithPassword, onError, }) {
    // Stable wrapper so `console.warn`-then-onError stays a single
    // pattern across every silent catch in this provider.
    const reportError = useCallback((error, context) => {
        if (__DEV__) {
            console.warn(`[auth:${context}]`, error);
        }
        try {
            onError?.(error, context);
        }
        catch {
            // Telemetry must never crash the auth flow.
        }
    }, [onError]);
    const [user, setUser] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [isAuthenticated, setIsAuthenticated] = useState(false);
    const [error, setError] = useState(null);
    const router = useNavigation();
    const { routes } = useAppConfig();
    const authCheckRef = useRef(false);
    const sessionGenRef = useRef(0);
    const clearError = useCallback(() => {
        setError(null);
    }, []);
    const routeAfterAuth = useCallback((response, email) => {
        const step = response.user.onboarding_step;
        if (step === "email_verification") {
            router.replace({ pathname: routes.verifyEmail, params: { email } });
        }
        else if (step === "mfa_enrollment") {
            router.replace(routes.mfaEnroll);
        }
        else if (step === "import") {
            router.replace(routes.importData);
        }
        else if (step === "complete" || step == null) {
            router.replace(routes.home);
        }
        else {
            // Unknown step (server added a new onboarding step the
            // client doesn't recognize). Route home — the consumer's
            // route guards will catch the still-incomplete state and
            // either prompt an app update or re-route to welcome.
            // Don't strand the user in the auth flow.
            router.replace(routes.home);
        }
    }, [router, routes]);
    const checkAuth = useCallback(async () => {
        if (authCheckRef.current) {
            return;
        }
        authCheckRef.current = true;
        const gen = ++sessionGenRef.current;
        try {
            setIsLoading(true);
            const isAuth = await AuthService.isAuthenticated();
            if (gen !== sessionGenRef.current) {
                return;
            }
            if (isAuth) {
                const userData = await AuthService.getCurrentUser();
                if (gen !== sessionGenRef.current) {
                    return;
                }
                if (userData) {
                    // Unblock navigation immediately with cached data.
                    setUser(userData);
                    setIsAuthenticated(true);
                    setIsLoading(false);
                    authCheckRef.current = false;
                    // Refresh from the server in the background.
                    AuthService.refreshUserData()
                        .then((fresh) => {
                        if (fresh && gen === sessionGenRef.current) {
                            setUser(fresh);
                        }
                    })
                        .catch((e) => reportError(e, "background-refresh"));
                    return;
                }
                else {
                    await AuthService.logout();
                    if (gen !== sessionGenRef.current) {
                        return;
                    }
                    setUser(null);
                    setIsAuthenticated(false);
                }
            }
            else {
                setUser(null);
                setIsAuthenticated(false);
            }
        }
        catch (e) {
            // Distinguish a transport failure (server unreachable on cold
            // start) from a real auth failure. Falling through to
            // "logged out" on transient network errors paints the welcome
            // screen unjustly to a user whose cookie is still valid, then
            // flips back the moment a request lands. Keep any persisted
            // user record while we wait for the next probe.
            if (gen !== sessionGenRef.current) {
                return;
            }
            const persisted = await AuthService.getCurrentUser().catch(() => null);
            if (gen !== sessionGenRef.current) {
                return;
            }
            if (persisted) {
                setUser(persisted);
                setIsAuthenticated(true);
                reportError(e instanceof Error ? e : new Error(String(e)), "check-auth-transient");
            }
            else {
                setUser(null);
                setIsAuthenticated(false);
            }
        }
        finally {
            if (gen === sessionGenRef.current) {
                setIsLoading(false);
            }
            authCheckRef.current = false;
        }
    }, [reportError]);
    useEffect(() => {
        AuthService.loadServerUrlOverride()
            .then(() => (onBeforeCheckAuth ? onBeforeCheckAuth() : undefined))
            .catch((e) => reportError(e, "pre-check-auth"))
            .then(() => checkAuth());
    }, [checkAuth, onBeforeCheckAuth, reportError]);
    const login = useCallback(async (email, password) => {
        try {
            setIsLoading(true);
            setError(null);
            const response = await AuthService.login(email, password);
            if (onAfterLogin) {
                try {
                    await onAfterLogin(password, response);
                }
                catch (hookErr) {
                    await AuthService.logout();
                    const message = hookErr instanceof Error ? hookErr.message : "Post-login hook failed";
                    // Deliberately do NOT attach `cause: hookErr` — the inner
                    // error from the consumer's post-login hook may carry
                    // key-derivation context (raw masterPassword references,
                    // server payloads) that would be exposed if downstream
                    // logging surfaces `err.cause` (Sentry, devtools, …).
                    throw new Error(message);
                }
            }
            setUser(response.user);
            setIsAuthenticated(true);
            routeAfterAuth(response, email);
        }
        catch (err) {
            const message = err instanceof Error ? err.message : "Login failed";
            setError(message);
            throw err;
        }
        finally {
            setIsLoading(false);
        }
    }, [onAfterLogin, routeAfterAuth]);
    const register = useCallback(async (email, password, displayName) => {
        try {
            setIsLoading(true);
            setError(null);
            const response = await AuthService.register(email, password, displayName);
            if (onAfterRegister) {
                try {
                    await onAfterRegister(password, response);
                }
                catch (hookErr) {
                    await AuthService.logout();
                    const message = hookErr instanceof Error ? hookErr.message : "Post-register hook failed";
                    // No `cause` chain — see login() above for rationale.
                    throw new Error(message);
                }
            }
            setUser(response.user);
            setIsAuthenticated(true);
            routeAfterAuth(response, email);
        }
        catch (err) {
            const message = err instanceof Error ? err.message : "Registration failed";
            setError(message);
            throw err;
        }
        finally {
            setIsLoading(false);
        }
    }, [onAfterRegister, routeAfterAuth]);
    const logout = useCallback(async () => {
        sessionGenRef.current++;
        setIsLoading(true);
        // Flip React auth state FIRST so any in-flight queries that
        // re-render against `user` / `isAuthenticated` see "logged out"
        // immediately. Previously this happened after `AuthService.logout()`
        // resolved, opening a window where the pre-logout hook had already
        // torn down KeyManager / queryClient / WS but components still
        // rendered as authenticated — any React-Query refetch in that
        // window would hit a half-disassembled client.
        setUser(null);
        setIsAuthenticated(false);
        if (onBeforeLogout) {
            try {
                await onBeforeLogout();
            }
            catch (e) {
                // pre-logout hook errors are non-fatal — proceed to clear
                // state. Telemetry sees it via the onError surface.
                reportError(e, "pre-logout-hook");
            }
        }
        router.replace(routes.login);
        // Server-side revoke runs in the background. Failure is also
        // surfaced through the onError hook so consumers can detect
        // half-broken logouts (server session still alive after the
        // user thinks they're gone).
        try {
            await AuthService.logout();
        }
        catch (e) {
            reportError(e, "logout-revoke");
        }
        setIsLoading(false);
    }, [onBeforeLogout, router, routes, reportError]);
    const refreshUser = useCallback(async () => {
        try {
            const userData = await AuthService.refreshUserData();
            if (userData) {
                setUser(userData);
                setIsAuthenticated(true);
            }
            else {
                setUser(null);
                setIsAuthenticated(false);
                router.replace(routes.login);
            }
        }
        catch {
            // Refresh failed — try again on next interval.
        }
    }, [router, routes]);
    const deleteAccount = useCallback(async (password) => {
        sessionGenRef.current++;
        try {
            setIsLoading(true);
            await AuthService.deleteAccount(password);
            if (onBeforeLogout) {
                try {
                    await onBeforeLogout();
                }
                catch {
                    // ignore
                }
            }
            setUser(null);
            setIsAuthenticated(false);
            router.replace(routes.welcome);
        }
        catch (err) {
            const message = err instanceof Error ? err.message : "Failed to delete account";
            setError(message);
            throw err;
        }
        finally {
            setIsLoading(false);
        }
    }, [onBeforeLogout, router, routes]);
    const loginWithPasskey = useCallback(async (email, signal, opts) => {
        if (!providedLoginWithPasskey) {
            throw new Error("entropy-ui: loginWithPasskey() called but AuthProvider wasn't given a loginWithPasskey handler.");
        }
        const silent = opts?.silent ?? false;
        try {
            if (!silent) {
                setIsLoading(true);
                setError(null);
            }
            const result = await providedLoginWithPasskey(email, signal, opts);
            setUser(result.user);
            setIsAuthenticated(true);
            return result;
        }
        catch (err) {
            if (!silent) {
                const message = err instanceof Error ? err.message : "Passkey sign-in failed";
                setError(message);
            }
            throw err;
        }
        finally {
            if (!silent) {
                setIsLoading(false);
            }
        }
    }, [providedLoginWithPasskey]);
    const unlockWithPassword = useCallback(async (password) => {
        if (!providedUnlockWithPassword) {
            throw new Error("entropy-ui: unlockWithPassword() called but AuthProvider wasn't given an unlockWithPassword handler.");
        }
        await providedUnlockWithPassword(password);
    }, [providedUnlockWithPassword]);
    const value = useMemo(() => ({
        user,
        isLoading,
        isAuthenticated,
        login,
        loginWithPasskey,
        unlockWithPassword,
        register,
        logout,
        refreshUser,
        deleteAccount,
        checkAuth,
        error,
        clearError,
    }), [
        user,
        isLoading,
        isAuthenticated,
        login,
        loginWithPasskey,
        unlockWithPassword,
        register,
        logout,
        refreshUser,
        deleteAccount,
        checkAuth,
        error,
        clearError,
    ]);
    return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
    const context = useContext(AuthContext);
    if (context === undefined) {
        throw new Error("useAuth must be used within an AuthProvider");
    }
    return context;
}
export function useRole(requiredRole) {
    const { user } = useAuth();
    if (!user) {
        return false;
    }
    if (requiredRole === "User") {
        return true;
    }
    return user.role === "Admin";
}
export function useRequireAuth(redirectTo) {
    const { isAuthenticated, isLoading } = useAuth();
    const router = useNavigation();
    const { routes } = useAppConfig();
    const target = redirectTo ?? routes.login;
    useEffect(() => {
        if (!isLoading && !isAuthenticated) {
            router.replace(target);
        }
    }, [isAuthenticated, isLoading, router, target]);
    return { isAuthenticated, isLoading };
}
export function useSession() {
    const { user, refreshUser, isAuthenticated } = useAuth();
    const intervalRef = useRef(undefined);
    const refreshingRef = useRef(false);
    useEffect(() => {
        if (isAuthenticated) {
            intervalRef.current = setInterval(() => {
                if (refreshingRef.current) {
                    return;
                }
                refreshingRef.current = true;
                refreshUser()
                    .catch((e) => {
                    // `useSession` lives outside the provider's
                    // reportError closure; auto-refresh failures only
                    // surface in dev. The `refreshUser()` impl itself
                    // reports via onError when wired.
                    if (__DEV__) {
                        console.warn("[auth:session-refresh-interval]", e);
                    }
                })
                    .finally(() => {
                    refreshingRef.current = false;
                });
            }, 10 * 60 * 1000);
        }
        return () => {
            if (intervalRef.current) {
                clearInterval(intervalRef.current);
            }
        };
    }, [isAuthenticated, refreshUser]);
    return { user };
}
// Validators live in `utils/validation` and are re-exported via the
// utils barrel. Existing consumers that imported them from this module
// pull them from the package root barrel instead.
export { validateEmail, validatePassword, validateUsername } from "../utils/validation";
export default useAuth;
//# sourceMappingURL=useAuth.jsx.map