import {
  createContext,
  type FC,
  type PropsWithChildren,
  useContext,
  useEffect,
  useReducer,
} from 'react';

import * as Sentry from '@sentry/react';
import i18n from 'i18next';

import api from 'src/config/api';
import { appInitializer } from 'src/config/app-initializer';
import { tokenManager } from 'src/config/tokens';
import { type Language } from 'src/constants/languages';
import * as authService from 'src/services/authService';
import * as modulesService from 'src/services/modulesService';
import * as rolesAndPermissionsService from 'src/services/rolesAndPermissions';
import { changeLanguageUser } from 'src/services/usersService';
import { type CommunityFeature } from 'src/types/communityFeatures';
import { type FeatureFlag, FeatureFlags } from 'src/types/featureFlags';
import { type MeInstance } from 'src/types/instance';
import {
  type InitMFAResponse,
  type LoginData,
  type LoginOTPData,
  type SSOLoginData,
} from 'src/types/login';
import { type Module } from 'src/types/modules';
import { type MeUser } from 'src/types/user';
import { kickOutUserThrottled } from 'src/utils/api';
import { setDocumentLanguage } from 'src/utils/languages';
import {
  clearUser,
  LogEvents,
  LoginEventTypes,
  logEvent,
  setUser,
} from 'src/utils/logging';
import { isOnMaintenance } from 'src/utils/maintenance';
import { getTranslationLanguage } from 'src/utils/translations';

type State = {
  isInitialized: boolean;
  isAuthenticated: boolean;
  user: MeUser | null;
  instance: MeInstance | null;
  modules: Module[] | null;
  featureFlags: FeatureFlag | null;
  communityFeatures?: CommunityFeature[] | null;
  permissions: string[];
};

type AuthContextValue = State & {
  platform: 'JWT' | null;
  login: ((loginData: LoginData) => Promise<void>) | null;
  loginViaJanus: ((loginData: LoginData) => Promise<void>) | null;
  loginWithMFA: ((loginData: LoginData) => Promise<InitMFAResponse>) | null;
  loginOTP: ((loginData: LoginOTPData) => Promise<void>) | null;
  loginSaml: Function | null;
  loginWithAzure: Function | null;
  loginWithOkta: Function | null;
  loginWithGoogle: Function | null;
  logout: ((notifyApi?: boolean) => Promise<void>) | null;
  register:
    | ((email: string, name: string, password: string) => Promise<void>)
    | null;
  updateInstance: ((instance: MeInstance) => Promise<void>) | null;
  permissions: string[];
  initialize: (() => void) | null;
  updateUser: ((user: MeUser) => Promise<void>) | null;
  updateUserRolesAndPermissions: (() => void) | null;
  communityFeatures?: CommunityFeature[] | null;
};

type InitializeAction = {
  type: 'INITIALIZE';
  payload: Omit<State, 'isInitialized'>;
};

type LoginAction = {
  type: 'LOGIN';
  payload: {
    user: MeUser;
    instance: MeInstance;
    type: LoginEventTypes;
    modules: Module[] | null;
    featureFlags: FeatureFlag | null;
    communityFeatures?: CommunityFeature[] | null;
    permissions: string[];
  };
};

type LogoutAction = {
  type: 'LOGOUT';
};

type RegisterAction = {
  type: 'REGISTER';
  payload: {
    user: MeUser;
  };
};

type UpdateInstanceAction = {
  type: 'UPDATE_INSTANCE';
  payload: {
    instance: MeInstance;
  };
};

type UpdateUserAction = {
  type: 'UPDATE_USER';
  payload: {
    user: MeUser;
  };
};

type UpdateUserRolesAndPermissionsAction = {
  type: 'UPDATE_USER_ROLES_AND_PERMISSIONS';
  payload: {
    user: MeUser;
    modules: Module[];
    communityFeatures?: CommunityFeature[] | null;
    permissions: string[];
  };
};

type Action =
  | InitializeAction
  | LoginAction
  | LogoutAction
  | RegisterAction
  | UpdateInstanceAction
  | UpdateUserAction
  | UpdateUserRolesAndPermissionsAction;

