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

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

import api from 'src/config/api';
import { appInitializer } from 'src/config/app-initializer';
import { logEvent, setUser } from 'src/config/logging';
import { tokenManager } from 'src/config/tokens';
import { ErrorCode } from 'src/constants/exceptions';
import { type Language } from 'src/constants/languages';
import {
  registerCallback,
  runCallbacks,
} from 'src/contexts/beforeLogoutCallbacks';
import {
  azureLogin as azureLoginService,
  getChildSessions,
  getJanusCommunityFeatures,
  getJanusFeatureFlags,
  getJanusMe,
  googleLogin as googleLoginService,
  initiate2FA,
  loginOTP as loginOTPService,
  login as loginService,
  loginViaJanus as loginViaJanusService,
  logout as logoutService,
  oktaLogin as oktaLoginService,
  tokenLogin,
  updatePassword as updatePasswordService,
} from 'src/services/authService';
import { EventName, LoginTypes } from 'src/types/amplitude';
import { type CommunityFeature } from 'src/types/communityFeatures';
import { type FeatureFlag } from 'src/types/featureFlags';
import {
  type Instance,
  type MeInstance,
  type StaticProfileSettings,
} from 'src/types/instance';
import {
  type AuthResponse,
  type AzureLoginData,
  type GoogleLoginData,
  type InitMFAResponse,
  type JanusMeRawResponse,
  type LegacyMeUser,
  type LoginData,
  type LoginOTPData,
  type LoginResponse,
  type OktaLoginData,
} from 'src/types/login';
import { type MeUser } from 'src/types/user';
import { kickOutUserThrottled } from 'src/utils/api';
import { clearLocalStorageDrafts } from 'src/utils/composerDrafts';
import { handleUserLanguage, setDocumentLanguage } from 'src/utils/languages';
import { isOnMaintenance } from 'src/utils/maintenance';
import {
  canLogin,
  type InstanceCapabilityNames,
  type UserCapabilityNames,
} from 'src/utils/permissions';
import { getTranslationLanguage } from 'src/utils/translations';
import { permissionsObjectToArray } from 'src/utils/userUtils';

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

type AuthContextValue = State & {
  platform: 'JWT' | null;
  login: ((loginData: LoginData) => Promise<LoginResponse>) | null;
  loginViaJanus: ((loginData: LoginData) => Promise<LoginResponse>) | null;
  loginWithMFA: ((loginData: LoginData) => Promise<InitMFAResponse>) | null;
  loginOTP: ((loginData: LoginOTPData) => Promise<LoginResponse>) | null;
  loginSaml: ((loginData: string) => Promise<LoginResponse>) | null;
  loginNewCommunity: ((loginData: LoginData) => Promise<LoginResponse>) | null;
  azureLogin: ((loginData: AzureLoginData) => Promise<LoginResponse>) | null;
  oktaLogin: ((loginData: OktaLoginData) => Promise<LoginResponse>) | null;
  googleLogin: ((loginData: GoogleLoginData) => Promise<LoginResponse>) | null;
  updatePassword:
    | ((
        user: MeUser,
        newPassword: string,
        accessToken: string,
        refreshToken: string,
      ) => Promise<void>)
    | null;
  logout: ((notifyApi?: boolean) => Promise<void>) | null;
  registerBeforeLogoutCallback:
    | ((fn: () => Promise<void>) => () => void)
    | null;
  updateInstance: ((instance: Instance) => void) | null;
  updateStaticProfileSettings:
    | ((settings: StaticProfileSettings) => void)
    | null;
  changeLanguage: ((langugage: string) => void) | null;
  changeLogoInstance: ((logo: string) => void) | null;
  acceptedTermsAndConditions: (() => void) | null;
  updateUserPermissions:
    | ((permissions: Record<UserCapabilityNames, boolean>) => void)
    | null;
  updateInstancePermissions:
    | ((permissions: Record<InstanceCapabilityNames, boolean>) => void)
    | null;
  updateAcknowledgementsLeftToGive:
    | (({
        acknowledgementsToGiveLeft,
      }: {
        acknowledgementsToGiveLeft: number;
      }) => void)
    | null;
  changePostTranslationLanguage: ((user: MeUser) => void) | null;
  updateUserRolesAndPermissions: (() => void) | null;
};

