/**
 * @file AuthContext.tsx - Manages authentication state for the RWP application, including the current username, JWT access token, refresh token, and login/logout operations.
 * Uses sessionStorage for persisting auth credentials and a BroadcastChannel to synchronize authentication sessions across multiple browser tabs.
 * One tab acts as the "owner" responsible for refreshing the access token, while non-owner tabs defer their refresh interval and fall back to ownership if the owner becomes unavailable.
 * Also integrates an idle timer (with cross-tab leader election) to automatically log out inactive users.
 */

import { useNavigate } from 'react-router-dom';
import {
  createContext,
  useEffect,
  useState,
  type ReactNode,
  useContext,
  useRef,
  useCallback,
} from 'react';
import {
  INACTIVITY_TIMEOUT,
  REFRESH_INTERVAL,
  AUTH_SESSION_SYNC_CHANNEL_NAME,
  AUTH_SESSION_SYNC_TIMEOUT,
  AUTH_SESSION_SYNC_MIN_LOADING_DISPLAY_TIME,
  AUTH_SESSION_SYNC_RECEIVED_EVENT_NAME,
  AUTH_SESSION_SYNC_UPDATED_EVENT_NAME,
  AUTH_SESSION_SYNC_REFRESHED_EVENT_NAME,
  AUTH_SESSION_SYNC_OWNERSHIP_FALLBACK_DELAY,
} from '../constants.ts';
import { useIdleTimer } from 'react-idle-timer';
import {
  clearStoredProcessId,
  clearStoredQueueId,
} from '../utils/localStorage.ts';
import { unlockWorkItem } from '../api/workItem.ts';
import { toast } from 'react-toastify';
import { useTranslation } from 'react-i18next';
import {
  clearAuthKeys,
  getAccessToken,
  getRefreshToken,
  getUserName,
  logIn,
  type LogInRequest,
  type LogInResponse,
  logOut,
  refreshAccessToken,
  type SessionSyncMessage,
  setAuthKeys,
} from '../services/authService.ts';

/** Shape of the authentication context value provided to consumers. */
interface AuthContextProps {
  /** The currently authenticated user's name, or null if not logged in */
  username: string | null;

  /** The current JWT access token, or null if not authenticated */
  token: string | null;

  /** True while the provider is waiting for a cross-tab session sync response */
  isInitializing: boolean;

  /**
   * Monotonically increasing key that increments when the session identity
   * changes (e.g. a different user logs in from another tab). Consumers can
   * use this as a React key to force re-mounting.
   */
  sessionKey: number;

  /** Performs API login and broadcasts the new session to other tabs */
  login: typeof logIn; // authService.ts

  /** Performs API logout, unlocks any open work item, and broadcasts logout to other tabs */
  logout: () => Promise<void>;
}

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

/**
 * Provider component that manages authentication state and exposes login/logout operations to the component tree.
 *
 * Responsible for:
 * - Persisting and hydrating auth credentials from sessionStorage
 * - Synchronizing sessions across browser tabs via BroadcastChannel
 * - Periodically refreshing the JWT access token
 * - Detecting user inactivity and triggering automatic logout
 *
 * @param children Child ReactNodes to be rendered within context
 */
