/**
 * Provider for navigation-aware request cancellation.
 *
 * Wraps route transitions and automatically aborts all active requests
 * when the user navigates to a new route. This prevents state-update
 * errors from responses arriving after the component unmounts and helps
 * conserve bandwidth by cancelling now-irrelevant requests.
 *
 * Installation:
 * Wrap the `<BrowserRouter>` and route definitions with this provider.
 * It must be inside BrowserRouter (to access useLocation) but can wrap
 * the entire router or just the protected routes.
 *
 * Example:
 *   <BrowserRouter>
 *     <NavigationCancellationProvider>
 *       <Routes>...</Routes>
 *     </NavigationCancellationProvider>
 *   </BrowserRouter>
 *
 * Behavior:
 * - On mount: creates initial AbortController for route 1
 * - On route change: aborts previous controller, creates new one
 * - Subscribers receive fresh signals for each route
 * - Long-lived background tasks can opt-out by not using this context
 */

import { useRef, useCallback, ReactNode } from "react";
import { useLocation } from "react-router-dom";
import {
  NavigationCancellationContext,
  type NavigationCancellationContextType,
  type GetNavigationSignalOptions,
} from "./NavigationCancellationContext";

interface NavigationCancellationProviderProps {
  children: ReactNode;
}

/**
 * Internal record tracking an AbortController and its associated pathname.
 */
interface TrackedController {
  controller: AbortController;
  pathname: string;
}

/**
 * Provider component for navigation-aware cancellation.
 *
 * Detects route changes via useLocation and automatically aborts
 * all AbortSignals obtained during the previous route.
 *
 * Timing contract:
 * - On navigation, ALL tracked controllers for the old pathname are aborted
 *   before any new controller is created for the new pathname.
 * - This ensures that rapidly navigating A → B → C cancels B's requests
 *   even if B's controller was replaced before B's requests checked it.
 * - Strict Mode double-invocation: abort is idempotent, so double-abort is safe.
 *
 * Cancellation contract:
 * - Every signal is associated with the pathname active when getNavigationSignal was called.
 * - On navigation, all controllers whose pathname differs from the new route are aborted.
 * - Requests that intentionally survive navigation (background syncs, polling) must
 *   pass `ignoreCancellation: true` to opt out.
 */
export function NavigationCancellationProvider(
  props: NavigationCancellationProviderProps,
): React.JSX.Element {
  const { children } = props;
  const location = useLocation();

  // All active AbortControllers.
  // On navigation, controllers whose pathname no longer matches the current
  // route are aborted. This fixes the "orphan request" bug where B's requests
  // complete after B → C navigation because B's signal was replaced before
  // B's requests had a chance to check it.
  const controllersRef = useRef<TrackedController[]>([]);

  const getNavigationSignal = useCallback(
    (options?: GetNavigationSignalOptions): AbortSignal => {
      const ignoreCancellation = options?.ignoreCancellation ?? false;

      if (ignoreCancellation) {
        // Opt-out: create a controller that is never aborted by navigation.
        // Caller is responsible for managing its lifecycle.
        const controller = new AbortController();
        return controller.signal;
      }

      const controller = new AbortController();

      controllersRef.current.push({
        controller,
        pathname: location.pathname,
      });

      return controller.signal;
    },
    [],
  );

  // Abort all controllers whose pathname no longer matches the current route.
  // Called on every render where pathname has changed.
  const currentControllers = controllersRef.current;
  for (const tracked of currentControllers) {
    if (tracked.pathname !== location.pathname) {
      tracked.controller.abort();
    }
  }

  // Prune aborted controllers to prevent memory leaks.
  controllersRef.current = currentControllers.filter((t) => !t.controller.signal.aborted);

  const contextValue: NavigationCancellationContextType = {
    getNavigationSignal,
  };

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

