/**
 * MemberEditorSheet — unified member editor sheet (Phase 20, D-05, D-06, D-07).
 *
 * Single component with a `mode` prop:
 *   - 'edit'   — three per-section saves: Profile, Set new password, App password
 *   - 'create' — single form: display name, username, initial password, confirm
 *
 * UI-SPEC §Surface B (20-UI-SPEC.md):
 *   - role="dialog", aria-modal, useFocusTrap, Escape closes, focus returns to trigger
 *   - Phone: fixed bottom bottom-sheet; Desktop: centered modal (480px)
 *   - zIndex 301 / backdrop 300 / overlay rgba(0,0,0,0.32)
 *   - Per-section saves keep the sheet open; create-mode save closes it
 *   - Admin toggle: role="switch", aria-checked (Accessibility Contract)
 *
 * Security:
 *   T-20-07: password/app-password fields are write-only: never prefilled,
 *            autoComplete="new-password", never logged (T-10-15/16 preserved)
 *   T-20-08: app-password save routes through saveCredential → server-side CalDAV
 *            validation; invalid password surfaces copy, nothing stored
 *   T-20-09: admin toggle is cosmetic; D-03 409 guard enforced server-side (Plan 20-01)
 *
 * Retired copy (D-06): per-row credential buttons and standalone password-reset button removed from this phase.
 */

import { useState, useEffect, useRef, useCallback } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Loader2 } from 'lucide-react';
import {
  updateMemberProfile,
  fetchAdminResetPassword,
  fetchCreateMember,
  saveCredential,
  type AdminMember,
} from '../api/client.js';
import { useIsPhone } from '../hooks/useIsPhone.js';
import { useFocusTrap } from '../hooks/useFocusTrap.js';

// ── Types ──────────────────────────────────────────────────────────────────

export type MemberEditorSheetMode = 'edit' | 'create';

interface MemberEditorSheetProps {
  isOpen: boolean;
  onClose: () => void;
  mode: MemberEditorSheetMode;
  /** Present in edit mode; absent in create mode */
  member?: AdminMember;
  /** Ref to the trigger element — focus returns here on close (a11y) */
  triggerRef?: React.RefObject<HTMLElement | null>;
  /** Lift toast copy up to AdminPage which owns the toast state */
  onToast: (message: string) => void;
}

// ── Copywriting (UI-SPEC §Copywriting Contract) ───────────────────────────

function headingFor(mode: MemberEditorSheetMode): string {
  return mode === 'edit' ? 'Edit member' : 'Add member';
}

const CALDAV_FAILURE_TEXT =
  "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again.";
const CALDAV_VALIDATING_TEXT = 'Validating against CalDAV…';
const CALDAV_LINK_HREF = 'https://app.fastmail.com/settings/security/devicetokens';
const CALDAV_LINK_TEXT = 'Get an app password';

// ── Shared field style helpers ─────────────────────────────────────────────

const inputStyle = (hasError: boolean): React.CSSProperties => ({
  width: '100%',
  boxSizing: 'border-box',
  padding: 'var(--space-3, 12px) var(--space-4, 16px)',
  border: `1px solid ${hasError ? 'var(--color-destructive, #DC2626)' : 'var(--color-border)'}`,
  borderRadius: 'var(--space-1, 4px)',
  fontSize: 'var(--text-body-size, 15px)',
  color: 'var(--color-text-primary)',
  background: 'var(--color-surface)',
  fontFamily: 'var(--font-family-base)',
  outline: 'none',
  minHeight: '44px',
});

const labelStyle: React.CSSProperties = {
  display: 'block',
  fontSize: 'var(--text-label-size, 13px)',
  fontWeight: 600,
  color: 'var(--color-text-primary)',
  marginBottom: 'var(--space-1, 4px)',
};

const sectionLabelStyle: React.CSSProperties = {
  fontSize: 'var(--text-label-size, 13px)',
  fontWeight: 600,
  color: 'var(--color-text-muted)',
  textTransform: 'uppercase',
  letterSpacing: '0.06em',
  marginBottom: 'var(--space-4, 16px)',
};