export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const navigate = useNavigate();

  const [username, setUsername] = useState<string | null>(getUserName());
  const [token, setToken] = useState<string | null>(getAccessToken());

  // If there is no existing access token, the provider starts in an "initializing" state while it waits for another tab to share its session
  const [isInitializing, setIsInitializing] = useState(() => {
    return !getAccessToken();
  });
  const [sessionKey, setSessionKey] = useState(0);
  const isLoggingOut = useRef(false);
  const channelRef = useRef<BroadcastChannel | null>(null);

  // The "session owner" is the tab responsible for proactively refreshing the JWT token
  // Tabs that received their session from another tab start as non-owners and defer refresh responsibility
  const [isSessionOwner, setIsSessionOwner] = useState(() => {
    return !!getAccessToken();
  });
  // Bumped when another tab refreshes or claims ownership, causing the token refresh effect to re-evaluate its internal timing
  const [refreshTrigger, setRefreshTrigger] = useState(0);

  const { t } = useTranslation(undefined, {
    keyPrefix: 'contexts.auth_context',
  });

  // Cross-tab session sync via BroadcastChannel
  // Opens a channel, listens for session messages from other tabs, and broadcasts session changes made in this tab
  useEffect(() => {
    const channel = new BroadcastChannel(AUTH_SESSION_SYNC_CHANNEL_NAME);
    channelRef.current = channel;

    /**
     * Persists received auth data to sessionStorage and updates React state.
     *
     * @param payload IWS5 API login response data
     */
    const updateAuthKeys = (payload: LogInResponse) => {
      const { token, refreshToken, userName } = payload;

      setAuthKeys({
        token,
        refreshToken,
        userName,
      });
      setUsername(userName);
      setToken(token);
    };

    // Timeout to stop waiting for session data if no other tabs respond
    let syncTimeoutId: ReturnType<typeof setTimeout> | null = null;
    let minDisplayTimeoutId: ReturnType<typeof setTimeout> | null = null;
    const startTime = Date.now();

    /**
     * Ends the initializing state, but ensures the loading indicator is shown for at least AUTH_SESSION_SYNC_MIN_LOADING_DISPLAY_TIME to prevent a brief flash of content.
     */
    const finishInitializing = () => {
      const elapsed = Date.now() - startTime;
      const remaining = AUTH_SESSION_SYNC_MIN_LOADING_DISPLAY_TIME - elapsed;

      if (remaining > 0) {
        minDisplayTimeoutId = setTimeout(() => {
          setIsInitializing(false);
        }, remaining);
      } else {
        setIsInitializing(false);
      }
    };

    channel.onmessage = (event: MessageEvent<SessionSyncMessage>) => {
      const data = event.data;

      switch (data.type) {
        case 'REQUEST_SESSION': {
          // Another tab is asking for session data
          const token = getAccessToken();
          const refreshToken = getRefreshToken();
          const userName = getUserName();

          if (token && refreshToken && userName) {
            channel.postMessage({
              type: 'SESSION_RECEIVED',
              payload: {
                token,
                refreshToken,
                userName,
              },
            });
          }

          break;
        }
        case 'SESSION_RECEIVED': {
          // Received session data from another tab
          const currentToken = getAccessToken();
          const payload = data.payload;

          if (!currentToken && payload.token) {
            updateAuthKeys(payload);

            setIsSessionOwner(false);

            // Notify components that a session was received
            window.dispatchEvent(
              new CustomEvent(AUTH_SESSION_SYNC_RECEIVED_EVENT_NAME)
            );
          }

          if (syncTimeoutId) {
            clearTimeout(syncTimeoutId);
          }
          finishInitializing();

          break;
        }
        case 'SESSION_UPDATED': {
          // Another tab logged in again (possibly with new credentials)
          const payload = data.payload;

          updateAuthKeys(payload);

          setIsSessionOwner(false);
          setRefreshTrigger((prev) => prev + 1);
          setSessionKey((prev) => prev + 1);

          // Notify components that the session was updated
          window.dispatchEvent(
            new CustomEvent(AUTH_SESSION_SYNC_UPDATED_EVENT_NAME)
          );

          break;
        }
        case 'SESSION_REFRESHED': {
          // Another tab refreshed the session
          const payload = data.payload;

          updateAuthKeys(payload);

          setIsSessionOwner(false);
          setRefreshTrigger((prev) => prev + 1);

          // Notify ReauthProvider that the session was refreshed
          window.dispatchEvent(
            new CustomEvent(AUTH_SESSION_SYNC_REFRESHED_EVENT_NAME)
          );

          break;
        }
        case 'OWNERSHIP_CLAIMED': {
          // Another tab claimed ownership
          setIsSessionOwner(false);
          setRefreshTrigger((prev) => prev + 1);

          break;
        }
        case 'IDLE_LOGOUT_NEEDED': {
          // Idle timeout leader tab is on /processing and can't log out (may have unsaved work)
          // Eligible non-processing tabs volunteer to perform the logout instead
          if (
            !window.location.pathname.startsWith('/processing') &&
            getAccessToken()
          ) {
            const jitter =
              Math.random() * (AUTH_SESSION_SYNC_OWNERSHIP_FALLBACK_DELAY / 2);
            setTimeout(() => {
              // Check if still logged in (another tab may have already logged out)
              if (getAccessToken()) {
                if (channelRef.current) {
                  channelRef.current.postMessage({
                    type: 'OWNERSHIP_CLAIMED',
                  });
                }

                performIdleLogout();
              }
            }, jitter);
          }

          break;
        }
        case 'LOGOUT': {
          // Another tab logged out
          if (!isLoggingOut.current) {
            clearAuthKeys();
            navigate('/login');
          }

          break;
        }
      }
    };

    // Defer the session request by one frame so the loading UI renders before we ask other tabs for their session
    // If no response arrives within the sync timeout, we finish initializing (the user will see the login page)
    if (isInitializing) {
      requestAnimationFrame(() => {
        channel.postMessage({
          type: 'REQUEST_SESSION',
        });

        syncTimeoutId = setTimeout(() => {
          finishInitializing();
        }, AUTH_SESSION_SYNC_TIMEOUT);
      });
    }

    return () => {
      if (syncTimeoutId) {
        clearTimeout(syncTimeoutId);
      }
      if (minDisplayTimeoutId) {
        clearTimeout(minDisplayTimeoutId);
      }
      channelRef.current = null;
      channel.close();
    };
  }, [navigate, isInitializing]);

  // Periodically refresh the JWT access token
  // The session owner refreshes at the standard REFRESH_INTERVAL, non-owner tabs add a delay plus random jitter so the owner has time to refresh first
  // If the owner is gone, a non-owner will eventually claim ownership
  useEffect(() => {
    if (!username || !token) {
      return;
    }

    // Non-owners add extra delay + jitter to avoid racing with the owner
    const jitter = isSessionOwner
      ? 0
      : Math.random() * AUTH_SESSION_SYNC_OWNERSHIP_FALLBACK_DELAY;
    const intervalTime = isSessionOwner
      ? REFRESH_INTERVAL
      : REFRESH_INTERVAL + AUTH_SESSION_SYNC_OWNERSHIP_FALLBACK_DELAY + jitter;

    const interval = setInterval(() => {
      // If we're not the owner by this point, the owner must be gone
      if (!isSessionOwner) {
        if (channelRef.current) {
          channelRef.current.postMessage({
            type: 'OWNERSHIP_CLAIMED',
          });
        }

        setIsSessionOwner(true);
      }

      // Perform API token refresh
      refreshAccessToken({
        userName: username as string,
        refreshToken: getRefreshToken() as string,
      })
        .then((newToken) => {
          setToken(newToken);

          const newRefreshToken = getRefreshToken();
          if (channelRef.current && newRefreshToken) {
            channelRef.current.postMessage({
              type: 'SESSION_REFRESHED',
              payload: {
                token: newToken,
                refreshToken: newRefreshToken,
                userName: username,
              },
            });
          }
        })
        .catch(() => {
          clearAuthKeys();
          setToken(null);
        });
    }, intervalTime);

    return () => clearInterval(interval);
  }, [username, token, isSessionOwner, refreshTrigger]);

  /**
   * Authenticates the user via the IWS5 API, updates local state, and broadcasts the new session to all other tabs via BroadcastChannel.
   */
  const login = useCallback(
    async ({ userName, password, domain }: LogInRequest) => {
      // Perform API login
      await logIn({
        userName,
        password,
        domain,
      });

      const newToken = getAccessToken();
      const newRefreshToken = getRefreshToken();
      const newUserName = getUserName();

      // Update context variables
      setUsername(newUserName);
      setToken(newToken);

      setIsSessionOwner(true);

      // Broadcast new session to other tabs
      if (channelRef.current && newToken && newRefreshToken) {
        channelRef.current.postMessage({
          type: 'SESSION_UPDATED',
          payload: {
            token: newToken,
            refreshToken: newRefreshToken,
            userName: newUserName,
          },
        });
      }
    },
    []
  );

  /**
   * Logs out the current user.
   * Unlocks any open work item (if on the processing page), calls the API logout endpoint, broadcasts a LOGOUT message to other tabs, and clears all stored auth and process state.
   */
  const logout = useCallback(async () => {
    if (isLoggingOut.current || !username) {
      return;
    }

    isLoggingOut.current = true;

    try {
      // Unlock open work item
      if (window.location.pathname.startsWith('/processing')) {
        const workItemReferenceId = Number(
          window.location.pathname.split('/').slice(-1)[0]
        );
        await unlockWorkItem(workItemReferenceId).catch(() => {
          toast.warn(t('functions.log_out.errors.failed_to_unlock'));
        });
      }

      // Perform API logout
      await logOut({
        userName: username,
      });
    } finally {
      // Broadcast logout to other tabs
      if (channelRef.current) {
        channelRef.current.postMessage({ type: 'LOGOUT' });
      }

      clearAuthKeys();
      clearStoredProcessId();
      clearStoredQueueId();

      // Clear context variables
      setUsername(null);
      setToken(null);

      isLoggingOut.current = false;
    }
  }, [username]);

  /**
   * Displays an inactivity toast and performs logout and redirect.
   */
  const performIdleLogout = () => {
    toast.info(t('hooks.use_idle_timer.on_idle.message'));
    logout().then(() => {
      navigate('/login');
    });
  };

  /**
   * Idle callback invoked only on the leader tab.
   * If the leader can safely log out (not on /processing), it does so directly.
   * Otherwise, it asks another tab to handle the logout via IDLE_LOGOUT_NEEDED.
   */
  const handleIdle = () => {
    if (!isLeader() || !token) {
      return;
    }

    if (!window.location.pathname.startsWith('/processing')) {
      performIdleLogout();
    } else {
      // Leader is on /processing, another tab must handle logout
      if (channelRef.current) {
        channelRef.current.postMessage({
          type: 'IDLE_LOGOUT_NEEDED',
        });
      }
    }
  };

  // Cross-tab idle timer with leader election
  // Only the leader fires onIdle
  const { isLeader } = useIdleTimer({
    timeout: INACTIVITY_TIMEOUT,
    onIdle: handleIdle,
    crossTab: true,
    syncTimers: AUTH_SESSION_SYNC_TIMEOUT,
    leaderElection: true,
    events: [
      'mousemove',
      'mousedown',
      'keydown',
      'touchstart',
      'click',
      'scroll',
    ],
    disabled: !token,
    debounce: 500,
  });

  return (
    <AuthContext.Provider
      value={{ username, token, isInitializing, sessionKey, login, logout }}
    >
      {children}
    </AuthContext.Provider>
  );
};

/**
 * Hook to access the AuthContext. Must be called within an AuthProvider.
 *
 * @returns The current authentication state and login/logout functions
 * @throws Error if called outside an AuthProvider
 */
export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within a AuthProvider');
  }
  return context;
};