const initialState: State = {
  isAuthenticated: false,
  isInitialized: false,
  user: null,
  instance: null,
  modules: null,
  featureFlags: null,
  communityFeatures: null,
  permissions: [],
};

const setSession = (
  accessToken: string | null,
  refreshToken?: string | null,
): void => {
  if (accessToken || refreshToken) {
    tokenManager.setTokens(accessToken as string, refreshToken as string);
  } else {
    tokenManager.removeTokens();
  }
};

const setLanguage = (user: MeUser) => {
  const language = getTranslationLanguage(user);

  if (!user.hasBeenLogged) {
    changeLanguageUser(user.id, language);
  }

  setTimeout(() => i18n.changeLanguage(language), 0); // timeout is needed to avoid this error https://reactjs.org/link/setstate-in-render
  setDocumentLanguage(language as Language);
};

const getModulesAvailable = async (
  featureFlags: FeatureFlag | null,
): Promise<Module[]> => {
  try {
    if (featureFlags?.[FeatureFlags.CERBERUS_ENABLED]) {
      const modulesResponse =
        await rolesAndPermissionsService.getModulesAvailable();
      return modulesResponse.data.modules;
    } else {
      const modulesResponse = await modulesService.getModulesAvailable();
      return modulesResponse.data.modules;
    }
  } catch (error) {
    Sentry.withScope(scope => {
      scope.setContext('modules', {
        service: featureFlags?.[FeatureFlags.CERBERUS_ENABLED]
          ? 'cerberus'
          : 'legacy',
      });
      Sentry.captureException(error);
    });
    return [];
  }
};

const resolveJanusBootstrapData = async (): Promise<{
  featureFlags: FeatureFlag | null;
  communityFeatures: CommunityFeature[] | null;
  user: MeUser;
  instance: MeInstance;
  permissions: string[];
}> => {
  try {
    const [ffResponse, cfResponse, meResponse] = await Promise.all([
      authService.getJanusFeatureFlags(),
      authService.getJanusCommunityFeatures(),
      authService.getJanusMe(),
    ]);

    return {
      featureFlags: ffResponse.data.featureFlags,
      communityFeatures: cfResponse.data.communityFeatures,
      user: meResponse.data.user,
      instance: meResponse.data.instance,
      permissions: meResponse.data.permissions,
    };
  } catch (error) {
    // Capture before rethrowing: initialize() is wrapped by appInitializer.run,
    // but the 6 login flows surface errors straight to the UI caller and would
    // otherwise be invisible to Sentry.
    Sentry.captureException(error);
    throw error;
  }
};

const handlers: Record<string, (state: State, action: Action) => State> = {
  INITIALIZE: (state: State, action: Action): State => {
    const {
      isAuthenticated,
      user,
      instance,
      modules,
      featureFlags,
      communityFeatures,
      permissions,
    } = (action as InitializeAction).payload;

    if (user) {
      setUser(user, instance!);
      setLanguage(user);
    }

    return {
      ...state,
      isAuthenticated,
      isInitialized: true,
      user,
      instance,
      modules,
      featureFlags,
      communityFeatures,
      permissions,
    };
  },
  LOGIN: (state: State, action: Action): State => {
    const {
      user,
      instance,
      type,
      modules,
      featureFlags,
      communityFeatures,
      permissions,
    } = (action as LoginAction).payload;

    setUser(user, instance);
    setLanguage(user);
    logEvent(LogEvents.USER_LOGIN, {
      userId: user?.id,
      login: type,
    });

    return {
      ...state,
      isAuthenticated: true,
      user,
      instance,
      modules,
      featureFlags,
      communityFeatures,
      permissions,
    };
  },
  LOGOUT: (state: State): State => {
    clearUser();

    return {
      ...state,
      isAuthenticated: false,
      user: null,
      instance: null,
    };
  },
  REGISTER: (state: State, action: Action): State => {
    const { user } = (action as RegisterAction).payload;

    return {
      ...state,
      isAuthenticated: true,
      user,
    };
  },
  UPDATE_INSTANCE: (state: State, action: Action): State => {
    const { instance } = (action as UpdateInstanceAction).payload;

    return {
      ...state,
      instance,
    };
  },
  UPDATE_USER: (state: State, action: Action): State => {
    const { user } = (action as UpdateUserAction).payload;

    return {
      ...state,
      user: {
        ...state.user!,
        ...user,
      },
    };
  },
  UPDATE_USER_ROLES_AND_PERMISSIONS: (state: State, action: Action): State => {
    const { user, modules, communityFeatures, permissions } = (
      action as UpdateUserRolesAndPermissionsAction
    ).payload;

    return {
      ...state,
      user,
      modules,
      communityFeatures,
      permissions,
    };
  },
};

