/**
 * Authentication context and provider.
 *
 * Manages the user's authenticated state and exposes `login`, `logout`, and
 * `isAuthenticated` through `useAuth()`.
 *
 * The authentication model is **cookie-based**: the backend sets a session
 * cookie (`bangui_session`) on successful login, and the frontend automatically
 * includes it in all requests via `credentials: "include"`. The backend is the
 * authority on session validity; the frontend is authenticated when the backend
 * accepts the request (returns 2xx) and not authenticated when the backend
 * rejects it (returns 401/403).
 *
 * On app mount, the provider validates the cached session state with the backend
 * by calling `GET /api/auth/session`. While this check is in flight, a loading
 * indicator is shown. If the backend returns 401/403, the session is cleared.
 * If a network error occurs, the user is not logged out (the backend may be
 * temporarily unreachable).
 *
 * The `isAuthenticated` state persists in `sessionStorage` to survive page
 * refreshes within the browser tab but is cleared on tab close. The session
 * cookie itself persists according to the backend's cookie settings.
 *
 * **Cross-Tab Synchronization:**
 * The provider uses BroadcastChannel API to synchronize logout events across
 * tabs in real-time. On logout, a message is broadcast to all other tabs so they
 * immediately reflect the logout state without requiring a page refresh or API call.
 * If the browser does not support BroadcastChannel, the storage event listener
 * serves as a fallback (though it only fires when storage is changed in another tab).
 *
 * **Auth Error Handling & Deduplication:**
 * AuthProvider registers two auth error handlers to ensure that 401/403 errors
 * are never silently swallowed:
 * - `setUnauthorizedHandler()` in `api/client.ts` — handles auth errors from the
 *   API client layer before they reach hooks
 * - `setAuthErrorHandler()` in `utils/fetchError.ts` — handles auth errors that
 *   reach hooks and ensures deterministic error handling with fallback logging
 *
 * A module-level `isLoggingOut` flag in each file prevents double-firing: the
 * first handler to fire sets its flag, and the second checks it before invoking
 * its handler. Both flags are reset in `handleSessionExpired()` so future 401s
 * can trigger the handlers again after the user reaches the login page.
 *
 * This dual-handler approach with deduplication ensures auth errors are caught
 * at multiple layers, preventing silent error loss regardless of where the
 * error occurs, while avoiding the React state mutation race caused by
 * concurrent logout dispatches.
 */

import React, {
  createContext,
  useCallback,
  useEffect,
  useMemo,
  useState,
} from "react";
import { useNavigate } from "react-router-dom";
import * as authApi from "../api/auth";
import { setUnauthorizedHandler, resetLogoutState, clearSessionCorrelationId } from "../api/client";
import { setAuthErrorHandler, resetLogoutState as resetFetchErrorLogoutState } from "../utils/fetchError";
import { STORAGE_KEY_AUTHENTICATED } from "../utils/constants";
import { SessionValidationLoading } from "../components/SessionValidationLoading";
import { useSessionValidation } from "../hooks/useSessionValidation";

// ---------------------------------------------------------------------------
// Context
// ---------------------------------------------------------------------------

export interface AuthContextValue {
  /** `true` when the backend considers the session valid. */
  isAuthenticated: boolean;
  /** `true` while the session is being validated on app mount. */
  isValidating: boolean;
  /**
   * Authenticate with the master password.
   * Throws an `ApiError` on failure.
   */
  login: (password: string) => Promise<void>;
  /** Revoke the current session and clear local state. */
  logout: () => Promise<void>;
}

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

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const BROADCAST_CHANNEL_NAME = "bangui_auth" as const;
const BROADCAST_MESSAGE_TYPE_LOGOUT = "logout" as const;

// ---------------------------------------------------------------------------
// Provider
// ---------------------------------------------------------------------------

/**
 * Wraps the application and provides authentication state to all children.
 *
 * Place this inside `<FluentProvider>` and `<BrowserRouter>` so all
 * descendants can call `useAuth()`.
 *
 * On mount, validates the cached session with the backend. While validation
 * is in progress, a loading indicator is shown. If validation fails with 401,
 * the user is logged out. Network errors do not cause logout.
 *
 * Listens for cross-tab logout events via BroadcastChannel (or storage events
 * as fallback) to immediately reflect logout in other tabs.
 */
