import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useLocation } from 'wouter';
import { useQuery } from '@tanstack/react-query';
import AppAuthenticatedShell from '@/components/AppAuthenticatedShell';
import AppGlobalOverlays, { type AppConfirmState } from '@/components/AppGlobalOverlays';
import AuthViews from '@/components/AuthViews';
import PublicSendPage from '@/components/PublicSendPage';
import RecoverTwoFactorPage from '@/components/RecoverTwoFactorPage';
import JwtWarningPage from '@/components/JwtWarningPage';
import {
  createAuthedFetch,
  getAuthorizedDevices,
  clearProfileSnapshot,
  getCurrentDeviceIdentifier,
  getPasswordHint,
  loadProfileSnapshot,
  saveProfileSnapshot,
  revokeCurrentSession,
  getTotpStatus,
  saveSession,
  stripProfileSecrets,
} from '@/lib/api/auth';
import { listAdminInvites, listAdminUsers } from '@/lib/api/admin';
import { buildSendShareKey, getSends } from '@/lib/api/send';
import {
  getCiphers,
  getFolders,
  updateFolder,
} from '@/lib/api/vault';
import { silentlyRepairBackupSettingsIfNeeded } from '@/lib/backup-settings-repair';
import { base64ToBytes, decryptBw, decryptStr } from '@/lib/crypto';
import {
  buildPublicSendUrl,
  deriveSendKeyParts,
  looksLikeCipherString,
  parseSignalRTextFrames,
  readInviteCodeFromUrl,
} from '@/lib/app-support';
import {
  bootstrapAppSession,
  type CompletedLogin,
  readInitialAppBootstrapState,
  performPasswordLogin,
  performRecoverTwoFactorLogin,
  performRegistration,
  performTotpLogin,
  hydrateLockedSession,
  performUnlock,
  type JwtUnsafeReason,
  type PendingTotp,
} from '@/lib/app-auth';
import useAccountSecurityActions from '@/hooks/useAccountSecurityActions';
import useAdminActions from '@/hooks/useAdminActions';
import useBackupActions from '@/hooks/useBackupActions';
import useVaultSendActions from '@/hooks/useVaultSendActions';
import { useToastManager } from '@/hooks/useToastManager';
import { t } from '@/lib/i18n';
import { APP_NOTIFY_EVENT, type AppNotifyDetail } from '@/lib/app-notify';
import { dispatchBackupProgress, type BackupProgressDetail } from '@/lib/backup-restore-progress';
import type { AppPhase, Cipher, Folder as VaultFolder, Profile, Send, SessionState } from '@/lib/types';

function isBackupProgressDetail(value: unknown): value is BackupProgressDetail {
  if (!value || typeof value !== 'object') return false;
  const detail = value as Record<string, unknown>;
  const operation = detail.operation;
  return (
    (operation === 'backup-restore' || operation === 'backup-export' || operation === 'backup-remote-run')
    && typeof detail.step === 'string'
    && typeof detail.fileName === 'string'
  );
}

const IMPORT_ROUTE = '/backup/import-export';
const IMPORT_ROUTE_PATHS = [IMPORT_ROUTE, '/tools/import', '/tools/import-export', '/tools/import-data', '/import', '/import-export'] as const;
const IMPORT_ROUTE_ALIASES: ReadonlySet<string> = new Set(IMPORT_ROUTE_PATHS.filter((path) => path !== IMPORT_ROUTE));
const SETTINGS_HOME_ROUTE = '/settings';
const SETTINGS_ACCOUNT_ROUTE = '/settings/account';
const THEME_STORAGE_KEY = 'nodewarden.theme.preference.v1';
const SIGNALR_RECORD_SEPARATOR = String.fromCharCode(0x1e);
const SIGNALR_UPDATE_TYPE_SYNC_VAULT = 5;
const SIGNALR_UPDATE_TYPE_LOG_OUT = 11;
const SIGNALR_UPDATE_TYPE_DEVICE_STATUS = 12;
const SIGNALR_UPDATE_TYPE_BACKUP_RESTORE_PROGRESS = 13;

type ThemePreference = 'system' | 'light' | 'dark';
type LockTimeoutMinutes = 0 | 1 | 5 | 15 | 30;
type SessionTimeoutAction = 'lock' | 'logout';

const LOCK_TIMEOUT_STORAGE_KEY = 'nodewarden.lock.timeout-minutes.v1';
const SESSION_TIMEOUT_ACTION_STORAGE_KEY = 'nodewarden.session.timeout-action.v1';
const LOCK_TIMEOUT_VALUES = new Set<LockTimeoutMinutes>([0, 1, 5, 15, 30]);

function readThemePreference(): ThemePreference {
  if (typeof window === 'undefined') return 'system';
  const stored = String(window.localStorage.getItem(THEME_STORAGE_KEY) || '').trim();
  if (stored === 'light' || stored === 'dark' || stored === 'system') return stored;
  return 'system';
}

function resolveSystemTheme(): 'light' | 'dark' {
  if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return 'light';
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}

function readLockTimeoutMinutes(): LockTimeoutMinutes {
  if (typeof window === 'undefined') return 15;
  const value = Number(window.localStorage.getItem(LOCK_TIMEOUT_STORAGE_KEY));
  return LOCK_TIMEOUT_VALUES.has(value as LockTimeoutMinutes) ? (value as LockTimeoutMinutes) : 15;
}

function readSessionTimeoutAction(): SessionTimeoutAction {
  if (typeof window === 'undefined') return 'lock';
  const value = String(window.localStorage.getItem(SESSION_TIMEOUT_ACTION_STORAGE_KEY) || '').trim();
  return value === 'logout' ? 'logout' : 'lock';
}

