/**
 * SettingsSheet — master notifications toggle (D-09).
 *
 * A bottom sheet opened by tapping the user avatar in AppNav.
 * Contains a single master on/off toggle for all FamilySync push notifications.
 *
 * UI-SPEC §Surface 2:
 *   - Bottom sheet, role="dialog", aria-modal, zIndex 301 (backdrop 300)
 *   - Heading "Settings" + X close button
 *   - Section label "Notifications" (uppercase, muted)
 *   - Bell icon + toggle row
 *   - Permission-denied hint when Notification.permission === 'denied'
 *
 * Toggle behavior:
 *   on → off: DELETE subscription + localStorage.notificationsEnabled='0'
 *   off → on (permission granted): silently subscribe (no OS dialog)
 *   off → on (permission default): triggers tap-gated subscribe() (OS dialog)
 *   off → on (permission denied): no-op, shows permission-denied hint inline
 *
 * Accessibility: role="switch", aria-checked, 44px touch targets, Escape closes.
 * Security: T-05-24 — all copy is plain-text JSX children, no dangerouslySetInnerHTML.
 */

import { useCallback, useEffect, useRef, useState } from 'react';
import { X, Bell, AlertCircle, Loader2, LogOut } from 'lucide-react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useNavigate } from 'react-router';
import { usePushSubscription } from '../hooks/usePushSubscription.js';
import { useIsPhone } from '../hooks/useIsPhone.js';
import { useFocusTrap } from '../hooks/useFocusTrap.js';
import { InstructionSheet } from './InstructionSheet.js';
import {
  fetchMe,
  fetchAuthMode,
  fetchChangePassword,
  fetchLinkOidc,
  fetchLocalLogout,
} from '../api/client.js';

// CR-04: fetch VAPID key (from sessionStorage cache if available) for the
// tap-gated subscribe() path.  Same logic as PushPermissionPrompt.
async function fetchVapidKeyForSettings(): Promise<string | null> {
  try {
    const cached = sessionStorage.getItem('vapidPublicKey');
    if (cached) return cached;
    const res = await fetch('/api/push/vapid-public-key', { credentials: 'include' });
    if (!res.ok) return null;
    const data = (await res.json()) as { publicKey: string };
    if (data.publicKey) {
      sessionStorage.setItem('vapidPublicKey', data.publicKey);
    }
    return data.publicKey ?? null;
  } catch {
    return null;
  }
}

interface SettingsSheetProps {
  isOpen: boolean;
  onClose: () => void;
}

