import { useEffect, useMemo, useState } from 'preact/hooks';
import { Link, Route, Switch, useLocation } from 'wouter';
import { useQuery } from '@tanstack/react-query';
import { CircleHelp, Lock, LogOut, Send as SendIcon, Settings as SettingsIcon, Shield, ShieldUser, Vault } from 'lucide-preact';
import AuthViews from '@/components/AuthViews';
import ConfirmDialog from '@/components/ConfirmDialog';
import ToastHost from '@/components/ToastHost';
import VaultPage from '@/components/VaultPage';
import SendsPage from '@/components/SendsPage';
import PublicSendPage from '@/components/PublicSendPage';
import RecoverTwoFactorPage from '@/components/RecoverTwoFactorPage';
import JwtWarningPage from '@/components/JwtWarningPage';
import SettingsPage from '@/components/SettingsPage';
import SecurityDevicesPage from '@/components/SecurityDevicesPage';
import AdminPage from '@/components/AdminPage';
import HelpPage from '@/components/HelpPage';
import {
  changeMasterPassword,
  createFolder,
  createCipher,
  createAuthedFetch,
  createInvite,
  createSend,
  deleteAllInvites,
  deleteCipher,
  deleteSend,
  deleteUser,
  deriveLoginHash,
  bulkMoveCiphers,
  getCiphers,
  getFolders,
  getProfile,
  getAuthorizedDevices,
  getSetupStatus,
  getSends,
  getTotpStatus,
  getTotpRecoveryCode,
  getWebConfig,
  listAdminInvites,
  listAdminUsers,
  loadSession,
  loginWithPassword,
  registerAccount,
  recoverTwoFactor,
  revokeInvite,
  revokeAuthorizedDeviceTrust,
  revokeAllAuthorizedDeviceTrust,
  saveSession,
  setTotp,
  setUserStatus,
  deleteAuthorizedDevice,
  updateCipher,
  updateSend,
  buildSendShareKey,
  unlockVaultKey,
  verifyMasterPassword,
} from '@/lib/api';
import { base64ToBytes, decryptBw, decryptStr, hkdf } from '@/lib/crypto';
import { t } from '@/lib/i18n';
import type { AppPhase, AuthorizedDevice, Cipher, Folder, Profile, Send, SendDraft, SessionState, ToastMessage, VaultDraft } from '@/lib/types';

interface PendingTotp {
  email: string;
  passwordHash: string;
  masterKey: Uint8Array;
}

type JwtUnsafeReason = 'missing' | 'default' | 'too_short';

const SEND_KEY_SALT = 'bitwarden-send';
const SEND_KEY_PURPOSE = 'send';

function buildPublicSendUrl(origin: string, accessId: string, keyPart: string): string {
  return `${origin}/#/send/${accessId}/${keyPart}`;
}

async function deriveSendKeyParts(sendKeyMaterial: Uint8Array): Promise<{ enc: Uint8Array; mac: Uint8Array }> {
  if (sendKeyMaterial.length >= 64) {
    return { enc: sendKeyMaterial.slice(0, 32), mac: sendKeyMaterial.slice(32, 64) };
  }
  const derived = await hkdf(sendKeyMaterial, SEND_KEY_SALT, SEND_KEY_PURPOSE, 64);
  return { enc: derived.slice(0, 32), mac: derived.slice(32, 64) };
}