const reducer = (state: State, action: Action): State =>
  handlers[action.type] ? handlers[action.type](state, action) : state;

const AuthContext = createContext<AuthContextValue>({
  ...initialState,
  platform: null,
  login: null,
  loginViaJanus: null,
  loginWithMFA: null,
  loginOTP: null,
  loginSaml: null,
  loginWithAzure: null,
  loginWithOkta: null,
  loginWithGoogle: null,
  logout: null,
  register: null,
  updateInstance: null,
  permissions: [],
  initialize: null,
  updateUser: null,
  updateUserRolesAndPermissions: null,
  communityFeatures: null,
});

export const AuthProvider: FC<PropsWithChildren> = props => {
  const { children } = props;
  const [state, dispatch] = useReducer(reducer, initialState);

  const initialize = (): void => {
    if (isOnMaintenance()) {
      return;
    }

    const { accessToken, refreshToken } = tokenManager.getCorrectToken();

    if (!accessToken && !refreshToken) {
      // No tokens in storage — user is definitively not authenticated
      dispatch({
        type: 'INITIALIZE',
        payload: {
          isAuthenticated: false,
          user: null,
          instance: null,
          modules: null,
          featureFlags: null,
          communityFeatures: null,
          permissions: [],
        },
      });
      return;
    }

    void appInitializer.run(
      async () => {
        // Re-read tokens on each attempt: they may have been refreshed between retries
        const {
          accessToken: currentAccessToken,
          refreshToken: currentRefreshToken,
        } = tokenManager.getCorrectToken();

        setSession(currentAccessToken, currentRefreshToken);

        if (!currentRefreshToken) {
          const response = await authService.tokenLogin(currentAccessToken!);
          const { accessToken: newAccessToken, refreshToken: newRefreshToken } =
            response.data;
          setSession(newAccessToken, newRefreshToken);
        }

        const { featureFlags, communityFeatures, user, instance, permissions } =
          await resolveJanusBootstrapData();

        const modules = await getModulesAvailable(featureFlags);

        if (modules.length === 0) {
          // Remove tokens synchronously before logout() — kickOutUserThrottled()
          // inside logout() is not awaited, so tokens may not be cleared yet when
          // bootFn returns. Explicit removal here guarantees a clean state.
          tokenManager.removeTokens();
          await logout();
          // Dispatch INITIALIZE so isInitialized flips to true and the app exits
          // LoadingScreen. Without this, isInitialized stays false permanently
          // because AppInitializer sees a successful bootFn and exits the loop
          // without anyone ever dispatching INITIALIZE.
          dispatch({
            type: 'INITIALIZE',
            payload: {
              isAuthenticated: false,
              user: null,
              instance: null,
              modules: null,
              featureFlags: null,
              communityFeatures: null,
              permissions: [],
            },
          });
          return;
        }

        dispatch({
          type: 'INITIALIZE',
          payload: {
            isAuthenticated: true,
            user,
            instance,
            modules,
            featureFlags,
            communityFeatures,
            permissions,
          },
        });
      },
      {
        hasValidTokens: () => !!tokenManager.getCorrectToken().refreshToken,
        onAuthFailure: err => {
          // Re-read tokens at call time — the closure captures stale values from
          // when initialize() first ran, which may be outdated after retries.
          const {
            accessToken: currentAccessToken,
            refreshToken: currentRefreshToken,
          } = tokenManager.getCorrectToken();
          Sentry.withScope(scope => {
            scope.setContext('initialize', {
              isMobileChildSession: [
                'org-chart-mobile',
                'events-nemak-mobile',
                'requests-banbajio-mobile',
                'scorm-courses-mobile',
                'recognitions-nemak-mobile',
                'documents-lacomer-mobile',
                'sports-pool-mobile',
                'payroll-mobility-ado-mobile',
              ].some(p => location.pathname.includes(p)),
              hasTokens: !!(currentAccessToken || currentRefreshToken),
            });
            Sentry.captureException(err);
          });
          dispatch({
            type: 'INITIALIZE',
            payload: {
              isAuthenticated: false,
              user: null,
              instance: null,
              modules: null,
              featureFlags: null,
              communityFeatures: null,
              permissions: [],
            },
          });
        },
      },
    );
  };

  useEffect(() => {
    initialize();
    return () => {
      appInitializer.abort();
    };
  }, []);

  const login = async (loginData: LoginData) => {
    const response = await authService.login(loginData);
    const {
      accessToken,
      refreshToken,
      user: { hasBeenLogged },
    } = response.data;
    setSession(accessToken, refreshToken);

    const { featureFlags, communityFeatures, user, instance, permissions } =
      await resolveJanusBootstrapData();

    const modules = await getModulesAvailable(featureFlags);

    const language = hasBeenLogged ? user.language : i18n.language;
    setDocumentLanguage(language as Language);
    if (hasBeenLogged) {
      i18n.changeLanguage(language);
    } else {
      await changeLanguageUser(user.id, language);
    }

    dispatch({
      type: 'LOGIN',
      payload: {
        user: { ...user, language },
        instance,
        type: LoginEventTypes.REGULAR,
        modules,
        featureFlags,
        communityFeatures,
        permissions,
      },
    });
  };

  const loginViaJanus = async (loginData: LoginData) => {
    const response = await authService.loginViaJanus(loginData);
    const { accessToken, refreshToken } = response.data;
    // Guard: the Janus /auth/login response shape is not yet contract-locked.
    // Fail loud here rather than store undefined tokens and surface a phantom
    // logged-in state where every subsequent authenticated request fails.
    if (!accessToken || !refreshToken) {
      throw new Error('Janus login response missing tokens');
    }
    setSession(accessToken, refreshToken);

    const { featureFlags, communityFeatures, user, instance, permissions } =
      await resolveJanusBootstrapData();

    const modules = await getModulesAvailable(featureFlags);

    const { hasBeenLogged } = user;
    const language = hasBeenLogged ? user.language : i18n.language;
    setDocumentLanguage(language as Language);
    if (hasBeenLogged) {
      i18n.changeLanguage(language);
    } else {
      await changeLanguageUser(user.id, language);
    }

    dispatch({
      type: 'LOGIN',
      payload: {
        user: { ...user, language },
        instance,
        type: LoginEventTypes.REGULAR,
        modules,
        featureFlags,
        communityFeatures,
        permissions,
      },
    });
  };

  const loginWithMFA = async (
    loginData: LoginData,
  ): Promise<InitMFAResponse> => {
    const response = await authService.initiate2FA(loginData);

    const { init2faToken } = response.data;

    return { init2faToken };
  };

  const loginOTP = async (loginData: LoginOTPData) => {
    const response = await authService.loginOTP(loginData);
    const { accessToken, refreshToken } = response.data;
    setSession(accessToken, refreshToken);

    const { featureFlags, communityFeatures, user, instance, permissions } =
      await resolveJanusBootstrapData();

    const modules = await getModulesAvailable(featureFlags);

    dispatch({
      type: 'LOGIN',
      payload: {
        user,
        instance,
        type: LoginEventTypes.OTP,
        modules,
        featureFlags,
        communityFeatures,
        permissions,
      },
    });
  };

  const loginWithAzure = async (loginData: SSOLoginData) => {
    const response = await authService.azureLogin(loginData);
    const { accessToken, refreshToken } = response.data;
    setSession(accessToken, refreshToken);

    const { featureFlags, communityFeatures, user, instance, permissions } =
      await resolveJanusBootstrapData();

    const modules = await getModulesAvailable(featureFlags);

    dispatch({
      type: 'LOGIN',
      payload: {
        user,
        instance,
        type: LoginEventTypes.AZURE,
        modules,
        featureFlags,
        communityFeatures,
        permissions,
      },
    });
  };

  const loginSaml = async (bearerToken: string) => {
    const response = await authService.tokenLogin(bearerToken);
    const { accessToken, refreshToken } = response.data;

    setSession(accessToken, refreshToken);

    const { featureFlags, communityFeatures, user, instance, permissions } =
      await resolveJanusBootstrapData();

    const modules = await getModulesAvailable(featureFlags);

    dispatch({
      type: 'LOGIN',
      payload: {
        user,
        instance,
        type: LoginEventTypes.REGULAR,
        modules,
        featureFlags,
        communityFeatures,
        permissions,
      },
    });

    return { accessToken, refreshToken, user, instance };
  };

  const loginWithOkta = async (loginData: SSOLoginData) => {
    const response = await authService.oktaLogin(loginData);
    const { accessToken, refreshToken } = response.data;
    setSession(accessToken, refreshToken);

    const { featureFlags, communityFeatures, user, instance, permissions } =
      await resolveJanusBootstrapData();

    const modules = await getModulesAvailable(featureFlags);

    dispatch({
      type: 'LOGIN',
      payload: {
        user,
        instance,
        type: LoginEventTypes.OKTA,
        modules,
        featureFlags,
        communityFeatures,
        permissions,
      },
    });
  };

  const loginWithGoogle = async (loginData: SSOLoginData) => {
    const response = await authService.googleLogin(loginData);
    const { accessToken, refreshToken } = response.data;
    setSession(accessToken, refreshToken);

    const { featureFlags, communityFeatures, user, instance, permissions } =
      await resolveJanusBootstrapData();

    const modules = await getModulesAvailable(featureFlags);

    dispatch({
      type: 'LOGIN',
      payload: {
        user,
        instance,
        type: LoginEventTypes.GOOGLE,
        modules,
        featureFlags,
        communityFeatures,
        permissions,
      },
    });
  };

  const logout = async (notifyApi = false): Promise<void> => {
    const isTokenValid = tokenManager.isTokenValid();
    if (notifyApi && isTokenValid) await authService.logout().catch(() => null);
    kickOutUserThrottled();
    dispatch({ type: 'LOGOUT' });
  };

  const register = async (
    email: string,
    name: string,
    password: string,
  ): Promise<void> => {
    const response = await api.post<{
      accessToken: string;
      refreshToken: string;
      user: MeUser;
    }>('/api/authentication/register', {
      email,
      name,
      password,
    });
    const { accessToken, refreshToken, user } = response.data;

    tokenManager.setTokens(accessToken, refreshToken);
    dispatch({
      type: 'REGISTER',
      payload: {
        user,
      },
    });
  };

  const updateInstance = async (instance: MeInstance) => {
    dispatch({ type: 'UPDATE_INSTANCE', payload: { instance } });
  };

  const updateUser = async (user: MeUser) => {
    dispatch({ type: 'UPDATE_USER', payload: { user } });
  };

  const updateUserRolesAndPermissions = async () => {
    const { featureFlags, communityFeatures, user, permissions } =
      await resolveJanusBootstrapData();
    const modules = await getModulesAvailable(featureFlags);
    if (modules.length > 0) {
      dispatch({
        type: 'UPDATE_USER_ROLES_AND_PERMISSIONS',
        payload: { user, modules, communityFeatures, permissions },
      });
    }
  };

  return (
    <AuthContext.Provider
      value={{
        ...state,
        platform: 'JWT',
        login,
        loginViaJanus,
        loginWithMFA,
        logout,
        loginOTP,
        loginSaml,
        loginWithAzure,
        loginWithOkta,
        loginWithGoogle,
        register,
        updateInstance,
        updateUser,
        initialize,
        updateUserRolesAndPermissions,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

const useAuth = () => useContext(AuthContext);

export const useRequiredAuth = () => {
  const context = useContext(AuthContext);
  if (!context.user) throw new Error('useRequiredAuth: user is missing');
  if (!context.instance)
    throw new Error('useRequiredAuth: instance is missing');
  return {
    ...context,
    // These types are now guaranteed at runtime:
    user: context.user,
    instance: context.instance,
  };
};

export default useAuth;