export default function App() {
  const initialBootstrap = useMemo(() => readInitialAppBootstrapState(), []);
  const initialInviteCode = useMemo(() => readInviteCodeFromUrl(), []);
  const initialProfileSnapshot = useMemo(() => loadProfileSnapshot(initialBootstrap.session?.email), [initialBootstrap]);
  const [pendingAuthAction, setPendingAuthAction] = useState<'login' | 'register' | 'unlock' | null>(null);
  const [location, navigate] = useLocation();
  const [phase, setPhase] = useState<AppPhase>(initialBootstrap.phase);
  const [session, setSessionState] = useState<SessionState | null>(initialBootstrap.session);
  const [profile, setProfile] = useState<Profile | null>(initialProfileSnapshot);
  const [defaultKdfIterations, setDefaultKdfIterations] = useState(initialBootstrap.defaultKdfIterations);
  const [jwtWarning, setJwtWarning] = useState<{ reason: JwtUnsafeReason; minLength: number } | null>(initialBootstrap.jwtWarning);

  const [loginValues, setLoginValues] = useState({ email: '', password: '' });
  const [registerValues, setRegisterValues] = useState({
    name: '',
    email: '',
    password: '',
    password2: '',
    passwordHint: '',
    inviteCode: initialInviteCode,
  });
  const [loginHintState, setLoginHintState] = useState<{
    email: string;
    loading: boolean;
    hint: string | null;
  }>({
    email: '',
    loading: false,
    hint: null,
  });
  const [inviteCodeFromUrl, setInviteCodeFromUrl] = useState(initialInviteCode);
  const [unlockPassword, setUnlockPassword] = useState('');
  const [pendingTotp, setPendingTotp] = useState<PendingTotp | null>(null);
  const [pendingTotpMode, setPendingTotpMode] = useState<'login' | 'unlock' | null>(null);
  const [totpCode, setTotpCode] = useState('');
  const [rememberDevice, setRememberDevice] = useState(true);
  const [totpSubmitting, setTotpSubmitting] = useState(false);

  const [disableTotpOpen, setDisableTotpOpen] = useState(false);
  const [disableTotpPassword, setDisableTotpPassword] = useState('');
  const [disableTotpSubmitting, setDisableTotpSubmitting] = useState(false);
  const [recoverValues, setRecoverValues] = useState({ email: '', password: '', recoveryCode: '' });
  const [themePreference, setThemePreference] = useState<ThemePreference>(() => readThemePreference());
  const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>(() => resolveSystemTheme());
  const [lockTimeoutMinutes, setLockTimeoutMinutesState] = useState<LockTimeoutMinutes>(() => readLockTimeoutMinutes());
  const [sessionTimeoutAction, setSessionTimeoutActionState] = useState<SessionTimeoutAction>(() => readSessionTimeoutAction());
  const [unlockPreparing, setUnlockPreparing] = useState(() => initialBootstrap.phase === 'locked' && !initialBootstrap.session?.email);

  const [confirm, setConfirm] = useState<AppConfirmState | null>(null);
  const [mobileLayout, setMobileLayout] = useState(false);
  const [mobileSidebarToggleKey, setMobileSidebarToggleKey] = useState(0);
  const [decryptedFolders, setDecryptedFolders] = useState<VaultFolder[]>([]);
  const [decryptedCiphers, setDecryptedCiphers] = useState<Cipher[]>([]);
  const [decryptedSends, setDecryptedSends] = useState<Send[]>([]);
  const sessionRef = useRef<SessionState | null>(initialBootstrap.session);
  const migratedPlainFolderIdsRef = useRef<Set<string>>(new Set());
  const silentRefreshVaultRef = useRef<() => Promise<void>>(async () => {});
  const refreshAuthorizedDevicesRef = useRef<() => Promise<void>>(async () => {});
  const repairAttemptRef = useRef<string>('');
  const { toasts, pushToast, removeToast } = useToastManager();

  useEffect(() => {
    const handleAppNotify = (event: Event) => {
      const detail = (event as CustomEvent<AppNotifyDetail>).detail;
      if (!detail?.text) return;
      pushToast(detail.type, detail.text);
    };

    window.addEventListener(APP_NOTIFY_EVENT, handleAppNotify as EventListener);
    return () => window.removeEventListener(APP_NOTIFY_EVENT, handleAppNotify as EventListener);
  }, [pushToast]);

  useEffect(() => {
    const syncInviteFromUrl = () => {
      setInviteCodeFromUrl(readInviteCodeFromUrl());
    };
    syncInviteFromUrl();
    window.addEventListener('hashchange', syncInviteFromUrl);
    window.addEventListener('popstate', syncInviteFromUrl);
    return () => {
      window.removeEventListener('hashchange', syncInviteFromUrl);
      window.removeEventListener('popstate', syncInviteFromUrl);
    };
  }, []);

  useEffect(() => {
    if (!inviteCodeFromUrl) return;
    setRegisterValues((prev) => (prev.inviteCode === inviteCodeFromUrl ? prev : { ...prev, inviteCode: inviteCodeFromUrl }));
  }, [inviteCodeFromUrl]);

  useEffect(() => {
    const normalizedEmail = loginValues.email.trim().toLowerCase();
    setLoginHintState((prev) => (
      prev.email && prev.email !== normalizedEmail
        ? { email: '', loading: false, hint: null }
        : prev
    ));
  }, [loginValues.email]);

  useEffect(() => {
    if (!inviteCodeFromUrl) return;
    if (phase === 'locked' || phase === 'app') return;
    setPhase('register');
    if (location !== '/register') navigate('/register');
    if (typeof window !== 'undefined' && typeof window.history?.replaceState === 'function') {
      window.history.replaceState(null, '', '/register');
    }
    setInviteCodeFromUrl('');
  }, [inviteCodeFromUrl, phase, location, navigate]);

  useEffect(() => {
    if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
    const media = window.matchMedia('(max-width: 1180px)');
    const sync = () => setMobileLayout(media.matches);
    sync();
    if (typeof media.addEventListener === 'function') {
      media.addEventListener('change', sync);
      return () => media.removeEventListener('change', sync);
    }
    media.addListener(sync);
    return () => media.removeListener(sync);
  }, []);

  useEffect(() => {
    if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
    const media = window.matchMedia('(prefers-color-scheme: dark)');
    const sync = () => setSystemTheme(media.matches ? 'dark' : 'light');
    sync();
    if (typeof media.addEventListener === 'function') {
      media.addEventListener('change', sync);
      return () => media.removeEventListener('change', sync);
    }
    media.addListener(sync);
    return () => media.removeListener(sync);
  }, []);

  const resolvedTheme = themePreference === 'system' ? systemTheme : themePreference;

  useEffect(() => {
    if (typeof document === 'undefined') return;
    document.documentElement.dataset.theme = resolvedTheme;
    document.documentElement.style.colorScheme = resolvedTheme;
  }, [resolvedTheme]);

  useEffect(() => {
    if (typeof window === 'undefined') return;
    window.localStorage.setItem(THEME_STORAGE_KEY, themePreference);
  }, [themePreference]);

  useEffect(() => {
    saveProfileSnapshot(profile);
  }, [profile]);

  useEffect(() => {
    if (phase === 'locked' && session?.email) {
      setUnlockPreparing(false);
    }
  }, [phase, profile, session]);

  useEffect(() => {
    if (typeof window === 'undefined') return;
    window.localStorage.setItem(LOCK_TIMEOUT_STORAGE_KEY, String(lockTimeoutMinutes));
  }, [lockTimeoutMinutes]);

  useEffect(() => {
    if (typeof window === 'undefined') return;
    window.localStorage.setItem(SESSION_TIMEOUT_ACTION_STORAGE_KEY, sessionTimeoutAction);
  }, [sessionTimeoutAction]);

  function handleToggleTheme() {
    setThemePreference((prev) => {
      const current = prev === 'system' ? systemTheme : prev;
      return current === 'dark' ? 'light' : 'dark';
    });
  }

  function setSession(next: SessionState | null) {
    sessionRef.current = next;
    setSessionState(next);
    saveSession(next);
  }

  function setLockTimeoutMinutes(next: LockTimeoutMinutes) {
    setLockTimeoutMinutesState(next);
    pushToast('success', t('txt_session_timeout_updated'));
  }

  function setSessionTimeoutAction(next: SessionTimeoutAction) {
    setSessionTimeoutActionState(next);
    pushToast('success', t('txt_session_timeout_updated'));
  }

  const authedFetch = useMemo(
    () =>
      createAuthedFetch(
        () => session,
        (next) => {
          setSession(next);
          if (!next) {
            setProfile(null);
            setPhase('login');
          }
        }
      ),
    [session]
  );
  const importAuthedFetch = useMemo(
    () => async (input: string, init?: RequestInit) => {
      const headers = new Headers(init?.headers || {});
      headers.set('X-NodeWarden-Import', '1');
      return authedFetch(input, { ...init, headers });
    },
    [authedFetch]
  );
  const backupActions = useBackupActions({
    authedFetch,
    onImported: () => {
      window.setTimeout(() => {
        logoutNow();
      }, 200);
    },
    onRestored: () => {
      window.setTimeout(() => {
        logoutNow();
      }, 200);
    },
  });

  useEffect(() => {
    let mounted = true;
    (async () => {
      const boot = await bootstrapAppSession(initialBootstrap);
      if (!mounted) return;
      setDefaultKdfIterations(boot.defaultKdfIterations);
      setJwtWarning(boot.jwtWarning);
      setSession(boot.session);
      setProfile(boot.profile);
      setPhase(boot.phase);
      setUnlockPreparing(boot.phase === 'locked' && !boot.session?.email);
    })();

    return () => {
      mounted = false;
    };
  }, [initialBootstrap]);

  useEffect(() => {
    if (phase !== 'locked' || !session) return;
    let cancelled = false;
    void (async () => {
      const result = await hydrateLockedSession(session, profile);
      if (cancelled) return;
      if (!result.session) {
        setSession(null);
        setProfile(null);
        setUnlockPreparing(false);
        setPhase('login');
        if (location !== '/login') navigate('/login');
        return;
      }
      setSession(result.session);
      if (result.profile) {
        setProfile(stripProfileSecrets(result.profile));
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [phase, session?.email, location, navigate]);

  async function finalizeLogin(login: CompletedLogin, successMessage = t('txt_login_success')) {
    setSession(login.session);
    setProfile(login.profile);
    setUnlockPreparing(false);
    setPendingTotp(null);
    setPendingTotpMode(null);
    setTotpCode('');
    setUnlockPassword('');
    setPhase('app');
    if (location === '/' || location === '/login' || location === '/register' || location === '/lock') {
      navigate('/vault');
    }
    pushToast('success', successMessage);
    void (async () => {
      try {
        const hydratedProfile = await login.profilePromise;
        if (sessionRef.current?.accessToken !== login.session.accessToken) return;
        setProfile(hydratedProfile);
      } catch {
        // Keep the in-memory transient profile for the current session.
      }
    })();
  }

  async function handleLogin() {
    if (pendingAuthAction) return;
    if (!loginValues.email || !loginValues.password) {
      pushToast('error', t('txt_please_input_email_and_password'));
      return;
    }
    setPendingAuthAction('login');
    try {
      const result = await performPasswordLogin(loginValues.email, loginValues.password, defaultKdfIterations);
      if (result.kind === 'success') {
        await finalizeLogin(result.login);
        return;
      }
      if (result.kind === 'totp') {
        setPendingTotp(result.pendingTotp);
        setPendingTotpMode('login');
        setTotpCode('');
        setRememberDevice(true);
        return;
      }
      pushToast('error', result.message || t('txt_login_failed'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_login_failed'));
    } finally {
      setPendingAuthAction(null);
    }
  }

  async function handleTotpVerify() {
    if (totpSubmitting) return;
    if (!pendingTotp) return;
    if (!totpCode.trim()) {
      pushToast('error', t('txt_please_input_totp_code'));
      return;
    }
    setTotpSubmitting(true);
    try {
      const login = await performTotpLogin(pendingTotp, totpCode, rememberDevice);
      await finalizeLogin(login, pendingTotpMode === 'unlock' ? t('txt_unlocked') : t('txt_login_success'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_totp_verify_failed'));
    } finally {
      setTotpSubmitting(false);
    }
  }

  async function handleRecoverTwoFactorSubmit() {
    const email = recoverValues.email.trim().toLowerCase();
    const password = recoverValues.password;
    const recoveryCode = recoverValues.recoveryCode.trim();
    if (!email || !password || !recoveryCode) {
      pushToast('error', t('txt_email_password_and_recovery_code_are_required'));
      return;
    }
    try {
      const recovered = await performRecoverTwoFactorLogin(email, password, recoveryCode, defaultKdfIterations);
      if (recovered.login) {
        await finalizeLogin(recovered.login);
        if (recovered.newRecoveryCode) {
          pushToast('success', t('txt_text_2fa_recovered_new_recovery_code_code', { code: recovered.newRecoveryCode }));
        } else {
          pushToast('success', t('txt_text_2fa_recovered'));
        }
        return;
      }
      pushToast('error', t('txt_recovered_but_auto_login_failed_please_sign_in'));
      navigate('/login');
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_recover_2fa_failed'));
    }
  }

  async function handleRegister() {
    if (pendingAuthAction) return;
    if (!registerValues.email || !registerValues.password) {
      pushToast('error', t('txt_please_input_email_and_password'));
      return;
    }
    if (registerValues.password.length < 12) {
      pushToast('error', t('txt_master_password_must_be_at_least_12_chars'));
      return;
    }
    if (registerValues.password !== registerValues.password2) {
      pushToast('error', t('txt_passwords_do_not_match'));
      return;
    }
    setPendingAuthAction('register');
    try {
      const resp = await performRegistration({
        email: registerValues.email,
        name: registerValues.name,
        password: registerValues.password,
        masterPasswordHint: registerValues.passwordHint,
        inviteCode: registerValues.inviteCode,
        fallbackIterations: defaultKdfIterations,
      });
      if (!resp.ok) {
        pushToast('error', resp.message);
        return;
      }
      setLoginValues({ email: registerValues.email.toLowerCase(), password: '' });
      setPhase('login');
      navigate('/login');
      pushToast('success', t('txt_registration_succeeded_please_sign_in'));
    } finally {
      setPendingAuthAction(null);
    }
  }

  function openPasswordHintDialog(hint: string | null) {
    setConfirm({
      title: t('txt_password_hint'),
      message: hint || t('txt_password_hint_not_set'),
      showIcon: false,
      confirmText: t('txt_close'),
      hideCancel: true,
      onConfirm: () => setConfirm(null),
    });
  }

  async function handleTogglePasswordHint() {
    if (pendingAuthAction) return;
    const email = loginValues.email.trim().toLowerCase();
    if (!email) return;

    if (loginHintState.email === email && !loginHintState.loading) {
      openPasswordHintDialog(loginHintState.hint);
      return;
    }

    setLoginHintState({
      email,
      loading: true,
      hint: null,
    });

    try {
      const result = await getPasswordHint(email);
      openPasswordHintDialog(result.masterPasswordHint);
      setLoginHintState({
        email,
        loading: false,
        hint: result.masterPasswordHint,
      });
    } catch (error) {
      setLoginHintState({
        email: '',
        loading: false,
        hint: null,
      });
      pushToast('error', error instanceof Error ? error.message : t('txt_password_hint_load_failed'));
    }
  }

  function handleShowLockedPasswordHint() {
    if (pendingAuthAction) return;
    openPasswordHintDialog(profile?.masterPasswordHint ?? null);
  }

  async function handleUnlock() {
    if (pendingAuthAction) return;
    if (!session?.email) return;
    if (!unlockPassword) {
      pushToast('error', t('txt_please_input_master_password'));
      return;
    }
    setPendingAuthAction('unlock');
    try {
      const result = await performUnlock(session, profile, unlockPassword, defaultKdfIterations);
      if (result.kind === 'success') {
        await finalizeLogin(result.login, t('txt_unlocked'));
        return;
      }
      if (result.kind === 'totp') {
        setPendingTotp(result.pendingTotp);
        setPendingTotpMode('unlock');
        setTotpCode('');
        setRememberDevice(true);
        return;
      }
      pushToast('error', result.message || t('txt_unlock_failed_master_password_is_incorrect'));
    } catch {
      pushToast('error', t('txt_unlock_failed_master_password_is_incorrect'));
    } finally {
      setPendingAuthAction(null);
    }
  }

  function lockCurrentSession() {
    const currentSession = sessionRef.current;
    if (!currentSession) return;
    const nextSession = { ...currentSession };
    delete nextSession.symEncKey;
    delete nextSession.symMacKey;
    setSession(nextSession);
    setProfile((prev) => stripProfileSecrets(prev));
    setDecryptedFolders([]);
    setDecryptedCiphers([]);
    setDecryptedSends([]);
    setUnlockPassword('');
    setPendingTotp(null);
    setPendingTotpMode(null);
    setTotpCode('');
    setUnlockPreparing(false);
    setPhase('locked');
    navigate('/lock');
  }

  function handleLock() {
    lockCurrentSession();
  }

  function logoutNow() {
    void revokeCurrentSession(sessionRef.current);
    setConfirm(null);
    setSession(null);
    clearProfileSnapshot();
    setProfile(null);
    setUnlockPreparing(false);
    setPendingTotp(null);
    setPendingTotpMode(null);
    setPhase('login');
    navigate('/login');
  }

  function handleLogout() {
    setConfirm({
      title: t('txt_log_out'),
      message: t('txt_are_you_sure_you_want_to_log_out'),
      showIcon: false,
      onConfirm: () => {
        logoutNow();
      },
    });
  }

  useEffect(() => {
    if (phase !== 'app' || lockTimeoutMinutes === 0) return;
    if (typeof window === 'undefined') return;

    let timerId: number | null = null;
    let lastActivityAt = 0;
    const timeoutMs = lockTimeoutMinutes * 60 * 1000;

    const clearTimer = () => {
      if (timerId !== null) {
        window.clearTimeout(timerId);
        timerId = null;
      }
    };
    const runTimeoutAction = () => {
      if (sessionTimeoutAction === 'logout') {
        logoutNow();
        return;
      }
      if (sessionRef.current?.symEncKey || sessionRef.current?.symMacKey) {
        lockCurrentSession();
      }
    };
    const scheduleTimeout = () => {
      clearTimer();
      timerId = window.setTimeout(() => {
        runTimeoutAction();
      }, timeoutMs);
    };
    const markActivity = () => {
      const now = Date.now();
      if (now - lastActivityAt < 1000) return;
      lastActivityAt = now;
      scheduleTimeout();
    };
    const handleVisibilityChange = () => {
      if (document.visibilityState === 'visible') markActivity();
    };

    scheduleTimeout();
    window.addEventListener('pointerdown', markActivity, { passive: true });
    window.addEventListener('keydown', markActivity);
    window.addEventListener('scroll', markActivity, { passive: true });
    window.addEventListener('touchstart', markActivity, { passive: true });
    document.addEventListener('visibilitychange', handleVisibilityChange);

    return () => {
      clearTimer();
      window.removeEventListener('pointerdown', markActivity);
      window.removeEventListener('keydown', markActivity);
      window.removeEventListener('scroll', markActivity);
      window.removeEventListener('touchstart', markActivity);
      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
  }, [phase, lockTimeoutMinutes, sessionTimeoutAction]);

  function renderPassiveOverlays() {
    return (
      <AppGlobalOverlays
        toasts={toasts}
        onCloseToast={removeToast}
        confirm={null}
        onCancelConfirm={() => {}}
        pendingTotpOpen={false}
        totpCode=""
        rememberDevice={false}
        onTotpCodeChange={() => {}}
        onRememberDeviceChange={() => {}}
        onConfirmTotp={() => {}}
        onCancelTotp={() => {}}
        onUseRecoveryCode={() => {}}
        totpSubmitting={false}
        disableTotpOpen={false}
        disableTotpPassword=""
        onDisableTotpPasswordChange={() => {}}
        onConfirmDisableTotp={() => {}}
        onCancelDisableTotp={() => {}}
        disableTotpSubmitting={false}
      />
    );
  }

  const ciphersQuery = useQuery({
    queryKey: ['ciphers', session?.accessToken],
    queryFn: () => getCiphers(authedFetch),
    enabled: phase === 'app' && !!session?.symEncKey && !!session?.symMacKey,
  });
  const foldersQuery = useQuery({
    queryKey: ['folders', session?.accessToken],
    queryFn: () => getFolders(authedFetch),
    enabled: phase === 'app' && !!session?.symEncKey && !!session?.symMacKey,
  });
  const sendsQuery = useQuery({
    queryKey: ['sends', session?.accessToken],
    queryFn: () => getSends(authedFetch),
    enabled: phase === 'app' && !!session?.symEncKey && !!session?.symMacKey,
  });
  const usersQuery = useQuery({
    queryKey: ['admin-users', session?.accessToken],
    queryFn: () => listAdminUsers(authedFetch),
    enabled: phase === 'app' && profile?.role === 'admin',
  });
  const invitesQuery = useQuery({
    queryKey: ['admin-invites', session?.accessToken],
    queryFn: () => listAdminInvites(authedFetch),
    enabled: phase === 'app' && profile?.role === 'admin',
  });
  const totpStatusQuery = useQuery({
    queryKey: ['totp-status', session?.accessToken],
    queryFn: () => getTotpStatus(authedFetch),
    enabled: phase === 'app' && !!session?.accessToken,
  });
  const authorizedDevicesQuery = useQuery({
    queryKey: ['authorized-devices', session?.accessToken],
    queryFn: () => getAuthorizedDevices(authedFetch),
    enabled: phase === 'app' && !!session?.accessToken,
  });

  useEffect(() => {
    if (phase !== 'app' || !session?.accessToken || !session?.symEncKey || !session?.symMacKey) return;
    if (!profile?.role || profile.role !== 'admin') return;
    if (repairAttemptRef.current === session.accessToken) return;

    repairAttemptRef.current = session.accessToken;
    void silentlyRepairBackupSettingsIfNeeded(session, profile);
  }, [phase, session?.accessToken, session?.symEncKey, session?.symMacKey, profile]);

  useEffect(() => {
    if (session?.accessToken) return;
    repairAttemptRef.current = '';
  }, [session?.accessToken]);

  useEffect(() => {
    if (!session?.symEncKey || !session?.symMacKey) {
      setDecryptedFolders([]);
      setDecryptedCiphers([]);
      setDecryptedSends([]);
      return;
    }
    if (!foldersQuery.data || !ciphersQuery.data || !sendsQuery.data) return;

    let active = true;
    (async () => {
      try {
        const encKey = base64ToBytes(session.symEncKey!);
        const macKey = base64ToBytes(session.symMacKey!);
        const decryptField = async (
          value: string | null | undefined,
          fieldEnc: Uint8Array = encKey,
          fieldMac: Uint8Array = macKey
        ): Promise<string> => {
          if (!value || typeof value !== 'string') return '';
          try {
            return await decryptStr(value, fieldEnc, fieldMac);
          } catch {
            // Backward-compatibility: some records may already be plain text.
            return value;
          }
        };

        const folders = await Promise.all(
          foldersQuery.data.map(async (folder) => ({
            ...folder,
            decName: await decryptField(folder.name, encKey, macKey),
          }))
        );

        const ciphers = await Promise.all(
          ciphersQuery.data.map(async (cipher) => {
            let itemEnc = encKey;
            let itemMac = macKey;
            if (cipher.key) {
              try {
                const itemKey = await decryptBw(cipher.key, encKey, macKey);
                itemEnc = itemKey.slice(0, 32);
                itemMac = itemKey.slice(32, 64);
              } catch {
                // keep user key when item key decrypt fails
              }
            }

            const nextCipher: Cipher = {
              ...cipher,
              decName: await decryptField(cipher.name || '', itemEnc, itemMac),
              decNotes: await decryptField(cipher.notes || '', itemEnc, itemMac),
            };
            if (cipher.login) {
              nextCipher.login = {
                ...cipher.login,
                decUsername: await decryptField(cipher.login.username || '', itemEnc, itemMac),
                decPassword: await decryptField(cipher.login.password || '', itemEnc, itemMac),
                decTotp: await decryptField(cipher.login.totp || '', itemEnc, itemMac),
                uris: await Promise.all(
                  (cipher.login.uris || []).map(async (u) => ({
                    ...u,
                    decUri: await decryptField(u.uri || '', itemEnc, itemMac),
                  }))
                ),
              };
            }
            if (Array.isArray(cipher.passwordHistory)) {
              nextCipher.passwordHistory = await Promise.all(
                cipher.passwordHistory.map(async (entry) => ({
                  ...entry,
                  decPassword: await decryptField(entry?.password || '', itemEnc, itemMac),
                }))
              );
            }
            if (cipher.card) {
              nextCipher.card = {
                ...cipher.card,
                decCardholderName: await decryptField(cipher.card.cardholderName || '', itemEnc, itemMac),
                decNumber: await decryptField(cipher.card.number || '', itemEnc, itemMac),
                decBrand: await decryptField(cipher.card.brand || '', itemEnc, itemMac),
                decExpMonth: await decryptField(cipher.card.expMonth || '', itemEnc, itemMac),
                decExpYear: await decryptField(cipher.card.expYear || '', itemEnc, itemMac),
                decCode: await decryptField(cipher.card.code || '', itemEnc, itemMac),
              };
            }
            if (cipher.identity) {
              nextCipher.identity = {
                ...cipher.identity,
                decTitle: await decryptField(cipher.identity.title || '', itemEnc, itemMac),
                decFirstName: await decryptField(cipher.identity.firstName || '', itemEnc, itemMac),
                decMiddleName: await decryptField(cipher.identity.middleName || '', itemEnc, itemMac),
                decLastName: await decryptField(cipher.identity.lastName || '', itemEnc, itemMac),
                decUsername: await decryptField(cipher.identity.username || '', itemEnc, itemMac),
                decCompany: await decryptField(cipher.identity.company || '', itemEnc, itemMac),
                decSsn: await decryptField(cipher.identity.ssn || '', itemEnc, itemMac),
                decPassportNumber: await decryptField(cipher.identity.passportNumber || '', itemEnc, itemMac),
                decLicenseNumber: await decryptField(cipher.identity.licenseNumber || '', itemEnc, itemMac),
                decEmail: await decryptField(cipher.identity.email || '', itemEnc, itemMac),
                decPhone: await decryptField(cipher.identity.phone || '', itemEnc, itemMac),
                decAddress1: await decryptField(cipher.identity.address1 || '', itemEnc, itemMac),
                decAddress2: await decryptField(cipher.identity.address2 || '', itemEnc, itemMac),
                decAddress3: await decryptField(cipher.identity.address3 || '', itemEnc, itemMac),
                decCity: await decryptField(cipher.identity.city || '', itemEnc, itemMac),
                decState: await decryptField(cipher.identity.state || '', itemEnc, itemMac),
                decPostalCode: await decryptField(cipher.identity.postalCode || '', itemEnc, itemMac),
                decCountry: await decryptField(cipher.identity.country || '', itemEnc, itemMac),
              };
            }
            if (cipher.sshKey) {
              const encryptedFingerprint = cipher.sshKey.keyFingerprint || cipher.sshKey.fingerprint || '';
              nextCipher.sshKey = {
                ...cipher.sshKey,
                decPrivateKey: await decryptField(cipher.sshKey.privateKey || '', itemEnc, itemMac),
                decPublicKey: await decryptField(cipher.sshKey.publicKey || '', itemEnc, itemMac),
                keyFingerprint: encryptedFingerprint || null,
                fingerprint: encryptedFingerprint || null,
                decFingerprint: await decryptField(encryptedFingerprint, itemEnc, itemMac),
              };
            }
            if (cipher.fields) {
              nextCipher.fields = await Promise.all(
                cipher.fields.map(async (field) => ({
                  ...field,
                  decName: await decryptField(field.name || '', itemEnc, itemMac),
                  decValue: await decryptField(field.value || '', itemEnc, itemMac),
                }))
              );
            }
            if (Array.isArray(cipher.attachments)) {
              nextCipher.attachments = await Promise.all(
                cipher.attachments.map(async (attachment) => ({
                  ...attachment,
                  decFileName: await decryptField(attachment.fileName || '', itemEnc, itemMac),
                }))
              );
            }
            return nextCipher;
          })
        );

        const sends = await Promise.all(
          sendsQuery.data.map(async (send) => {
            const nextSend: Send = { ...send };
            try {
              if (send.key) {
                const sendKeyRaw = await decryptBw(send.key, encKey, macKey);
                const derived = await deriveSendKeyParts(sendKeyRaw);
                nextSend.decName = await decryptField(send.name || '', derived.enc, derived.mac);
                nextSend.decNotes = await decryptField(send.notes || '', derived.enc, derived.mac);
                nextSend.decText = await decryptField(send.text?.text || '', derived.enc, derived.mac);
                if (send.file?.fileName) {
                  const decFileName = await decryptField(send.file.fileName, derived.enc, derived.mac);
                  nextSend.file = {
                    ...(send.file || {}),
                    fileName: decFileName || send.file.fileName,
                  };
                }
                const shareKey = await buildSendShareKey(send.key, session.symEncKey!, session.symMacKey!);
                nextSend.decShareKey = shareKey;
                nextSend.shareUrl = buildPublicSendUrl(window.location.origin, send.accessId, shareKey);
              } else {
                nextSend.decName = '';
                nextSend.decNotes = '';
                nextSend.decText = '';
              }
            } catch {
              nextSend.decName = t('txt_decrypt_failed');
            }
            return nextSend;
          })
        );

        if (!active) return;
        setDecryptedFolders(folders);
        setDecryptedCiphers(ciphers);
        setDecryptedSends(sends);
      } catch (error) {
        if (!active) return;
        pushToast('error', error instanceof Error ? error.message : t('txt_decrypt_failed_2'));
      }
    })();

    return () => {
      active = false;
    };
  }, [session?.symEncKey, session?.symMacKey, foldersQuery.data, ciphersQuery.data, sendsQuery.data]);

  useEffect(() => {
    if (!session?.symEncKey || !session?.symMacKey || !foldersQuery.data?.length) return;
    let cancelled = false;
    (async () => {
      const pending = foldersQuery.data.filter((folder) => {
        if (!folder?.id || !folder?.name) return false;
        if (migratedPlainFolderIdsRef.current.has(folder.id)) return false;
        return !looksLikeCipherString(String(folder.name));
      });
      if (!pending.length) return;
      for (const folder of pending) {
        try {
          await updateFolder(authedFetch, session, folder.id, String(folder.name));
          migratedPlainFolderIdsRef.current.add(folder.id);
        } catch {
          // keep silent; web still supports plaintext fallback display
        }
      }
      if (!cancelled) await foldersQuery.refetch();
    })();
    return () => {
      cancelled = true;
    };
  }, [session?.symEncKey, session?.symMacKey, foldersQuery.data, authedFetch]);

  async function refreshVaultSilently() {
    await Promise.all([ciphersQuery.refetch(), foldersQuery.refetch(), sendsQuery.refetch()]);
  }

  silentRefreshVaultRef.current = refreshVaultSilently;

  useEffect(() => {
    if (phase !== 'app' || !session?.accessToken || !session?.symEncKey || !session?.symMacKey) return;

    let disposed = false;
    let socket: WebSocket | null = null;
    let reconnectTimer: number | null = null;
    let reconnectAttempts = 0;

    const clearReconnectTimer = () => {
      if (reconnectTimer !== null) {
        window.clearTimeout(reconnectTimer);
        reconnectTimer = null;
      }
    };

    const scheduleReconnect = () => {
      if (disposed) return;
      clearReconnectTimer();
      const delay = Math.min(10000, 1000 * Math.max(1, reconnectAttempts + 1));
      reconnectAttempts += 1;
      reconnectTimer = window.setTimeout(() => {
        reconnectTimer = null;
        connect();
      }, delay);
    };

    const connect = () => {
      if (disposed) return;
      const accessToken = session.accessToken;
      if (!accessToken) return;
      try {
        const hubUrl = new URL('/notifications/hub', window.location.origin);
        hubUrl.searchParams.set('access_token', accessToken);
        hubUrl.protocol = hubUrl.protocol === 'https:' ? 'wss:' : 'ws:';
        socket = new WebSocket(hubUrl.toString());
      } catch {
        scheduleReconnect();
        return;
      }

      let pingTimer: number | null = null;

      const clearPingTimer = () => {
        if (pingTimer !== null) {
          window.clearInterval(pingTimer);
          pingTimer = null;
        }
      };

      socket.addEventListener('open', () => {
        reconnectAttempts = 0;
        void refreshAuthorizedDevicesRef.current();
        try {
          socket?.send(`{"protocol":"json","version":1}${SIGNALR_RECORD_SEPARATOR}`);
        } catch {
          socket?.close();
          return;
        }
        clearPingTimer();
        pingTimer = window.setInterval(() => {
          try {
            socket?.send(`{"type":6}${SIGNALR_RECORD_SEPARATOR}`);
          } catch {
            // send failure will trigger close event
          }
        }, 15_000);
      });

      socket.addEventListener('message', (event) => {
        if (disposed) return;
        if (typeof event.data !== 'string') return;

        const frames = parseSignalRTextFrames(event.data);
        for (const frame of frames) {
          if (frame.type !== 1 || frame.target !== 'ReceiveMessage') continue;
          const updateType = Number(frame.arguments?.[0]?.Type || 0);
          if (updateType === SIGNALR_UPDATE_TYPE_LOG_OUT) {
            logoutNow();
            return;
          }
          if (updateType === SIGNALR_UPDATE_TYPE_DEVICE_STATUS) {
            void refreshAuthorizedDevicesRef.current();
            continue;
          }
          if (updateType === SIGNALR_UPDATE_TYPE_BACKUP_RESTORE_PROGRESS) {
            const payload = frame.arguments?.[0]?.Payload;
            if (isBackupProgressDetail(payload)) dispatchBackupProgress(payload);
            continue;
          }
          if (updateType !== SIGNALR_UPDATE_TYPE_SYNC_VAULT) continue;
          const contextId = String(frame.arguments?.[0]?.ContextId || '').trim();
          if (contextId && contextId === getCurrentDeviceIdentifier()) continue;
          void silentRefreshVaultRef.current();
        }
      });

      socket.addEventListener('close', () => {
        socket = null;
        clearPingTimer();
        void refreshAuthorizedDevicesRef.current();
        scheduleReconnect();
      });

      socket.addEventListener('error', () => {
        try {
          socket?.close();
        } catch {
          // ignore close races
        }
      });
    };

    connect();

    return () => {
      disposed = true;
      clearReconnectTimer();
      if (socket) {
        const s = socket;
        socket = null;
        try {
          s.close();
        } catch {
          // ignore close races
        }
      }
    };
  }, [phase, session?.accessToken, session?.symEncKey, session?.symMacKey]);

  const vaultSendActions = useVaultSendActions({
    authedFetch,
    importAuthedFetch,
    session,
    profile,
    defaultKdfIterations,
    encryptedCiphers: ciphersQuery.data,
    encryptedFolders: foldersQuery.data,
    refetchCiphers: ciphersQuery.refetch,
    refetchFolders: foldersQuery.refetch,
    refetchSends: sendsQuery.refetch,
    onNotify: pushToast,
  });
  const accountSecurityActions = useAccountSecurityActions({
    authedFetch,
    profile,
    defaultKdfIterations,
    disableTotpPassword,
    clearDisableTotpDialog: () => {
      setDisableTotpOpen(false);
      setDisableTotpPassword('');
    },
    onLogoutNow: logoutNow,
    onNotify: pushToast,
    onProfileUpdated: setProfile,
    onSetConfirm: setConfirm,
    refetchTotpStatus: totpStatusQuery.refetch,
    refetchAuthorizedDevices: authorizedDevicesQuery.refetch,
  });
  const adminActions = useAdminActions({
    authedFetch,
    onNotify: pushToast,
    onSetConfirm: setConfirm,
    refetchUsers: usersQuery.refetch,
    refetchInvites: invitesQuery.refetch,
  });

  refreshAuthorizedDevicesRef.current = async () => {
    await authorizedDevicesQuery.refetch();
  };

  const hashPathRaw = typeof window !== 'undefined' ? window.location.hash || '' : '';
  const hashPath = hashPathRaw.startsWith('#') ? hashPathRaw.slice(1) : hashPathRaw;
  const hashPathOnly = String(hashPath || '').split('?')[0].split('#')[0];
  const trimmedHashPath = hashPathOnly.replace(/^\/+/, '').replace(/\/+$/, '');
  const normalizedHashPath = trimmedHashPath ? `/${trimmedHashPath}` : '/';
  const isImportHashRoute = IMPORT_ROUTE_ALIASES.has(normalizedHashPath);
  const effectiveLocation = hashPath.startsWith('/send/') || hashPath === '/recover-2fa' ? hashPath : location;
  const publicSendMatch = effectiveLocation.match(/^\/send\/([^/]+)(?:\/([^/]+))?\/?$/i);
  const isRecoverTwoFactorRoute = effectiveLocation === '/recover-2fa';
  const isPublicSendRoute = !!publicSendMatch;
  const isImportRoute = location === IMPORT_ROUTE || IMPORT_ROUTE_ALIASES.has(location);
  const showSidebarToggle = mobileLayout && (location === '/vault' || location === '/sends');
  const sidebarToggleTitle = location === '/vault' ? t('txt_folders') : t('txt_type');
  const mobilePrimaryRoute =
    location === '/sends'
      ? '/sends'
      : location === '/vault/totp'
        ? '/vault/totp'
        : location === '/vault'
          ? '/vault'
          : '/settings';
  const currentPageTitle = (() => {
    if (location === '/vault/totp') return t('txt_verification_code');
    if (location === '/sends') return t('nav_sends');
    if (location === '/admin') return t('nav_admin_panel');
    if (location === '/security/devices') return t('nav_device_management');
    if (location === '/backup') return t('nav_backup_strategy');
    if (isImportRoute) return t('nav_import_export');
    if (location === SETTINGS_ACCOUNT_ROUTE) return t('nav_account_settings');
    if (location === SETTINGS_HOME_ROUTE) return t('txt_settings');
    return t('nav_my_vault');
  })();

  useEffect(() => {
    if (phase === 'app' && location === '/' && !isPublicSendRoute) navigate('/vault');
  }, [phase, location, isPublicSendRoute, navigate]);

  useEffect(() => {
    if (phase === 'app' && isImportHashRoute && location !== IMPORT_ROUTE) {
      navigate(IMPORT_ROUTE);
    }
  }, [phase, isImportHashRoute, location, navigate]);

  useEffect(() => {
    if (phase === 'app' && profile?.role !== 'admin' && location === '/backup') {
      navigate('/vault');
    }
  }, [phase, profile?.role, location, navigate]);

  useEffect(() => {
    if (phase === 'app' && !mobileLayout && location === SETTINGS_HOME_ROUTE) {
      navigate(SETTINGS_ACCOUNT_ROUTE);
    }
  }, [phase, mobileLayout, location, navigate]);

  const mainRoutesProps = {
    profile,
    session,
    mobileLayout,
    mobileSidebarToggleKey,
    importRoute: IMPORT_ROUTE,
    settingsHomeRoute: SETTINGS_HOME_ROUTE,
    settingsAccountRoute: SETTINGS_ACCOUNT_ROUTE,
    decryptedCiphers,
    decryptedFolders,
    decryptedSends,
    ciphersLoading: ciphersQuery.isFetching,
    foldersLoading: foldersQuery.isFetching,
    sendsLoading: sendsQuery.isFetching,
    users: usersQuery.data || [],
    invites: invitesQuery.data || [],
    totpEnabled: !!totpStatusQuery.data?.enabled,
    lockTimeoutMinutes,
    sessionTimeoutAction,
    authorizedDevices: authorizedDevicesQuery.data || [],
    authorizedDevicesLoading: authorizedDevicesQuery.isFetching,
    onNavigate: navigate,
    onLogout: handleLogout,
    onNotify: pushToast,
    onImport: vaultSendActions.importVault,
    onImportEncryptedRaw: vaultSendActions.importEncryptedRaw,
    onExport: vaultSendActions.exportVault,
    onCreateVaultItem: vaultSendActions.createVaultItem,
    onUpdateVaultItem: vaultSendActions.updateVaultItem,
    onDeleteVaultItem: vaultSendActions.deleteVaultItem,
    onArchiveVaultItem: vaultSendActions.archiveVaultItem,
    onUnarchiveVaultItem: vaultSendActions.unarchiveVaultItem,
    onBulkDeleteVaultItems: vaultSendActions.bulkDeleteVaultItems,
    onBulkPermanentDeleteVaultItems: vaultSendActions.bulkPermanentDeleteVaultItems,
    onBulkRestoreVaultItems: vaultSendActions.bulkRestoreVaultItems,
    onBulkArchiveVaultItems: vaultSendActions.bulkArchiveVaultItems,
    onBulkUnarchiveVaultItems: vaultSendActions.bulkUnarchiveVaultItems,
    onBulkMoveVaultItems: vaultSendActions.bulkMoveVaultItems,
    onVerifyMasterPassword: vaultSendActions.verifyMasterPassword,
    onCreateFolder: vaultSendActions.createFolder,
    onRenameFolder: vaultSendActions.renameFolder,
    onDeleteFolder: vaultSendActions.deleteFolder,
    onBulkDeleteFolders: vaultSendActions.bulkDeleteFolders,
    onDownloadVaultAttachment: vaultSendActions.downloadVaultAttachment,
    downloadingAttachmentKey: vaultSendActions.downloadingAttachmentKey,
    attachmentDownloadPercent: vaultSendActions.attachmentDownloadPercent,
    uploadingAttachmentName: vaultSendActions.uploadingAttachmentName,
    attachmentUploadPercent: vaultSendActions.attachmentUploadPercent,
    onRefreshVault: vaultSendActions.refreshVault,
    onCreateSend: vaultSendActions.createSend,
    onUpdateSend: vaultSendActions.updateSend,
    onDeleteSend: vaultSendActions.deleteSend,
    onBulkDeleteSends: vaultSendActions.bulkDeleteSends,
    uploadingSendFileName: vaultSendActions.uploadingSendFileName,
    sendUploadPercent: vaultSendActions.sendUploadPercent,
    onChangePassword: accountSecurityActions.changePassword,
    onSavePasswordHint: accountSecurityActions.savePasswordHint,
    onEnableTotp: async (secret: string, token: string) => {
      await accountSecurityActions.enableTotp(secret, token);
      await totpStatusQuery.refetch();
    },
    onOpenDisableTotp: () => setDisableTotpOpen(true),
    onGetRecoveryCode: accountSecurityActions.getRecoveryCode,
    onGetApiKey: accountSecurityActions.getApiKey,
    onRotateApiKey: accountSecurityActions.rotateApiKey,
    onLockTimeoutChange: setLockTimeoutMinutes,
    onSessionTimeoutActionChange: setSessionTimeoutAction,
    onRefreshAuthorizedDevices: accountSecurityActions.refreshAuthorizedDevices,
    onRenameAuthorizedDevice: accountSecurityActions.renameAuthorizedDevice,
    onRevokeDeviceTrust: accountSecurityActions.openRevokeDeviceTrust,
    onRemoveDevice: accountSecurityActions.openRemoveDevice,
    onRevokeAllDeviceTrust: accountSecurityActions.openRevokeAllDeviceTrust,
    onRemoveAllDevices: accountSecurityActions.openRemoveAllDevices,
    onRefreshAdmin: adminActions.refreshAdmin,
    onCreateInvite: adminActions.createInvite,
    onDeleteAllInvites: adminActions.deleteAllInvites,
    onToggleUserStatus: adminActions.toggleUserStatus,
    onDeleteUser: adminActions.deleteUser,
    onRevokeInvite: adminActions.revokeInvite,
    onExportBackup: backupActions.exportBackup,
    onImportBackup: backupActions.importBackup,
    onImportBackupAllowingChecksumMismatch: backupActions.importBackupAllowingChecksumMismatch,
    onLoadBackupSettings: backupActions.loadSettings,
    onSaveBackupSettings: backupActions.saveSettings,
    onRunRemoteBackup: backupActions.runRemoteBackup,
    onListRemoteBackups: backupActions.listRemoteBackups,
    onDownloadRemoteBackup: backupActions.downloadRemoteBackup,
    onInspectRemoteBackup: backupActions.inspectRemoteBackup,
    onDeleteRemoteBackup: backupActions.deleteRemoteBackup,
    onRestoreRemoteBackup: backupActions.restoreRemoteBackup,
    onRestoreRemoteBackupAllowingChecksumMismatch: backupActions.restoreRemoteBackupAllowingChecksumMismatch,
  };

  if (jwtWarning) {
    return <JwtWarningPage reason={jwtWarning.reason} minLength={jwtWarning.minLength} />;
  }

  if (publicSendMatch) {
    return (
      <>
        <PublicSendPage accessId={decodeURIComponent(publicSendMatch[1])} keyPart={publicSendMatch[2] ? decodeURIComponent(publicSendMatch[2]) : null} />
        {renderPassiveOverlays()}
      </>
    );
  }

  if (isRecoverTwoFactorRoute && phase !== 'app') {
    return (
      <>
        <RecoverTwoFactorPage
          values={recoverValues}
          onChange={setRecoverValues}
          onSubmit={() => void handleRecoverTwoFactorSubmit()}
          onCancel={() => {
            setRecoverValues({ email: '', password: '', recoveryCode: '' });
            navigate('/login');
          }}
        />
        {renderPassiveOverlays()}
      </>
    );
  }

  if (phase === 'register' || phase === 'login' || phase === 'locked') {
    return (
      <>
        <AuthViews
          mode={phase}
          pendingAction={pendingAuthAction}
          unlockReady={!!session?.email}
          unlockPreparing={unlockPreparing}
          loginValues={loginValues}
          registerValues={registerValues}
          unlockPassword={unlockPassword}
          emailForLock={profile?.email || session?.email || ''}
          loginHintLoading={loginHintState.loading}
          onChangeLogin={setLoginValues}
          onChangeRegister={setRegisterValues}
          onChangeUnlock={setUnlockPassword}
          onSubmitLogin={() => void handleLogin()}
          onSubmitRegister={() => void handleRegister()}
          onSubmitUnlock={() => void handleUnlock()}
          onGotoLogin={() => {
            setPhase('login');
            navigate('/login');
          }}
          onGotoRegister={() => {
            if (inviteCodeFromUrl) {
              setRegisterValues((prev) => ({ ...prev, inviteCode: inviteCodeFromUrl }));
            }
            setPhase('register');
            navigate('/register');
          }}
          onLogout={logoutNow}
          onTogglePasswordHint={() => void handleTogglePasswordHint()}
          onShowLockedPasswordHint={handleShowLockedPasswordHint}
        />
        <AppGlobalOverlays
          toasts={toasts}
          onCloseToast={removeToast}
          confirm={confirm}
          onCancelConfirm={() => setConfirm(null)}
          pendingTotpOpen={!!pendingTotp}
          totpCode={totpCode}
          rememberDevice={rememberDevice}
          onTotpCodeChange={setTotpCode}
          onRememberDeviceChange={setRememberDevice}
          onConfirmTotp={() => void handleTotpVerify()}
          onCancelTotp={() => {
            if (totpSubmitting) return;
            setPendingTotp(null);
            setPendingTotpMode(null);
            setTotpCode('');
            setRememberDevice(true);
          }}
          onUseRecoveryCode={() => {
            if (totpSubmitting) return;
            setPendingTotp(null);
            setPendingTotpMode(null);
            setTotpCode('');
            setRememberDevice(true);
            navigate('/recover-2fa');
          }}
          totpSubmitting={totpSubmitting}
          disableTotpOpen={false}
          disableTotpPassword=""
          onDisableTotpPasswordChange={() => {}}
          onConfirmDisableTotp={() => {}}
          onCancelDisableTotp={() => {}}
          disableTotpSubmitting={false}
        />
      </>
    );
  }

  return (
    <>
      <AppAuthenticatedShell
        profile={profile}
        location={location}
        mobilePrimaryRoute={mobilePrimaryRoute}
        currentPageTitle={currentPageTitle}
        showSidebarToggle={showSidebarToggle}
        sidebarToggleTitle={sidebarToggleTitle}
        settingsAccountRoute={SETTINGS_ACCOUNT_ROUTE}
        importRoute={IMPORT_ROUTE}
        isImportRoute={isImportRoute}
        darkMode={resolvedTheme === 'dark'}
        themeToggleTitle={resolvedTheme === 'dark' ? t('txt_switch_to_light_mode') : t('txt_switch_to_dark_mode')}
        onLock={handleLock}
        onLogout={handleLogout}
        onToggleTheme={handleToggleTheme}
        onToggleMobileSidebar={() => setMobileSidebarToggleKey((key) => key + 1)}
        mainRoutesProps={mainRoutesProps}
      />

      <AppGlobalOverlays
        toasts={toasts}
        onCloseToast={removeToast}
        confirm={confirm}
        onCancelConfirm={() => setConfirm(null)}
        pendingTotpOpen={false}
        totpCode=""
        rememberDevice={false}
        onTotpCodeChange={() => {}}
        onRememberDeviceChange={() => {}}
        onConfirmTotp={() => {}}
        onCancelTotp={() => {}}
        onUseRecoveryCode={() => {}}
        totpSubmitting={false}
        disableTotpOpen={disableTotpOpen}
        disableTotpPassword={disableTotpPassword}
        onDisableTotpPasswordChange={setDisableTotpPassword}
        onConfirmDisableTotp={() => {
          if (disableTotpSubmitting) return;
          void (async () => {
            setDisableTotpSubmitting(true);
            try {
              await accountSecurityActions.disableTotp();
            } finally {
              setDisableTotpSubmitting(false);
            }
          })();
        }}
        onCancelDisableTotp={() => {
          if (disableTotpSubmitting) return;
          setDisableTotpOpen(false);
          setDisableTotpPassword('');
        }}
        disableTotpSubmitting={disableTotpSubmitting}
      />
    </>
  );
}