export default function App() {
  const [location, navigate] = useLocation();
  const [phase, setPhase] = useState<AppPhase>('loading');
  const [session, setSessionState] = useState<SessionState | null>(null);
  const [profile, setProfile] = useState<Profile | null>(null);
  const [defaultKdfIterations, setDefaultKdfIterations] = useState(600000);
  const [setupRegistered, setSetupRegistered] = useState(true);
  const [jwtWarning, setJwtWarning] = useState<{ reason: JwtUnsafeReason; minLength: number } | null>(null);

  const [loginValues, setLoginValues] = useState({ email: '', password: '' });
  const [registerValues, setRegisterValues] = useState({
    name: '',
    email: '',
    password: '',
    password2: '',
    inviteCode: '',
  });
  const [unlockPassword, setUnlockPassword] = useState('');
  const [pendingTotp, setPendingTotp] = useState<PendingTotp | null>(null);
  const [totpCode, setTotpCode] = useState('');
  const [rememberDevice, setRememberDevice] = useState(true);

  const [disableTotpOpen, setDisableTotpOpen] = useState(false);
  const [disableTotpPassword, setDisableTotpPassword] = useState('');
  const [recoverValues, setRecoverValues] = useState({ email: '', password: '', recoveryCode: '' });

  const [confirm, setConfirm] = useState<{
    title: string;
    message: string;
    danger?: boolean;
    showIcon?: boolean;
    onConfirm: () => void;
  } | null>(null);

  const [toasts, setToasts] = useState<ToastMessage[]>([]);
  const [decryptedFolders, setDecryptedFolders] = useState<Folder[]>([]);
  const [decryptedCiphers, setDecryptedCiphers] = useState<Cipher[]>([]);
  const [decryptedSends, setDecryptedSends] = useState<Send[]>([]);

  function setSession(next: SessionState | null) {
    setSessionState(next);
    saveSession(next);
  }

  function pushToast(type: ToastMessage['type'], text: string) {
    const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
    setToasts((prev) => [...prev.slice(-3), { id, type, text }]);
    window.setTimeout(() => {
      setToasts((prev) => prev.filter((x) => x.id !== id));
    }, 4500);
  }

  const authedFetch = useMemo(
    () =>
      createAuthedFetch(
        () => session,
        (next) => {
          setSession(next);
          if (!next) {
            setProfile(null);
            setPhase(setupRegistered ? 'login' : 'register');
          }
        }
      ),
    [session, setupRegistered]
  );

  useEffect(() => {
    let mounted = true;
    (async () => {
      const [setup, config] = await Promise.all([getSetupStatus(), getWebConfig()]);
      if (!mounted) return;
      setSetupRegistered(setup.registered);
      setDefaultKdfIterations(Number(config.defaultKdfIterations || 600000));
      const jwtUnsafeReason = config.jwtUnsafeReason || null;
      if (jwtUnsafeReason) {
        setJwtWarning({
          reason: jwtUnsafeReason,
          minLength: Number(config.jwtSecretMinLength || 32),
        });
        setSession(null);
        setProfile(null);
        setPhase('login');
        return;
      }
      setJwtWarning(null);

      const loaded = loadSession();
      if (!loaded) {
        setPhase(setup.registered ? 'login' : 'register');
        return;
      }
      setSession(loaded);

      try {
        const profileResp = await getProfile(
          createAuthedFetch(
            () => loaded,
            (next) => {
              if (!next) return;
              setSession(next);
            }
          )
        );
        if (!mounted) return;
        setProfile(profileResp);
        setPhase('locked');
      } catch {
        setSession(null);
        setPhase(setup.registered ? 'login' : 'register');
      }
    })();

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

  async function finalizeLogin(tokenAccess: string, tokenRefresh: string, email: string, masterKey: Uint8Array) {
    const baseSession: SessionState = { accessToken: tokenAccess, refreshToken: tokenRefresh, email };
    const tempFetch = createAuthedFetch(
      () => baseSession,
      () => {}
    );
    const profileResp = await getProfile(tempFetch);
    const keys = await unlockVaultKey(profileResp.key, masterKey);
    const nextSession = { ...baseSession, ...keys };
    setSession(nextSession);
    setProfile(profileResp);
    setPendingTotp(null);
    setTotpCode('');
    setPhase('app');
    if (location === '/' || location === '/login' || location === '/register' || location === '/lock') {
      navigate('/vault');
    }
    pushToast('success', t('txt_login_success'));
  }

  async function handleLogin() {
    if (!loginValues.email || !loginValues.password) {
      pushToast('error', t('txt_please_input_email_and_password'));
      return;
    }
    try {
      const derived = await deriveLoginHash(loginValues.email, loginValues.password, defaultKdfIterations);
      const token = await loginWithPassword(loginValues.email, derived.hash, { useRememberToken: true });
      if ('access_token' in token && token.access_token) {
        await finalizeLogin(token.access_token, token.refresh_token, loginValues.email.toLowerCase(), derived.masterKey);
        return;
      }
      const tokenError = token as { TwoFactorProviders?: unknown; error_description?: string; error?: string };
      if (tokenError.TwoFactorProviders) {
        setPendingTotp({
          email: loginValues.email.toLowerCase(),
          passwordHash: derived.hash,
          masterKey: derived.masterKey,
        });
        setTotpCode('');
        setRememberDevice(true);
        return;
      }
      pushToast('error', tokenError.error_description || tokenError.error || t('txt_login_failed'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_login_failed'));
    }
  }

  async function handleTotpVerify() {
    if (!pendingTotp) return;
    if (!totpCode.trim()) {
      pushToast('error', t('txt_please_input_totp_code'));
      return;
    }
    const token = await loginWithPassword(pendingTotp.email, pendingTotp.passwordHash, {
      totpCode: totpCode.trim(),
      rememberDevice,
    });
    if ('access_token' in token && token.access_token) {
      await finalizeLogin(token.access_token, token.refresh_token, pendingTotp.email, pendingTotp.masterKey);
      return;
    }
    const tokenError = token as { error_description?: string; error?: string };
    pushToast('error', tokenError.error_description || tokenError.error || t('txt_totp_verify_failed'));
  }

  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 derived = await deriveLoginHash(email, password, defaultKdfIterations);
      const recovered = await recoverTwoFactor(email, derived.hash, recoveryCode);
      const token = await loginWithPassword(email, derived.hash, { useRememberToken: false });
      if ('access_token' in token && token.access_token) {
        await finalizeLogin(token.access_token, token.refresh_token, email, derived.masterKey);
        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 (!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;
    }
    const resp = await registerAccount({
      email: registerValues.email.toLowerCase(),
      name: registerValues.name.trim(),
      password: registerValues.password,
      inviteCode: registerValues.inviteCode.trim(),
      fallbackIterations: defaultKdfIterations,
    });
    if (!resp.ok) {
      pushToast('error', resp.message);
      return;
    }
    setLoginValues({ email: registerValues.email.toLowerCase(), password: '' });
    setPhase('login');
    pushToast('success', t('txt_registration_succeeded_please_sign_in'));
  }

  async function handleUnlock() {
    if (!session || !profile) return;
    if (!unlockPassword) {
      pushToast('error', t('txt_please_input_master_password'));
      return;
    }
    try {
      const derived = await deriveLoginHash(profile.email || session.email, unlockPassword, defaultKdfIterations);
      const keys = await unlockVaultKey(profile.key, derived.masterKey);
      setSession({ ...session, ...keys });
      setUnlockPassword('');
      setPhase('app');
      if (location === '/' || location === '/lock') navigate('/vault');
      pushToast('success', t('txt_unlocked'));
    } catch {
      pushToast('error', t('txt_unlock_failed_master_password_is_incorrect'));
    }
  }

  function handleLock() {
    if (!session) return;
    const nextSession = { ...session };
    delete nextSession.symEncKey;
    delete nextSession.symMacKey;
    setSession(nextSession);
    setPhase('locked');
    navigate('/lock');
  }

  function logoutNow() {
    setConfirm(null);
    setSession(null);
    setProfile(null);
    setPendingTotp(null);
    setPhase(setupRegistered ? 'login' : 'register');
    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();
      },
    });
  }

  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 (!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 (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) {
              nextCipher.sshKey = {
                ...cipher.sshKey,
                decPrivateKey: await decryptField(cipher.sshKey.privateKey || '', itemEnc, itemMac),
                decPublicKey: await decryptField(cipher.sshKey.publicKey || '', itemEnc, itemMac),
                decFingerprint: await decryptField(cipher.sshKey.fingerprint || '', 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),
                }))
              );
            }
            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]);

  async function changePasswordAction(currentPassword: string, nextPassword: string, nextPassword2: string) {
    if (!profile) return;
    if (!currentPassword || !nextPassword) {
      pushToast('error', t('txt_current_new_password_is_required'));
      return;
    }
    if (nextPassword.length < 12) {
      pushToast('error', t('txt_new_password_must_be_at_least_12_chars'));
      return;
    }
    if (nextPassword !== nextPassword2) {
      pushToast('error', t('txt_new_passwords_do_not_match'));
      return;
    }
    try {
      await changeMasterPassword(authedFetch, {
        email: profile.email,
        currentPassword,
        newPassword: nextPassword,
        currentIterations: defaultKdfIterations,
        profileKey: profile.key,
      });
      handleLogout();
      pushToast('success', t('txt_master_password_changed_please_login_again'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_change_password_failed'));
    }
  }

  async function enableTotpAction(secret: string, token: string) {
    if (!secret.trim() || !token.trim()) {
      pushToast('error', t('txt_secret_and_code_are_required'));
      return;
    }
    try {
      await setTotp(authedFetch, { enabled: true, secret: secret.trim(), token: token.trim() });
      pushToast('success', t('txt_totp_enabled'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_enable_totp_failed'));
    }
  }

  async function disableTotpAction() {
    if (!profile) return;
    if (!disableTotpPassword) {
      pushToast('error', t('txt_please_input_master_password'));
      return;
    }
    try {
      const derived = await deriveLoginHash(profile.email, disableTotpPassword, defaultKdfIterations);
      await setTotp(authedFetch, { enabled: false, masterPasswordHash: derived.hash });
      if (profile?.id) localStorage.removeItem(`nodewarden.totp.secret.${profile.id}`);
      setDisableTotpOpen(false);
      setDisableTotpPassword('');
      await totpStatusQuery.refetch();
      pushToast('success', t('txt_totp_disabled'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_disable_totp_failed'));
    }
  }

  async function refreshVault() {
    await Promise.all([ciphersQuery.refetch(), foldersQuery.refetch(), sendsQuery.refetch()]);
    pushToast('success', t('txt_vault_synced'));
  }

  async function refreshAuthorizedDevices() {
    await authorizedDevicesQuery.refetch();
  }

  async function revokeDeviceTrustAction(device: AuthorizedDevice) {
    await revokeAuthorizedDeviceTrust(authedFetch, device.identifier);
    await authorizedDevicesQuery.refetch();
    pushToast('success', t('txt_device_authorization_revoked'));
  }

  async function revokeAllDeviceTrustAction() {
    await revokeAllAuthorizedDeviceTrust(authedFetch);
    await authorizedDevicesQuery.refetch();
    pushToast('success', t('txt_all_device_authorizations_revoked'));
  }

  async function removeDeviceAction(device: AuthorizedDevice) {
    await deleteAuthorizedDevice(authedFetch, device.identifier);
    await authorizedDevicesQuery.refetch();
    pushToast('success', t('txt_device_removed'));
  }

  async function createVaultItem(draft: VaultDraft) {
    if (!session) return;
    try {
      await createCipher(authedFetch, session, draft);
      await Promise.all([ciphersQuery.refetch(), foldersQuery.refetch()]);
      pushToast('success', t('txt_item_created'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_create_item_failed'));
      throw error;
    }
  }

  async function updateVaultItem(cipher: Cipher, draft: VaultDraft) {
    if (!session) return;
    try {
      await updateCipher(authedFetch, session, cipher, draft);
      await Promise.all([ciphersQuery.refetch(), foldersQuery.refetch()]);
      pushToast('success', t('txt_item_updated'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_update_item_failed'));
      throw error;
    }
  }

  async function deleteVaultItem(cipher: Cipher) {
    try {
      await deleteCipher(authedFetch, cipher.id);
      await Promise.all([ciphersQuery.refetch(), foldersQuery.refetch()]);
      pushToast('success', t('txt_item_deleted'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_delete_item_failed'));
      throw error;
    }
  }

  async function bulkDeleteVaultItems(ids: string[]) {
    try {
      for (const id of ids) {
        await deleteCipher(authedFetch, id);
      }
      await Promise.all([ciphersQuery.refetch(), foldersQuery.refetch()]);
      pushToast('success', t('txt_deleted_selected_items'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_bulk_delete_failed'));
      throw error;
    }
  }

  async function bulkMoveVaultItems(ids: string[], folderId: string | null) {
    try {
      await bulkMoveCiphers(authedFetch, ids, folderId);
      await Promise.all([ciphersQuery.refetch(), foldersQuery.refetch()]);
      pushToast('success', t('txt_moved_selected_items'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_bulk_move_failed'));
      throw error;
    }
  }

  async function getRecoveryCodeAction(masterPassword: string): Promise<string> {
    if (!profile) throw new Error(t('txt_profile_unavailable'));
    const normalized = String(masterPassword || '');
    if (!normalized) throw new Error(t('txt_master_password_is_required'));
    const derived = await deriveLoginHash(profile.email, normalized, defaultKdfIterations);
    const code = await getTotpRecoveryCode(authedFetch, derived.hash);
    if (!code) throw new Error(t('txt_recovery_code_is_empty'));
    return code;
  }

  async function createSendItem(draft: SendDraft, autoCopyLink: boolean) {
    if (!session) return;
    try {
      const created = await createSend(authedFetch, session, draft);
      await sendsQuery.refetch();
      if (autoCopyLink && created.key && session.symEncKey && session.symMacKey) {
        const keyPart = await buildSendShareKey(created.key, session.symEncKey, session.symMacKey);
        const shareUrl = buildPublicSendUrl(window.location.origin, created.accessId, keyPart);
        await navigator.clipboard.writeText(shareUrl);
      }
      pushToast('success', t('txt_send_created'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_create_send_failed'));
      throw error;
    }
  }

  async function updateSendItem(send: Send, draft: SendDraft, autoCopyLink: boolean) {
    if (!session) return;
    try {
      const updated = await updateSend(authedFetch, session, send, draft);
      await sendsQuery.refetch();
      if (autoCopyLink && updated.key && session.symEncKey && session.symMacKey) {
        const keyPart = await buildSendShareKey(updated.key, session.symEncKey, session.symMacKey);
        const shareUrl = buildPublicSendUrl(window.location.origin, updated.accessId, keyPart);
        await navigator.clipboard.writeText(shareUrl);
      }
      pushToast('success', t('txt_send_updated'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_update_send_failed'));
      throw error;
    }
  }

  async function deleteSendItem(send: Send) {
    try {
      await deleteSend(authedFetch, send.id);
      await sendsQuery.refetch();
      pushToast('success', t('txt_send_deleted'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_delete_send_failed'));
      throw error;
    }
  }

  async function bulkDeleteSendItems(ids: string[]) {
    try {
      for (const id of ids) {
        await deleteSend(authedFetch, id);
      }
      await sendsQuery.refetch();
      pushToast('success', t('txt_deleted_selected_sends'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_bulk_delete_sends_failed'));
      throw error;
    }
  }

  async function verifyMasterPasswordAction(email: string, password: string) {
    const derived = await deriveLoginHash(email, password, defaultKdfIterations);
    await verifyMasterPassword(authedFetch, derived.hash);
  }

  async function createFolderAction(name: string) {
    const folderName = name.trim();
    if (!folderName) {
      pushToast('error', t('txt_folder_name_is_required'));
      return;
    }
    try {
      await createFolder(authedFetch, folderName);
      await foldersQuery.refetch();
      pushToast('success', t('txt_folder_created'));
    } catch (error) {
      pushToast('error', error instanceof Error ? error.message : t('txt_create_folder_failed'));
      throw error;
    }
  }

  const hashPathRaw = typeof window !== 'undefined' ? window.location.hash || '' : '';
  const hashPath = hashPathRaw.startsWith('#') ? hashPathRaw.slice(1) : hashPathRaw;
  const effectiveLocation = hashPath.startsWith('/send/') || hashPath === '/recover-2fa' ? hashPath : location;
  const publicSendMatch = effectiveLocation.match(/^\/send\/([^/]+)(?:\/([^/]+))?\/?$/i);
  const isRecoverTwoFactorRoute = effectiveLocation === '/recover-2fa';
  const isPublicSendRoute = !!publicSendMatch;

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

  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} />
        <ToastHost toasts={toasts} onClose={(id) => setToasts((prev) => prev.filter((x) => x.id !== id))} />
      </>
    );
  }

  if (isRecoverTwoFactorRoute && phase !== 'app') {
    return (
      <>
        <RecoverTwoFactorPage
          values={recoverValues}
          onChange={setRecoverValues}
          onSubmit={() => void handleRecoverTwoFactorSubmit()}
          onCancel={() => {
            setRecoverValues({ email: '', password: '', recoveryCode: '' });
            navigate('/login');
          }}
        />
        <ToastHost toasts={toasts} onClose={(id) => setToasts((prev) => prev.filter((x) => x.id !== id))} />
      </>
    );
  }

  if (phase === 'loading') {
    return (
      <>
        <div className="loading-screen">{t('txt_loading_nodewarden')}</div>
        <ToastHost toasts={toasts} onClose={(id) => setToasts((prev) => prev.filter((x) => x.id !== id))} />
      </>
    );
  }

  if (phase === 'register' || phase === 'login' || phase === 'locked') {
    return (
      <>
        <AuthViews
          mode={phase}
          loginValues={loginValues}
          registerValues={registerValues}
          unlockPassword={unlockPassword}
          emailForLock={profile?.email || session?.email || ''}
          onChangeLogin={setLoginValues}
          onChangeRegister={setRegisterValues}
          onChangeUnlock={setUnlockPassword}
          onSubmitLogin={() => void handleLogin()}
          onSubmitRegister={() => void handleRegister()}
          onSubmitUnlock={() => void handleUnlock()}
          onGotoLogin={() => setPhase('login')}
          onGotoRegister={() => setPhase('register')}
          onLogout={logoutNow}
        />
        <ToastHost toasts={toasts} onClose={(id) => setToasts((prev) => prev.filter((x) => x.id !== id))} />

        <ConfirmDialog
          open={!!pendingTotp}
          title={t('txt_two_step_verification')}
          message={t('txt_password_is_already_verified')}
          confirmText={t('txt_verify')}
          cancelText={t('txt_cancel')}
          showIcon={false}
          onConfirm={() => void handleTotpVerify()}
          onCancel={() => {
            setPendingTotp(null);
            setTotpCode('');
            setRememberDevice(true);
          }}
          afterActions={(
            <div className="dialog-extra">
              <div className="dialog-divider" />
              <button
                type="button"
                className="btn btn-secondary dialog-btn"
                onClick={() => {
                  setPendingTotp(null);
                  setTotpCode('');
                  setRememberDevice(true);
                  navigate('/recover-2fa');
                }}
              >
                {t('txt_use_recovery_code')}
              </button>
            </div>
          )}
        >
          <label className="field">
            <span>{t('txt_totp_code')}</span>
            <input className="input" value={totpCode} onInput={(e) => setTotpCode((e.currentTarget as HTMLInputElement).value)} />
          </label>
          <label className="check-line" style={{ marginBottom: 0 }}>
            <input type="checkbox" checked={rememberDevice} onChange={(e) => setRememberDevice((e.currentTarget as HTMLInputElement).checked)} />
            <span>{t('txt_trust_this_device_for_30_days')}</span>
          </label>
        </ConfirmDialog>
      </>
    );
  }

  return (
    <>
      <div className="app-page">
        <div className="app-shell">
          <header className="topbar">
            <div className="brand">
              <img src="/logo-64.png" alt="NodeWarden logo" className="brand-logo" />
              <span>NodeWarden</span>
            </div>
            <div className="topbar-actions">
              <div className="user-chip">
                <ShieldUser size={16} />
                <span>{profile?.email}</span>
              </div>
              <button type="button" className="btn btn-secondary small" onClick={handleLock}>
                <Lock size={14} className="btn-icon" /> {t('txt_lock')}
              </button>
              <button type="button" className="btn btn-secondary small" onClick={handleLogout}>
                <LogOut size={14} className="btn-icon" /> {t('txt_sign_out')}
              </button>
            </div>
          </header>

          <div className="app-main">
            <aside className="app-side">
              <Link href="/vault" className={`side-link ${location === '/vault' ? 'active' : ''}`}>
                <Vault size={16} />
                <span>{t('nav_my_vault')}</span>
              </Link>
              <Link href="/sends" className={`side-link ${location === '/sends' ? 'active' : ''}`}>
                <SendIcon size={16} />
                <span>{t('nav_sends')}</span>
              </Link>
              {profile?.role === 'admin' && (
                <Link href="/admin" className={`side-link ${location === '/admin' ? 'active' : ''}`}>
                  <ShieldUser size={16} />
                  <span>{t('nav_admin_panel')}</span>
                </Link>
              )}
              <Link href="/settings" className={`side-link ${location === '/settings' ? 'active' : ''}`}>
                <SettingsIcon size={16} />
                <span>{t('nav_account_settings')}</span>
              </Link>
              <Link href="/security/devices" className={`side-link ${location === '/security/devices' ? 'active' : ''}`}>
                <Shield size={16} />
                <span>{t('nav_device_management')}</span>
              </Link>
              <Link href="/help" className={`side-link ${location === '/help' ? 'active' : ''}`}>
                <CircleHelp size={16} />
                <span>{t('nav_support_center')}</span>
              </Link>
            </aside>
            <main className="content">
              <Switch>
                <Route path="/sends">
                  <SendsPage
                    sends={decryptedSends}
                    loading={sendsQuery.isFetching}
                    onRefresh={refreshVault}
                    onCreate={createSendItem}
                    onUpdate={updateSendItem}
                    onDelete={deleteSendItem}
                    onBulkDelete={bulkDeleteSendItems}
                    onNotify={pushToast}
                  />
                </Route>
                <Route path="/vault">
                  <VaultPage
                    ciphers={decryptedCiphers}
                    folders={decryptedFolders}
                    loading={ciphersQuery.isFetching || foldersQuery.isFetching}
                    emailForReprompt={profile?.email || session?.email || ''}
                    onRefresh={refreshVault}
                    onCreate={createVaultItem}
                    onUpdate={updateVaultItem}
                    onDelete={deleteVaultItem}
                    onBulkDelete={bulkDeleteVaultItems}
                    onBulkMove={bulkMoveVaultItems}
                    onVerifyMasterPassword={verifyMasterPasswordAction}
                    onNotify={pushToast}
                    onCreateFolder={createFolderAction}
                  />
                </Route>
                <Route path="/settings">
                  {profile && (
                    <SettingsPage
                      profile={profile}
                      totpEnabled={!!totpStatusQuery.data?.enabled}
                      onChangePassword={changePasswordAction}
                      onEnableTotp={async (secret, token) => {
                        await enableTotpAction(secret, token);
                        await totpStatusQuery.refetch();
                      }}
                      onOpenDisableTotp={() => setDisableTotpOpen(true)}
                      onGetRecoveryCode={getRecoveryCodeAction}
                      onNotify={pushToast}
                    />
                  )}
                </Route>
                <Route path="/security/devices">
                  <SecurityDevicesPage
                    devices={authorizedDevicesQuery.data || []}
                    loading={authorizedDevicesQuery.isFetching}
                    onRefresh={() => void refreshAuthorizedDevices()}
                    onRevokeTrust={(device) => {
                      setConfirm({
                        title: t('txt_revoke_device_authorization'),
                        message: t('txt_revoke_30_day_totp_trust_for_name', { name: device.name }),
                        danger: true,
                        onConfirm: () => {
                          setConfirm(null);
                          void revokeDeviceTrustAction(device);
                        },
                      });
                    }}
                    onRemoveDevice={(device) => {
                      setConfirm({
                        title: t('txt_remove_device'),
                        message: t('txt_remove_device_name_and_clear_its_2fa_trust', { name: device.name }),
                        danger: true,
                        onConfirm: () => {
                          setConfirm(null);
                          void removeDeviceAction(device);
                        },
                      });
                    }}
                    onRevokeAll={() => {
                      setConfirm({
                        title: t('txt_revoke_all_trusted_devices'),
                        message: t('txt_revoke_30_day_totp_trust_from_all_devices'),
                        danger: true,
                        onConfirm: () => {
                          setConfirm(null);
                          void revokeAllDeviceTrustAction();
                        },
                      });
                    }}
                  />
                </Route>
                <Route path="/admin">
                  <AdminPage
                    currentUserId={profile?.id || ''}
                    users={usersQuery.data || []}
                    invites={invitesQuery.data || []}
                    onRefresh={() => {
                      void usersQuery.refetch();
                      void invitesQuery.refetch();
                    }}
                    onCreateInvite={async (hours) => {
                      await createInvite(authedFetch, hours);
                      await invitesQuery.refetch();
                      pushToast('success', t('txt_invite_created'));
                    }}
                    onDeleteAllInvites={async () => {
                      setConfirm({
                        title: t('txt_delete_all_invites'),
                        message: t('txt_delete_all_invite_codes_active_inactive'),
                        danger: true,
                        onConfirm: () => {
                          setConfirm(null);
                          void (async () => {
                            await deleteAllInvites(authedFetch);
                            await invitesQuery.refetch();
                            pushToast('success', t('txt_all_invites_deleted'));
                          })();
                        },
                      });
                    }}
                    onToggleUserStatus={async (userId, status) => {
                      await setUserStatus(authedFetch, userId, status === 'active' ? 'banned' : 'active');
                      await usersQuery.refetch();
                      pushToast('success', t('txt_user_status_updated'));
                    }}
                    onDeleteUser={async (userId) => {
                      setConfirm({
                        title: t('txt_delete_user'),
                        message: t('txt_delete_this_user_and_all_user_data'),
                        danger: true,
                        onConfirm: () => {
                          setConfirm(null);
                          void (async () => {
                            await deleteUser(authedFetch, userId);
                            await usersQuery.refetch();
                            pushToast('success', t('txt_user_deleted'));
                          })();
                        },
                      });
                    }}
                    onRevokeInvite={async (code) => {
                      await revokeInvite(authedFetch, code);
                      await invitesQuery.refetch();
                      pushToast('success', t('txt_invite_revoked'));
                    }}
                  />
                </Route>
                <Route path="/help">
                  <HelpPage />
                </Route>
              </Switch>
            </main>
          </div>
        </div>
      </div>

      <ConfirmDialog
        open={!!confirm}
        title={confirm?.title || ''}
        message={confirm?.message || ''}
        danger={confirm?.danger}
        showIcon={confirm?.showIcon}
        onConfirm={() => confirm?.onConfirm()}
        onCancel={() => setConfirm(null)}
      />

      <ConfirmDialog
        open={disableTotpOpen}
        title={t('txt_disable_totp')}
        message={t('txt_enter_master_password_to_disable_two_step_verification')}
        confirmText={t('txt_disable_totp')}
        cancelText={t('txt_cancel')}
        danger
        showIcon={false}
        onConfirm={() => void disableTotpAction()}
        onCancel={() => {
          setDisableTotpOpen(false);
          setDisableTotpPassword('');
        }}
      >
        <label className="field">
          <span>{t('txt_master_password')}</span>
          <input
            className="input"
            type="password"
            value={disableTotpPassword}
            onInput={(e) => setDisableTotpPassword((e.currentTarget as HTMLInputElement).value)}
          />
        </label>
      </ConfirmDialog>

      <ToastHost toasts={toasts} onClose={(id) => setToasts((prev) => prev.filter((x) => x.id !== id))} />
    </>
  );
}