const sectionDividerStyle: React.CSSProperties = {
  borderTop: '1px solid var(--color-border-subtle)',
  margin: 'var(--space-6, 24px) 0',
};

const fieldContainerStyle: React.CSSProperties = {
  marginBottom: 'var(--space-3, 12px)',
};

const inlineErrorStyle: React.CSSProperties = {
  fontSize: 'var(--text-label-size, 13px)',
  fontWeight: 400,
  color: 'var(--color-destructive, #DC2626)',
  marginTop: 'var(--space-2, 8px)',
};

const helperTextStyle: React.CSSProperties = {
  fontSize: 'var(--text-label-size, 13px)',
  fontWeight: 400,
  color: 'var(--color-text-secondary)',
  marginBottom: 'var(--space-3, 12px)',
  lineHeight: 1.4,
};

function primaryButtonStyle(disabled: boolean): React.CSSProperties {
  return {
    background: disabled ? 'var(--color-border, #E2E4E9)' : 'var(--color-member-0, #e8915a)',
    color: '#ffffff',
    border: 'none',
    cursor: disabled ? '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',
    display: 'flex',
    alignItems: 'center',
    gap: 'var(--space-2, 8px)',
  };
}

const cancelButtonStyle: React.CSSProperties = {
  background: 'none',
  border: 'none',
  cursor: 'pointer',
  fontSize: 'var(--text-label-size, 13px)',
  fontWeight: 600,
  color: 'var(--color-text-secondary)',
  minHeight: '44px',
  minWidth: '44px',
  padding: '0 var(--space-4, 16px)',
  fontFamily: 'var(--font-family-base)',
  borderRadius: 'var(--space-1, 4px)',
};

const actionsRowStyle: React.CSSProperties = {
  display: 'flex',
  justifyContent: 'flex-end',
  gap: 'var(--space-3, 12px)',
  marginTop: 'var(--space-4, 16px)',
};

// ── Component ──────────────────────────────────────────────────────────────