type AuthProviderProps = {};

type InitializeAction = {
  type: 'INITIALIZE';
  payload: {
    isAuthenticated: boolean;
    user: LegacyMeUser | null;
    instance: MeInstance | null;
    featureFlags: FeatureFlag | null;
    communityFeatures: CommunityFeature[] | null;
    permissions: string[];
  };
};

type LoginAction = {
  type: 'LOGIN';
  payload: {
    isAuthenticated: boolean;
    user: LegacyMeUser;
    instance: MeInstance;
    featureFlags: FeatureFlag | null;
    communityFeatures: CommunityFeature[] | null;
    permissions: string[];
  };
};

type ChangeLogoInstanceAction = {
  type: 'CHANGE_LOGO_INSTANCE';
  payload: {
    logo: string;
  };
};

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

type AcceptedTermsAndConditionsAction = {
  type: 'ACCEPTED_TEMS_AND_CONDITIONS';
};

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

type UpdateStaticProfileSettingsAction = {
  type: 'UPDATE_STATIC_PROFILE_SETTINGS';
  payload: {
    settings: StaticProfileSettings;
  };
};

type UpdatePermissionsAction = {
  type: 'UPDATE_PERMISSIONS';
  payload: {
    permissions: string[];
  };
};

type ApplyPermissionsDeltaAction = {
  type: 'APPLY_PERMISSIONS_DELTA';
  payload: {
    capabilities:
      | Record<UserCapabilityNames, boolean>
      | Record<InstanceCapabilityNames, boolean>;
  };
};

type ChangeLanguageAction = {
  type: 'CHANGE_LANGUAGE';
  payload: {
    language: string;
  };
};

type UpdateAcknowledgementsLeftToGiveAction = {
  type: 'UPDATE_ACKNOWLEDGEMENTS_LEFT_TO_GIVE';
  payload: {
    acknowledgementsToGiveLeft: number;
  };
};

type ChangePostTranslationLanguage = {
  type: 'CHANGE_POST_TRANSLATION_LANGUAGE';
  payload: {
    user: MeUser;
  };
};

type UpdateUserRolesAndPermissionsAction = {
  type: 'UPDATE_USER_ROLES_AND_PERMISSIONS';
  payload: {
    user: LegacyMeUser;
  };
};

type Action =
  | InitializeAction
  | LoginAction
  | LogoutAction
  | UpdateInstanceAction
  | UpdateStaticProfileSettingsAction
  | UpdatePermissionsAction
  | ApplyPermissionsDeltaAction
  | ChangeLanguageAction
  | AcceptedTermsAndConditionsAction
  | ChangeLogoInstanceAction
  | UpdateAcknowledgementsLeftToGiveAction
  | ChangePostTranslationLanguage
  | UpdateUserRolesAndPermissionsAction;

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

const CHILD_SESSION_PATHS: readonly string[] = [
  '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',
];

const isChildSessionPath = (): boolean =>
  CHILD_SESSION_PATHS.some(p => location.pathname.includes(p));

/**
 * Returns the refresh token embedded in the URL for child-session mobile paths
 * (e.g. `/org-chart-mobile/:token/...`), or null when the current pathname is
 * not a child-session path, or when the expected segment is missing/empty.
 *
 * The URL contract (`/<module-mobile>/<token>/...`) is enforced by the app's
 * routing in `src/routes.tsx` — the index-based split here relies on that.
 * If the mobile routes ever change shape, update both sides together.
 */
