// ============================================================
// AUTH.JS — Login, Register, 2FA, Session Management
// ============================================================

// Wait for DOM to be fully ready
document.addEventListener('DOMContentLoaded', function() {

  var authScreen = document.getElementById('auth-screen');
  var mainApp = document.getElementById('main-app');
  var loginForm = document.getElementById('login-form');
  var registerForm = document.getElementById('register-form');
  var forgotForm = document.getElementById('forgot-form');
  var userName = document.getElementById('user-name');

  var TOKEN_KEY = 'ped_scribe_token';
  var USER_KEY = 'ped_scribe_user';
  var SESSION_KEY = 'ped_session_id';

  // Runtime-split auth model:
  //  - Web browser → httpOnly cookie only (no localStorage token, XSS-safe).
  //    fetch() defaults to credentials:'same-origin' so cookies are sent.
  //  - Capacitor native app → Bearer token in iOS Keychain / Android Keystore
  //    via SecureStorage wrapper. WebView cookies can be evicted; Keychain
  //    survives cold starts reliably.
  function isNativeApp() {
    try {
      return !!(window.Capacitor && typeof window.Capacitor.isNativePlatform === 'function' && window.Capacitor.isNativePlatform());
    } catch(e) { return false; }
  }
  window.IS_NATIVE_APP = isNativeApp();

  // Hide elements that only make sense for the web audience (e.g. the
  // "Download Android APK" link on the login page — you're already in
  // the app). Add more element IDs here as other web-only UI appears.
  if (isNativeApp()) {
    ['apk-download-link'].forEach(function(id) {
      var el = document.getElementById(id);
      if (el) el.style.display = 'none';
    });
  }

  // ── Biometric login (Capacitor only) ────────────────────────────────
  // Uses capacitor-native-biometric to gate stored credentials behind
  // Face ID / Touch ID / fingerprint. Server is identity (email);
  // server-side credential is the user's password (not the JWT — JWTs
  // expire and would force a fallback password login to refresh).
  // 2FA still applies on top: biometric replaces the password step but
  // a 2FA-enabled account still gets the TOTP prompt afterwards. That
  // is the intended defense-in-depth.
  // Two plugins cooperate here, because the one that does the biometric
  // prompt does not store anything:
  //   - BiometricAuthNative (@aparajita/capacitor-biometric-auth) — presents
  //     the Face ID / fingerprint prompt. checkBiometry() + authenticate().
  //   - SecureStoragePlugin (capacitor-secure-storage-plugin), via the
  //     window.SecureStorage wrapper — holds the credentials in the iOS
  //     Keychain / Android EncryptedSharedPreferences.
  //
  // This previously called window.Capacitor.Plugins.NativeBiometric, the API
  // of capacitor-native-biometric — a package that is not a dependency of this
  // project. The plugin object was always undefined, so bioAvailable() always
  // resolved {ok:false} and the biometric button was never revealed. The
  // feature has been dead since it was written.
  var BIO_CREDS_KEY   = 'ped_bio_creds';   // SecureStorage key holding {username,password}
  var BIO_ENABLED_KEY = 'ped_bio_enabled'; // localStorage flag — used to decide whether to even probe

  // BiometryType enum from the plugin (numeric) → human label.
  var BIO_TYPE_NAMES = {
    1: 'Touch ID',
    2: 'Face ID',
    3: 'fingerprint',
    4: 'face recognition',
    5: 'iris recognition'
  };

  function bioPlugin() {
    try {
      if (!isNativeApp()) return null;
      var p = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.BiometricAuthNative;
      return p || null;
    } catch (e) { return null; }
  }
  function bioAvailable() {
    var p = bioPlugin();
    if (!p) return Promise.resolve({ ok: false });
    return p.checkBiometry()
      .then(function (r) {
        return {
          ok: !!(r && r.isAvailable),
          type: r && r.biometryType,
          typeName: (r && BIO_TYPE_NAMES[r.biometryType]) || 'biometric'
        };
      })
      .catch(function () { return { ok: false }; });
  }
  function bioStored() {
    // Cheap check first — was biometric ever enrolled? If not, skip the
    // prompt path entirely so we don't rattle the user.
    try { return localStorage.getItem(BIO_ENABLED_KEY) === '1'; } catch (e) { return false; }
  }
  function bioEnroll(email, password) {
    if (!bioPlugin()) return Promise.reject(new Error('Biometric plugin unavailable'));
    if (!window.SecureStorage) return Promise.reject(new Error('Secure storage unavailable'));
    return Promise.resolve(
      window.SecureStorage.set(BIO_CREDS_KEY, JSON.stringify({ username: email, password: password }))
    ).then(function () { try { localStorage.setItem(BIO_ENABLED_KEY, '1'); } catch (e) {} });
  }
  function bioRetrieve() {
    var p = bioPlugin();
    if (!p) return Promise.reject(new Error('Biometric plugin unavailable'));
    if (!window.SecureStorage) return Promise.reject(new Error('Secure storage unavailable'));
    // authenticate() resolves on success and rejects on cancel/failure, so the
    // credentials are only read after the OS has verified the user.
    return p.authenticate({
      reason: 'Sign in to PedScribe',
      androidTitle: 'PedScribe',
      androidSubtitle: 'Use biometric to sign in',
      cancelTitle: 'Use password',
      allowDeviceCredential: false
    }).then(function () {
      return window.SecureStorage.get(BIO_CREDS_KEY);
    }).then(function (raw) {
      if (!raw) throw new Error('No stored credentials');
      return JSON.parse(raw);
    });
  }
  function bioForget() {
    try { localStorage.removeItem(BIO_ENABLED_KEY); } catch (e) {}
    if (!window.SecureStorage) return Promise.resolve();
    return Promise.resolve(window.SecureStorage.remove(BIO_CREDS_KEY)).catch(function () { /* fine if missing */ });
  }
  // Expose a small surface so settings/logout/etc can call into it.
  window.PedBio = { available: bioAvailable, stored: bioStored, enroll: bioEnroll, retrieve: bioRetrieve, forget: bioForget };

  // Auth module initialized

  // Make sure forms are visible/hidden correctly on load
  showLoginForm();

  // Biometric login button: reveal on the login form when the device
  // supports it AND the user has previously enrolled. Fire-and-forget;
  // any failure (no plugin, locked out, hardware missing) just leaves
  // the button hidden.
  function maybeRevealBioButton() {
    var btn = document.getElementById('btn-bio-login');
    var div = document.getElementById('bio-divider');
    if (!btn || !isNativeApp()) return;
    if (!bioStored()) return;
    bioAvailable().then(function (s) {
      if (!s.ok) return;
      // Tweak the label to the actual biometry type when known.
      var label = document.getElementById('bio-login-label');
      if (label && s.typeName) {
        label.textContent = 'Sign in with ' + s.typeName;
      }
      btn.classList.remove('hidden'); btn.style.display = '';
      if (div) { div.classList.remove('hidden'); div.style.display = ''; }
    });
  }
  maybeRevealBioButton();

  // Auth screen is hidden by CSS default — only show it when there is no valid session
  function showAuthScreen() {
    if (authScreen) authScreen.style.display = 'flex';
  }

  // ── Check for SSO redirect (token is in httpOnly cookie) ──
  var urlParams = new URLSearchParams(window.location.search);
  var ssoOk = urlParams.get('sso');
  var ssoError = urlParams.get('error');
  if (ssoOk === 'ok') {
    var ssoSid = urlParams.get('sid');
    history.replaceState(null, '', window.location.pathname);
    if (ssoSid && isNativeApp()) {
      window.SecureStorage.set(SESSION_KEY, ssoSid);
    }
    // Token is in httpOnly cookie — verify via /me endpoint (cookie sent automatically)
    fetch('/api/auth/me', { credentials: 'same-origin' })
      .then(function(r) { if (r.ok) return r.json(); throw new Error('invalid'); })
      .then(function(data) {
        if (data && data.user) {
          enterApp(data.user, '');
          showToast('Welcome, ' + data.user.name + '!', 'success');
        } else { showAuthScreen(); clearSession(); }
      })
      .catch(function() { showAuthScreen(); clearSession(); });
  } else if (ssoError) {
    history.replaceState(null, '', window.location.pathname);
    var errorMsgs = {
      invalid_state: 'SSO session expired',
      expired: 'SSO session expired',
      no_email: 'Your identity provider did not return an email',
      disabled: 'Account disabled',
      sso_failed: 'SSO login failed',
      email_unverified: 'Your identity provider did not confirm your email is verified. Contact your administrator.',
      sub_mismatch: 'Your SSO identity does not match the linked account. Contact your administrator.'
    };
    showAuthScreen();
    setTimeout(function() { showToast(errorMsgs[ssoError] || 'SSO error', 'error'); }, 300);
  }

  // ── Check OIDC status to show/hide SSO button ──
  fetch('/api/auth/oidc-status')
    .then(function(r) { return r.json(); })
    .then(function(data) {
      if (data.oidcEnabled) {
        var ssoBtn = document.getElementById('btn-sso');
        var ssoDivider = document.getElementById('sso-divider');
        var ssoLabel = document.getElementById('sso-label');
        if (ssoBtn) ssoBtn.style.display = 'block';
        if (ssoDivider) ssoDivider.style.display = 'block';
        if (ssoLabel && data.buttonLabel) ssoLabel.textContent = data.buttonLabel;
        if (data.disableLocalAuth) {
          // Hide local login form fields, only show SSO
          var localFields = document.querySelectorAll('#login-form .form-group, #btn-local-login, #show-register, #show-forgot');
          localFields.forEach(function(el) { el.style.display = 'none'; });
          if (ssoDivider) ssoDivider.style.display = 'none';
        }
      }
    })
    .catch(function() {});

  if (isNativeApp()) {
    // Native app path: token lives in Keychain/Keystore via SecureStorage
    window.SecureStorage.hydrate([TOKEN_KEY, USER_KEY, SESSION_KEY]).then(function() {
      var savedToken = window.SecureStorage.getSync(TOKEN_KEY);
      if (ssoOk || ssoError) return;  // SSO branch handled above
      if (!savedToken) { showAuthScreen(); return; }
      fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + savedToken } })
        .then(function(r) { if (r.ok) return r.json(); throw new Error('expired'); })
        .then(function(data) {
          if (data && data.user) enterApp(data.user, savedToken);
          else { showAuthScreen(); clearSession(); }
        })
        .catch(function() { showAuthScreen(); clearSession(); });
    });
  } else {
    // Web path: rely on httpOnly cookie. fetch sends same-origin cookies
    // automatically in modern browsers — no Authorization header needed.
    if (ssoOk || ssoError) {
      // SSO branch handled above
    } else {
      fetch('/api/auth/me', { credentials: 'same-origin' })
        .then(function(r) { if (r.ok) return r.json(); throw new Error('not-logged-in'); })
        .then(function(data) {
          if (data && data.user) enterApp(data.user, '');
          else showAuthScreen();
        })
        .catch(function() { showAuthScreen(); });
    }
  }

  // ---- CLOUDFLARE TURNSTILE ----
  // Gates registration and password reset. Login is deliberately NOT gated:
  // it is already covered by a 10-per-15-min rate limit and a constant-time
  // credential check, and the widget is unreliable inside the Capacitor
  // WebView — which locked mobile users out of the app entirely.
  //
  // Tokens are captured from the render callback rather than read back out of
  // the injected [name="cf-turnstile-response"] input. That lookup is easy to
  // leave unscoped, which is exactly how the register form ended up
  // submitting the login widget's token (single-use, 5-minute expiry).
  //
  // Rendering is explicit and deferred until the owning form is visible:
  // both widgets live in forms that start at display:none, and Turnstile does
  // not reliably complete a challenge inside a hidden container.
  var turnstileWidgets = {
    register: { el: 'turnstile-register', id: null, token: '', pending: false },
    forgot:   { el: 'turnstile-forgot',   id: null, token: '', pending: false }
  };
  var turnstileReady = false;

  // api.js?render=explicit invokes this once the Turnstile API is available.
  window.onloadTurnstileCallback = function() {
    turnstileReady = true;
    Object.keys(turnstileWidgets).forEach(function(name) {
      // Catch up on any form shown before the script finished loading.
      if (turnstileWidgets[name].pending) renderTurnstile(name);
    });
  };

  function renderTurnstile(name) {
    var w = turnstileWidgets[name];
    if (!w || w.id !== null) return;            // already rendered
    var el = document.getElementById(w.el);
    if (!el) return;
    if (!turnstileReady || !window.turnstile) { w.pending = true; return; }
    w.pending = false;
    w.id = window.turnstile.render(el, {
      sitekey: el.getAttribute('data-sitekey'),
      theme: 'light',
      callback: function(token) { w.token = token; },
      'expired-callback': function() { w.token = ''; },
      'timeout-callback': function() { w.token = ''; },
      // Without this a widget failure is silent and the user only ever sees
      // the generic "complete the verification" toast with no way to tell
      // whether the challenge failed, expired, or never loaded at all.
      'error-callback': function(code) {
        w.token = '';
        console.error('[Auth] Turnstile error on ' + name + ' widget:', code);
        showToast('Verification unavailable (' + (code || 'error') + '). Check your connection and try again.', 'error');
      }
    });
  }

  function turnstileToken(name) {
    var w = turnstileWidgets[name];
    return w ? w.token : '';
  }

  function resetTurnstile(name) {
    var w = turnstileWidgets[name];
    if (!w) return;
    w.token = '';
    if (w.id !== null && window.turnstile) window.turnstile.reset(w.id);
  }

  // ---- HELPER FUNCTIONS ----

  function showLoginForm() {
    if (loginForm) loginForm.style.display = 'block';
    if (registerForm) registerForm.style.display = 'none';
    if (forgotForm) forgotForm.style.display = 'none';
  }

  function showRegisterForm() {
    if (loginForm) loginForm.style.display = 'none';
    if (registerForm) registerForm.style.display = 'block';
    if (forgotForm) forgotForm.style.display = 'none';
    renderTurnstile('register');
  }

  function showForgotForm() {
    if (loginForm) loginForm.style.display = 'none';
    if (registerForm) registerForm.style.display = 'none';
    if (forgotForm) forgotForm.style.display = 'block';
    renderTurnstile('forgot');
  }

  function enterApp(user, token) {
    window.AUTH_TOKEN = token;
    window.CURRENT_USER = user;  // cached so settings page doesn't re-fetch /me just to read canLocalAuth
    if (isNativeApp()) {
      // Mobile: persist token to Keychain/Keystore
      window.SecureStorage.set(TOKEN_KEY, token);
      window.SecureStorage.set(USER_KEY, JSON.stringify(user));
    }
    // Web: do not persist the token anywhere — session lives in the
    // httpOnly cookie already set by the server. User info is
    // re-fetched via /api/auth/me on each boot.

    if (authScreen) authScreen.style.display = 'none';
    if (mainApp) {
      mainApp.style.display = 'block';
      mainApp.className = mainApp.className.replace('hidden', '').trim();
    }
    if (userName) userName.textContent = user.name || user.email;

    // Show admin tab for admins only
    var adminTabBtn = document.getElementById('admin-tab-btn');
    if (adminTabBtn) {
      if (user.role === 'admin') adminTabBtn.classList.remove('hidden');
      else adminTabBtn.classList.add('hidden');
    }

    // Show Docs tab for admins only (same gate as Admin)
    var docsTabBtn = document.getElementById('docs-tab-btn');
    if (docsTabBtn) {
      if (user.role === 'admin') docsTabBtn.classList.remove('hidden');
      else docsTabBtn.classList.add('hidden');
    }

    // Show Content Manager tab for admin + moderator
    var cmsTabBtn = document.getElementById('cms-tab-btn');
    if (cmsTabBtn) {
      if (user.role === 'admin' || user.role === 'moderator') cmsTabBtn.classList.remove('hidden');
      else cmsTabBtn.classList.add('hidden');
    }

    // Store role globally for JS modules to check
    window._userRole = user.role;

    console.log('[Auth] ✅ Entered app as:', user.email, 'Role:', user.role);

    // Restore last visited tab (saved by activateTab)
    var lastTab = null;
    try { lastTab = localStorage.getItem('ped_last_tab'); } catch(e) {}
    if (typeof window.activateTab === 'function') {
      if (!lastTab || !window.activateTab(lastTab)) {
        window.activateTab('encounter');
      }
    }

    // Load announcement banner if function is available
    if (typeof loadAnnouncement === 'function') loadAnnouncement();

    // Check if server-side transcription is configured
    if (typeof checkTranscribeStatus === 'function') checkTranscribeStatus();
  }

  function exitApp() {
    // Destroy session server-side before clearing local state
    fetch('/api/auth/logout', { method: 'POST', headers: getAuthHeaders(), credentials: 'same-origin' }).catch(function() {});
    // Tell sibling tabs to drop their UI too.
    try { if (window.__authBroadcast) window.__authBroadcast.postMessage({ type: 'logout' }); } catch(e) {}
    clearSession();
    document.documentElement.classList.remove('has-session');
    history.replaceState(null, '', window.location.pathname);
    if (authScreen) authScreen.style.display = 'flex';
    if (mainApp) mainApp.style.display = 'none';
    var adminTabBtn = document.getElementById('admin-tab-btn');
    if (adminTabBtn) adminTabBtn.classList.add('hidden');
    var docsTabBtn = document.getElementById('docs-tab-btn');
    if (docsTabBtn) docsTabBtn.classList.add('hidden');
    // Explicit logout clears biometric — assume the user is leaving the
    // device for someone else. Auto-logout (token expiry, network) does
    // NOT come through this path, so biometric persists across silent
    // session resets.
    if (window.PedBio && typeof window.PedBio.forget === 'function') {
      window.PedBio.forget();
    }
    var bioBtn = document.getElementById('btn-bio-login');
    if (bioBtn) { bioBtn.classList.add('hidden'); bioBtn.style.display = 'none'; }
    var bioDiv = document.getElementById('bio-divider');
    if (bioDiv) { bioDiv.classList.add('hidden'); bioDiv.style.display = 'none'; }
    showLoginForm();
    // Clear fields after browser autofill has had a chance to run
    setTimeout(function() {
      ['login-email', 'login-password', 'reg-name', 'reg-email', 'reg-password', 'login-totp'].forEach(function(id) {
        var el = document.getElementById(id);
        if (el) el.value = '';
      });
    }, 100);
  }

  function clearSession() {
    if (isNativeApp()) {
      window.SecureStorage.remove(TOKEN_KEY);
      window.SecureStorage.remove(USER_KEY);
      window.SecureStorage.remove(SESSION_KEY);
    } else {
      // Web: cookie cleared by /api/auth/logout. Also wipe any legacy
      // localStorage entries from the dual-mode era so they don't linger.
      try {
        localStorage.removeItem(TOKEN_KEY);
        localStorage.removeItem(USER_KEY);
        localStorage.removeItem(SESSION_KEY);
      } catch(e) {}
    }
    window.AUTH_TOKEN = null;
    // Clear service worker caches so a logged-out user on a shared device
    // cannot read cached pages from offline mode.
    try {
      if (window.caches && caches.keys) {
        caches.keys().then(function(names) {
          names.forEach(function(n) { caches.delete(n); });
        }).catch(function(){});
      }
    } catch(e) {}
  }

  window.getAuthHeaders = function() {
    // Web: no Authorization header. The httpOnly cookie is sent automatically
    // by fetch (default credentials = same-origin). Server middleware falls
    // back to cookie when Bearer is absent.
    if (!isNativeApp()) {
      return { 'Content-Type': 'application/json' };
    }
    // Native: Bearer from Keychain/Keystore via SecureStorage.
    var token = window.AUTH_TOKEN || window.SecureStorage.getSync(TOKEN_KEY) || '';
    return {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + token
    };
  };

  // ---- FORM TOGGLE LINKS ----

  document.addEventListener('click', function(e) {
    var target = e.target;

    // Handle link clicks by ID
    if (target.id === 'show-register' || target.closest('#show-register')) {
      e.preventDefault();
      showRegisterForm();
    }
    if (target.id === 'show-login' || target.closest('#show-login')) {
      e.preventDefault();
      showLoginForm();
    }
    if (target.id === 'show-login-2' || target.closest('#show-login-2')) {
      e.preventDefault();
      showLoginForm();
    }
    if (target.id === 'show-forgot' || target.closest('#show-forgot')) {
      e.preventDefault();
      showForgotForm();
    }

    // Logout button
    if (target.id === 'btn-logout' || target.closest('#btn-logout')) {
      e.preventDefault();
      exitApp();
      showToast('Logged out', 'info');
    }

    // About modal
    if (target.id === 'btn-show-about' || target.closest('#btn-show-about')) {
      e.preventDefault();
      var aboutModal = document.getElementById('about-modal');
      if (aboutModal) aboutModal.classList.remove('hidden');
    }
    if (target.id === 'btn-close-about' || target.closest('#btn-close-about')) {
      e.preventDefault();
      var aboutModal = document.getElementById('about-modal');
      if (aboutModal) aboutModal.classList.add('hidden');
    }

    // Settings button — navigate to settings tab
    if (target.id === 'btn-settings' || target.closest('#btn-settings')) {
      e.preventDefault();
      var settingsBtn = document.querySelector('.tab-btn[data-tab="settings"]');
      if (settingsBtn) settingsBtn.click();
      load2FAStatus();
      if (typeof loadNextcloudStatus === 'function') loadNextcloudStatus();
      if (typeof loadMemories === 'function') loadMemories();
      if (typeof loadSavedEncountersList === 'function') loadSavedEncountersList();
    }

    // Setup 2FA
    if (target.id === 'btn-setup-2fa' || target.closest('#btn-setup-2fa')) {
      e.preventDefault();
      fetch('/api/auth/setup-2fa', { method: 'POST', headers: getAuthHeaders() })
      .then(function(r) { return r.json(); })
      .then(function(data) {
        if (data.success) {
          var qr = document.getElementById('2fa-qr');
          var secret = document.getElementById('2fa-secret');
          var setup = document.getElementById('2fa-setup');
          if (qr) qr.src = data.qrCode;
          if (secret) secret.textContent = data.secret;
          if (setup) { setup.style.display = 'block'; setup.className = setup.className.replace('hidden', '').trim(); }
        }
      });
    }

    // Verify 2FA
    if (target.id === 'btn-verify-2fa' || target.closest('#btn-verify-2fa')) {
      e.preventDefault();
      var code = document.getElementById('2fa-verify-code').value;
      fetch('/api/auth/verify-2fa', {
        method: 'POST', headers: getAuthHeaders(),
        body: JSON.stringify({ code: code })
      })
      .then(function(r) { return r.json(); })
      .then(function(data) {
        if (data.success) {
          showToast('2FA enabled!', 'success');
          var setup = document.getElementById('2fa-setup');
          if (setup) setup.style.display = 'none';
          if (data.backupCodes && data.backupCodes.length) {
            showBackupCodesModal(data.backupCodes);
          }
          load2FAStatus();
        } else {
          showToast(data.error || 'Invalid code', 'error');
        }
      });
    }

    // Regenerate backup codes
    if (target.id === 'btn-regen-backup-codes' || target.closest('#btn-regen-backup-codes')) {
      e.preventDefault();
      showConfirm('Regenerate backup codes? This invalidates your existing ones. Enter your current password to confirm.', function(pw) {
        if (!pw) return;
        fetch('/api/auth/2fa/backup-codes', {
          method: 'POST', headers: getAuthHeaders(),
          body: JSON.stringify({ password: pw })
        })
        .then(function(r) { return r.json(); })
        .then(function(data) {
          if (data.success && data.codes) showBackupCodesModal(data.codes);
          else showToast(data.error || 'Failed to regenerate codes', 'error');
        });
      }, {
        input: true,
        inputType: 'password',
        placeholder: 'Current password',
        confirmText: 'Regenerate',
        required: true,
        requiredMsg: 'Password is required'
      });
    }

    // Disable 2FA
    if (target.id === 'btn-disable-2fa' || target.closest('#btn-disable-2fa')) {
      e.preventDefault();
      var confirmBox = document.getElementById('2fa-disable-confirm');
      if (confirmBox) { confirmBox.classList.remove('hidden'); }
      return;
    }
    if (target.id === 'btn-disable-2fa-cancel' || target.closest('#btn-disable-2fa-cancel')) {
      e.preventDefault();
      var confirmBox = document.getElementById('2fa-disable-confirm');
      if (confirmBox) { confirmBox.classList.add('hidden'); }
      var pwField = document.getElementById('2fa-disable-password');
      if (pwField) pwField.value = '';
      return;
    }
    if (target.id === 'btn-disable-2fa-confirm' || target.closest('#btn-disable-2fa-confirm')) {
      e.preventDefault();
      var pwField = document.getElementById('2fa-disable-password');
      var pw = pwField ? pwField.value : '';
      if (!pw) { showToast('Enter your password', 'error'); return; }
      fetch('/api/auth/disable-2fa', {
        method: 'POST', headers: getAuthHeaders(),
        body: JSON.stringify({ password: pw })
      })
      .then(function(r) { return r.json(); })
      .then(function(data) {
        var confirmBox = document.getElementById('2fa-disable-confirm');
        if (confirmBox) confirmBox.classList.add('hidden');
        if (pwField) pwField.value = '';
        if (data.success) { showToast('2FA disabled', 'info'); load2FAStatus(); }
        else showToast(data.error || 'Failed', 'error');
      });
    }
  });

  // ---- BIOMETRIC LOGIN BUTTON ----
  // Reads the email + password from the OS-secured keychain (gated behind
  // Face ID / Touch ID / fingerprint) and fills the login form. Submits the
  // form so all the existing flow (2FA prompt, error handling,
  // session storage) runs unchanged. If biometric verification fails, the
  // user just gets a toast and falls through to typing the password.
  var bioBtn = document.getElementById('btn-bio-login');
  if (bioBtn) {
    bioBtn.addEventListener('click', function () {
      bioRetrieve()
        .then(function (creds) {
          if (!creds || !creds.username || !creds.password) {
            showToast('No stored credentials', 'error');
            return;
          }
          var emailEl = document.getElementById('login-email');
          var pwEl    = document.getElementById('login-password');
          if (emailEl) emailEl.value = creds.username;
          if (pwEl)    pwEl.value    = creds.password;
          // Trigger the same submit path as the password form so all the
          // existing handling (2FA, session storage, etc.) runs unchanged.
          if (loginForm && typeof loginForm.requestSubmit === 'function') loginForm.requestSubmit();
          else if (loginForm) loginForm.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
        })
        .catch(function (err) {
          // User cancelled or biometric failed (locked out, no enrolled
          // biometric, etc). Stay quiet for cancel; toast for hard errors.
          var msg = (err && (err.message || err.code)) || '';
          if (/cancel/i.test(msg)) return;
          showToast('Biometric sign-in failed', 'error');
        });
    });
  }

  // ---- LOGIN FORM SUBMIT ----
  if (loginForm) {
    loginForm.addEventListener('submit', function(e) {
      e.preventDefault();
      e.stopPropagation();

      var email = document.getElementById('login-email').value.trim();
      var password = document.getElementById('login-password').value;
      var totpEl = document.getElementById('login-totp');
      var totpCode = totpEl ? totpEl.value.trim() : '';

      if (!email || !password) {
        showToast('Enter email and password', 'error');
        return false;
      }

      showLoading('Signing in...');

      var body = { email: email, password: password };
      if (totpCode) body.totpCode = totpCode;

      fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body)
      })
      .then(function(r) { return r.json(); })
      .then(function(data) {
        hideLoading();

        if (data.requires2FA) {
          var grp = document.getElementById('totp-group');
          if (grp) { grp.style.display = 'block'; grp.className = grp.className.replace('hidden', '').trim(); }
          showToast('Enter your 2FA code', 'info');
          return;
        }

        if (data.needsVerification) {
          var resendBox = document.getElementById('resend-verify-box');
          if (resendBox) resendBox.className = resendBox.className.replace('hidden', '').trim();
          showToast('Verify your email first. Check inbox.', 'error');
          return;
        }

        if (data.success && data.token && data.user) {
          if (data.sessionId && isNativeApp()) {
            window.SecureStorage.set(SESSION_KEY, data.sessionId);
          }
          enterApp(data.user, data.token);
          showToast('Welcome, ' + data.user.name + '!', 'success');
          // Offer biometric enrollment after the very first successful
          // password login on a Capacitor device. Only ask once per
          // (device, account) — enrollment flips the BIO_ENABLED_KEY flag.
          if (isNativeApp() && !bioStored()) {
            bioAvailable().then(function (s) {
              if (!s.ok) return;
              var typeName = s.typeName || 'biometric';
              if (typeof showConfirm === 'function') {
                showConfirm('Enable ' + typeName + ' for faster sign-in next time?', function () {
                  bioEnroll(email, password)
                    .then(function () { showToast(typeName + ' enabled. Use it next time you sign in.', 'success'); })
                    .catch(function () { showToast('Could not enable ' + typeName, 'error'); });
                });
              }
            });
          }
        } else {
          showToast(data.error || 'Login failed', 'error');
        }
      })
      .catch(function(err) {
        hideLoading();
        console.error('[Auth] Login error:', err);
        showToast('Connection error', 'error');
      });

      return false;
    });
  }

  // ---- RESEND VERIFICATION LINK ----
  var resendLink = document.getElementById('resend-verify-link');
  if (resendLink) {
    resendLink.addEventListener('click', function(e) {
      e.preventDefault();
      var email = document.getElementById('login-email').value.trim();
      if (!email) { showToast('Enter your email first', 'error'); return; }
      resendLink.style.pointerEvents = 'none';
      resendLink.textContent = 'Sending...';
      fetch('/api/auth/resend-verification', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: email })
      })
      .then(function(r) { return r.json(); })
      .then(function(data) {
        resendLink.style.pointerEvents = '';
        resendLink.textContent = 'Resend verification link';
        if (data.error) {
          showToast(data.error, 'error');
        } else {
          showToast('Verification email sent! Check your inbox.', 'success');
        }
      })
      .catch(function() {
        resendLink.style.pointerEvents = '';
        resendLink.textContent = 'Resend verification link';
        showToast('Connection error', 'error');
      });
    });
  }

  // ---- REGISTER FORM SUBMIT ----
  if (registerForm) {
    registerForm.addEventListener('submit', function(e) {
      e.preventDefault();
      e.stopPropagation();

      var name = document.getElementById('reg-name').value.trim();
      var email = document.getElementById('reg-email').value.trim();
      var password = document.getElementById('reg-password').value;

      if (!name || !email || !password) {
        showToast('Fill all fields', 'error');
        return false;
      }
      if (password.length < 8) {
        showToast('Password must be 8+ characters', 'error');
        return false;
      }

      // Cloudflare Turnstile verification
      var regToken = turnstileToken('register');
      if (!regToken) {
        showToast('Please complete the verification challenge', 'error');
        return false;
      }

      showLoading('Creating account...');

      fetch('/api/auth/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: name, email: email, password: password, turnstileToken: regToken })
      })
      .then(function(r) { return r.json(); })
      .then(function(data) {
        hideLoading();

        if (data.success && data.token && data.user) {
          if (data.sessionId && isNativeApp()) {
            window.SecureStorage.set(SESSION_KEY, data.sessionId);
          }
          enterApp(data.user, data.token);
          showToast(data.message || 'Account created!', 'success');
        } else if (data.success && data.needsVerification) {
          showToast(data.message || 'Check email to verify', 'success');
          // The token was just consumed server-side — clear it so coming back
          // to this form doesn't resubmit a spent one.
          resetTurnstile('register');
          showLoginForm();
        } else {
          showToast(data.error || 'Registration failed', 'error');
          resetTurnstile('register');
        }
      })
      .catch(function(err) {
        hideLoading();
        console.error('[Auth] Register error:', err);
        showToast('Connection error', 'error');
        resetTurnstile('register');
      });

      return false;
    });
  }

  // ---- FORGOT PASSWORD SUBMIT ----
  if (forgotForm) {
    forgotForm.addEventListener('submit', function(e) {
      e.preventDefault();
      e.stopPropagation();

      var email = document.getElementById('forgot-email').value.trim();
      if (!email) { showToast('Enter email', 'error'); return false; }

      // Cloudflare Turnstile
      var forgotToken = turnstileToken('forgot');
      if (!forgotToken) {
        showToast('Please complete the verification', 'error');
        return false;
      }

      showLoading('Sending...');

      fetch('/api/auth/forgot-password', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: email, turnstileToken: forgotToken })
      })
      .then(function(r) { return r.json(); })
      .then(function(data) {
        hideLoading();
        showToast(data.message || 'Check your email', 'success');
        resetTurnstile('forgot');
        showLoginForm();
      })
      .catch(function(err) {
        hideLoading();
        showToast('Error', 'error');
        resetTurnstile('forgot');
      });

      return false;
    });
  }

  // ---- SESSION MANAGEMENT ----
  function timeAgo(date) {
    var s = Math.floor((Date.now() - date.getTime()) / 1000);
    if (s < 60) return 'just now';
    if (s < 3600) return Math.floor(s / 60) + 'm ago';
    if (s < 86400) return Math.floor(s / 3600) + 'h ago';
    return Math.floor(s / 86400) + 'd ago';
  }
  function escH(str) { var d = document.createElement('div'); d.textContent = str || ''; return d.innerHTML; }

  window.loadSessions = function() {
    var list = document.getElementById('sessions-list');
    if (!list) return;
    fetch('/api/sessions', { headers: getAuthHeaders(), credentials: 'same-origin' })
      .then(function(r) { return r.json(); })
      .then(function(data) {
        if (!data.success || !data.sessions || data.sessions.length === 0) {
          list.innerHTML = '<p style="color:var(--g400);font-size:13px;">No active sessions found.</p>';
          return;
        }
        var currentSid = data.currentSessionId || (window.SecureStorage ? window.SecureStorage.getSync(SESSION_KEY) : localStorage.getItem(SESSION_KEY));
        list.innerHTML = data.sessions.map(function(s) {
          var isCurrent = s.id === currentSid;
          var created = new Date(s.created_at).toLocaleDateString();
          var lastAct = timeAgo(new Date(s.last_activity));
          return '<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 12px;background:var(--g50);border-radius:8px;border:1.5px solid ' + (isCurrent ? 'var(--blue)' : 'var(--g200)') + ';">'
            + '<div>'
            + '<div style="font-size:13px;font-weight:600;color:var(--g800);">' + escH(s.device_label || 'Unknown device') + (isCurrent ? ' <span style="font-size:11px;color:var(--blue);font-weight:500;">(this device)</span>' : '') + '</div>'
            + '<div style="font-size:12px;color:var(--g500);margin-top:2px;">' + escH(s.ip_address || '') + ' &middot; Created ' + created + ' &middot; Active ' + lastAct + '</div>'
            + '</div>'
            + (isCurrent ? '' : '<button class="btn-sm btn-ghost session-revoke-btn" data-sid="' + s.id + '" style="color:var(--red);font-size:12px;"><i class="fas fa-xmark"></i> Revoke</button>')
            + '</div>';
        }).join('');

        // Wire revoke buttons
        list.querySelectorAll('.session-revoke-btn').forEach(function(btn) {
          btn.addEventListener('click', function() {
            showConfirm('Revoke this session? That device will be logged out.', function() {
              fetch('/api/sessions/' + btn.dataset.sid, { method: 'DELETE', headers: getAuthHeaders() })
                .then(function(r) { return r.json(); })
                .then(function(d) {
                  if (d.success) { showToast('Session revoked', 'info'); loadSessions(); }
                  else showToast(d.error || 'Failed', 'error');
                });
            });
          });
        });
      })
      .catch(function() {
        list.innerHTML = '<p style="color:var(--g400);font-size:13px;">Could not load sessions.</p>';
      });
  };

  // Revoke all other sessions button
  document.addEventListener('click', function(e) {
    if (e.target.id === 'btn-revoke-all-sessions' || e.target.closest('#btn-revoke-all-sessions')) {
      e.preventDefault();
      showConfirm('Revoke all other sessions? All other devices will be logged out.', function() {
        fetch('/api/sessions', { method: 'DELETE', headers: getAuthHeaders() })
          .then(function(r) { return r.json(); })
          .then(function(d) {
            if (d.success) { showToast('All other sessions revoked (' + (d.revoked || 0) + ' removed)', 'info'); loadSessions(); }
          });
      }, { danger: true, confirmText: 'Revoke All' });
    }
  });

  // ---- CHANGE PASSWORD ----
  document.addEventListener('click', function(e) {
    if (e.target.id === 'btn-change-password' || e.target.closest('#btn-change-password')) {
      e.preventDefault();
      var current = document.getElementById('pw-current');
      var newPw = document.getElementById('pw-new');
      var confirmPw = document.getElementById('pw-confirm');
      var status = document.getElementById('pw-change-status');
      if (!current || !newPw || !confirmPw) return;

      if (!current.value || !newPw.value) { showToast('Fill in all fields', 'error'); return; }
      if (newPw.value.length < 8) { showToast('New password must be 8+ characters', 'error'); return; }
      if (newPw.value !== confirmPw.value) { showToast('Passwords do not match', 'error'); return; }

      if (status) { status.textContent = 'Changing...'; status.style.color = 'var(--g500)'; }

      fetch('/api/auth/change-password', {
        method: 'POST',
        headers: getAuthHeaders(),
        body: JSON.stringify({ currentPassword: current.value, newPassword: newPw.value })
      })
      .then(function(r) { return r.json(); })
      .then(function(data) {
        if (data.success) {
          showToast(data.message || 'Password changed', 'success');
          if (data.passwordWarning) setTimeout(function() { showToast(data.passwordWarning, 'warning'); }, 1500);
          current.value = ''; newPw.value = ''; confirmPw.value = '';
          if (status) { status.textContent = ''; }
          if (typeof loadSessions === 'function') loadSessions();
        } else {
          showToast(data.error || 'Failed', 'error');
          if (status) { status.textContent = ''; }
        }
      })
      .catch(function() { showToast('Connection error', 'error'); if (status) status.textContent = ''; });
    }
  });

  // ---- 2FA STATUS ----
  function load2FAStatus() {
    fetch('/api/auth/me', { credentials: 'same-origin', headers: getAuthHeaders() })
    .then(function(r) { return r.json(); })
    .then(function(data) {
      if (!data || !data.user) {
        // /me failed — if we already have a cached user from login, fall back to that
        // so reloaded Settings pages still render for local-auth users.
        if (window.CURRENT_USER && window.CURRENT_USER.canLocalAuth === true) {
          data = { user: window.CURRENT_USER };
        } else {
          return;
        }
      }
      // Keep cache in sync
      window.CURRENT_USER = data.user;
      // Sections are display:none by default. Show them unless the server
      // explicitly marks this user as SSO-only (canLocalAuth === false).
      // Missing/undefined flag defaults to showing — safer than hiding a
      // legit local user's sections due to a transient fetch hiccup.
      var pwSection = document.getElementById('change-password-section');
      var twofaSection = document.getElementById('2fa-section');
      var sessionsSection = document.getElementById('sessions-section');
      if (data.user.canLocalAuth === false) {
        return; // SSO-only — keep sections hidden
      }
      if (pwSection) pwSection.style.display = '';
      if (twofaSection) twofaSection.style.display = '';
      if (sessionsSection) sessionsSection.style.display = '';
      var status = document.getElementById('2fa-status');
      var setupBtn = document.getElementById('btn-setup-2fa');
      var disableBtn = document.getElementById('btn-disable-2fa');

      if (data.user.totp_enabled) {
        if (status) { status.textContent = 'Status: ✅ Enabled'; status.style.color = '#10b981'; }
        if (setupBtn) setupBtn.style.display = 'none';
        if (disableBtn) disableBtn.style.display = 'inline-flex';
        // Show backup-code row and remaining count
        fetch('/api/auth/2fa/backup-codes/count', { credentials: 'same-origin', headers: getAuthHeaders() })
          .then(function(r) { return r.json(); })
          .then(function(bc) {
            var el = document.getElementById('2fa-backup-info');
            if (el && typeof bc.remaining === 'number') {
              var color = bc.remaining <= 2 ? '#f97316' : 'var(--g500)';
              el.innerHTML = '<div style="margin-top:12px;font-size:13px;color:' + color + ';">'
                + '<span>' + bc.remaining + ' backup codes remaining.</span> '
                + '<button id="btn-regen-backup-codes" class="btn-sm btn-ghost" style="margin-left:6px;">Regenerate</button>'
                + '</div>';
            }
          }).catch(function(){});
      } else {
        if (status) { status.textContent = 'Status: ❌ Not enabled'; status.style.color = '#ef4444'; }
        if (setupBtn) setupBtn.style.display = 'inline-flex';
        if (disableBtn) disableBtn.style.display = 'none';
        var bi = document.getElementById('2fa-backup-info');
        if (bi) bi.innerHTML = '';
      }
    })
    .catch(function() {
      var status = document.getElementById('2fa-status');
      if (status) status.textContent = 'Status: Unable to load';
    });
  }
  // Expose globally so app.js tabChanged handler can call it
  window.load2FAStatus = load2FAStatus;

  // Modal that shows backup codes exactly once. Uses textContent and a copy
  // button — codes never land in innerHTML so a stray value can't produce XSS.
  function showBackupCodesModal(codes) {
    var existing = document.getElementById('backup-codes-modal');
    if (existing) existing.remove();
    var modal = document.createElement('div');
    modal.id = 'backup-codes-modal';
    modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:9999;padding:20px;';

    var box = document.createElement('div');
    box.style.cssText = 'background:#fff;border-radius:12px;max-width:460px;width:100%;padding:24px;box-shadow:0 20px 60px rgba(0,0,0,.3);';

    var title = document.createElement('div');
    title.style.cssText = 'font-size:18px;font-weight:700;margin-bottom:4px;';
    title.textContent = 'Save your backup codes';
    var hint = document.createElement('div');
    hint.style.cssText = 'font-size:13px;color:#6b7280;margin-bottom:16px;line-height:1.5;';
    hint.textContent = 'Each code works once. Store them somewhere safe — you will NOT see them again. Use one in place of your 2FA code if you lose your authenticator.';

    var list = document.createElement('pre');
    list.style.cssText = 'background:#f3f4f6;padding:12px 14px;border-radius:8px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:14px;line-height:1.8;margin:0;white-space:pre-wrap;user-select:all;';
    list.textContent = codes.join('\n');

    var btnRow = document.createElement('div');
    btnRow.style.cssText = 'display:flex;gap:8px;margin-top:16px;justify-content:flex-end;';

    var copyBtn = document.createElement('button');
    copyBtn.className = 'btn-sm btn-ghost';
    copyBtn.textContent = 'Copy';
    copyBtn.addEventListener('click', function() {
      navigator.clipboard.writeText(codes.join('\n')).then(function() {
        copyBtn.textContent = 'Copied ✓';
        setTimeout(function() { copyBtn.textContent = 'Copy'; }, 1500);
      }).catch(function() { showToast('Copy failed', 'error'); });
    });

    var downloadBtn = document.createElement('button');
    downloadBtn.className = 'btn-sm btn-ghost';
    downloadBtn.textContent = 'Download .txt';
    downloadBtn.addEventListener('click', function() {
      var blob = new Blob(['PedScribe 2FA backup codes\nGenerated: ' + new Date().toISOString() + '\n\n' + codes.join('\n') + '\n'], { type: 'text/plain' });
      var url = URL.createObjectURL(blob);
      var a = document.createElement('a'); a.href = url; a.download = 'pedscribe-backup-codes.txt'; a.click();
      URL.revokeObjectURL(url);
    });

    var doneBtn = document.createElement('button');
    doneBtn.className = 'btn-sm btn-primary';
    doneBtn.textContent = "I've saved them";
    doneBtn.addEventListener('click', function() { modal.remove(); });

    btnRow.appendChild(copyBtn); btnRow.appendChild(downloadBtn); btnRow.appendChild(doneBtn);
    box.appendChild(title); box.appendChild(hint); box.appendChild(list); box.appendChild(btnRow);
    modal.appendChild(box);
    document.body.appendChild(modal);
  }

  // Check registration status — link is hidden by default, shown only when enabled
  fetch('/api/auth/registration-status')
    .then(function(r) { return r.json(); })
    .then(function(data) {
      if (data.registrationEnabled) {
        var link = document.getElementById('show-register');
        if (link) link.style.display = '';
      }
    })
    .catch(function() {});

  console.log('[Auth] ✅ Module ready');
});