export function MemberEditorSheet({
  isOpen,
  onClose,
  mode,
  member,
  triggerRef,
  onToast,
}: MemberEditorSheetProps) {
  const queryClient = useQueryClient();
  const phone = useIsPhone();

  // Focus management refs
  const headingRef = useRef<HTMLHeadingElement>(null);
  const dialogRef = useRef<HTMLDivElement>(null);
  const handleDialogKeyDown = useFocusTrap(dialogRef);

  // ── Profile section state ────────────────────────────────────────────────
  const [displayName, setDisplayName] = useState(member?.displayName ?? '');
  const [isAdmin, setIsAdmin] = useState(member?.isAdmin ?? false);
  const [profileError, setProfileError] = useState<string | null>(null);

  // ── Set new password section state ──────────────────────────────────────
  const [newPassword, setNewPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  const [passwordError, setPasswordError] = useState<string | null>(null);

  // ── App password section state ───────────────────────────────────────────
  // NOTE: GET /api/admin/members does NOT return fastmailEmail; the stored
  // fastmail_email lives only on the member_credentials row, which is not
  // surfaced to the admin list. So the email field starts BLANK on edit —
  // the admin must re-enter it. This is intentional (see D-CONTEXT note).
  const [fastmailEmail, setFastmailEmail] = useState('');
  const [appPassword, setAppPassword] = useState('');
  const [appPasswordError, setAppPasswordError] = useState<string | null>(null);

  // ── Create mode state ───────────────────────────────────────────────────
  const [createDisplayName, setCreateDisplayName] = useState('');
  const [createUsername, setCreateUsername] = useState('');
  const [createPassword, setCreatePassword] = useState('');
  const [createConfirmPassword, setCreateConfirmPassword] = useState('');
  const [createError, setCreateError] = useState<string | null>(null);

  // ── handleClose ──────────────────────────────────────────────────────────

  // WR-04: only reset ephemeral fields (password inputs, error states, create-mode
  // fields). Member-derived fields (displayName, isAdmin) are owned by the useEffect
  // below and will re-sync from the live member prop when the sheet re-opens or when
  // membersQuery refetches — no stale closure problem.
  const handleClose = useCallback(() => {
    setProfileError(null);
    setNewPassword('');
    setConfirmPassword('');
    setPasswordError(null);
    setFastmailEmail('');
    setAppPassword('');
    setAppPasswordError(null);
    setCreateDisplayName('');
    setCreateUsername('');
    setCreatePassword('');
    setCreateConfirmPassword('');
    setCreateError(null);
    onClose();
    // Return focus to trigger element (a11y)
    if (triggerRef?.current) {
      triggerRef.current.focus();
    }
  }, [onClose, triggerRef]);

  // Sync profile state when member changes (different row opened)
  useEffect(() => {
    setDisplayName(member?.displayName ?? '');
    setIsAdmin(member?.isAdmin ?? false);
    setProfileError(null);
  }, [member?.id, member?.displayName, member?.isAdmin]);

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

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

  // ── Profile mutation (Section 1) ─────────────────────────────────────────

  const profileMutation = useMutation({
    mutationFn: async () => {
      if (!member) throw new Error('no-member');
      // WR-01: send only the fields that actually changed to avoid re-writing
      // displayName on an admin-toggle-only save (and to prevent 400s when a member
      // has a null displayName and the admin only wants to toggle the admin flag).
      const payload: { displayName?: string; isAdmin?: boolean } = {};
      const trimmed = displayName.trim();
      if (trimmed !== (member.displayName ?? '')) {
        if (trimmed.length === 0) throw new Error('name-required');
        payload.displayName = trimmed;
      }
      if (isAdmin !== member.isAdmin) payload.isAdmin = isAdmin;
      // No-op guard — nothing changed, skip the network call and signal no write
      // (IN-04: avoids firing the "Profile saved." toast + refetch on a no-op save).
      if (Object.keys(payload).length === 0) return false;
      await updateMemberProfile(member.id, payload);
      return true;
    },
    onSuccess: (changed) => {
      // IN-04: only surface success feedback when an actual write occurred.
      if (!changed) return;
      void queryClient.invalidateQueries({ queryKey: ['admin', 'members'] });
      onToast('Profile saved.');
      // Sheet stays open — per-section save (D-05)
    },
    onError: (err) => {
      const msg = err instanceof Error ? err.message : 'server';
      if (msg === 'last-admin') {
        // D-03 guard: revert toggle to the actual prior value on the member object.
        // WR-02: use member!.isAdmin explicitly rather than `?? true` — the `?? true`
        // was accidentally correct only because the guard fires when demoting an admin,
        // but it would incorrectly set isAdmin=true for any future error path where
        // member is non-null but isAdmin is false.
        setIsAdmin(member!.isAdmin);
        setProfileError('Cannot remove admin — at least one admin must remain.');
      } else if (msg === 'name-required') {
        setProfileError('A display name is required before saving.');
      } else {
        setProfileError('Something went wrong. Please try again.');
      }
    },
  });

  // ── Set new password mutation (Section 2) ────────────────────────────────

  const passwordMutation = useMutation({
    mutationFn: async () => {
      if (!member) throw new Error('no-member');
      if (newPassword !== confirmPassword) throw new Error('mismatch');
      if (newPassword.length < 8) throw new Error('short');
      await fetchAdminResetPassword(member.id, newPassword);
    },
    onSuccess: () => {
      void queryClient.invalidateQueries({ queryKey: ['admin', 'members'] });
      setNewPassword('');
      setConfirmPassword('');
      setPasswordError(null);
      onToast('Password updated.');
      // Sheet stays open — per-section save (D-05)
    },
    onError: (err) => {
      const msg = err instanceof Error ? err.message : 'server';
      if (msg === 'mismatch') {
        setPasswordError('Passwords do not match.');
      } else if (msg === 'short') {
        setPasswordError('Password must be at least 8 characters.');
      } else {
        setPasswordError('Something went wrong. Please try again.');
      }
    },
  });

  // ── App password mutation (Section 3) ────────────────────────────────────

  const appPasswordMutation = useMutation({
    mutationFn: async () => {
      if (!member) throw new Error('no-member');
      await saveCredential({
        userId: member.id,
        providerType: 'caldav',
        fastmailEmail: fastmailEmail.trim(),
        appPassword,
      });
    },
    onSuccess: () => {
      void queryClient.invalidateQueries({ queryKey: ['admin', 'members'] });
      void queryClient.invalidateQueries({ queryKey: ['me'] });
      setFastmailEmail('');
      setAppPassword('');
      setAppPasswordError(null);
      onToast('App password saved.');
      // Sheet stays open — per-section save (D-05)
    },
    onError: () => {
      setAppPasswordError(CALDAV_FAILURE_TEXT);
    },
  });

  // ── Create member mutation ────────────────────────────────────────────────

  const createMutation = useMutation({
    mutationFn: async () => {
      if (createPassword !== createConfirmPassword) throw new Error('mismatch');
      if (createPassword.length < 8) throw new Error('short');
      await fetchCreateMember({
        displayName: createDisplayName.trim(),
        username: createUsername.trim(),
        password: createPassword,
      });
    },
    onSuccess: () => {
      void queryClient.invalidateQueries({ queryKey: ['admin', 'members'] });
      onToast('Member added.');
      // Create mode closes on success (D-07)
      handleClose();
    },
    onError: (err) => {
      const msg = err instanceof Error ? err.message : 'server';
      if (msg === 'mismatch') {
        setCreateError('Passwords do not match.');
      } else if (msg === 'short') {
        setCreateError('Password must be at least 8 characters.');
      } else if (msg === 'conflict' || msg.includes('409')) {
        setCreateError('That username is already in use. Choose a different one.');
      } else {
        setCreateError('Something went wrong. Please try again.');
      }
    },
  });

  // ── Render guard ──────────────────────────────────────────────────────────

  if (!isOpen) return null;

  const heading = headingFor(mode);

  // ── Sheet style ───────────────────────────────────────────────────────────

  const sheetStyle: React.CSSProperties = phone
    ? {
        position: 'fixed',
        bottom: 0,
        left: 0,
        right: 0,
        // WR-03: cap height so content does not spill off screen on short phones
        // (iPhone SE 667px with three sections visible). overflowY:'auto' enables
        // scroll when content exceeds maxHeight.
        maxHeight: '90dvh',
        overflowY: 'auto',
        background: 'var(--color-surface)',
        borderRadius: '12px 12px 0 0',
        boxShadow: '0 -4px 24px rgba(0,0,0,0.15)',
        padding: 'var(--space-6, 24px)',
        // IN-03: pad the bottom to clear iOS home indicator / Android gesture bar
        paddingBottom: 'calc(var(--space-6, 24px) + env(safe-area-inset-bottom, 0px))',
        zIndex: 301,
        fontFamily: 'var(--font-family-base)',
      }
    : {
        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)',
        borderRadius: '12px',
        boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
        padding: 'var(--space-6, 24px)',
        zIndex: 301,
        fontFamily: 'var(--font-family-base)',
      };

  return (
    <>
      {/* Backdrop — uses --color-overlay per UI-SPEC §Surface B (matches ResetPasswordSheet) */}
      <div
        onClick={handleClose}
        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={heading}
        onKeyDown={handleDialogKeyDown}
        style={sheetStyle}
      >
        {/* Heading */}
        <h2
          ref={headingRef}
          tabIndex={-1}
          style={{
            margin: '0 0 var(--space-1, 4px) 0',
            fontSize: 'var(--text-heading-size, 18px)',
            fontWeight: 600,
            lineHeight: 'var(--text-heading-line-height, 1.25)',
            color: 'var(--color-text-primary)',
            outline: 'none',
          }}
        >
          {heading}
        </h2>

        {/* Member subtitle (edit mode only) */}
        {mode === 'edit' && member && (
          <div
            style={{
              fontSize: 'var(--text-body-size, 15px)',
              fontWeight: 400,
              color: 'var(--color-text-secondary)',
              marginBottom: 'var(--space-6, 24px)',
            }}
          >
            {member.displayName ?? 'Member'}
          </div>
        )}

        {/* ── EDIT MODE ──────────────────────────────────────────────────── */}
        {mode === 'edit' && member && (
          <>
            {/* Section 1 — Profile */}
            <div>
              <div style={sectionLabelStyle}>Profile</div>

              {/* Display name */}
              <div style={fieldContainerStyle}>
                <label htmlFor="editor-display-name" style={labelStyle}>
                  Display name
                </label>
                <input
                  id="editor-display-name"
                  type="text"
                  maxLength={256}
                  value={displayName}
                  onChange={(e) => setDisplayName(e.target.value)}
                  aria-describedby={profileError ? 'profile-error' : undefined}
                  style={inputStyle(!!profileError)}
                />
              </div>

              {/* Admin toggle */}
              <div
                style={{
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'space-between',
                  marginBottom: 'var(--space-4, 16px)',
                }}
              >
                <div>
                  <div
                    style={{
                      fontSize: 'var(--text-body-size, 15px)',
                      fontWeight: 400,
                      color: 'var(--color-text-primary)',
                    }}
                  >
                    Admin
                  </div>
                  <div
                    style={{
                      fontSize: 'var(--text-label-size, 13px)',
                      fontWeight: 400,
                      color: 'var(--color-text-secondary)',
                    }}
                  >
                    Can access admin settings
                  </div>
                </div>

                {/* Toggle pill — role="switch" per UI-SPEC Accessibility Contract */}
                <button
                  type="button"
                  role="switch"
                  aria-checked={isAdmin}
                  aria-label="Admin"
                  aria-describedby={profileError ? 'profile-error' : undefined}
                  onClick={() => {
                    setProfileError(null);
                    setIsAdmin((prev) => !prev);
                  }}
                  style={{
                    width: '44px',
                    height: '24px',
                    borderRadius: '12px',
                    border: 'none',
                    cursor: 'pointer',
                    background: isAdmin
                      ? 'var(--color-member-0, #e8915a)'
                      : 'var(--color-border, #E2E4E9)',
                    position: 'relative',
                    transition: 'background 0.15s ease',
                    flexShrink: 0,
                  }}
                >
                  {/* Thumb */}
                  <span
                    aria-hidden="true"
                    style={{
                      position: 'absolute',
                      top: '2px',
                      left: isAdmin ? '22px' : '2px',
                      width: '20px',
                      height: '20px',
                      borderRadius: '50%',
                      background: '#ffffff',
                      transition: 'left 0.15s ease',
                    }}
                  />
                </button>
              </div>

              {/* IN-01: helper text when display name is empty (e.g. null-displayName
                  OIDC member) so the admin knows why Save is disabled */}
              {displayName.trim().length === 0 && !profileError && (
                <p style={helperTextStyle}>Enter a display name to enable Save.</p>
              )}

              {/* Last-admin guard inline error */}
              {profileError && (
                <div id="profile-error" style={inlineErrorStyle}>
                  {profileError}
                </div>
              )}

              <div style={actionsRowStyle}>
                <button type="button" onClick={handleClose} style={cancelButtonStyle}>
                  Cancel
                </button>
                <button
                  type="button"
                  disabled={profileMutation.isPending || displayName.trim().length === 0}
                  onClick={() => {
                    setProfileError(null);
                    profileMutation.mutate();
                  }}
                  style={primaryButtonStyle(
                    profileMutation.isPending || displayName.trim().length === 0,
                  )}
                >
                  {profileMutation.isPending && (
                    <Loader2
                      size={14}
                      aria-hidden="true"
                      style={{ animation: 'spin 1s linear infinite', flexShrink: 0 }}
                    />
                  )}
                  Save
                </button>
              </div>
            </div>

            {/* Section 2 — Set new password (only for members with local credential) */}
            {member.hasLocalCredential && (
              <>
                <div style={sectionDividerStyle} />
                <div>
                  <div style={sectionLabelStyle}>Set new password</div>
                  <div style={helperTextStyle}>Leave blank to keep the current password.</div>

                  {/* New password */}
                  <div style={fieldContainerStyle}>
                    <label htmlFor="editor-new-password" style={labelStyle}>
                      New password
                    </label>
                    <input
                      id="editor-new-password"
                      type="password"
                      autoComplete="new-password"
                      value={newPassword}
                      onChange={(e) => setNewPassword(e.target.value)}
                      aria-describedby={passwordError ? 'password-error' : undefined}
                      style={inputStyle(!!passwordError)}
                    />
                  </div>

                  {/* Confirm new password */}
                  <div style={fieldContainerStyle}>
                    <label htmlFor="editor-confirm-password" style={labelStyle}>
                      Confirm new password
                    </label>
                    <input
                      id="editor-confirm-password"
                      type="password"
                      autoComplete="new-password"
                      value={confirmPassword}
                      onChange={(e) => setConfirmPassword(e.target.value)}
                      aria-describedby={passwordError ? 'password-error' : undefined}
                      style={inputStyle(!!passwordError)}
                    />
                  </div>

                  {/* Password error */}
                  {passwordError && (
                    <div id="password-error" style={inlineErrorStyle}>
                      {passwordError}
                    </div>
                  )}

                  <div style={actionsRowStyle}>
                    <button
                      type="button"
                      disabled={
                        passwordMutation.isPending ||
                        newPassword.length === 0 ||
                        confirmPassword.length === 0
                      }
                      onClick={() => {
                        setPasswordError(null);
                        passwordMutation.mutate();
                      }}
                      style={primaryButtonStyle(
                        passwordMutation.isPending ||
                          newPassword.length === 0 ||
                          confirmPassword.length === 0,
                      )}
                    >
                      {passwordMutation.isPending && (
                        <Loader2
                          size={14}
                          aria-hidden="true"
                          style={{ animation: 'spin 1s linear infinite', flexShrink: 0 }}
                        />
                      )}
                      Set password
                    </button>
                  </div>
                </div>
              </>
            )}

            {/* Section 3 — App password (always in edit mode) */}
            <>
              <div style={sectionDividerStyle} />
              <div>
                <div style={sectionLabelStyle}>App password</div>

                {/* Helper text with link */}
                <div style={{ ...helperTextStyle, marginBottom: 'var(--space-4, 16px)' }}>
                  Fastmail app password scoped to Calendars &amp; Contacts (CalDAV).{' '}
                  <a
                    href={CALDAV_LINK_HREF}
                    target="_blank"
                    rel="noopener noreferrer"
                    style={{
                      color: 'var(--color-member-0, #e8915a)',
                      textDecoration: 'underline',
                    }}
                  >
                    {CALDAV_LINK_TEXT}
                  </a>
                </div>

                {/* In-flight validating state */}
                {appPasswordMutation.isPending && (
                  <div
                    style={{
                      display: 'flex',
                      alignItems: 'center',
                      gap: 'var(--space-2, 8px)',
                      fontSize: 'var(--text-label-size, 13px)',
                      color: 'var(--color-text-secondary)',
                      marginBottom: 'var(--space-4, 16px)',
                    }}
                  >
                    <Loader2
                      size={16}
                      aria-hidden="true"
                      style={{ animation: 'spin 1s linear infinite', flexShrink: 0 }}
                    />
                    {CALDAV_VALIDATING_TEXT}
                  </div>
                )}

                {/* Fastmail email */}
                <div style={fieldContainerStyle}>
                  <label htmlFor="editor-fastmail-email" style={labelStyle}>
                    Fastmail email
                  </label>
                  <input
                    id="editor-fastmail-email"
                    type="email"
                    autoComplete="email"
                    value={fastmailEmail}
                    onChange={(e) => setFastmailEmail(e.target.value)}
                    placeholder="user@fastmail.com"
                    aria-describedby={appPasswordError ? 'app-password-error' : undefined}
                    style={inputStyle(!!appPasswordError)}
                  />
                </div>

                {/* App password field — NEVER prefilled (T-20-07) */}
                <div style={fieldContainerStyle}>
                  <label htmlFor="editor-app-password" style={labelStyle}>
                    App password
                  </label>
                  <input
                    id="editor-app-password"
                    type="password"
                    autoComplete="new-password"
                    value={appPassword}
                    onChange={(e) => setAppPassword(e.target.value)}
                    aria-describedby={appPasswordError ? 'app-password-error' : undefined}
                    style={inputStyle(!!appPasswordError)}
                  />
                </div>

                {/* CalDAV error */}
                {appPasswordError && (
                  <div id="app-password-error" style={inlineErrorStyle}>
                    {appPasswordError}
                  </div>
                )}

                <div style={actionsRowStyle}>
                  <button
                    type="button"
                    disabled={
                      appPasswordMutation.isPending ||
                      fastmailEmail.trim().length === 0 ||
                      appPassword.length === 0
                    }
                    onClick={() => {
                      setAppPasswordError(null);
                      appPasswordMutation.mutate();
                    }}
                    style={primaryButtonStyle(
                      appPasswordMutation.isPending ||
                        fastmailEmail.trim().length === 0 ||
                        appPassword.length === 0,
                    )}
                  >
                    {appPasswordMutation.isPending && (
                      <Loader2
                        size={14}
                        aria-hidden="true"
                        style={{ animation: 'spin 1s linear infinite', flexShrink: 0 }}
                      />
                    )}
                    Save app password
                  </button>
                </div>
              </div>
            </>
          </>
        )}

        {/* ── CREATE MODE ────────────────────────────────────────────────── */}
        {mode === 'create' && (
          <div>
            {/* Display name */}
            <div style={fieldContainerStyle}>
              <label htmlFor="create-display-name" style={labelStyle}>
                Display name
              </label>
              <input
                id="create-display-name"
                type="text"
                maxLength={256}
                value={createDisplayName}
                onChange={(e) => setCreateDisplayName(e.target.value)}
                aria-describedby={createError ? 'create-error' : undefined}
                style={inputStyle(!!createError)}
              />
            </div>

            {/* Username */}
            <div style={fieldContainerStyle}>
              <label htmlFor="create-username" style={labelStyle}>
                Username
              </label>
              <input
                id="create-username"
                type="text"
                maxLength={128}
                autoComplete="off"
                spellCheck={false}
                autoCapitalize="none"
                value={createUsername}
                onChange={(e) => setCreateUsername(e.target.value)}
                aria-describedby={createError ? 'create-error' : undefined}
                style={inputStyle(!!createError)}
              />
            </div>

            {/* Initial password */}
            <div style={fieldContainerStyle}>
              <label htmlFor="create-initial-password" style={labelStyle}>
                Initial password
              </label>
              <input
                id="create-initial-password"
                type="password"
                autoComplete="new-password"
                value={createPassword}
                onChange={(e) => setCreatePassword(e.target.value)}
                aria-describedby={createError ? 'create-error' : undefined}
                style={inputStyle(!!createError)}
              />
            </div>

            {/* Confirm password */}
            <div style={fieldContainerStyle}>
              <label htmlFor="create-confirm-password" style={labelStyle}>
                Confirm password
              </label>
              <input
                id="create-confirm-password"
                type="password"
                autoComplete="new-password"
                value={createConfirmPassword}
                onChange={(e) => setCreateConfirmPassword(e.target.value)}
                aria-describedby={createError ? 'create-error' : undefined}
                style={inputStyle(!!createError)}
              />
            </div>

            {/* Create error */}
            {createError && (
              <div id="create-error" style={inlineErrorStyle}>
                {createError}
              </div>
            )}

            <div style={actionsRowStyle}>
              <button type="button" onClick={handleClose} style={cancelButtonStyle}>
                Cancel
              </button>
              <button
                type="button"
                disabled={
                  createMutation.isPending ||
                  createDisplayName.trim().length === 0 ||
                  createUsername.trim().length === 0 ||
                  createPassword.length === 0 ||
                  createConfirmPassword.length === 0
                }
                onClick={() => {
                  setCreateError(null);
                  createMutation.mutate();
                }}
                style={primaryButtonStyle(
                  createMutation.isPending ||
                    createDisplayName.trim().length === 0 ||
                    createUsername.trim().length === 0 ||
                    createPassword.length === 0 ||
                    createConfirmPassword.length === 0,
                )}
              >
                {createMutation.isPending && (
                  <Loader2
                    size={14}
                    aria-hidden="true"
                    style={{ animation: 'spin 1s linear infinite', flexShrink: 0 }}
                  />
                )}
                Add member
              </button>
            </div>
          </div>
        )}
      </div>
    </>
  );
}