export function SettingsSheet({ isOpen, onClose }: SettingsSheetProps) {
  const { subscribe, permission, isSubscribed, setEnabled } = usePushSubscription();
  const navigate = useNavigate();
  // WR-05: resize-aware so an iPad rotation across 767px reflows the sheet.
  const phone = useIsPhone();
  const [isTogglingOn, setIsTogglingOn] = useState(false);
  const [instructionsOpen, setInstructionsOpen] = useState(false);

  // Phase 19: read meData (same query key as App.tsx — TanStack deduplicates the request)
  const meQuery = useQuery({
    queryKey: ['me'],
    queryFn: fetchMe,
    retry: false,
    staleTime: 0,
  });
  const authModeQuery = useQuery({
    queryKey: ['authMode'],
    queryFn: fetchAuthMode,
    retry: false,
    staleTime: 60_000,
  });

  const hasLocalCredential = meQuery.data?.user.hasLocalCredential ?? false;
  const oidcEnabled = authModeQuery.data?.oidcEnabled ?? false;

  // Change-password sheet state (Surface 12)
  const [changePasswordOpen, setChangePasswordOpen] = useState(false);
  // Link-OIDC confirmation sheet state (Surface 13)
  const [linkOidcOpen, setLinkOidcOpen] = useState(false);
  // CR-04: pre-fetch the VAPID key into state so the toggle tap handler can call
  // subscribe(registration, vapidKey) without any network await before pushManager.subscribe().
  const [vapidKey, setVapidKey] = useState<string | null>(null);
  // NEW-CR-01: Pre-resolve the ServiceWorkerRegistration into state so the toggle
  // tap handler has ZERO awaits between the user gesture and pushManager.subscribe().
  const [swRegistration, setSwRegistration] = useState<ServiceWorkerRegistration | null>(null);
  const closeButtonRef = useRef<HTMLButtonElement>(null);
  // WR-01: trap Tab/Shift+Tab inside the dialog (matches aria-modal="true").
  const dialogRef = useRef<HTMLDivElement>(null);
  const handleDialogKeyDown = useFocusTrap(dialogRef);

  // Compute initial toggle on/off state per UI-SPEC toggle initial state rule:
  // on when notificationsEnabled !== '0' AND permission === 'granted' AND isSubscribed
  const isOn = permission === 'granted' && isSubscribed;

  // Escape key listener (CreateListSheet pattern)
  useEffect(() => {
    if (!isOpen) return;
    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    };
    document.addEventListener('keydown', onKeyDown);
    return () => document.removeEventListener('keydown', onKeyDown);
  }, [isOpen, onClose]);

  // Focus close button on open (a11y)
  useEffect(() => {
    if (isOpen && closeButtonRef.current) {
      closeButtonRef.current.focus();
    }
  }, [isOpen]);

  // CR-04: Pre-fetch the VAPID key while the sheet is open so the OS-dialog path
  // (permission === 'default') has the key ready before the user taps.
  useEffect(() => {
    if (!isOpen) return;
    if (permission === 'denied') return;
    void fetchVapidKeyForSettings().then((key) => {
      if (key) setVapidKey(key);
    });
  }, [isOpen, permission]);

  // NEW-CR-01: Pre-resolve the ServiceWorkerRegistration into state so the toggle
  // tap handler never needs to await navigator.serviceWorker.ready. Any await
  // between the tap gesture and pushManager.subscribe() breaks iOS.
  useEffect(() => {
    if (!isOpen) return;
    if (permission === 'denied') return;
    if (!navigator.serviceWorker) return;
    void navigator.serviceWorker.ready.then((reg) => {
      setSwRegistration(reg);
    });
  }, [isOpen, permission]);

  if (!isOpen) return null;

  // D-07: fire-and-best-effort logout — navigate to /login regardless of API success/failure
  const handleSignOut = async () => {
    try {
      await fetchLocalLogout();
    } catch {
      // API failure does not block navigation — server cookie was cleared or already expired
    }
    onClose();
    void navigate('/login');
  };

  const handleToggle = async () => {
    if (permission === 'denied') return; // no-op — show hint below

    if (isOn) {
      // on → off: unsubscribe
      await setEnabled(false);
    } else if (permission === 'granted') {
      // off → on, permission already granted: silent subscribe
      setIsTogglingOn(true);
      try {
        await setEnabled(true);
      } finally {
        setIsTogglingOn(false);
      }
    } else {
      // off → on, permission 'default': needs tap-gated subscribe with OS dialog
      // The toggle click IS the tap gesture — call subscribe() directly here.
      // NEW-CR-01: Both vapidKey AND swRegistration are pre-resolved into state
      // (useEffects above). There is ZERO await between the tap and
      // registration.pushManager.subscribe() — iOS gesture gate satisfied.
      if (!vapidKey || !swRegistration) return; // not ready — useEffects still resolving
      const resolvedVapidKey = vapidKey;
      const resolvedRegistration = swRegistration;
      setIsTogglingOn(true);
      try {
        await subscribe(resolvedRegistration, resolvedVapidKey);
      } catch {
        // Permission denied by OS or error — permission state will update reactively
      } finally {
        setIsTogglingOn(false);
      }
    }
  };

  const isDisabled = permission === 'denied';

  return (
    <>
      {/* Backdrop */}
      <div
        onClick={onClose}
        aria-hidden="true"
        style={{
          position: 'fixed',
          inset: 0,
          background: 'var(--color-overlay, rgba(0,0,0,0.32))',
          zIndex: 300,
        }}
      />

      {/* Sheet */}
      <div
        ref={dialogRef}
        role="dialog"
        aria-modal="true"
        aria-label="Settings"
        onKeyDown={handleDialogKeyDown}
        style={
          phone
            ? {
                position: 'fixed',
                bottom: 0,
                left: 0,
                right: 0,
                background: 'var(--color-surface-raised, #ffffff)',
                borderRadius: '12px 12px 0 0',
                boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
                padding: 'var(--space-6, 24px)',
                zIndex: 301,
                fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
              }
            : {
                position: 'fixed',
                top: '50%',
                left: '50%',
                transform: 'translate(-50%, -50%)',
                maxWidth: '480px',
                width: 'calc(100% - var(--space-8, 32px))',
                maxHeight: 'calc(100dvh - var(--space-8, 32px))',
                overflowY: 'auto',
                background: 'var(--color-surface-raised, #ffffff)',
                borderRadius: '12px',
                boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
                padding: 'var(--space-6, 24px)',
                zIndex: 301,
                fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
              }
        }
      >
        {/* Heading row */}
        <div
          style={{
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'space-between',
            marginBottom: 'var(--space-6, 24px)',
          }}
        >
          <h2
            style={{
              margin: 0,
              fontSize: 'var(--text-heading-size, 18px)',
              fontWeight: 600,
              lineHeight: 'var(--text-heading-line-height, 1.25)',
              color: 'var(--color-text-primary, #111318)',
            }}
          >
            Settings
          </h2>
          <button
            ref={closeButtonRef}
            onClick={onClose}
            aria-label="Close settings"
            style={{
              background: 'none',
              border: 'none',
              cursor: 'pointer',
              minWidth: '44px',
              minHeight: '44px',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              color: 'var(--color-text-secondary, #5c6472)',
              borderRadius: 'var(--space-1, 4px)',
            }}
          >
            <X size={20} aria-hidden="true" />
          </button>
        </div>

        {/* Section label */}
        <div
          style={{
            fontSize: 'var(--text-label-size, 13px)',
            fontWeight: 600,
            color: 'var(--color-text-muted, #9CA3AF)',
            textTransform: 'uppercase',
            letterSpacing: '0.06em',
            marginBottom: 'var(--space-2, 8px)',
          }}
        >
          Notifications
        </div>

        {/* Toggle row */}
        <div
          style={{
            display: 'flex',
            alignItems: 'center',
            gap: 'var(--space-3, 12px)',
            padding: 'var(--space-2, 8px) 0',
          }}
        >
          <Bell
            size={20}
            aria-hidden="true"
            style={{
              color: 'var(--color-text-secondary, #5c6472)',
              flexShrink: 0,
            }}
          />

          {/* Label column */}
          <div style={{ flex: 1, minWidth: 0 }}>
            <div
              style={{
                fontSize: 'var(--text-body-size, 15px)',
                fontWeight: 400,
                color: 'var(--color-text-primary, #111318)',
                lineHeight: 1.4,
              }}
            >
              FamilySync Notifications
            </div>
            <div
              style={{
                fontSize: 'var(--text-label-size, 13px)',
                color: 'var(--color-text-secondary, #5c6472)',
                lineHeight: 1.4,
                marginTop: '2px',
              }}
            >
              Reminders, event changes, list updates
            </div>
          </div>

          {/* Toggle switch or spinner */}
          {isTogglingOn ? (
            <Loader2
              size={20}
              aria-hidden="true"
              style={{
                color: 'var(--color-text-secondary, #5c6472)',
                animation: 'spin 1s linear infinite',
                flexShrink: 0,
              }}
            />
          ) : (
            <button
              role="switch"
              aria-checked={isOn}
              aria-label={isOn ? 'FamilySync Notifications, on' : 'FamilySync Notifications, off'}
              onClick={() => {
                void handleToggle();
              }}
              disabled={isDisabled}
              style={{
                // 44px touch target
                minWidth: '44px',
                minHeight: '44px',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                background: 'none',
                border: 'none',
                cursor: isDisabled ? 'default' : 'pointer',
                padding: 0,
                flexShrink: 0,
                opacity: isDisabled ? 0.5 : 1,
              }}
            >
              {/* Toggle track */}
              <div
                style={{
                  width: '44px',
                  height: '24px',
                  borderRadius: '12px',
                  background: isOn
                    ? 'var(--color-member-0, #4A90D9)'
                    : 'var(--color-border, #E2E4E9)',
                  position: 'relative',
                  transition: 'background 0.15s ease',
                  pointerEvents: 'none',
                }}
              >
                {/* Toggle thumb */}
                <div
                  style={{
                    position: 'absolute',
                    top: '2px',
                    left: isOn ? '22px' : '2px',
                    width: '20px',
                    height: '20px',
                    borderRadius: '50%',
                    background: '#ffffff',
                    boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
                    transition: 'left 0.15s ease',
                  }}
                />
              </div>
            </button>
          )}
        </div>

        {/* Surface 12 — Change password row (hasLocalCredential gate) */}
        {hasLocalCredential && (
          <>
            <div
              style={{
                height: '1px',
                background: 'var(--color-border-subtle, var(--color-border))',
                margin: 'var(--space-4, 16px) 0',
              }}
            />
            <div
              style={{
                fontSize: 'var(--text-label-size, 13px)',
                fontWeight: 600,
                color: 'var(--color-text-muted, #9CA3AF)',
                textTransform: 'uppercase',
                letterSpacing: '0.06em',
                marginBottom: 'var(--space-2, 8px)',
              }}
            >
              Account
            </div>
            <button
              type="button"
              onClick={() => setChangePasswordOpen(true)}
              style={{
                display: 'flex',
                alignItems: 'center',
                width: '100%',
                minHeight: '44px',
                background: 'none',
                border: 'none',
                cursor: 'pointer',
                padding: 'var(--space-2, 8px) 0',
                fontSize: 'var(--text-body-size, 15px)',
                fontWeight: 400,
                color: 'var(--color-text-primary, #111318)',
                fontFamily: 'var(--font-family-base)',
                textAlign: 'left',
              }}
            >
              Change password
            </button>

            {/* Surface 13 — Link OIDC identity row (hasLocalCredential + oidcEnabled gate) */}
            {oidcEnabled && (
              <button
                type="button"
                onClick={() => setLinkOidcOpen(true)}
                style={{
                  display: 'flex',
                  alignItems: 'center',
                  width: '100%',
                  minHeight: '44px',
                  background: 'none',
                  border: 'none',
                  cursor: 'pointer',
                  padding: 'var(--space-2, 8px) 0',
                  fontSize: 'var(--text-body-size, 15px)',
                  fontWeight: 400,
                  color: 'var(--color-text-primary, #111318)',
                  fontFamily: 'var(--font-family-base)',
                  textAlign: 'left',
                }}
              >
                Link OIDC identity
              </button>
            )}
          </>
        )}

        {/* Permission-denied hint — only when OS permission === 'denied' */}
        {permission === 'denied' && (
          <div
            style={{
              display: 'flex',
              alignItems: 'flex-start',
              gap: 'var(--space-2, 8px)',
              marginTop: 'var(--space-2, 8px)',
              padding: 'var(--space-3, 12px)',
              background: 'var(--color-surface-dim, #F7F7F8)',
              borderRadius: 'var(--space-1, 4px)',
            }}
          >
            <AlertCircle
              size={16}
              aria-hidden="true"
              style={{
                color: 'var(--color-destructive, #DC2626)',
                flexShrink: 0,
                marginTop: '1px',
              }}
            />
            <div>
              <span
                style={{
                  fontSize: 'var(--text-label-size, 13px)',
                  color: 'var(--color-text-secondary, #5c6472)',
                  lineHeight: 1.4,
                }}
              >
                Notifications are blocked in your browser settings.{' '}
              </span>
              <button
                onClick={() => setInstructionsOpen(true)}
                style={{
                  background: 'none',
                  border: 'none',
                  padding: 0,
                  cursor: 'pointer',
                  fontSize: 'var(--text-label-size, 13px)',
                  color: 'var(--color-focus-ring, #4A90D9)',
                  textDecoration: 'underline',
                  fontFamily: 'inherit',
                }}
              >
                How to enable
              </button>
            </div>
          </div>
        )}

        {/* D-07: Sign out — always visible at the bottom of the sheet */}
        <div
          style={{
            height: '1px',
            background: 'var(--color-border-subtle, var(--color-border))',
            margin: 'var(--space-4, 16px) 0',
          }}
        />
        <button
          type="button"
          onClick={() => {
            void handleSignOut();
          }}
          aria-label="Sign out"
          style={{
            display: 'flex',
            alignItems: 'center',
            gap: 'var(--space-2, 8px)',
            width: '100%',
            minHeight: '44px',
            background: 'none',
            border: 'none',
            cursor: 'pointer',
            padding: 'var(--space-2, 8px) 0',
            fontSize: 'var(--text-body-size, 15px)',
            fontWeight: 400,
            color: 'var(--color-destructive, #dc2626)',
            fontFamily: 'var(--font-family-base)',
            textAlign: 'left',
          }}
        >
          <LogOut size={16} aria-hidden="true" />
          Sign out
        </button>
      </div>

      {instructionsOpen && <InstructionSheet onClose={() => setInstructionsOpen(false)} />}

      {/* Surface 12 — Change-password sheet (hasLocalCredential gate) */}
      {changePasswordOpen && (
        <ChangePasswordSheet
          isOpen={changePasswordOpen}
          onClose={() => setChangePasswordOpen(false)}
        />
      )}

      {/* Surface 13 — Link OIDC confirmation sheet (hasLocalCredential + oidcEnabled gate) */}
      {linkOidcOpen && (
        <LinkOidcSheet isOpen={linkOidcOpen} onClose={() => setLinkOidcOpen(false)} />
      )}
    </>
  );
}