const getChildSessionRefreshToken = (): string | null => {
  if (!isChildSessionPath()) return null;
  const segment = location.pathname.split('/')[2];
  if (!segment || segment.trim().length === 0) return null;
  return segment;
};

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

const mapJanusMeResponse = (
  raw: JanusMeRawResponse,
): {
  user: JanusMeRawResponse['user'];
  instance: MeInstance;
  permissions: string[];
} => ({
  user: raw.user,
  instance: {
    ...raw.instance,
    forceOTP: raw.instance.forceOtp,
    maxPostSizeInMB: raw.instance.maxPostSizeInMb,
  } as MeInstance,
  permissions: raw.permissions,
});

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

  const janusMe = mapJanusMeResponse(meResponse.data);

  return {
    featureFlags: ffResponse.data.featureFlags,
    communityFeatures: cfResponse.data.communityFeatures,
    user: janusMe.user,
    instance: janusMe.instance,
    permissions: janusMe.permissions,
  };
};

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

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

    return {
      ...state,
      isAuthenticated,
      user,
      instance,
      permissions,
      featureFlags,
      communityFeatures,
    };
  },
  LOGOUT: (state: State): State => ({
    ...state,
    isAuthenticated: false,
    user: null,
    instance: null,
    permissions: [],
    featureFlags: null,
    communityFeatures: null,
  }),
  UPDATE_STATIC_PROFILE_SETTINGS: (state: State, action: Action): State => {
    const { settings } = (action as UpdateStaticProfileSettingsAction).payload;

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

    return {
      ...state,
      instance,
    };
  },
  UPDATE_ACKNOWLEDGEMENTS_LEFT_TO_GIVE: (
    state: State,
    action: Action,
  ): State => {
    const { acknowledgementsToGiveLeft } = (
      action as UpdateAcknowledgementsLeftToGiveAction
    ).payload;

    return {
      ...state,
      user: {
        ...state.user!,
        acknowledgementsToGiveLeft,
      },
    };
  },
  UPDATE_PERMISSIONS: (state: State, action: Action): State => {
    const { permissions } = (action as UpdatePermissionsAction).payload;

    return {
      ...state,
      permissions,
    };
  },
  APPLY_PERMISSIONS_DELTA: (state: State, action: Action): State => {
    const { capabilities } = (action as ApplyPermissionsDeltaAction).payload;

    // Compute the new array inside the reducer so we always merge against
    // the latest permissions, not a stale closure value. This prevents
    // back-to-back socket events from overwriting each other.
    const next = new Set(state.permissions);
    Object.entries(capabilities).forEach(([key, value]) => {
      if (value) next.add(key);
      else next.delete(key);
    });

    return {
      ...state,
      permissions: Array.from(next),
    };
  },
  CHANGE_LANGUAGE: (state: State, action: Action): State => {
    const { language } = (action as ChangeLanguageAction).payload;

    return {
      ...state,
      user: {
        ...state.user!,
        language,
      },
    };
  },
  CHANGE_LOGO_INSTANCE: (state: State, action: Action): State => {
    const { logo } = (action as ChangeLogoInstanceAction).payload;

    return {
      ...state,
      instance: {
        ...state.instance!,
        logo,
      },
    };
  },
  ACCEPTED_TEMS_AND_CONDITIONS: (state: State): State => ({
    ...state,
    user: {
      ...state.user!,
      lastTermsAndConditionsAccepted: true,
    },
  }),
  CHANGE_POST_TRANSLATION_LANGUAGE: (state: State, action: Action): State => {
    const { user } = (action as ChangePostTranslationLanguage).payload;

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

    return {
      ...state,
      user,
      permissions: permissionsObjectToArray(user?.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,
  loginNewCommunity: null,
  azureLogin: null,
  oktaLogin: null,
  googleLogin: null,
  updatePassword: null,
  logout: null,
  registerBeforeLogoutCallback: null,
  changeLanguage: null,
  updateInstance: null,
  updateStaticProfileSettings: null,
  changeLogoInstance: null,
  acceptedTermsAndConditions: null,
  updateUserPermissions: null,
  updateInstancePermissions: null,
  updateAcknowledgementsLeftToGive: null,
  permissions: [],
  changePostTranslationLanguage: null,
  updateUserRolesAndPermissions: null,
  communityFeatures: null,
});

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

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

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

      if (!childRefreshToken && !accessToken && !refreshToken) {
        dispatch({
          type: 'INITIALIZE',
          payload: {
            isAuthenticated: false,
            user: null,
            instance: null,
            featureFlags: null,
            communityFeatures: null,
            permissions: [],
          },
        });
        return;
      }

      void appInitializer.run(
        async () => {
          // Re-resolve tokens on each attempt — tokenManager is the source of
          // truth after the first successful setSession.
          let currentAccessToken: string | null;
          let currentRefreshToken: string | null;

          const childRT = getChildSessionRefreshToken();
          if (childRT) {
            const childSessionResponse = await getChildSessions(childRT);
            currentAccessToken =
              childSessionResponse?.data?.accessToken ?? null;
            currentRefreshToken =
              childSessionResponse?.data?.refreshToken ?? null;
          } else {
            const latest = tokenManager.getCorrectToken();
            currentAccessToken = latest.accessToken;
            currentRefreshToken = latest.refreshToken;
          }

          setSession(currentAccessToken, currentRefreshToken);

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

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

          dispatch({
            type: 'INITIALIZE',
            payload: {
              isAuthenticated: true,
              user,
              instance,
              featureFlags,
              communityFeatures: communityFeatures ?? null,
              permissions,
            },
          });
        },
        {
          hasValidTokens: () => {
            const { accessToken: at, refreshToken: rt } =
              tokenManager.getCorrectToken();
            return !!(at || rt || getChildSessionRefreshToken());
          },
          onAuthFailure: err => {
            // Re-read tokens at call time — NOT from the outer closure (stale).
            const { accessToken: cur, refreshToken: curRT } =
              tokenManager.getCorrectToken();
            Sentry.withScope(scope => {
              scope.setContext('initialize', {
                hasTokens: !!(cur || curRT),
                isChildSession: isChildSessionPath(),
              });
              Sentry.captureException(err);
            });
            dispatch({
              type: 'INITIALIZE',
              payload: {
                isAuthenticated: false,
                user: null,
                instance: null,
                featureFlags: null,
                communityFeatures: null,
                permissions: [],
              },
            });
          },
        },
      );
    };

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

  const setLanguage = (user: MeUser) => {
    const language = getTranslationLanguage(user);
    i18next.changeLanguage(language);
    setDocumentLanguage(language as Language);
  };

  const loginOTP = async (loginData: LoginOTPData): Promise<LoginResponse> => {
    const response = await loginOTPService(loginData);
    const { accessToken, refreshToken } = response.data;
    api.defaults.headers.Authorization = `Bearer ${accessToken}`;

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

    const needsToResetPassword =
      instance.otpAfterRegularLogin && !user.hasPasswordChanged;

    const canAccessPlatform = !needsToResetPassword;

    if (canAccessPlatform) {
      await setUser(user, instance);
      setSession(accessToken, refreshToken);
    }

    logEvent(EventName.USER_LOGIN, {
      userId: user?.id,
      login: LoginTypes.OTP,
    });

    dispatch({
      type: 'LOGIN',
      payload: {
        isAuthenticated: canAccessPlatform,
        user,
        instance,
        featureFlags,
        communityFeatures: communityFeatures ?? null,
        permissions,
      },
    });

    return {
      accessToken,
      refreshToken,
      user,
      instance,
      featureFlags,
      communityFeatures: communityFeatures ?? undefined,
    };
  };

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

    setSession(accessToken, refreshToken);

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

    const language = await handleUserLanguage(user, accessToken);
    await setUser(user, instance);

    logEvent(EventName.USER_LOGIN, {
      userId: user?.id,
      login: LoginTypes.REGULAR,
    });

    dispatch({
      type: 'LOGIN',
      payload: {
        isAuthenticated: true,
        user: {
          ...user,
          language,
        },
        instance,
        featureFlags,
        communityFeatures: communityFeatures ?? null,
        permissions,
      },
    });

    return {
      accessToken,
      refreshToken,
      user: { ...user, language },
      instance,
      featureFlags,
      communityFeatures: communityFeatures ?? undefined,
    };
  };

  // Shared body for the two password-based logins (monolith and Janus). The
  // post-token flow is identical — only the service call and a token guard
  // differ. `hasBeenLogged` is preserved from the response when the upstream
  // payload includes it (classic monolith login); the Janus path returns only
  // tokens and falls back to the bootstrap user's value.
  const loginWithTokens = async (
    getTokens: () => Promise<{ data: AuthResponse | LoginResponse }>,
    options: { guardTokens?: boolean } = {},
  ): Promise<LoginResponse> => {
    const response = await getTokens();
    const { accessToken, refreshToken } = response.data;

    if (options.guardTokens && (!accessToken || !refreshToken)) {
      throw new Error('Janus login response missing tokens');
    }

    const hasBeenLoggedFromResponse =
      'user' in response.data ? response.data.user?.hasBeenLogged : undefined;

    api.defaults.headers.Authorization = `Bearer ${accessToken}`;

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

    if (!canLogin(permissions)) {
      throw new Error(ErrorCode.NOT_ENOUGH_PERMISSIONS);
    }

    if (user.hasPasswordChanged) {
      await setUser(user, instance);
      setSession(accessToken, refreshToken);
    }

    logEvent(EventName.USER_LOGIN, {
      userId: user?.id,
      login: LoginTypes.REGULAR,
    });

    dispatch({
      type: 'LOGIN',
      payload: {
        isAuthenticated: user.hasPasswordChanged,
        user,
        instance,
        featureFlags,
        communityFeatures: communityFeatures ?? null,
        permissions,
      },
    });

    return {
      accessToken,
      refreshToken,
      user:
        hasBeenLoggedFromResponse !== undefined
          ? { ...user, hasBeenLogged: hasBeenLoggedFromResponse }
          : user,
      instance,
      featureFlags,
      communityFeatures: communityFeatures ?? undefined,
    };
  };

  const login = (loginData: LoginData) =>
    loginWithTokens(() => loginService(loginData));

  const loginViaJanus = (loginData: LoginData) =>
    loginWithTokens(() => loginViaJanusService(loginData), {
      guardTokens: true,
    });

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

    const { init2faToken } = response.data;

    return { init2faToken };
  };

  const loginNewCommunity = async (
    loginData: LoginData,
  ): Promise<LoginResponse> => {
    const response = await loginService(loginData);
    const {
      accessToken,
      refreshToken,
      user: { hasBeenLogged },
    } = response.data;
    setSession(accessToken, refreshToken);

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

    if (!canLogin(permissions)) {
      throw new Error(ErrorCode.NOT_ENOUGH_PERMISSIONS);
    }

    const language = await handleUserLanguage(user, accessToken);
    await setUser(user, instance);
    logEvent(EventName.USER_LOGIN, {
      userId: user?.id,
      login: LoginTypes.REGULAR,
    });

    dispatch({
      type: 'LOGIN',
      payload: {
        isAuthenticated: true,
        user: {
          ...user,
          language,
        },
        instance,
        featureFlags,
        communityFeatures: communityFeatures ?? null,
        permissions,
      },
    });

    return {
      accessToken,
      refreshToken,
      user: {
        ...user,
        hasBeenLogged,
        language,
      },
      instance,
      featureFlags,
      communityFeatures: communityFeatures ?? undefined,
    };
  };

  const azureLogin = async (
    loginData: AzureLoginData,
  ): Promise<LoginResponse> => {
    const response = await azureLoginService(loginData);
    const { accessToken, refreshToken } = response.data;
    setSession(accessToken, refreshToken);

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

    const language = await handleUserLanguage(user, accessToken);
    await setUser(user, instance);

    logEvent(EventName.USER_LOGIN, {
      userId: user?.id,
      login: LoginTypes.AZURE,
    });

    dispatch({
      type: 'LOGIN',
      payload: {
        isAuthenticated: true,
        user: {
          ...user,
          language,
        },
        instance,
        featureFlags,
        communityFeatures: communityFeatures ?? null,
        permissions,
      },
    });

    return {
      accessToken,
      refreshToken,
      user: { ...user, language },
      instance,
      featureFlags,
      communityFeatures: communityFeatures ?? undefined,
    };
  };

  const oktaLogin = async (
    loginData: OktaLoginData,
  ): Promise<LoginResponse> => {
    const response = await oktaLoginService(loginData);
    const { accessToken, refreshToken } = response.data;
    setSession(accessToken, refreshToken);

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

    const language = await handleUserLanguage(user, accessToken);
    await setUser(user, instance);

    logEvent(EventName.USER_LOGIN, {
      userId: user?.id,
      login: LoginTypes.OKTA,
    });

    dispatch({
      type: 'LOGIN',
      payload: {
        isAuthenticated: true,
        user: {
          ...user,
          language,
        },
        instance,
        featureFlags,
        communityFeatures: communityFeatures ?? null,
        permissions,
      },
    });

    return {
      accessToken,
      refreshToken,
      user: { ...user, language },
      instance,
      featureFlags,
      communityFeatures: communityFeatures ?? undefined,
    };
  };

  const googleLogin = async (
    loginData: GoogleLoginData,
  ): Promise<LoginResponse> => {
    const response = await googleLoginService(loginData);
    const {
      accessToken,
      refreshToken,
      user: { hasBeenLogged },
    } = response.data;
    setSession(accessToken, refreshToken);

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

    const language = await handleUserLanguage(user, accessToken);
    await setUser(user, instance);

    logEvent(EventName.USER_LOGIN, {
      userId: user?.id,
      login: LoginTypes.GOOGLE,
    });

    dispatch({
      type: 'LOGIN',
      payload: {
        isAuthenticated: true,
        user: {
          ...user,
          language,
        },
        instance,
        featureFlags,
        communityFeatures: communityFeatures ?? null,
        permissions,
      },
    });

    return {
      accessToken,
      refreshToken,
      user: {
        ...user,
        hasBeenLogged,
        language,
      },
      instance,
      featureFlags,
      communityFeatures: communityFeatures ?? undefined,
    };
  };

  const updatePassword = async (
    user: MeUser,
    newPassword: string,
    accessToken: string,
    refreshToken: string,
  ): Promise<void> => {
    await updatePasswordService(user.id, newPassword, accessToken);
    setSession(accessToken, refreshToken);

    // Re-resolve bootstrap data so permissions come from the same source as
    // login()/initialize(). The `user` passed in by FormUpdatePassword comes
    // from Janus (no `user.permissions`), so deriving permissions from it
    // produced an empty array and the sidebar rendered with no modules until
    // the next page refresh.
    try {
      const resolved = await resolveJanusBootstrapData();

      await setUser(resolved.user, resolved.instance);

      dispatch({
        type: 'LOGIN',
        payload: {
          isAuthenticated: true,
          user: resolved.user,
          instance: resolved.instance,
          featureFlags: resolved.featureFlags,
          communityFeatures: resolved.communityFeatures,
          permissions: resolved.permissions,
        },
      });
    } catch (err) {
      // The password change and the session token are already committed —
      // refusing to log the user in here would trap them on the password
      // screen. Fall back to the bootstrap data login() already left in
      // state (same user, captured seconds ago) so they can enter the app;
      // a navigation or refresh will refresh via initialize() if needed.
      Sentry.withScope(scope => {
        scope.setContext('updatePassword', {
          userId: user?.id,
          hasStateUser: !!state.user,
          hasStateInstance: !!state.instance,
          statePermissionsCount: state.permissions?.length ?? 0,
        });
        Sentry.captureException(err);
      });

      if (state.user && state.instance) {
        dispatch({
          type: 'LOGIN',
          payload: {
            isAuthenticated: true,
            user: state.user,
            instance: state.instance,
            featureFlags: state.featureFlags,
            communityFeatures: state.communityFeatures,
            permissions: state.permissions,
          },
        });
        return;
      }

      // No cached bootstrap data — let FormUpdatePassword show the error.
      throw err;
    }
  };

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

  const changeLanguage = (language: string) =>
    dispatch({ type: 'CHANGE_LANGUAGE', payload: { language } });

  const changeLogoInstance = (logo: string) =>
    dispatch({ type: 'CHANGE_LOGO_INSTANCE', payload: { logo } });

  const acceptedTermsAndConditions = () =>
    dispatch({ type: 'ACCEPTED_TEMS_AND_CONDITIONS' });

  const updateStaticProfileSettings = (settings: StaticProfileSettings) =>
    dispatch({ type: 'UPDATE_STATIC_PROFILE_SETTINGS', payload: { settings } });

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

  const fetchJanusMe = async () => {
    try {
      const meResponse = await getJanusMe();
      return mapJanusMeResponse(meResponse.data);
    } catch {
      return null;
    }
  };

  const updateUserPermissions = (
    permissions: Record<UserCapabilityNames, boolean>,
  ) => {
    // Dispatch the raw delta and let the reducer merge it against the
    // latest state. Computing the merge here would close over a stale
    // state.permissions when two socket events arrive back-to-back.
    dispatch({
      type: 'APPLY_PERMISSIONS_DELTA',
      payload: { capabilities: permissions },
    });
  };

  const updateInstancePermissions = (
    permissions: Record<InstanceCapabilityNames, boolean>,
  ) => {
    dispatch({
      type: 'APPLY_PERMISSIONS_DELTA',
      payload: { capabilities: permissions },
    });
  };

  const updateAcknowledgementsLeftToGive = ({
    acknowledgementsToGiveLeft,
  }: {
    acknowledgementsToGiveLeft: number;
  }) => {
    dispatch({
      type: 'UPDATE_ACKNOWLEDGEMENTS_LEFT_TO_GIVE',
      payload: { acknowledgementsToGiveLeft },
    });
  };

  const changePostTranslationLanguage = (user: MeUser) =>
    dispatch({ type: 'CHANGE_POST_TRANSLATION_LANGUAGE', payload: { user } });

  const updateUserRolesAndPermissions = async () => {
    const janusMe = await fetchJanusMe();
    if (!janusMe) return;

    dispatch({
      type: 'UPDATE_USER_ROLES_AND_PERMISSIONS',
      payload: { user: janusMe.user },
    });
    dispatch({
      type: 'UPDATE_PERMISSIONS',
      payload: { permissions: janusMe.permissions },
    });
  };

  return (
    <AuthContext.Provider
      value={{
        ...state,
        platform: 'JWT',
        login,
        loginViaJanus,
        loginWithMFA,
        loginOTP,
        loginSaml,
        loginNewCommunity,
        azureLogin,
        oktaLogin,
        googleLogin,
        updatePassword,
        logout,
        registerBeforeLogoutCallback: registerCallback,
        updateStaticProfileSettings,
        updateInstance,
        updateUserPermissions,
        updateInstancePermissions,
        changeLanguage,
        changeLogoInstance,
        acceptedTermsAndConditions,
        updateAcknowledgementsLeftToGive,
        changePostTranslationLanguage,
        permissions: state.permissions,
        updateUserRolesAndPermissions,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

export 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 AuthContext;