export function AuthProvider({
  children,
}: {
  children: React.ReactNode;
}): React.JSX.Element {
  const [isAuthenticated, setIsAuthenticated] = useState<boolean>(() => {
    const stored = sessionStorage.getItem(STORAGE_KEY_AUTHENTICATED);
    return stored === "true";
  });
  const [isValidating, setIsValidating] = useState<boolean>(isAuthenticated);
  const navigate = useNavigate();

  const handleSessionExpired = useCallback((): void => {
    sessionStorage.removeItem(STORAGE_KEY_AUTHENTICATED);
    setIsAuthenticated(false);
    // Reset deduplication flags so future 401s can trigger handlers again.
    resetLogoutState();
    resetFetchErrorLogoutState();
    navigate("/login", { replace: true });
  }, [navigate]);

  const handleSessionValid = useCallback((): void => {
    // Session is already set from sessionStorage; no action needed.
  }, []);

  const handleValidationError = useCallback(
    (error: Error): void => {
      // Network error — log but don't logout.
      console.warn("Session validation network error:", error);
    },
    [],
  );

  // Handle storage events from other tabs (fallback sync mechanism)
  useEffect((): (() => void) => {
    const handleStorageChange = (event: StorageEvent): void => {
      // Only respond to changes in our authentication key
      if (event.key === STORAGE_KEY_AUTHENTICATED) {
        // If the storage was cleared in another tab, logout this tab
        if (event.newValue === null) {
          handleSessionExpired();
        }
      }
    };

    window.addEventListener("storage", handleStorageChange);
    return (): void => {
      window.removeEventListener("storage", handleStorageChange);
    };
  }, [handleSessionExpired]);

  // Handle BroadcastChannel messages for real-time cross-tab sync
  useEffect((): (() => void) => {
    let channel: BroadcastChannel | null = null;

    try {
      channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
      channel.onmessage = (event: MessageEvent): void => {
        if (event.data?.type === BROADCAST_MESSAGE_TYPE_LOGOUT) {
          handleSessionExpired();
        }
      };
    } catch {
      // BroadcastChannel not supported; storage event listener serves as fallback
    }

    return (): void => {
      if (channel) {
        channel.close();
      }
    };
  }, [handleSessionExpired]);

  useEffect((): (() => void) => {
    setUnauthorizedHandler((): void => {
      handleSessionExpired();
    });
    setAuthErrorHandler((): void => {
      handleSessionExpired();
    });
    return (): void => {
      setUnauthorizedHandler(null);
      setAuthErrorHandler(null);
    };
  }, [handleSessionExpired]);

  const validationResult = useSessionValidation(
    handleSessionValid,
    handleSessionExpired,
    handleValidationError,
  );

  useEffect(() => {
    setIsValidating(validationResult.isLoading);
  }, [validationResult.isLoading]);

  const login = useCallback(async (password: string): Promise<void> => {
    await authApi.login(password);
    sessionStorage.setItem(STORAGE_KEY_AUTHENTICATED, "true");
    setIsAuthenticated(true);
  }, []);

  const logout = useCallback(async (): Promise<void> => {
    try {
      await authApi.logout();
    } finally {
      // Always clear local state even if the API call fails (e.g. expired session).
      sessionStorage.removeItem(STORAGE_KEY_AUTHENTICATED);
      setIsAuthenticated(false);
      clearSessionCorrelationId();

      // Broadcast logout event to other tabs for immediate sync
      try {
        const channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
        channel.postMessage({ type: BROADCAST_MESSAGE_TYPE_LOGOUT });
        channel.close();
      } catch {
        // BroadcastChannel not supported; storage event will fire on other tabs automatically
      }
    }
  }, []);

  const value = useMemo<AuthContextValue>(
    () => ({ isAuthenticated, isValidating, login, logout }),
    [isAuthenticated, isValidating, login, logout],
  );

  // Show loading spinner while validating session on mount.
  if (isValidating && isAuthenticated) {
    return <SessionValidationLoading />;
  }

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