// ── ChangePasswordSheet (Surface 12) ─────────────────────────────────────────

/**
 * Surface 12 — Self-service password change sheet.
 * Opens from the "Change password" row in SettingsSheet.
 * Pattern: CredentialSheet (role=dialog, aria-modal, Escape closes, focus heading on open).
 * Fields: Current password / New password / Confirm — correct autoComplete values.
 * Security: T-19-18 — password fields are controlled state only; never written to storage.
 */

interface ChangePasswordSheetProps {
  isOpen: boolean;
  onClose: () => void;
}

function ChangePasswordSheet({ isOpen, onClose }: ChangePasswordSheetProps) {
  const phone = useIsPhone();
  const [currentPassword, setCurrentPassword] = useState('');
  const [newPassword, setNewPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  const [error, setError] = useState<string | null>(null);
  const headingRef = useRef<HTMLHeadingElement>(null);
  // WR-01: trap Tab/Shift+Tab inside the dialog (matches aria-modal="true").
  const dialogRef = useRef<HTMLDivElement>(null);
  const handleDialogKeyDown = useFocusTrap(dialogRef);

  // IN-02: useCallback so handleClose is a real dependency of the Escape effect.
  const handleClose = useCallback(() => {
    setCurrentPassword('');
    setNewPassword('');
    setConfirmPassword('');
    setError(null);
    onClose();
  }, [onClose]);

  useEffect(() => {
    if (!isOpen) return;
    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') handleClose();
    };
    document.addEventListener('keydown', onKeyDown);
    return () => document.removeEventListener('keydown', onKeyDown);
  }, [isOpen, handleClose]);

  useEffect(() => {
    if (isOpen && headingRef.current) {
      headingRef.current.focus();
    }
  }, [isOpen]);

  const changeMutation = useMutation({
    mutationFn: async () => {
      if (newPassword !== confirmPassword) throw new Error('mismatch');
      await fetchChangePassword({ currentPassword, newPassword });
    },
    onSuccess: () => {
      handleClose();
    },
    onError: (err) => {
      const msg = err instanceof Error ? err.message : 'server';
      if (msg === 'mismatch') {
        setError('Passwords do not match.');
      } else if (msg === 'wrong-current') {
        setError('Current password is incorrect.');
      } else {
        setError('Something went wrong. Please try again.');
      }
    },
  });

  const isPending = changeMutation.isPending;
  const submitDisabled =
    isPending ||
    currentPassword.length === 0 ||
    newPassword.length === 0 ||
    confirmPassword.length === 0;

  if (!isOpen) return null;

  return (
    <>
      {/* Backdrop */}
      <div
        onClick={handleClose}
        aria-hidden="true"
        style={{
          position: 'fixed',
          inset: 0,
          background: 'var(--color-overlay, rgba(0,0,0,0.32))',
          zIndex: 302,
        }}
      />

      {/* Sheet */}
      <div
        ref={dialogRef}
        role="dialog"
        aria-modal="true"
        aria-label="Change password"
        onKeyDown={handleDialogKeyDown}
        style={
          phone
            ? {
                position: 'fixed',
                bottom: 0,
                left: 0,
                right: 0,
                background: 'var(--color-surface-raised, #ffffff)',
                borderRadius: '12px 12px 0 0',
                boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
                padding: 'var(--space-6, 24px)',
                zIndex: 303,
                fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
              }
            : {
                position: 'fixed',
                top: '50%',
                left: '50%',
                transform: 'translate(-50%, -50%)',
                maxWidth: '480px',
                width: 'calc(100% - var(--space-8, 32px))',
                maxHeight: 'calc(100dvh - var(--space-8, 32px))',
                overflowY: 'auto',
                background: 'var(--color-surface-raised, #ffffff)',
                borderRadius: '12px',
                boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
                padding: 'var(--space-6, 24px)',
                zIndex: 303,
                fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
              }
        }
      >
        <h2
          ref={headingRef}
          tabIndex={-1}
          style={{
            margin: '0 0 var(--space-6, 24px) 0',
            fontSize: 'var(--text-heading-size, 18px)',
            fontWeight: 600,
            lineHeight: 'var(--text-heading-line-height, 1.25)',
            color: 'var(--color-text-primary, #111318)',
            outline: 'none',
          }}
        >
          Change password
        </h2>

        {/* Current password */}
        <div style={{ marginBottom: 'var(--space-3, 12px)' }}>
          <label
            htmlFor="change-current-password"
            style={{
              display: 'block',
              fontSize: 'var(--text-label-size, 13px)',
              fontWeight: 600,
              color: 'var(--color-text-primary, #111318)',
              marginBottom: 'var(--space-1, 4px)',
            }}
          >
            Current password
          </label>
          <input
            id="change-current-password"
            type="password"
            autoComplete="current-password"
            value={currentPassword}
            onChange={(e) => setCurrentPassword(e.target.value)}
            style={{
              width: '100%',
              boxSizing: 'border-box',
              padding: 'var(--space-3, 12px) var(--space-4, 16px)',
              border: `1px solid ${error === 'Current password is incorrect.' ? 'var(--color-destructive)' : 'var(--color-border)'}`,
              borderRadius: 'var(--space-1, 4px)',
              fontSize: 'var(--text-body-size, 15px)',
              color: 'var(--color-text-primary, #111318)',
              background: 'var(--color-surface, #ffffff)',
              fontFamily: 'var(--font-family-base)',
              outline: 'none',
              minHeight: '44px',
            }}
          />
        </div>

        {/* New password */}
        <div style={{ marginBottom: 'var(--space-3, 12px)' }}>
          <label
            htmlFor="change-new-password"
            style={{
              display: 'block',
              fontSize: 'var(--text-label-size, 13px)',
              fontWeight: 600,
              color: 'var(--color-text-primary, #111318)',
              marginBottom: 'var(--space-1, 4px)',
            }}
          >
            New password
          </label>
          <input
            id="change-new-password"
            type="password"
            autoComplete="new-password"
            value={newPassword}
            onChange={(e) => setNewPassword(e.target.value)}
            style={{
              width: '100%',
              boxSizing: 'border-box',
              padding: 'var(--space-3, 12px) var(--space-4, 16px)',
              border: `1px solid ${error === 'Passwords do not match.' ? 'var(--color-destructive)' : 'var(--color-border)'}`,
              borderRadius: 'var(--space-1, 4px)',
              fontSize: 'var(--text-body-size, 15px)',
              color: 'var(--color-text-primary, #111318)',
              background: 'var(--color-surface, #ffffff)',
              fontFamily: 'var(--font-family-base)',
              outline: 'none',
              minHeight: '44px',
            }}
          />
        </div>

        {/* Confirm new password */}
        <div style={{ marginBottom: 'var(--space-4, 16px)' }}>
          <label
            htmlFor="change-confirm-password"
            style={{
              display: 'block',
              fontSize: 'var(--text-label-size, 13px)',
              fontWeight: 600,
              color: 'var(--color-text-primary, #111318)',
              marginBottom: 'var(--space-1, 4px)',
            }}
          >
            Confirm
          </label>
          <input
            id="change-confirm-password"
            type="password"
            autoComplete="new-password"
            value={confirmPassword}
            onChange={(e) => setConfirmPassword(e.target.value)}
            aria-describedby={error ? 'change-password-error' : undefined}
            style={{
              width: '100%',
              boxSizing: 'border-box',
              padding: 'var(--space-3, 12px) var(--space-4, 16px)',
              border: `1px solid ${error === 'Passwords do not match.' ? 'var(--color-destructive)' : 'var(--color-border)'}`,
              borderRadius: 'var(--space-1, 4px)',
              fontSize: 'var(--text-body-size, 15px)',
              color: 'var(--color-text-primary, #111318)',
              background: 'var(--color-surface, #ffffff)',
              fontFamily: 'var(--font-family-base)',
              outline: 'none',
              minHeight: '44px',
            }}
          />
        </div>

        {/* Error */}
        {error && (
          <div
            id="change-password-error"
            style={{
              fontSize: 'var(--text-label-size, 13px)',
              fontWeight: 400,
              color: 'var(--color-destructive, #dc2626)',
              marginBottom: 'var(--space-4, 16px)',
            }}
          >
            {error}
          </div>
        )}

        {/* Action row */}
        <div
          style={{
            display: 'flex',
            justifyContent: 'flex-end',
            gap: 'var(--space-3, 12px)',
          }}
        >
          <button
            type="button"
            onClick={handleClose}
            disabled={isPending}
            style={{
              background: 'none',
              border: 'none',
              cursor: isPending ? 'default' : 'pointer',
              fontSize: 'var(--text-label-size, 13px)',
              fontWeight: 600,
              color: 'var(--color-text-secondary, #6b7280)',
              minHeight: '44px',
              minWidth: '44px',
              padding: '0 var(--space-4, 16px)',
              fontFamily: 'var(--font-family-base)',
              borderRadius: 'var(--space-1, 4px)',
            }}
          >
            Cancel
          </button>

          <button
            type="button"
            disabled={submitDisabled}
            onClick={() => {
              setError(null);
              changeMutation.mutate();
            }}
            style={{
              background: submitDisabled
                ? 'var(--color-border, #e2e4e9)'
                : 'var(--color-member-0, #4a90d9)',
              color: '#ffffff',
              border: 'none',
              cursor: submitDisabled ? 'default' : 'pointer',
              fontSize: 'var(--text-label-size, 13px)',
              fontWeight: 600,
              minHeight: '44px',
              minWidth: '44px',
              padding: '0 var(--space-4, 16px)',
              borderRadius: 'var(--space-1, 4px)',
              fontFamily: 'var(--font-family-base)',
              transition: 'background 0.15s ease',
            }}
          >
            Change password
          </button>
        </div>
      </div>
    </>
  );
}

