// src/auth/AuthProvider.tsx
import { createContext, useCallback, useEffect, useMemo, useRef, useState, type PropsWithChildren } from 'react';
import type { User } from 'oidc-client-ts';
import { userManager } from './config';

type AuthContextValue = {
  isAuthenticated: boolean;
  isLoading: boolean;
  login: () => Promise<void>;
  logout: () => Promise<void>;
  user: User | null;
  getAccessToken: () => string | null;
  /** Renouvelle le token silencieusement. Retourne le nouvel access_token, ou null si échec. */
  renewToken: () => Promise<string | null>;
  /**
   * Résout une fois la résolution d'auth initiale terminée (callback OIDC ou
   * `getUser()`). À attendre avant de lire le token dans un appel API : évite la
   * course où un effet enfant part avant que la session soit chargée et
   * déclenche un login interactif intempestif.
   */
  whenReady: () => Promise<void>;
};

export const AuthContext = createContext<AuthContextValue | null>(null);

const LOGIN_REDIRECT_COOLDOWN_MS = 15000;
const LAST_LOGIN_REDIRECT_AT_KEY = 'inwink.auth.lastLoginRedirectAt';

function isOidcCallback(): boolean {
  const params = new URLSearchParams(window.location.search);
  return params.has('code') && params.has('state');
}

export function AuthProvider({ children }: PropsWithChildren) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const userRef = useRef(user);
  const loginInFlightRef = useRef(false);
  const renewInFlightRef = useRef<Promise<string | null> | null>(null);
  useEffect(() => { userRef.current = user; }, [user]);

  // Deferred résolu à la fin de la résolution d'auth initiale (cf. `whenReady`).
  // Créé en lazy-init pour exister dès le premier rendu (avant tout effet enfant).
  const readyRef = useRef<{ promise: Promise<void>; resolve: () => void } | null>(null);
  if (!readyRef.current) {
    let resolve!: () => void;
    const promise = new Promise<void>((r) => { resolve = r; });
    readyRef.current = { promise, resolve };
  }

  useEffect(() => {
    let isMounted = true;

    async function init() {
      try {
        if (isOidcCallback()) {
          const callbackUser = await userManager.signinCallback(window.location.href);
          window.history.replaceState({}, '', window.location.pathname);
          if (isMounted) { setUser(callbackUser ?? null); setIsLoading(false); }
          return;
        }
        const loadedUser = await userManager.getUser();
        if (isMounted) { setUser(loadedUser); setIsLoading(false); }
      } catch {
        if (isMounted) { setUser(null); setIsLoading(false); }
      }
    }

    // Le gate `ready` est résolu quelle que soit l'issue de init() (succès,
    // absence de session, ou erreur) : les appels en attente reprennent alors,
    // token présent ou non — le cas « réellement non authentifié » mène à login().
    void init().finally(() => readyRef.current?.resolve());

    const handleUserLoaded = (u: User) => { setUser(u); setIsLoading(false); };
    const handleUserUnloaded = () => { setUser(null); setIsLoading(false); };
    userManager.events.addUserLoaded(handleUserLoaded);
    userManager.events.addUserUnloaded(handleUserUnloaded);
    return () => {
      isMounted = false;
      userManager.events.removeUserLoaded(handleUserLoaded);
      userManager.events.removeUserUnloaded(handleUserUnloaded);
    };
  }, []);

  const login = useCallback(async () => {
    if (loginInFlightRef.current) {
      return;
    }

    const now = Date.now();
    const lastRedirectAt = Number(sessionStorage.getItem(LAST_LOGIN_REDIRECT_AT_KEY) ?? '0');
    if (Number.isFinite(lastRedirectAt) && now - lastRedirectAt < LOGIN_REDIRECT_COOLDOWN_MS) {
      return;
    }

    loginInFlightRef.current = true;
    setIsLoading(true);
    sessionStorage.setItem(LAST_LOGIN_REDIRECT_AT_KEY, String(now));

    try {
      await userManager.signinRedirect();
    } finally {
      loginInFlightRef.current = false;
      setIsLoading(false);
    }
  }, []);

  const logout = useCallback(async () => {
    await userManager.removeUser();
    setUser(null);
  }, []);

  const getAccessToken = useCallback(() => userRef.current?.access_token ?? null, []);

  const whenReady = useCallback(() => readyRef.current!.promise, []);

  const renewToken = useCallback(async () => {
    // Déduplication : des appels concurrents recevant un 401 partagent un seul
    // signinSilent() en vol plutôt que d'en lancer un chacun.
    if (renewInFlightRef.current) {
      return renewInFlightRef.current;
    }
    const renewal = (async () => {
      try {
        const renewed = await userManager.signinSilent();
        setUser(renewed);
        return renewed?.access_token ?? null;
      } catch {
        return null;
      } finally {
        renewInFlightRef.current = null;
      }
    })();
    renewInFlightRef.current = renewal;
    return renewal;
  }, []);

  const value = useMemo(() => ({
    isAuthenticated: Boolean(user && !user.expired),
    isLoading,
    login,
    logout,
    user,
    getAccessToken,
    renewToken,
    whenReady,
  }), [user, isLoading]);

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