// ── LinkOidcSheet (Surface 13) ────────────────────────────────────────────────

/**
 * Surface 13 — Link OIDC identity confirmation sheet.
 * NOT a form — the actual linking happens via OIDC redirect.
 * Two-step confirmation: open sheet (step 1) + tap "Continue with OIDC" (step 2).
 *
 * Copywriting rules (UI-SPEC §Copywriting Contract):
 *   - Never use "Authelia" (D-06) — use "your OIDC provider"
 *   - Never say "delete" or "remove" when describing consequence — use "will be removed" (passive)
 *   - Body copy: non-alarming, frames linking as an upgrade
 *
 * Security: T-19-21 — no provider-specific branding that leaks infrastructure details.
 */

interface LinkOidcSheetProps {
  isOpen: boolean;
  onClose: () => void;
}

function LinkOidcSheet({ isOpen, onClose }: LinkOidcSheetProps) {
  const phone = useIsPhone();
  const [error, setError] = useState<string | null>(null);
  const headingRef = useRef<HTMLHeadingElement>(null);
  // WR-01: trap Tab/Shift+Tab inside the dialog (matches aria-modal="true").
  const dialogRef = useRef<HTMLDivElement>(null);
  const handleDialogKeyDown = useFocusTrap(dialogRef);

  useEffect(() => {
    if (!isOpen) return;
    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    };
    document.addEventListener('keydown', onKeyDown);
    return () => document.removeEventListener('keydown', onKeyDown);
  }, [isOpen, onClose]);

  useEffect(() => {
    if (isOpen && headingRef.current) {
      headingRef.current.focus();
    }
  }, [isOpen]);

  const linkMutation = useMutation({
    mutationFn: fetchLinkOidc,
    onSuccess: (data) => {
      // authorizationUrl is null when OIDC is not configured in env (server could not
      // build the URL). Do NOT navigate to null — surface an error and keep the sheet open.
      if (!data.authorizationUrl) {
        setError('Something went wrong. Please try again.');
        return;
      }
      // Close the sheet and initiate the OIDC link flow via top-level navigation.
      onClose();
      window.location.href = data.authorizationUrl;
    },
    onError: () => {
      setError('Something went wrong. Please try again.');
    },
  });

  if (!isOpen) return null;

  return (
    <>
      {/* Backdrop */}
      <div
        onClick={onClose}
        aria-hidden="true"
        style={{
          position: 'fixed',
          inset: 0,
          background: 'var(--color-overlay, rgba(0,0,0,0.32))',
          zIndex: 302,
        }}
      />

      {/* Sheet */}
      <div
        ref={dialogRef}
        role="dialog"
        aria-modal="true"
        aria-label="Link OIDC identity"
        onKeyDown={handleDialogKeyDown}
        style={
          phone
            ? {
                position: 'fixed',
                bottom: 0,
                left: 0,
                right: 0,
                background: 'var(--color-surface-raised, #ffffff)',
                borderRadius: '12px 12px 0 0',
                boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
                padding: 'var(--space-6, 24px)',
                zIndex: 303,
                fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
              }
            : {
                position: 'fixed',
                top: '50%',
                left: '50%',
                transform: 'translate(-50%, -50%)',
                maxWidth: '480px',
                width: 'calc(100% - var(--space-8, 32px))',
                maxHeight: 'calc(100dvh - var(--space-8, 32px))',
                overflowY: 'auto',
                background: 'var(--color-surface-raised, #ffffff)',
                borderRadius: '12px',
                boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
                padding: 'var(--space-6, 24px)',
                zIndex: 303,
                fontFamily: 'var(--font-family-base, system-ui, sans-serif)',
              }
        }
      >
        <h2
          ref={headingRef}
          tabIndex={-1}
          style={{
            margin: '0 0 var(--space-4, 16px) 0',
            fontSize: 'var(--text-heading-size, 18px)',
            fontWeight: 600,
            lineHeight: 'var(--text-heading-line-height, 1.25)',
            color: 'var(--color-text-primary, #111318)',
            outline: 'none',
          }}
        >
          Link OIDC identity
        </h2>

        {/* Body — informational, not alarming (UI-SPEC §Copywriting Contract) */}
        <p
          style={{
            margin: '0 0 var(--space-3, 12px) 0',
            fontSize: 'var(--text-body-size, 15px)',
            fontWeight: 400,
            lineHeight: 'var(--text-body-line-height, 1.5)',
            color: 'var(--color-text-secondary, #6b7280)',
          }}
        >
          {
            "After linking, you'll sign in with your OIDC provider instead of a username and password. Your local password will be removed."
          }
        </p>

        {/* Secondary note */}
        <p
          style={{
            margin: '0 0 var(--space-6, 24px) 0',
            fontSize: 'var(--text-label-size, 13px)',
            fontWeight: 400,
            lineHeight: 'var(--text-label-line-height, 1.4)',
            color: 'var(--color-text-muted, #9ca3af)',
          }}
        >
          {"This can't be undone from the app. Contact your admin if you need to revert."}
        </p>

        {/* Error (post-fetch) */}
        {error && (
          <div
            style={{
              fontSize: 'var(--text-label-size, 13px)',
              color: 'var(--color-destructive, #dc2626)',
              marginBottom: 'var(--space-4, 16px)',
            }}
          >
            {error}
          </div>
        )}

        {/* Action row */}
        <div
          style={{
            display: 'flex',
            justifyContent: 'flex-end',
            gap: 'var(--space-3, 12px)',
          }}
        >
          <button
            type="button"
            onClick={onClose}
            disabled={linkMutation.isPending}
            style={{
              background: 'none',
              border: 'none',
              cursor: linkMutation.isPending ? 'default' : 'pointer',
              fontSize: 'var(--text-label-size, 13px)',
              fontWeight: 600,
              color: 'var(--color-text-secondary, #6b7280)',
              minHeight: '44px',
              minWidth: '44px',
              padding: '0 var(--space-4, 16px)',
              fontFamily: 'var(--font-family-base)',
              borderRadius: 'var(--space-1, 4px)',
            }}
          >
            Cancel
          </button>

          <button
            type="button"
            disabled={linkMutation.isPending}
            onClick={() => {
              setError(null);
              linkMutation.mutate();
            }}
            style={{
              background: linkMutation.isPending
                ? 'var(--color-border, #e2e4e9)'
                : 'var(--color-member-0, #4a90d9)',
              color: '#ffffff',
              border: 'none',
              cursor: linkMutation.isPending ? 'default' : 'pointer',
              fontSize: 'var(--text-label-size, 13px)',
              fontWeight: 600,
              minHeight: '44px',
              minWidth: '44px',
              padding: '0 var(--space-4, 16px)',
              borderRadius: 'var(--space-1, 4px)',
              fontFamily: 'var(--font-family-base)',
              transition: 'background 0.15s ease',
            }}
          >
            Continue with OIDC
          </button>
        </div>
      </div>
    </>
  );
}
