import { Download, Edit3, Link2, Plus, RefreshCw, RotateCcw, Save, Search, Trash2, Upload, UserPlus, X } from "lucide-react";
import { useCallback, useEffect, useMemo, useState, type DragEvent as ReactDragEvent, type FormEvent } from "react";
import {
  ApiError,
  Button,
  ConfirmDialog,
  DataGridPaginationBar,
  Dialog,
  DismissibleAlert,
  ExplorerTree,
  fetchAuthGroups,
  formatDateTime,
  FormField,
  LoadingFrame,
  PasswordField,
  SegmentedControl,
  SelectionList,
  SelectionListItem,
  StatusBadge,
  ToggleSwitch,
  hasScope,
  type ApiSettings,
  type AuthInfo,
  type AuthUpdate,
  type FormatDateTimeOptions
} from "@govoplan/core-webui";
import {
  createAddressBook,
  createAddressList,
  createAddressListEntry,
  createCardDavSyncSource,
  createContact,
  deleteAddressBook,
  deleteAddressList,
  deleteAddressListEntry,
  deleteContact,
  deleteAddressSyncSource,
  discoverCardDavAddressBooks,
  exportAddressBookVcards,
  exportContactVcard,
  importAddressBookVcards,
  listAddressBooks,
  listAddressCredentials,
  listAddressListEntries,
  listAddressLists,
  listAddressSyncConflicts,
  listAddressSyncDiagnostics,
  listAddressSyncSources,
  listAddressSyncTombstones,
  listContacts,
  listContactsPage,
  previewAddressSyncSource,
  restoreAddressBook,
  restoreAddressList,
  restoreContact,
  resolveAddressSyncConflict,
  runAddressSyncSource,
  updateAddressBook,
  updateAddressList,
  updateAddressSyncSource,
  updateContact,
  type AddressCardDavAddressBook,
  type AddressBook,
  type AddressBookScope,
  type AddressCredentialEnvelope,
  type AddressList,
  type AddressListEntry,
  type AddressSyncConflict,
  type AddressSyncDiagnostic,
  type AddressSyncPlan,
  type AddressSyncSource,
  type AddressSyncTombstone,
  type Contact
} from "../../api/addresses";

type Props = {
  settings: ApiSettings;
  auth: AuthInfo;
  onAuthChange?: (auth: AuthUpdate | null, accessToken?: string) => void;
};

type BookDialogState = {
  mode: "create" | "edit";
  book?: AddressBook;
};

type BookFormState = {
  scope_type: AddressBookScope;
  group_id: string;
  name: string;
  description: string;
};

type ContactDialogState = {
  mode: "create" | "edit";
  contact?: Contact;
};

type ListDialogState = {
  mode: "create" | "edit";
  list?: AddressList;
};

type ContactEmailRow = {
  rowId: string;
  label: string;
  email: string;
  is_primary: boolean;
};

type ContactPhoneRow = {
  rowId: string;
  label: string;
  phone: string;
  is_primary: boolean;
};

type ContactPostalAddressRow = {
  rowId: string;
  label: string;
  street: string;
  postal_code: string;
  locality: string;
  region: string;
  country: string;
  is_primary: boolean;
};

type ContactFormState = {
  display_name: string;
  given_name: string;
  family_name: string;
  organization: string;
  role_title: string;
  emails: ContactEmailRow[];
  phones: ContactPhoneRow[];
  postal_addresses: ContactPostalAddressRow[];
  tags: string;
  note: string;
};

type ListFormState = {
  name: string;
  description: string;
};

type CardDavFormState = {
  collection_url: string;
  display_name: string;
  auth_type: "none" | "basic" | "bearer";
  credential_envelope_id: string;
  username: string;
  password: string;
  bearer_token: string;
  sync_direction: "read_only" | "import" | "export" | "two_way";
};

type SyncInspectorState = {
  source: AddressSyncSource;
} | null;

type AddressListTargetOption = {
  key: string;
  label: string;
  payload: {
    contact_email_id?: string | null;
    contact_postal_address_id?: string | null;
    label?: string | null;
  };
};

type ConfirmState =
  | { kind: "book"; book: AddressBook }
  | { kind: "list"; list: AddressList }
  | { kind: "contact"; contact: Contact }
  | { kind: "sync-source"; source: AddressSyncSource }
  | null;

type AddressTreeNode = {
  id: string;
  kind: "source" | "scope" | "book" | "list";
  label: string;
  sourceGroup?: "local" | "linked";
  scope?: AddressBookScope;
  book?: AddressBook;
  list?: AddressList;
  children: AddressTreeNode[];
};

type ConflictMergeChoice = "local" | "remote";

const EMPTY_BOOK_FORM: BookFormState = {
  scope_type: "user",
  group_id: "",
  name: "",
  description: ""
};

const EMPTY_LIST_FORM: ListFormState = {
  name: "",
  description: ""
};

const EMPTY_CARDDAV_FORM: CardDavFormState = {
  collection_url: "",
  display_name: "",
  auth_type: "basic",
  credential_envelope_id: "",
  username: "",
  password: "",
  bearer_token: "",
  sync_direction: "read_only"
};

const ADDRESS_CONTACT_DRAG_TYPE = "application/x-govoplan-address-contact-id";
const CONFLICT_PAYLOAD_FIELDS = ["display_name", "given_name", "family_name", "organization", "role_title", "emails", "phones", "postal_addresses", "tags", "note"] as const;

let contactRowSequence = 0;

function nextContactRowId(prefix: string): string {
  contactRowSequence += 1;
  return `${prefix}-${contactRowSequence}`;
}

function emptyEmailRow(isPrimary = true): ContactEmailRow {
  return { rowId: nextContactRowId("email"), label: "work", email: "", is_primary: isPrimary };
}

function emptyPhoneRow(isPrimary = true): ContactPhoneRow {
  return { rowId: nextContactRowId("phone"), label: "work", phone: "", is_primary: isPrimary };
}

function emptyPostalAddressRow(isPrimary = true): ContactPostalAddressRow {
  return {
    rowId: nextContactRowId("address"),
    label: "work",
    street: "",
    postal_code: "",
    locality: "",
    region: "",
    country: "",
    is_primary: isPrimary
  };
}

function emptyContactForm(): ContactFormState {
  return {
    display_name: "",
    given_name: "",
    family_name: "",
    organization: "",
    role_title: "",
    emails: [emptyEmailRow()],
    phones: [emptyPhoneRow()],
    postal_addresses: [emptyPostalAddressRow()],
    tags: "",
    note: ""
  };
}

const EMPTY_CONTACT_FORM: ContactFormState = emptyContactForm();

function errorMessage(error: unknown): string {
  if (error instanceof ApiError) {
    try {
      const parsed = JSON.parse(error.body) as { detail?: unknown };
      if (typeof parsed.detail === "string") return parsed.detail;
    } catch {
      return error.message;
    }
  }
  return error instanceof Error ? error.message : String(error);
}

function scopeLabel(scope: AddressBookScope): string {
  if (scope === "user") return "Personal";
  if (scope === "group") return "Group";
  if (scope === "tenant") return "Tenant";
  return "System";
}

function primaryEmail(contact: Contact): string {
  return contact.emails.find((email) => email.is_primary)?.email ?? contact.emails[0]?.email ?? "";
}

function primaryPhone(contact: Contact): string {
  return contact.phones.find((phone) => phone.is_primary)?.phone ?? contact.phones[0]?.phone ?? "";
}

function sourceGroupForBook(book: AddressBook): "local" | "linked" {
  return book.source_kind === "local" ? "local" : "linked";
}

function sourceGroupLabel(sourceGroup: "local" | "linked"): string {
  return sourceGroup === "local" ? "Local" : "Linked";
}

function syncSourceLabel(source: AddressSyncSource): string {
  return source.display_name || source.external_address_book_ref || source.connector_type;
}

const ADDRESS_DATE_TIME_OPTIONS: FormatDateTimeOptions = {
  fallback: "Never",
  year: undefined,
  month: undefined,
  day: undefined,
  hour: undefined,
  minute: undefined,
  timeZoneName: undefined,
  dateStyle: "short",
  timeStyle: "short"
};

function planSummary(plan: AddressSyncPlan | null): string {
  if (!plan) return "No preview loaded.";
  const { stats } = plan;
  return `${stats.created} create, ${stats.updated} update, ${stats.deleted} delete, ${stats.conflicts} conflict, ${stats.unchanged} unchanged, ${stats.errors} error`;
}

function syncActionLabel(action: string): string {
  if (action === "remote_create") return "Remote create";
  if (action === "remote_update") return "Remote update";
  if (action === "remote_delete") return "Remote delete";
  return action;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
}

function conflictPayload(value: Record<string, unknown> | null | undefined): Record<string, unknown> | null {
  if (!isRecord(value)) return null;
  return isRecord(value.payload) ? value.payload : null;
}

function conflictRemotePayload(conflict: AddressSyncConflict): Record<string, unknown> | null {
  const remoteValuePayload = conflictPayload(conflict.remote_value);
  if (remoteValuePayload) return remoteValuePayload;
  const metadataPayload = conflict.metadata?.remote_payload;
  return isRecord(metadataPayload) ? metadataPayload : null;
}

function canApplyRemoteConflict(conflict: AddressSyncConflict): boolean {
  return Boolean(conflictRemotePayload(conflict));
}

function canMergeConflict(conflict: AddressSyncConflict): boolean {
  return Boolean(conflictPayload(conflict.local_value) && conflictRemotePayload(conflict));
}

function defaultConflictMergeChoices(conflict: AddressSyncConflict): Record<string, ConflictMergeChoice> {
  return Object.fromEntries(CONFLICT_PAYLOAD_FIELDS.map((field) => [field, "local" as ConflictMergeChoice]));
}

function buildMergedConflictPayload(conflict: AddressSyncConflict, choices: Record<string, ConflictMergeChoice>): Record<string, unknown> | null {
  const localPayload = conflictPayload(conflict.local_value);
  const remotePayload = conflictRemotePayload(conflict);
  if (!localPayload || !remotePayload) return null;
  const merged: Record<string, unknown> = { ...localPayload };
  for (const field of CONFLICT_PAYLOAD_FIELDS) {
    merged[field] = choices[field] === "remote" ? remotePayload[field] : localPayload[field];
  }
  return merged;
}

function shortJson(value: unknown): string {
  if (value === null || value === undefined || value === "") return "—";
  if (Array.isArray(value)) {
    if (value.length === 0) return "—";
    return value.map((item) => shortJson(item)).join("; ");
  }
  if (isRecord(value)) {
    const entries = Object.entries(value)
      .filter(([, entryValue]) => entryValue !== null && entryValue !== undefined && entryValue !== "")
      .map(([key, entryValue]) => `${key}: ${shortJson(entryValue)}`);
    return entries.length ? entries.join(", ") : "—";
  }
  return String(value);
}

function conflictFieldRows(conflict: AddressSyncConflict): Array<{ field: string; local: string; remote: string; differs: boolean }> {
  const localPayload = conflictPayload(conflict.local_value);
  const remotePayload = conflictRemotePayload(conflict);
  if (!localPayload && !remotePayload) {
    return [
      {
        field: conflict.field_path,
        local: shortJson(conflict.local_value),
        remote: shortJson(conflict.remote_value),
        differs: shortJson(conflict.local_value) !== shortJson(conflict.remote_value)
      }
    ];
  }
  return CONFLICT_PAYLOAD_FIELDS.map((field) => {
    const local = shortJson(localPayload?.[field]);
    const remote = shortJson(remotePayload?.[field]);
    return { field, local, remote, differs: local !== remote };
  });
}

function buildAddressTree(books: AddressBook[], addressLists: AddressList[]): AddressTreeNode[] {
  const listsByBook = new Map<string, AddressList[]>();
  for (const list of addressLists) {
    const current = listsByBook.get(list.address_book_id) ?? [];
    current.push(list);
    listsByBook.set(list.address_book_id, current);
  }

  return (["local", "linked"] as const).map((sourceGroup) => {
    const sourceBooks = books.filter((book) => sourceGroupForBook(book) === sourceGroup);
    const scopeNodes = (["user", "group", "tenant", "system"] as const).map((scope) => {
      const scopeBooks = sourceBooks.filter((book) => book.scope_type === scope);
      return {
        id: `scope:${sourceGroup}:${scope}`,
        kind: "scope" as const,
        label: scopeLabel(scope),
        sourceGroup,
        scope,
        children: scopeBooks.map((book) => ({
          id: `book:${book.id}`,
          kind: "book" as const,
          label: book.name,
          sourceGroup,
          scope: book.scope_type,
          book,
          children: (listsByBook.get(book.id) ?? []).map((list) => ({
            id: `list:${list.id}`,
            kind: "list" as const,
            label: list.name,
            sourceGroup,
            scope: book.scope_type,
            list,
            children: []
          }))
        }))
      };
    }).filter((node) => node.children.length > 0);

    return {
      id: `source:${sourceGroup}`,
      kind: "source" as const,
      label: sourceGroupLabel(sourceGroup),
      sourceGroup,
      children: scopeNodes
    };
  }).filter((node) => node.children.length > 0);
}

function expandedAddressTreeIds(nodes: AddressTreeNode[]): Set<string> {
  const expanded = new Set<string>();
  function visit(node: AddressTreeNode) {
    if (node.children.length > 0) expanded.add(node.id);
    node.children.forEach(visit);
  }
  nodes.forEach(visit);
  return expanded;
}

function addressListEntryKey(entry: Pick<AddressListEntry, "contact_id" | "contact_email_id" | "contact_postal_address_id">): string {
  if (entry.contact_email_id) return `${entry.contact_id}:email:${entry.contact_email_id}`;
  if (entry.contact_postal_address_id) return `${entry.contact_id}:postal:${entry.contact_postal_address_id}`;
  return `${entry.contact_id}:contact`;
}

function listEntryContactIds(entries: AddressListEntry[]): Set<string> {
  return new Set(entries.map((entry) => entry.contact_id));
}

function contactListEntries(entries: AddressListEntry[], contactId: string): AddressListEntry[] {
  return entries.filter((entry) => entry.contact_id === contactId);
}

function listFormFromList(list?: AddressList): ListFormState {
  if (!list) return EMPTY_LIST_FORM;
  return {
    name: list.name,
    description: list.description ?? ""
  };
}

function formatPostalAddress(address: Contact["postal_addresses"][number]): string {
  return [address.street, [address.postal_code, address.locality].filter(Boolean).join(" "), address.region, address.country]
    .filter((part) => part && part.trim())
    .join(", ");
}

function addressListTargetOptions(contact: Contact): AddressListTargetOption[] {
  const options: AddressListTargetOption[] = [{
    key: "contact",
    label: "Whole contact",
    payload: { label: null }
  }];
  for (const email of contact.emails) {
    if (!email.id) continue;
    const label = `${email.label || "email"}${email.is_primary ? " primary" : ""}: ${email.email}`;
    options.push({
      key: `email:${email.id}`,
      label,
      payload: { contact_email_id: email.id, label: email.label || null }
    });
  }
  for (const address of contact.postal_addresses) {
    if (!address.id) continue;
    const formatted = formatPostalAddress(address) || "Postal address";
    const label = `${address.label || "address"}${address.is_primary ? " primary" : ""}: ${formatted}`;
    options.push({
      key: `postal:${address.id}`,
      label,
      payload: { contact_postal_address_id: address.id, label: address.label || null }
    });
  }
  return options;
}

function entryTargetLabel(entry: AddressListEntry): string {
  if (entry.target_kind === "email") return entry.email ? `Email: ${entry.email}` : "Email target";
  if (entry.target_kind === "postal_address") return entry.postal_address ? `Postal: ${entry.postal_address}` : "Postal address target";
  return "Whole contact";
}

function bookFormFromBook(book?: AddressBook): BookFormState {
  if (!book) return EMPTY_BOOK_FORM;
  return {
    scope_type: book.scope_type,
    group_id: book.scope_type === "group" ? book.scope_id ?? "" : "",
    name: book.name,
    description: book.description ?? ""
  };
}

function contactFormFromContact(contact?: Contact): ContactFormState {
  if (!contact) return emptyContactForm();
  return {
    display_name: contact.display_name,
    given_name: contact.given_name ?? "",
    family_name: contact.family_name ?? "",
    organization: contact.organization ?? "",
    role_title: contact.role_title ?? "",
    emails: normalizePrimaryRows(contact.emails.map((email) => ({
      rowId: nextContactRowId("email"),
      label: email.label ?? "work",
      email: email.email,
      is_primary: email.is_primary
    })), emptyEmailRow),
    phones: normalizePrimaryRows(contact.phones.map((phone) => ({
      rowId: nextContactRowId("phone"),
      label: phone.label ?? "work",
      phone: phone.phone,
      is_primary: phone.is_primary
    })), emptyPhoneRow),
    postal_addresses: normalizePrimaryRows(contact.postal_addresses.map((postal) => ({
      rowId: nextContactRowId("address"),
      label: postal.label ?? "work",
      street: postal.street ?? "",
      postal_code: postal.postal_code ?? "",
      locality: postal.locality ?? "",
      region: postal.region ?? "",
      country: postal.country ?? "",
      is_primary: postal.is_primary
    })), emptyPostalAddressRow),
    tags: contact.tags.join(", "),
    note: contact.note ?? ""
  };
}

function normalizePrimaryFlags<T extends { is_primary: boolean }>(rows: T[]): T[] {
  if (!rows.length) return rows;
  const primaryIndex = rows.findIndex((row) => row.is_primary);
  return rows.map((row, index) => ({ ...row, is_primary: primaryIndex >= 0 ? index === primaryIndex : index === 0 }));
}

function normalizePrimaryRows<T extends { is_primary: boolean }>(rows: T[], fallback: () => T): T[] {
  const nextRows = rows.length ? rows : [fallback()];
  return normalizePrimaryFlags(nextRows);
}

function tagsFromForm(value: string): string[] {
  const seen = new Set<string>();
  return value.split(",").map((tag) => tag.trim()).filter((tag) => {
    const key = tag.toLowerCase();
    if (!tag || seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

function postalRowHasContent(row: ContactPostalAddressRow): boolean {
  return [row.street, row.postal_code, row.locality, row.region, row.country].some((value) => value.trim());
}

function contactFormHasIdentity(form: ContactFormState): boolean {
  return Boolean(
    form.display_name.trim() ||
    form.given_name.trim() ||
    form.family_name.trim() ||
    form.emails.some((email) => email.email.trim())
  );
}

function safeFilename(value: string): string {
  return (value.trim().replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "address-book") + ".vcf";
}

function downloadText(filename: string, content: string, type = "text/vcard;charset=utf-8") {
  const blob = new Blob([content], { type });
  const url = window.URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = filename;
  document.body.append(anchor);
  anchor.click();
  anchor.remove();
  window.URL.revokeObjectURL(url);
}

function disabledReason(...conditions: Array<[boolean, string]>): string {
  return conditions.find(([applies]) => applies)?.[1] ?? "";
}

export default function AddressBookPage({ settings, auth, onAuthChange }: Props) {
  const [books, setBooks] = useState<AddressBook[]>([]);
  const [addressLists, setAddressLists] = useState<AddressList[]>([]);
  const [addressListEntries, setAddressListEntries] = useState<AddressListEntry[]>([]);
  const [syncSources, setSyncSources] = useState<AddressSyncSource[]>([]);
  const [contacts, setContacts] = useState<Contact[]>([]);
  const [contactTotal, setContactTotal] = useState(0);
  const [contactPage, setContactPage] = useState(1);
  const [contactPageSize, setContactPageSize] = useState(50);
  const [selectedBookId, setSelectedBookId] = useState("");
  const [selectedListId, setSelectedListId] = useState("");
  const [selectedContactId, setSelectedContactId] = useState("");
  const [expandedTreeIds, setExpandedTreeIds] = useState<Set<string>>(() => new Set());
  const [query, setQuery] = useState("");
  const [showArchived, setShowArchived] = useState(false);
  const [loading, setLoading] = useState(false);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState("");
  const [notice, setNotice] = useState("");
  const [bookDialog, setBookDialog] = useState<BookDialogState | null>(null);
  const [bookForm, setBookForm] = useState<BookFormState>(EMPTY_BOOK_FORM);
  const [listDialog, setListDialog] = useState<ListDialogState | null>(null);
  const [listForm, setListForm] = useState<ListFormState>(EMPTY_LIST_FORM);
  const [contactDialog, setContactDialog] = useState<ContactDialogState | null>(null);
  const [contactForm, setContactForm] = useState<ContactFormState>(EMPTY_CONTACT_FORM);
  const [memberDialogOpen, setMemberDialogOpen] = useState(false);
  const [memberCandidates, setMemberCandidates] = useState<Contact[]>([]);
  const [memberQuery, setMemberQuery] = useState("");
  const [memberTargetByContactId, setMemberTargetByContactId] = useState<Record<string, string>>({});
  const [dropTargetListId, setDropTargetListId] = useState("");
  const [importOpen, setImportOpen] = useState(false);
  const [vcardContent, setVcardContent] = useState("");
  const [cardDavOpen, setCardDavOpen] = useState(false);
  const [cardDavForm, setCardDavForm] = useState<CardDavFormState>(EMPTY_CARDDAV_FORM);
  const [cardDavDiscovery, setCardDavDiscovery] = useState<AddressCardDavAddressBook[]>([]);
  const [cardDavCredentials, setCardDavCredentials] = useState<AddressCredentialEnvelope[]>([]);
  const [cardDavCredentialsError, setCardDavCredentialsError] = useState("");
  const [syncInspector, setSyncInspector] = useState<SyncInspectorState>(null);
  const [syncPlan, setSyncPlan] = useState<AddressSyncPlan | null>(null);
  const [syncDiagnostics, setSyncDiagnostics] = useState<AddressSyncDiagnostic[]>([]);
  const [syncTombstones, setSyncTombstones] = useState<AddressSyncTombstone[]>([]);
  const [syncConflicts, setSyncConflicts] = useState<AddressSyncConflict[]>([]);
  const [conflictDialog, setConflictDialog] = useState<AddressSyncConflict | null>(null);
  const [conflictMergeChoices, setConflictMergeChoices] = useState<Record<string, ConflictMergeChoice>>({});
  const [confirmState, setConfirmState] = useState<ConfirmState>(null);

  const canWriteBooks = hasScope(auth, "addresses:address_book:write");
  const canDeleteBooks = hasScope(auth, "addresses:address_book:delete");
  const canAdminBooks = hasScope(auth, "addresses:address_book:admin");
  const canWriteLists = hasScope(auth, "addresses:address_list:write");
  const canDeleteLists = hasScope(auth, "addresses:address_list:delete");
  const canWriteContacts = hasScope(auth, "addresses:contact:write");
  const canDeleteContacts = hasScope(auth, "addresses:contact:delete");
  const canReadSync = hasScope(auth, "addresses:sync:read");
  const canWriteSync = hasScope(auth, "addresses:sync:write");

  useEffect(() => {
    if (!cardDavOpen || !canWriteSync) {
      setCardDavCredentials([]);
      setCardDavCredentialsError("");
      return;
    }
    let active = true;
    listAddressCredentials(settings)
      .then((credentials) => {
        if (active) {
          setCardDavCredentials(credentials);
          setCardDavCredentialsError("");
        }
      })
      .catch((err) => {
        if (active) {
          setCardDavCredentials([]);
          setCardDavCredentialsError(errorMessage(err));
        }
      });
    return () => {
      active = false;
    };
  }, [
    canWriteSync,
    cardDavOpen,
    settings.accessToken,
    settings.apiBaseUrl,
    settings.apiKey
  ]);

  useEffect(() => {
    if (auth.groups_loaded || !onAuthChange) return;
    let active = true;
    fetchAuthGroups(settings)
      .then((next) => {if (active) onAuthChange(next);})
      .catch(() => undefined);
    return () => {active = false;};
  }, [auth.groups_loaded, auth.user.id, auth.active_tenant?.id, auth.tenant.id, settings.accessToken, settings.apiBaseUrl, settings.apiKey]);

  const selectedBook = books.find((book) => book.id === selectedBookId) ?? books[0] ?? null;
  const selectedList = addressLists.find((list) => list.id === selectedListId) ?? null;
  const selectedBookSyncSources = useMemo(
    () => selectedBook ? syncSources.filter((source) => source.address_book_id === selectedBook.id) : [],
    [selectedBook, syncSources]
  );
  const selectedSyncSource = selectedBookSyncSources[0] ?? null;
  const addressTreeNodes = useMemo(() => buildAddressTree(books, addressLists), [addressLists, books]);
  const selectedListContactIds = useMemo(() => listEntryContactIds(addressListEntries), [addressListEntries]);
  const selectedListEntryKeys = useMemo(() => new Set(addressListEntries.map(addressListEntryKey)), [addressListEntries]);
  const visibleContacts = contacts;
  const memberCandidateContacts = useMemo(() => {
    const normalizedQuery = memberQuery.trim().toLowerCase();
    return memberCandidates
      .filter((contact) => !contact.deleted_at)
      .filter((contact) => addressListTargetOptions(contact).some((option) => !selectedListEntryKeys.has(`${contact.id}:${option.key}`)))
      .filter((contact) => {
        if (!normalizedQuery) return true;
        return [
          contact.display_name,
          contact.given_name,
          contact.family_name,
          contact.organization,
          contact.role_title,
          primaryEmail(contact),
          primaryPhone(contact),
          contact.tags.join(" ")
        ].some((value) => (value ?? "").toLowerCase().includes(normalizedQuery));
      });
  }, [memberCandidates, memberQuery, selectedListContactIds]);
  const selectedContact = visibleContacts.find((contact) => contact.id === selectedContactId) ?? null;
  const selectedContactListEntries = selectedContact && selectedList ? contactListEntries(addressListEntries, selectedContact.id) : [];
  const activeTreeId = selectedList ? `list:${selectedList.id}` : selectedBook ? `book:${selectedBook.id}` : "";
  const loadingReason = loading ? "Address books are loading." : "";
  const savingReason = saving ? "An address-book action is already in progress." : "";
  const createBookReason = disabledReason(
    [!canWriteBooks, "You need permission to manage address books."],
    [saving, savingReason]
  );
  const refreshReason = disabledReason(
    [loading, loadingReason],
    [saving, savingReason]
  );
  const toggleArchiveReason = disabledReason(
    [loading, loadingReason],
    [saving, savingReason]
  );
  const exportBookReason = disabledReason(
    [!selectedBook, "Select an address book before exporting vCards."],
    [saving, savingReason]
  );
  const importBookReason = disabledReason(
    [!selectedBook, "Select an address book before importing vCards."],
    [!canWriteContacts, "You need permission to manage contacts."],
    [Boolean(selectedBook?.read_only), "This address book is read-only."],
    [Boolean(selectedBook?.deleted_at), "Restore this address book before importing contacts."],
    [saving, savingReason]
  );
  const connectCardDavReason = disabledReason(
    [!selectedBook, "Select an address book before connecting CardDAV."],
    [!canWriteSync, "You need permission to manage address sync."],
    [Boolean(selectedBook?.deleted_at), "Restore this address book before connecting sync."],
    [saving, savingReason]
  );
  const inspectSyncReason = disabledReason(
    [!selectedSyncSource, "This address book has no sync source."],
    [!canReadSync, "You need permission to view address sync."],
    [saving, savingReason]
  );
  const previewSyncReason = disabledReason(
    [!selectedSyncSource, "This address book has no sync source."],
    [!canReadSync, "You need permission to view address sync."],
    [saving, savingReason]
  );
  const runSyncReason = disabledReason(
    [!selectedSyncSource, "This address book has no sync source."],
    [!canWriteSync, "You need permission to run address sync."],
    [saving, savingReason]
  );
  const createContactReason = disabledReason(
    [!selectedBook, "Select an address book before adding a contact."],
    [!canWriteContacts, "You need permission to manage contacts."],
    [Boolean(selectedBook?.read_only), "This address book is read-only."],
    [Boolean(selectedBook?.deleted_at), "Restore this address book before adding contacts."],
    [saving, savingReason]
  );
  const createListReason = disabledReason(
    [!selectedBook, "Select an address book before adding a list."],
    [!canWriteLists, "You need permission to manage address lists."],
    [Boolean(selectedBook?.read_only), "This address book is read-only."],
    [Boolean(selectedBook?.deleted_at), "Restore this address book before adding lists."],
    [saving, savingReason]
  );
  const listSaveReason = disabledReason(
    [saving, savingReason],
    [!listForm.name.trim(), "Enter an address-list name before saving."]
  );
  const addMembersReason = disabledReason(
    [!selectedList, "Select an address list before adding contacts."],
    [!canWriteLists, "You need permission to manage address lists."],
    [Boolean(selectedList?.read_only), "This address list is read-only."],
    [Boolean(selectedList?.deleted_at), "Restore this address list before adding contacts."],
    [Boolean(selectedBook?.read_only), "This address book is read-only."],
    [Boolean(selectedBook?.deleted_at), "Restore this address book before adding contacts to lists."],
    [saving, savingReason]
  );
  const bookSaveReason = disabledReason(
    [saving, savingReason],
    [!bookForm.name.trim(), "Enter an address book name before saving."]
  );
  const contactSaveReason = disabledReason(
    [saving, savingReason],
    [!contactFormHasIdentity(contactForm), "Enter a display name, name component, or email address before saving."]
  );
  const dialogCancelReason = disabledReason([saving, savingReason]);
  const vcardImportReason = disabledReason(
    [saving, savingReason],
    [!selectedBook, "Select an address book before importing vCards."],
    [!vcardContent.trim(), "Paste vCard content before importing."]
  );
  const addContactRowReason = disabledReason([saving, savingReason]);
  const removeEmailRowReason = disabledReason(
    [saving, savingReason],
    [contactForm.emails.length === 1, "Keep at least one email row. Empty rows are ignored on save."]
  );
  const removePhoneRowReason = disabledReason(
    [saving, savingReason],
    [contactForm.phones.length === 1, "Keep at least one phone row. Empty rows are ignored on save."]
  );
  const removePostalAddressRowReason = disabledReason(
    [saving, savingReason],
    [contactForm.postal_addresses.length === 1, "Keep at least one postal address row. Empty rows are ignored on save."]
  );

  function restoreBookReason(_book: AddressBook): string {
    return disabledReason(
      [!canWriteBooks, "You need permission to manage address books."],
      [saving, savingReason]
    );
  }

  function editBookReason(book: AddressBook): string {
    return disabledReason(
      [!canWriteBooks, "You need permission to manage address books."],
      [book.read_only, "This address book is read-only."],
      [saving, savingReason]
    );
  }

  function deleteBookReason(book: AddressBook): string {
    return disabledReason(
      [!canDeleteBooks, "You need permission to delete address books."],
      [book.read_only, "This address book is read-only."],
      [saving, savingReason]
    );
  }

  function editListReason(list: AddressList): string {
    return disabledReason(
      [!canWriteLists, "You need permission to manage address lists."],
      [list.read_only, "This address list is read-only."],
      [Boolean(list.deleted_at), "Restore this address list before editing it."],
      [saving, savingReason]
    );
  }

  function deleteListReason(list: AddressList): string {
    return disabledReason(
      [!canDeleteLists, "You need permission to delete address lists."],
      [list.read_only, "This address list is read-only."],
      [Boolean(list.deleted_at), "This address list is already archived."],
      [saving, savingReason]
    );
  }

  function restoreListReason(list: AddressList): string {
    return disabledReason(
      [!canWriteLists, "You need permission to manage address lists."],
      [list.read_only, "This address list is read-only."],
      [Boolean(selectedBook?.read_only), "This address book is read-only."],
      [saving, savingReason]
    );
  }

  function addContactToListReason(list: AddressList | null, contact: Contact | null, targetKey = ""): string {
    return disabledReason(
      [!list, "Select an address list before adding contacts."],
      [!contact, "Select a contact before adding it to a list."],
      [!canWriteLists, "You need permission to manage address lists."],
      [Boolean(list?.read_only), "This address list is read-only."],
      [Boolean(list?.deleted_at), "Restore this address list before adding contacts."],
      [Boolean(selectedBook?.read_only), "This address book is read-only."],
      [Boolean(selectedBook?.deleted_at), "Restore this address book before adding contacts to lists."],
      [Boolean(contact?.deleted_at), "Restore this contact before adding it to a list."],
      [Boolean(list && contact && list.address_book_id !== contact.address_book_id), "Address-list entries must use contacts from the same address book."],
      [Boolean(contact && targetKey && selectedListEntryKeys.has(`${contact.id}:${targetKey}`) && list?.id === selectedList?.id), "This target is already in the selected address list."],
      [saving, savingReason]
    );
  }

  function removeContactFromListReason(): string {
    return disabledReason(
      [!selectedList, "Select an address list before removing contacts."],
      [selectedContactListEntries.length === 0, "This contact is not in the selected address list."],
      [!canWriteLists, "You need permission to manage address lists."],
      [Boolean(selectedList?.read_only), "This address list is read-only."],
      [Boolean(selectedList?.deleted_at), "Restore this address list before removing contacts."],
      [saving, savingReason]
    );
  }

  function restoreContactReason(): string {
    return disabledReason(
      [!canWriteContacts, "You need permission to manage contacts."],
      [Boolean(selectedBook?.read_only), "This address book is read-only."],
      [Boolean(selectedBook?.deleted_at), "Restore this address book before restoring contacts."],
      [saving, savingReason]
    );
  }

  function exportContactReason(): string {
    return disabledReason([saving, savingReason]);
  }

  function editContactReason(): string {
    return disabledReason(
      [!canWriteContacts, "You need permission to manage contacts."],
      [Boolean(selectedBook?.read_only), "This address book is read-only."],
      [Boolean(selectedBook?.deleted_at), "Restore this address book before editing contacts."],
      [saving, savingReason]
    );
  }

  function deleteContactReason(): string {
    return disabledReason(
      [!canDeleteContacts, "You need permission to delete contacts."],
      [Boolean(selectedBook?.read_only), "This address book is read-only."],
      [Boolean(selectedBook?.deleted_at), "Restore this address book before deleting contacts."],
      [saving, savingReason]
    );
  }

  function availableTargetOptionsForContact(contact: Contact): AddressListTargetOption[] {
    return addressListTargetOptions(contact).filter((option) => !selectedListEntryKeys.has(`${contact.id}:${option.key}`));
  }

  function selectedTargetOptionForContact(contact: Contact): AddressListTargetOption | null {
    const options = availableTargetOptionsForContact(contact);
    if (options.length === 0) return null;
    const selectedKey = memberTargetByContactId[contact.id];
    return options.find((option) => option.key === selectedKey) ?? options[0];
  }

  function setMemberTarget(contactId: string, targetKey: string) {
    setMemberTargetByContactId((current) => ({ ...current, [contactId]: targetKey }));
  }

  const refreshBooks = useCallback(async () => {
    const [nextBooks, nextAddressLists, nextSyncSources] = await Promise.all([
      listAddressBooks(settings, { includeDeleted: showArchived }),
      listAddressLists(settings, { includeDeleted: showArchived }),
      canReadSync ? listAddressSyncSources(settings, { includeDisabled: true }) : Promise.resolve([])
    ]);
    setBooks(nextBooks);
    setAddressLists(nextAddressLists);
    setSyncSources(nextSyncSources);
    setSelectedBookId((current) => nextBooks.some((book) => book.id === current) ? current : nextBooks[0]?.id ?? "");
    setSelectedListId((current) => nextAddressLists.some((list) => list.id === current) ? current : "");
    return nextBooks;
  }, [canReadSync, settings, showArchived]);

  const refreshContacts = useCallback(async (bookId: string, search: string) => {
    if (!bookId) {
      setContacts([]);
      setContactTotal(0);
      return;
    }
    const response = await listContactsPage(settings, {
      addressBookId: bookId,
      addressListId: selectedListId || undefined,
      query: search,
      limit: contactPageSize,
      offset: (contactPage - 1) * contactPageSize,
      includeDeleted: showArchived
    });
    const pageCount = Math.max(1, Math.ceil(response.total / contactPageSize));
    if (contactPage > pageCount) {
      setContactPage(pageCount);
      return;
    }
    setContacts(response.contacts);
    setContactTotal(response.total);
  }, [contactPage, contactPageSize, selectedListId, settings, showArchived]);

  const refreshListEntries = useCallback(async (listId: string) => {
    if (!listId) {
      setAddressListEntries([]);
      return [];
    }
    const nextEntries = await listAddressListEntries(settings, listId);
    setAddressListEntries(nextEntries);
    return nextEntries;
  }, [settings]);

  const refreshAll = useCallback(async () => {
    setLoading(true);
    setError("");
    try {
      const nextBooks = await refreshBooks();
      const nextSelected = nextBooks.some((book) => book.id === selectedBookId) ? selectedBookId : nextBooks[0]?.id ?? "";
      await refreshContacts(nextSelected, query);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setLoading(false);
    }
  }, [query, refreshBooks, refreshContacts, selectedBookId]);

  useEffect(() => {
    let active = true;
    async function loadInitial() {
      setLoading(true);
      setError("");
      try {
        await refreshBooks();
      } catch (err) {
        if (active) setError(errorMessage(err));
      } finally {
        if (active) setLoading(false);
      }
    }
    void loadInitial();
    return () => {active = false;};
  }, [refreshBooks]);

  useEffect(() => {
    void refreshContacts(selectedBookId, query).catch((err) => setError(errorMessage(err)));
  }, [query, refreshContacts, selectedBookId]);

  useEffect(() => {
    const defaultExpanded = expandedAddressTreeIds(addressTreeNodes);
    if (defaultExpanded.size === 0) return;
    setExpandedTreeIds((current) => {
      const next = new Set(current);
      for (const id of defaultExpanded) next.add(id);
      return next;
    });
  }, [addressTreeNodes]);

  useEffect(() => {
    let active = true;
    async function loadListEntries() {
      if (!selectedListId || selectedList?.deleted_at) {
        setAddressListEntries([]);
        return;
      }
      try {
        const nextEntries = await listAddressListEntries(settings, selectedListId);
        if (active) setAddressListEntries(nextEntries);
      } catch (err) {
        if (active) {
          setAddressListEntries([]);
          setError(errorMessage(err));
        }
      }
    }
    void loadListEntries();
    return () => {active = false;};
  }, [selectedList?.deleted_at, selectedListId, settings]);

  useEffect(() => {
    if (!selectedContactId) return;
    if (!visibleContacts.some((contact) => contact.id === selectedContactId)) setSelectedContactId("");
  }, [selectedContactId, visibleContacts]);

  function openCreateBookDialog() {
    setBookForm({
      ...EMPTY_BOOK_FORM,
      group_id: auth.groups[0]?.id ?? ""
    });
    setBookDialog({ mode: "create" });
  }

  function openEditBookDialog(book: AddressBook) {
    setBookForm(bookFormFromBook(book));
    setBookDialog({ mode: "edit", book });
  }

  function openCreateListDialog() {
    setListForm(EMPTY_LIST_FORM);
    setListDialog({ mode: "create" });
  }

  function openEditListDialog(list: AddressList) {
    setListForm(listFormFromList(list));
    setListDialog({ mode: "edit", list });
  }

  function openCreateContactDialog() {
    setContactForm(emptyContactForm());
    setContactDialog({ mode: "create" });
  }

  function openEditContactDialog(contact: Contact) {
    setContactForm(contactFormFromContact(contact));
    setContactDialog({ mode: "edit", contact });
  }

  function updateEmailRow(rowId: string, patch: Partial<Omit<ContactEmailRow, "rowId">>) {
    setContactForm((current) => ({
      ...current,
      emails: current.emails.map((row) => row.rowId === rowId ? { ...row, ...patch } : row)
    }));
  }

  function updatePhoneRow(rowId: string, patch: Partial<Omit<ContactPhoneRow, "rowId">>) {
    setContactForm((current) => ({
      ...current,
      phones: current.phones.map((row) => row.rowId === rowId ? { ...row, ...patch } : row)
    }));
  }

  function updatePostalAddressRow(rowId: string, patch: Partial<Omit<ContactPostalAddressRow, "rowId">>) {
    setContactForm((current) => ({
      ...current,
      postal_addresses: current.postal_addresses.map((row) => row.rowId === rowId ? { ...row, ...patch } : row)
    }));
  }

  function setPrimaryEmailRow(rowId: string) {
    setContactForm((current) => ({ ...current, emails: current.emails.map((row) => ({ ...row, is_primary: row.rowId === rowId })) }));
  }

  function setPrimaryPhoneRow(rowId: string) {
    setContactForm((current) => ({ ...current, phones: current.phones.map((row) => ({ ...row, is_primary: row.rowId === rowId })) }));
  }

  function setPrimaryPostalAddressRow(rowId: string) {
    setContactForm((current) => ({ ...current, postal_addresses: current.postal_addresses.map((row) => ({ ...row, is_primary: row.rowId === rowId })) }));
  }

  function addEmailRow() {
    setContactForm((current) => ({ ...current, emails: [...current.emails, emptyEmailRow(false)] }));
  }

  function addPhoneRow() {
    setContactForm((current) => ({ ...current, phones: [...current.phones, emptyPhoneRow(false)] }));
  }

  function addPostalAddressRow() {
    setContactForm((current) => ({ ...current, postal_addresses: [...current.postal_addresses, emptyPostalAddressRow(false)] }));
  }

  function removeEmailRow(rowId: string) {
    setContactForm((current) => ({ ...current, emails: normalizePrimaryRows(current.emails.filter((row) => row.rowId !== rowId), emptyEmailRow) }));
  }

  function removePhoneRow(rowId: string) {
    setContactForm((current) => ({ ...current, phones: normalizePrimaryRows(current.phones.filter((row) => row.rowId !== rowId), emptyPhoneRow) }));
  }

  function removePostalAddressRow(rowId: string) {
    setContactForm((current) => ({ ...current, postal_addresses: normalizePrimaryRows(current.postal_addresses.filter((row) => row.rowId !== rowId), emptyPostalAddressRow) }));
  }

  function openTreeNode(node: AddressTreeNode) {
    if (node.kind === "book" && node.book) {
      setContactPage(1);
      setSelectedBookId(node.book.id);
      setSelectedListId("");
      setSelectedContactId("");
      return;
    }
    if (node.kind === "list" && node.list) {
      setContactPage(1);
      setSelectedBookId(node.list.address_book_id);
      setSelectedListId(node.list.id);
      setSelectedContactId("");
      return;
    }
    toggleTreeNode(node);
  }

  function toggleTreeNode(node: AddressTreeNode) {
    if (node.children.length === 0) return;
    setExpandedTreeIds((current) => {
      const next = new Set(current);
      if (next.has(node.id)) next.delete(node.id);
      else next.add(node.id);
      return next;
    });
  }

  async function submitBook(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setSaving(true);
    setError("");
    setNotice("");
    try {
      if (bookDialog?.mode === "edit" && bookDialog.book) {
        await updateAddressBook(settings, bookDialog.book.id, { name: bookForm.name, description: bookForm.description || null });
      } else {
        await createAddressBook(settings, {
          scope_type: bookForm.scope_type,
          group_id: bookForm.scope_type === "group" ? bookForm.group_id : null,
          name: bookForm.name,
          description: bookForm.description || null
        });
      }
      setBookDialog(null);
      await refreshAll();
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  async function submitList(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!selectedBook) return;
    setSaving(true);
    setError("");
    setNotice("");
    try {
      let savedList: AddressList;
      if (listDialog?.mode === "edit" && listDialog.list) {
        savedList = await updateAddressList(settings, listDialog.list.id, {
          name: listForm.name,
          description: listForm.description || null
        });
      } else {
        savedList = await createAddressList(settings, selectedBook.id, {
          name: listForm.name,
          description: listForm.description || null
        });
      }
      setListDialog(null);
      setSelectedBookId(savedList.address_book_id);
      setSelectedListId(savedList.id);
      setExpandedTreeIds((current) => {
        const next = new Set(current);
        next.add(`book:${savedList.address_book_id}`);
        return next;
      });
      await refreshBooks();
      await refreshListEntries(savedList.id);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  async function submitContact(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!selectedBook) return;
    setSaving(true);
    setError("");
    setNotice("");
    const emails = normalizePrimaryFlags(contactForm.emails
      .filter((email) => email.email.trim())
      .map((email) => ({ label: email.label || null, email: email.email.trim(), is_primary: email.is_primary })));
    const phones = normalizePrimaryFlags(contactForm.phones
      .filter((phone) => phone.phone.trim())
      .map((phone) => ({ label: phone.label || null, phone: phone.phone.trim(), is_primary: phone.is_primary })));
    const postal_addresses = normalizePrimaryFlags(contactForm.postal_addresses
      .filter(postalRowHasContent)
      .map((address) => ({
        label: address.label || null,
        street: address.street || null,
        postal_code: address.postal_code || null,
        locality: address.locality || null,
        region: address.region || null,
        country: address.country || null,
        is_primary: address.is_primary
      })));
    const payload = {
      display_name: contactForm.display_name || null,
      given_name: contactForm.given_name || null,
      family_name: contactForm.family_name || null,
      organization: contactForm.organization || null,
      role_title: contactForm.role_title || null,
      note: contactForm.note || null,
      tags: tagsFromForm(contactForm.tags),
      emails,
      phones,
      postal_addresses,
      provenance: {}
    };
    try {
      let savedContact: Contact;
      if (contactDialog?.mode === "edit" && contactDialog.contact) {
        savedContact = await updateContact(settings, contactDialog.contact.id, payload);
      } else {
        savedContact = await createContact(settings, selectedBook.id, payload);
      }
      setContactDialog(null);
      setSelectedContactId(savedContact.id);
      await refreshBooks();
      await refreshContacts(selectedBook.id, query);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  async function confirmDelete() {
    if (!confirmState) return;
    setSaving(true);
    setError("");
    setNotice("");
    try {
      if (confirmState.kind === "book") {
        await deleteAddressBook(settings, confirmState.book.id);
        setConfirmState(null);
        await refreshAll();
      } else if (confirmState.kind === "list") {
        await deleteAddressList(settings, confirmState.list.id);
        setConfirmState(null);
        if (!showArchived && selectedListId === confirmState.list.id) {
          setSelectedListId("");
          setAddressListEntries([]);
        }
        await refreshBooks();
      } else if (confirmState.kind === "contact") {
        await deleteContact(settings, confirmState.contact.id);
        if (selectedContactId === confirmState.contact.id) setSelectedContactId("");
        setConfirmState(null);
        await refreshBooks();
        await refreshContacts(selectedBookId, query);
      } else {
        await deleteAddressSyncSource(settings, confirmState.source.id);
        if (syncInspector?.source.id === confirmState.source.id) {
          setSyncInspector(null);
          setSyncPlan(null);
          setSyncDiagnostics([]);
          setSyncTombstones([]);
          setSyncConflicts([]);
          setConflictDialog(null);
        }
        setConfirmState(null);
        setNotice("Sync source disconnected. Local contacts were kept.");
        await refreshBooks();
        await refreshContacts(selectedBookId, query);
      }
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  async function restoreDeletedList(list: AddressList) {
    setSaving(true);
    setError("");
    setNotice("");
    try {
      const restored = await restoreAddressList(settings, list.id);
      setNotice(`Restored "${list.name}".`);
      setSelectedBookId(restored.address_book_id);
      setSelectedListId(restored.id);
      await refreshBooks();
      await refreshListEntries(restored.id);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  async function restoreBook(book: AddressBook) {
    setSaving(true);
    setError("");
    setNotice("");
    try {
      await restoreAddressBook(settings, book.id);
      setNotice(`Restored "${book.name}".`);
      await refreshAll();
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  async function restoreDeletedContact(contact: Contact) {
    setSaving(true);
    setError("");
    setNotice("");
    try {
      await restoreContact(settings, contact.id);
      setNotice(`Restored "${contact.display_name}".`);
      await refreshBooks();
      await refreshContacts(selectedBookId, query);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  async function openAddMembersDialog() {
    if (!selectedBook || !selectedList) return;
    setMemberQuery("");
    setMemberTargetByContactId({});
    setMemberDialogOpen(true);
    setError("");
    try {
      setMemberCandidates(await listContacts(settings, { addressBookId: selectedBook.id, limit: 500, includeDeleted: showArchived }));
    } catch (err) {
      setError(errorMessage(err));
    }
  }

  async function addContactToAddressList(
    list: AddressList,
    contact: Contact,
    targetOption: AddressListTargetOption | null = null,
    successMessage = `Added "${contact.display_name}" to "${list.name}".`
  ) {
    const option = targetOption ?? selectedTargetOptionForContact(contact);
    const reason = addContactToListReason(list, contact, option?.key ?? "");
    if (reason) {
      setError(reason);
      return;
    }
    if (!option) {
      setError("This contact has no available target left for the selected list.");
      return;
    }
    setSaving(true);
    setError("");
    setNotice("");
    try {
      const currentEntries = list.id === selectedListId ? addressListEntries : await listAddressListEntries(settings, list.id);
      if (currentEntries.some((entry) => addressListEntryKey(entry) === `${contact.id}:${option.key}`)) {
        setNotice(`"${option.label}" is already in "${list.name}".`);
        return;
      }
      await createAddressListEntry(settings, list.id, { contact_id: contact.id, ...option.payload });
      setNotice(successMessage);
      await refreshBooks();
      if (list.id === selectedListId) await refreshListEntries(list.id);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  async function removeAddressListEntry(entry: AddressListEntry) {
    if (!selectedList || !selectedContact) return;
    setSaving(true);
    setError("");
    setNotice("");
    try {
      await deleteAddressListEntry(settings, entry.id);
      setNotice(`Removed "${entryTargetLabel(entry)}" for "${selectedContact.display_name}" from "${selectedList.name}".`);
      await refreshBooks();
      await refreshListEntries(selectedList.id);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  function handleContactDragStart(event: ReactDragEvent<HTMLButtonElement>, contact: Contact) {
    event.dataTransfer.effectAllowed = "copy";
    event.dataTransfer.setData(ADDRESS_CONTACT_DRAG_TYPE, contact.id);
    event.dataTransfer.setData("text/plain", contact.display_name);
  }

  function dragContactFromEvent(event: ReactDragEvent<HTMLElement>): Contact | null {
    const contactId = event.dataTransfer.getData(ADDRESS_CONTACT_DRAG_TYPE);
    if (!contactId) return null;
    return contacts.find((contact) => contact.id === contactId) ?? memberCandidates.find((contact) => contact.id === contactId) ?? null;
  }

  function handleTreeDragOver(event: ReactDragEvent<HTMLElement>, node: AddressTreeNode) {
    if (node.kind !== "list" || !node.list) return;
    const hasContact = Array.from(event.dataTransfer.types).includes(ADDRESS_CONTACT_DRAG_TYPE);
    if (!hasContact) return;
    event.preventDefault();
    event.dataTransfer.dropEffect = "copy";
    setDropTargetListId(node.list.id);
  }

  function handleTreeDragLeave(_event: ReactDragEvent<HTMLElement>, node: AddressTreeNode) {
    if (node.kind === "list" && node.list?.id === dropTargetListId) setDropTargetListId("");
  }

  async function handleTreeDrop(event: ReactDragEvent<HTMLElement>, node: AddressTreeNode) {
    if (node.kind !== "list" || !node.list) return;
    event.preventDefault();
    setDropTargetListId("");
    const contact = dragContactFromEvent(event);
    if (!contact) return;
    await addContactToAddressList(node.list, contact, null, `Added "${contact.display_name}" to "${node.list.name}" by drag and drop.`);
  }

  async function exportSelectedBook() {
    if (!selectedBook) return;
    setSaving(true);
    setError("");
    setNotice("");
    try {
      const content = await exportAddressBookVcards(settings, selectedBook.id);
      downloadText(safeFilename(selectedBook.name), content);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  async function exportOneContact(contact: Contact) {
    setSaving(true);
    setError("");
    setNotice("");
    try {
      const content = await exportContactVcard(settings, contact.id);
      downloadText(safeFilename(contact.display_name), content);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  async function submitVcardImport(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!selectedBook) return;
    setSaving(true);
    setError("");
    setNotice("");
    try {
      const result = await importAddressBookVcards(settings, selectedBook.id, vcardContent);
      setImportOpen(false);
      setVcardContent("");
      const issueCount = result.issues.length;
      setNotice(`Imported ${result.imported} contact${result.imported === 1 ? "" : "s"}${result.skipped ? `, skipped ${result.skipped}` : ""}${issueCount ? ` (${issueCount} import issue${issueCount === 1 ? "" : "s"})` : ""}.`);
      await refreshBooks();
      await refreshContacts(selectedBook.id, query);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  function openCardDavDialog() {
    setCardDavForm(EMPTY_CARDDAV_FORM);
    setCardDavDiscovery([]);
    setCardDavCredentialsError("");
    setCardDavOpen(true);
  }

  function cardDavPayload() {
    const credentialRef = cardDavForm.auth_type !== "none" && cardDavForm.credential_envelope_id
      ? `credential-envelope:${cardDavForm.credential_envelope_id}`
      : null;
    return {
      url: cardDavForm.collection_url,
      auth_type: cardDavForm.auth_type,
      username: cardDavForm.username || null,
      password: credentialRef ? null : cardDavForm.password || null,
      bearer_token: credentialRef ? null : cardDavForm.bearer_token || null,
      credential_ref: credentialRef
    };
  }

  async function discoverCardDavSources() {
    setSaving(true);
    setError("");
    setNotice("");
    try {
      const discovered = await discoverCardDavAddressBooks(settings, cardDavPayload());
      setCardDavDiscovery(discovered);
      setNotice(`Found ${discovered.length} CardDAV address book${discovered.length === 1 ? "" : "s"}.`);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  function useDiscoveredCardDavBook(item: AddressCardDavAddressBook) {
    setCardDavForm((current) => ({
      ...current,
      collection_url: item.collection_url,
      display_name: current.display_name || item.display_name || selectedBook?.name || "CardDAV address book"
    }));
  }

  async function submitCardDavSource(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!selectedBook) return;
    setSaving(true);
    setError("");
    setNotice("");
    try {
      await createCardDavSyncSource(settings, selectedBook.id, {
        collection_url: cardDavForm.collection_url,
        display_name: cardDavForm.display_name || selectedBook.name,
        auth_type: cardDavForm.auth_type,
        username: cardDavForm.username || null,
        password: cardDavForm.credential_envelope_id ? null : cardDavForm.password || null,
        bearer_token: cardDavForm.credential_envelope_id ? null : cardDavForm.bearer_token || null,
        credential_ref: cardDavForm.auth_type !== "none" && cardDavForm.credential_envelope_id
          ? `credential-envelope:${cardDavForm.credential_envelope_id}`
          : null,
        sync_direction: cardDavForm.sync_direction,
        read_only: cardDavForm.sync_direction === "read_only" || cardDavForm.sync_direction === "import"
      });
      setCardDavOpen(false);
      setCardDavDiscovery([]);
      setNotice("CardDAV source connected.");
      await refreshBooks();
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  async function loadSyncDetails(source: AddressSyncSource) {
    const [diagnostics, tombstones, conflicts] = await Promise.all([
      listAddressSyncDiagnostics(settings, source.id),
      listAddressSyncTombstones(settings, source.id),
      listAddressSyncConflicts(settings, source.id, "open")
    ]);
    setSyncDiagnostics(diagnostics);
    setSyncTombstones(tombstones);
    setSyncConflicts(conflicts);
  }

  async function openSyncInspector(source: AddressSyncSource) {
    setSyncInspector({ source });
    setSyncPlan(null);
    setError("");
    try {
      await loadSyncDetails(source);
    } catch (err) {
      setError(errorMessage(err));
    }
  }

  async function previewSelectedSync(source = selectedSyncSource) {
    if (!source) return;
    setSaving(true);
    setError("");
    setNotice("");
    try {
      const plan = await previewAddressSyncSource(settings, source.id);
      setSyncPlan(plan);
      setSyncInspector({ source: plan.sync_source });
      setNotice(`Sync preview: ${planSummary(plan)}.`);
      await loadSyncDetails(plan.sync_source);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  async function runSelectedSync(source = selectedSyncSource) {
    if (!source) return;
    setSaving(true);
    setError("");
    setNotice("");
    try {
      const plan = await runAddressSyncSource(settings, source.id);
      setSyncPlan(plan);
      setSyncInspector({ source: plan.sync_source });
      setNotice(`Sync completed: ${planSummary(plan)}.`);
      await refreshBooks();
      await refreshContacts(selectedBookId, query);
      await loadSyncDetails(plan.sync_source);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  async function toggleSyncSourceEnabled(source: AddressSyncSource) {
    setSaving(true);
    setError("");
    setNotice("");
    try {
      const next = await updateAddressSyncSource(settings, source.id, { enabled: !source.enabled });
      setSyncInspector({ source: next });
      setNotice(next.enabled ? "Sync source enabled." : "Sync source disabled.");
      await refreshBooks();
      await loadSyncDetails(next);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  function openConflictReview(conflict: AddressSyncConflict) {
    setConflictDialog(conflict);
    setConflictMergeChoices(defaultConflictMergeChoices(conflict));
  }

  function setConflictMergeChoice(field: string, choice: ConflictMergeChoice) {
    setConflictMergeChoices((current) => ({ ...current, [field]: choice }));
  }

  async function resolveConflictWith(
    conflict: AddressSyncConflict,
    resolution: "keep_local" | "use_remote" | "merge" | "manual" | "ignored",
    status: "resolved" | "ignored" = "resolved",
    mergedPayload: Record<string, unknown> | null = null
  ) {
    setSaving(true);
    setError("");
    setNotice("");
    try {
      await resolveAddressSyncConflict(settings, conflict.id, resolution, status, { mergedPayload });
      setConflictDialog(null);
      setConflictMergeChoices({});
      setNotice(status === "ignored" ? "Sync conflict ignored." : "Sync conflict resolved.");
      if (syncInspector) await loadSyncDetails(syncInspector.source);
      await refreshBooks();
      await refreshContacts(selectedBookId, query);
    } catch (err) {
      setError(errorMessage(err));
    } finally {
      setSaving(false);
    }
  }

  async function applyMergedConflict(conflict: AddressSyncConflict) {
    const mergedPayload = buildMergedConflictPayload(conflict, conflictMergeChoices);
    if (!mergedPayload) {
      setError("This conflict does not contain local and remote field payloads that can be merged.");
      return;
    }
    await resolveConflictWith(conflict, "merge", "resolved", mergedPayload);
  }

  function renderTreeNodeContent(node: AddressTreeNode) {
    if (node.kind === "source") {
      const bookCount = books.filter((book) => sourceGroupForBook(book) === node.sourceGroup).length;
      return <span className="address-tree-node-content"><strong>{node.label}</strong><small>{bookCount} book{bookCount === 1 ? "" : "s"}</small></span>;
    }
    if (node.kind === "scope" && node.scope) {
      const contactCount = books.filter((book) => sourceGroupForBook(book) === node.sourceGroup && book.scope_type === node.scope).reduce((sum, book) => sum + book.contact_count, 0);
      return <span className="address-tree-node-content"><strong>{node.label}</strong><small>{contactCount} contact{contactCount === 1 ? "" : "s"}</small></span>;
    }
    if (node.kind === "book" && node.book) {
      return (
        <span className="address-tree-node-content">
          <strong>{node.book.name}</strong>
          <small>{node.book.contact_count} contact{node.book.contact_count === 1 ? "" : "s"} · {node.book.source_kind}</small>
        </span>
      );
    }
    if (node.kind === "list" && node.list) {
      return (
        <span className="address-tree-node-content">
          <strong>{node.list.name}</strong>
          <small>{node.list.entry_count} entr{node.list.entry_count === 1 ? "y" : "ies"}</small>
        </span>
      );
    }
    return <span>{node.label}</span>;
  }

  function renderSelectedBookPanel() {
    if (!selectedBook) return <p className="address-empty-note">No address book selected.</p>;
    return (
      <div className="address-source-summary">
        <div>
          <strong>{selectedBook.name}</strong>
          <p>{selectedBook.description || "No description."}</p>
        </div>
        <div className="status-row">
          <StatusBadge status={scopeLabel(selectedBook.scope_type)} />
          <StatusBadge status={selectedBook.source_kind} />
          {selectedBook.sync_status && <StatusBadge status={`sync ${selectedBook.sync_status}`} />}
          {selectedBook.read_only && <StatusBadge status="read-only" />}
          {selectedBook.deleted_at && <StatusBadge status="archived" />}
        </div>
        {selectedBookSyncSources.length > 0 &&
          <div className="address-source-subsummary">
            <strong>Sync</strong>
            {selectedBookSyncSources.map((source) => (
              <p key={source.id}>
                {syncSourceLabel(source)} · {source.connector_type} · {source.status}
                {source.last_success_at ? ` · last success ${formatDateTime(source.last_success_at, ADDRESS_DATE_TIME_OPTIONS)}` : ""}
                {source.last_error ? ` · ${source.last_error}` : ""}
              </p>
            ))}
          </div>
        }
        {selectedList &&
          <div className="address-source-subsummary">
            <strong>{selectedList.name}</strong>
            <p>{selectedList.description || "No list description."}</p>
            <div className="status-row">
              <StatusBadge status={`${selectedList.entry_count} entries`} />
              {selectedList.read_only && <StatusBadge status="read-only" />}
              {selectedList.deleted_at && <StatusBadge status="archived" />}
            </div>
          </div>
        }
      </div>
    );
  }

  function renderSelectedBookActions() {
    return (
      <>
        <Button type="button" title="Refresh address books" aria-label="Refresh address books" onClick={() => void refreshAll()} disabledReason={refreshReason}><RefreshCw size={15} /></Button>
        <Button type="button" title="Add address book" aria-label="Add address book" variant="primary" onClick={openCreateBookDialog} disabledReason={createBookReason}><Plus size={15} /></Button>
        <Button type="button" title="Add address list" aria-label="Add address list" onClick={openCreateListDialog} disabledReason={createListReason}><Plus size={15} /></Button>
        <Button type="button" title="Import vCard into selected address book" aria-label="Import vCard into selected address book" onClick={() => setImportOpen(true)} disabledReason={importBookReason}><Upload size={15} /></Button>
        <Button type="button" title="Export selected address book as vCard" aria-label="Export selected address book as vCard" onClick={() => void exportSelectedBook()} disabledReason={exportBookReason}><Download size={15} /></Button>
        <Button type="button" title="Connect CardDAV" aria-label="Connect CardDAV" onClick={openCardDavDialog} disabledReason={connectCardDavReason}><Link2 size={15} /></Button>
        <Button type="button" title="Inspect sync source" aria-label="Inspect sync source" onClick={() => selectedSyncSource && void openSyncInspector(selectedSyncSource)} disabledReason={inspectSyncReason}><Search size={15} /></Button>
        <Button type="button" title="Preview sync" aria-label="Preview sync" onClick={() => void previewSelectedSync()} disabledReason={previewSyncReason}>Preview</Button>
        <Button type="button" title="Run sync" aria-label="Run sync" onClick={() => void runSelectedSync()} disabledReason={runSyncReason}>Sync</Button>
        {selectedList ?
          selectedList.deleted_at ?
            <Button type="button" title="Restore address list" aria-label="Restore address list" disabledReason={restoreListReason(selectedList)} onClick={() => void restoreDeletedList(selectedList)}><RotateCcw size={15} /></Button> :
            <>
              <Button type="button" title="Edit address list" aria-label="Edit address list" disabledReason={editListReason(selectedList)} onClick={() => openEditListDialog(selectedList)}><Edit3 size={15} /></Button>
              <Button type="button" variant="danger" title="Delete address list" aria-label="Delete address list" disabledReason={deleteListReason(selectedList)} onClick={() => setConfirmState({ kind: "list", list: selectedList })}><Trash2 size={15} /></Button>
            </> :
          selectedBook?.deleted_at ?
          <Button type="button" title="Restore address book" aria-label="Restore address book" disabledReason={restoreBookReason(selectedBook)} onClick={() => void restoreBook(selectedBook)}><RotateCcw size={15} /></Button> :
          <>
            <Button type="button" title="Edit address book" aria-label="Edit address book" disabledReason={selectedBook ? editBookReason(selectedBook) : "Select an address book before editing."} onClick={() => selectedBook && openEditBookDialog(selectedBook)}><Edit3 size={15} /></Button>
            <Button type="button" variant="danger" title="Delete address book" aria-label="Delete address book" disabledReason={selectedBook ? deleteBookReason(selectedBook) : "Select an address book before deleting."} onClick={() => selectedBook && setConfirmState({ kind: "book", book: selectedBook })}><Trash2 size={15} /></Button>
          </>
        }
      </>
    );
  }

  function renderContactRow(contact: Contact) {
    const selected = selectedContactId === contact.id;
    return (
      <SelectionListItem
        key={contact.id}
        selected={selected}
        className={`address-contact-row ${contact.deleted_at ? "is-archived-row" : ""}`}
        draggable={!contact.deleted_at && !saving}
        onClick={() => setSelectedContactId(contact.id)}
        onDragStart={(event) => handleContactDragStart(event, contact)}>
        <span className="address-contact-row-main">
          <strong>{contact.display_name}</strong>
          <small>{primaryEmail(contact) || contact.organization || "No primary email"}</small>
        </span>
        <span className="address-contact-row-meta">
          {contact.deleted_at ? <StatusBadge status="archived" /> : contact.tags.slice(0, 2).map((tag) => <span key={tag} className="address-tag">{tag}</span>)}
        </span>
      </SelectionListItem>
    );
  }

  function renderContactDetail() {
    if (!selectedContact) {
      return (
        <div className="address-detail-empty">
          <strong>No contact selected</strong>
          <p>Select a contact from the middle list to view details here.</p>
        </div>
      );
    }
    return (
      <div className="address-contact-detail">
        <header className="address-detail-header">
          <div>
            <h2>{selectedContact.display_name}</h2>
            <p>{[selectedContact.role_title, selectedContact.organization].filter(Boolean).join(" · ") || "Contact"}</p>
          </div>
          <div className="button-row compact-actions">
            {selectedContact.deleted_at ?
              <Button type="button" title="Restore contact" aria-label="Restore contact" disabledReason={restoreContactReason()} onClick={() => void restoreDeletedContact(selectedContact)}><RotateCcw size={15} /> Restore</Button> :
              <>
                <Button type="button" title="Export contact vCard" aria-label="Export contact vCard" disabledReason={exportContactReason()} onClick={() => void exportOneContact(selectedContact)}><Download size={15} /> vCard</Button>
                <Button type="button" title="Edit contact" aria-label="Edit contact" disabledReason={editContactReason()} onClick={() => openEditContactDialog(selectedContact)}><Edit3 size={15} /> Edit</Button>
                <Button type="button" variant="danger" title="Delete contact" aria-label="Delete contact" disabledReason={deleteContactReason()} onClick={() => setConfirmState({ kind: "contact", contact: selectedContact })}><Trash2 size={15} /> Delete</Button>
              </>
            }
          </div>
        </header>
        <div className="status-row">
          {selectedContact.deleted_at && <StatusBadge status="archived" />}
          {selectedContact.source_kind && <StatusBadge status={selectedContact.source_kind} />}
          {selectedContact.tags.map((tag) => <span key={tag} className="address-tag">{tag}</span>)}
        </div>
        {selectedList &&
          <section className="address-detail-section">
            <h3>List membership</h3>
            <div className="address-membership-list">
              {selectedContactListEntries.map((entry) => (
                <div className="address-membership-row" key={entry.id}>
                  <p className="muted">{entryTargetLabel(entry)}</p>
                  <Button
                    type="button"
                    variant="danger"
                    onClick={() => void removeAddressListEntry(entry)}
                    disabledReason={removeContactFromListReason()}>
                    <X size={15} /> Remove
                  </Button>
                </div>
              ))}
            </div>
          </section>
        }
        <section className="address-detail-section">
          <h3>Email</h3>
          {selectedContact.emails.length === 0 ? <p className="muted">No email addresses.</p> :
            <dl className="address-detail-list">
              {selectedContact.emails.map((email, index) => (
                <div key={email.id ?? `${email.email}-${index}`}>
                  <dt>{email.label || "email"}{email.is_primary ? " · primary" : ""}</dt>
                  <dd>{email.email}</dd>
                </div>
              ))}
            </dl>
          }
        </section>
        <section className="address-detail-section">
          <h3>Phone</h3>
          {selectedContact.phones.length === 0 ? <p className="muted">No phone numbers.</p> :
            <dl className="address-detail-list">
              {selectedContact.phones.map((phone, index) => (
                <div key={phone.id ?? `${phone.phone}-${index}`}>
                  <dt>{phone.label || "phone"}{phone.is_primary ? " · primary" : ""}</dt>
                  <dd>{phone.phone}</dd>
                </div>
              ))}
            </dl>
          }
        </section>
        <section className="address-detail-section">
          <h3>Postal addresses</h3>
          {selectedContact.postal_addresses.length === 0 ? <p className="muted">No postal addresses.</p> :
            <dl className="address-detail-list">
              {selectedContact.postal_addresses.map((address, index) => (
                <div key={address.id ?? `${address.label}-${index}`}>
                  <dt>{address.label || "address"}{address.is_primary ? " · primary" : ""}</dt>
                  <dd>{formatPostalAddress(address) || "No formatted address."}</dd>
                </div>
              ))}
            </dl>
          }
        </section>
        {selectedContact.note &&
          <section className="address-detail-section">
            <h3>Note</h3>
            <p>{selectedContact.note}</p>
          </section>
        }
      </div>
    );
  }

  return (
    <div className="workspace-data-page module-entry-page address-book-page address-book-fullscreen">
      {error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
      {notice && !error && <DismissibleAlert tone="success" resetKey={notice}>{notice}</DismissibleAlert>}

      <LoadingFrame loading={loading} label="Loading address books..." className="address-workspace-frame">
        <div className="address-book-workspace">
          <aside className="file-tree-panel address-tree-panel" aria-label="Address sources">
            <header className="address-panel-header address-tree-header">
              <div>
                <h2>Address books</h2>
                <p>{books.length} book{books.length === 1 ? "" : "s"}</p>
              </div>
              <div className="button-row compact-actions address-icon-actions">
                {renderSelectedBookActions()}
              </div>
            </header>
            <div className="address-tree-filter-row">
              <ToggleSwitch
                label="Show archived"
                checked={showArchived}
                disabled={Boolean(toggleArchiveReason)}
                onChange={() => {
                  setContactPage(1);
                  setShowArchived((current) => !current);
                }}
                help="Include archived address books, lists, and contacts."
              />
            </div>
            <div className="file-tree-list address-tree-list">
              {addressTreeNodes.length === 0 ?
                <div className="address-empty-note">No address books found.</div> :
                <ExplorerTree
                  nodes={addressTreeNodes}
                  getNodeId={(node) => node.id}
                  getNodeLabel={(node) => node.label}
                  getNodeChildren={(node) => node.children}
                  activeId={activeTreeId}
                  expandedIds={expandedTreeIds}
                  onOpen={(node) => openTreeNode(node)}
                  onToggle={(node) => toggleTreeNode(node)}
                  renderNodeContent={(node) => renderTreeNodeContent(node)}
                  getNodeWrapClassName={(node) => node.kind === "list" && node.list?.id === dropTargetListId ? "is-drop-target" : undefined}
                  onDragOver={handleTreeDragOver}
                  onDragLeave={handleTreeDragLeave}
                  onDrop={(event, node) => void handleTreeDrop(event, node)}
                />
              }
            </div>
            <div className="address-tree-summary">
              {renderSelectedBookPanel()}
            </div>
          </aside>

          <section className="address-list-panel" aria-label="Contacts">
            <header className="address-panel-header">
              <div>
                <h2>{selectedList ? selectedList.name : selectedBook ? selectedBook.name : "Contacts"}</h2>
                <p>
                  {contactTotal} contact{contactTotal === 1 ? "" : "s"}
                  {selectedList ? " in selected list" : selectedBook ? " in selected book" : ""}
                </p>
              </div>
              <div className="button-row compact-actions">
                {selectedList &&
                  <Button type="button" onClick={() => void openAddMembersDialog()} disabledReason={addMembersReason}><UserPlus size={16} /> Add to list</Button>
                }
                <Button type="button" variant="primary" onClick={openCreateContactDialog} disabledReason={createContactReason}><Plus size={16} /> Contact</Button>
              </div>
            </header>
            <div className="address-contact-toolbar">
              <input
                value={query}
                onChange={(event) => {
                  setContactPage(1);
                  setQuery(event.target.value);
                }}
                placeholder="Search contacts" />
              {selectedBook?.read_only && <StatusBadge status="read-only" />}
              {selectedBook?.deleted_at && <StatusBadge status="archived" />}
            </div>
            <div className="address-contact-list">
              {visibleContacts.length === 0 ?
                <div className="address-empty-note">{selectedBook ? "No contacts found." : "Select an address book."}</div> :
                <SelectionList label="Contacts" className="address-contact-selection-list">
                  {visibleContacts.map(renderContactRow)}
                </SelectionList>
              }
            </div>
            <DataGridPaginationBar
              page={contactPage}
              pageSize={contactPageSize}
              totalRows={contactTotal}
              pageSizeOptions={[25, 50, 100, 200]}
              disabled={loading || saving || !selectedBook}
              className="address-contact-pagination"
              ariaLabel="Contact pagination"
              onPageChange={setContactPage}
              onPageSizeChange={(pageSize) => {
                setContactPageSize(pageSize);
                setContactPage(1);
              }} />
          </section>

          <section className="address-detail-panel" aria-label="Contact detail">
            {renderContactDetail()}
          </section>
        </div>
      </LoadingFrame>

      <Dialog
        open={Boolean(bookDialog)}
        title={bookDialog?.mode === "edit" ? "Edit address book" : "Add address book"}
        onClose={() => setBookDialog(null)}
        closeDisabled={saving}
        footerClassName="button-row compact-actions"
        footer={
          <>
            <Button type="button" onClick={() => setBookDialog(null)} disabledReason={dialogCancelReason}>Cancel</Button>
            <Button type="submit" form="address-book-form" variant="primary" disabledReason={bookSaveReason}><Save size={16} /> Save</Button>
          </>
        }>
        <form id="address-book-form" className="address-dialog-form" onSubmit={(event) => void submitBook(event)}>
          <div className="form-grid two">
            <FormField label="Scope">
              <select value={bookForm.scope_type} disabled={bookDialog?.mode === "edit"} onChange={(event) => setBookForm((current) => ({ ...current, scope_type: event.target.value as AddressBookScope }))}>
                <option value="user">Personal</option>
                <option value="group" disabled={auth.groups.length === 0}>Group</option>
                <option value="tenant">Tenant</option>
                {canAdminBooks && <option value="system">System</option>}
              </select>
            </FormField>
            {bookForm.scope_type === "group" &&
              <FormField label="Group">
                <select value={bookForm.group_id} disabled={bookDialog?.mode === "edit"} onChange={(event) => setBookForm((current) => ({ ...current, group_id: event.target.value }))}>
                  {auth.groups.length === 0 && <option value="">No groups available</option>}
                  {auth.groups.map((group) => <option key={group.id} value={group.id}>{group.name}</option>)}
                </select>
              </FormField>
            }
          </div>
          <FormField label="Name"><input value={bookForm.name} onChange={(event) => setBookForm((current) => ({ ...current, name: event.target.value }))} autoFocus /></FormField>
          <FormField label="Description"><textarea value={bookForm.description} onChange={(event) => setBookForm((current) => ({ ...current, description: event.target.value }))} rows={3} /></FormField>
        </form>
      </Dialog>

      <Dialog
        open={Boolean(listDialog)}
        title={listDialog?.mode === "edit" ? "Edit address list" : "Add address list"}
        onClose={() => setListDialog(null)}
        closeDisabled={saving}
        footerClassName="button-row compact-actions"
        footer={
          <>
            <Button type="button" onClick={() => setListDialog(null)} disabledReason={dialogCancelReason}>Cancel</Button>
            <Button type="submit" form="address-list-form" variant="primary" disabledReason={listSaveReason}><Save size={16} /> Save</Button>
          </>
        }>
        <form id="address-list-form" className="address-dialog-form" onSubmit={(event) => void submitList(event)}>
          <p className="muted">Lists group contacts inside the selected address book.</p>
          <FormField label="Address book">
            <input value={selectedBook?.name ?? ""} disabled readOnly />
          </FormField>
          <FormField label="Name">
            <input value={listForm.name} onChange={(event) => setListForm((current) => ({ ...current, name: event.target.value }))} autoFocus />
          </FormField>
          <FormField label="Description">
            <textarea value={listForm.description} onChange={(event) => setListForm((current) => ({ ...current, description: event.target.value }))} rows={3} />
          </FormField>
        </form>
      </Dialog>

      <Dialog
        open={Boolean(contactDialog)}
        title={contactDialog?.mode === "edit" ? "Edit contact" : "Add contact"}
        onClose={() => setContactDialog(null)}
        closeDisabled={saving}
        className="address-contact-dialog"
        footerClassName="button-row compact-actions"
        footer={
          <>
            <Button type="button" onClick={() => setContactDialog(null)} disabledReason={dialogCancelReason}>Cancel</Button>
            <Button type="submit" form="address-contact-form" variant="primary" disabledReason={contactSaveReason}><Save size={16} /> Save</Button>
          </>
        }>
        <form id="address-contact-form" className="address-dialog-form" onSubmit={(event) => void submitContact(event)}>
          <div className="form-grid three">
            <FormField label="Display name"><input value={contactForm.display_name} onChange={(event) => setContactForm((current) => ({ ...current, display_name: event.target.value }))} autoFocus /></FormField>
            <FormField label="Given name"><input value={contactForm.given_name} onChange={(event) => setContactForm((current) => ({ ...current, given_name: event.target.value }))} /></FormField>
            <FormField label="Family name"><input value={contactForm.family_name} onChange={(event) => setContactForm((current) => ({ ...current, family_name: event.target.value }))} /></FormField>
          </div>
          <div className="form-grid two">
            <FormField label="Organization"><input value={contactForm.organization} onChange={(event) => setContactForm((current) => ({ ...current, organization: event.target.value }))} /></FormField>
            <FormField label="Role title"><input value={contactForm.role_title} onChange={(event) => setContactForm((current) => ({ ...current, role_title: event.target.value }))} /></FormField>
          </div>
          <section className="address-form-section">
            <div className="address-form-section-heading">
              <strong>Email addresses</strong>
              <Button type="button" onClick={addEmailRow} disabledReason={addContactRowReason}><Plus size={15} /> Email</Button>
            </div>
            <div className="address-form-list">
              {contactForm.emails.map((email) => (
                <div className="address-form-row address-form-row-email" key={email.rowId}>
                  <label className="address-primary-choice"><input type="radio" name="address-primary-email" checked={email.is_primary} onChange={() => setPrimaryEmailRow(email.rowId)} /> Primary</label>
                  <input value={email.label} placeholder="Label" onChange={(event) => updateEmailRow(email.rowId, { label: event.target.value })} />
                  <input type="email" value={email.email} placeholder="Email address" onChange={(event) => updateEmailRow(email.rowId, { email: event.target.value })} />
                  <Button type="button" variant="danger" title="Remove email" aria-label="Remove email" disabledReason={removeEmailRowReason} onClick={() => removeEmailRow(email.rowId)}><Trash2 size={15} /></Button>
                </div>
              ))}
            </div>
          </section>

          <section className="address-form-section">
            <div className="address-form-section-heading">
              <strong>Phone numbers</strong>
              <Button type="button" onClick={addPhoneRow} disabledReason={addContactRowReason}><Plus size={15} /> Phone</Button>
            </div>
            <div className="address-form-list">
              {contactForm.phones.map((phone) => (
                <div className="address-form-row address-form-row-phone" key={phone.rowId}>
                  <label className="address-primary-choice"><input type="radio" name="address-primary-phone" checked={phone.is_primary} onChange={() => setPrimaryPhoneRow(phone.rowId)} /> Primary</label>
                  <input value={phone.label} placeholder="Label" onChange={(event) => updatePhoneRow(phone.rowId, { label: event.target.value })} />
                  <input value={phone.phone} placeholder="Phone number" onChange={(event) => updatePhoneRow(phone.rowId, { phone: event.target.value })} />
                  <Button type="button" variant="danger" title="Remove phone" aria-label="Remove phone" disabledReason={removePhoneRowReason} onClick={() => removePhoneRow(phone.rowId)}><Trash2 size={15} /></Button>
                </div>
              ))}
            </div>
          </section>

          <section className="address-form-section">
            <div className="address-form-section-heading">
              <strong>Postal addresses</strong>
              <Button type="button" onClick={addPostalAddressRow} disabledReason={addContactRowReason}><Plus size={15} /> Address</Button>
            </div>
            <div className="address-form-list">
              {contactForm.postal_addresses.map((address) => (
                <div className="address-form-row address-form-row-postal" key={address.rowId}>
                  <label className="address-primary-choice"><input type="radio" name="address-primary-postal" checked={address.is_primary} onChange={() => setPrimaryPostalAddressRow(address.rowId)} /> Primary</label>
                  <input value={address.label} placeholder="Label" onChange={(event) => updatePostalAddressRow(address.rowId, { label: event.target.value })} />
                  <input value={address.street} placeholder="Street" onChange={(event) => updatePostalAddressRow(address.rowId, { street: event.target.value })} />
                  <input value={address.postal_code} placeholder="Postal code" onChange={(event) => updatePostalAddressRow(address.rowId, { postal_code: event.target.value })} />
                  <input value={address.locality} placeholder="Locality" onChange={(event) => updatePostalAddressRow(address.rowId, { locality: event.target.value })} />
                  <input value={address.region} placeholder="Region" onChange={(event) => updatePostalAddressRow(address.rowId, { region: event.target.value })} />
                  <input value={address.country} placeholder="Country" onChange={(event) => updatePostalAddressRow(address.rowId, { country: event.target.value })} />
                  <Button type="button" variant="danger" title="Remove postal address" aria-label="Remove postal address" disabledReason={removePostalAddressRowReason} onClick={() => removePostalAddressRow(address.rowId)}><Trash2 size={15} /></Button>
                </div>
              ))}
            </div>
          </section>
          <FormField label="Tags"><input value={contactForm.tags} placeholder="Comma-separated tags" onChange={(event) => setContactForm((current) => ({ ...current, tags: event.target.value }))} /></FormField>
          <FormField label="Note"><textarea rows={3} value={contactForm.note} onChange={(event) => setContactForm((current) => ({ ...current, note: event.target.value }))} /></FormField>
        </form>
      </Dialog>

      <Dialog
        open={memberDialogOpen}
        title={selectedList ? `Add contacts to ${selectedList.name}` : "Add contacts to list"}
        onClose={() => setMemberDialogOpen(false)}
        closeDisabled={saving}
        className="address-member-dialog"
        footerClassName="button-row compact-actions"
        footer={<Button type="button" onClick={() => setMemberDialogOpen(false)} disabledReason={dialogCancelReason}>Close</Button>}>
        <div className="address-member-picker">
          <div className="address-contact-toolbar address-member-search">
            <input value={memberQuery} onChange={(event) => setMemberQuery(event.target.value)} placeholder="Search contacts to add" autoFocus />
          </div>
          <div className="address-member-candidate-list" role="list">
            {memberCandidateContacts.length === 0 ?
              <div className="address-empty-note">No contacts available to add.</div> :
              memberCandidateContacts.map((contact) => {
                const targetOptions = availableTargetOptionsForContact(contact);
                const selectedTarget = selectedTargetOptionForContact(contact);
                return (
                  <div className="address-member-candidate" key={contact.id}>
                    <span className="address-contact-row-main">
                      <strong>{contact.display_name}</strong>
                      <small>{primaryEmail(contact) || contact.organization || "No primary email"}</small>
                    </span>
                    <select
                      value={selectedTarget?.key ?? ""}
                      aria-label={`Target for ${contact.display_name}`}
                      onChange={(event) => setMemberTarget(contact.id, event.target.value)}
                      disabled={saving || targetOptions.length === 0}>
                      {targetOptions.map((option) => <option key={option.key} value={option.key}>{option.label}</option>)}
                    </select>
                    <Button
                      type="button"
                      onClick={() => selectedList && void addContactToAddressList(selectedList, contact, selectedTarget)}
                      disabledReason={addContactToListReason(selectedList, contact, selectedTarget?.key ?? "")}>
                      <Plus size={15} /> Add
                    </Button>
                  </div>
                );
              })
            }
          </div>
          <p className="muted small-text">Tip: drag and drop adds the first available target. Use this dialog for a specific email or postal address.</p>
        </div>
      </Dialog>

      <Dialog
        open={importOpen}
        title="Import vCard"
        onClose={() => setImportOpen(false)}
        closeDisabled={saving}
        footerClassName="button-row compact-actions"
        footer={
          <>
            <Button type="button" onClick={() => setImportOpen(false)} disabledReason={dialogCancelReason}>Cancel</Button>
            <Button type="submit" form="address-vcard-import-form" variant="primary" disabledReason={vcardImportReason}><Upload size={16} /> Import</Button>
          </>
        }>
        <form id="address-vcard-import-form" className="address-dialog-form" onSubmit={(event) => void submitVcardImport(event)}>
          <p className="muted">Paste one or more vCard entries into the selected address book.</p>
          <FormField label="vCard content">
            <textarea
              className="address-vcard-textarea"
              value={vcardContent}
              onChange={(event) => setVcardContent(event.target.value)}
              rows={14}
              placeholder={"BEGIN:VCARD\nVERSION:4.0\nFN:Ada Lovelace\nEMAIL;TYPE=work:ada@example.local\nEND:VCARD"}
            />
          </FormField>
        </form>
      </Dialog>

      <Dialog
        open={cardDavOpen}
        title="Connect CardDAV"
        onClose={() => setCardDavOpen(false)}
        closeDisabled={saving}
        className="address-sync-dialog"
        footerClassName="button-row compact-actions"
        footer={
          <>
            <Button type="button" onClick={() => setCardDavOpen(false)} disabledReason={dialogCancelReason}>Cancel</Button>
            <Button type="button" onClick={() => void discoverCardDavSources()} disabledReason={disabledReason([saving, savingReason], [!cardDavForm.collection_url.trim(), "Enter a CardDAV URL before discovery."])}><Search size={16} /> Discover</Button>
            <Button type="submit" form="address-carddav-form" variant="primary" disabledReason={disabledReason([saving, savingReason], [!cardDavForm.collection_url.trim(), "Enter a CardDAV address-book URL before saving."])}><Save size={16} /> Connect</Button>
          </>
        }>
        <form id="address-carddav-form" className="address-dialog-form" onSubmit={(event) => void submitCardDavSource(event)}>
          <FormField label="Address book">
            <input value={selectedBook?.name ?? ""} disabled readOnly />
          </FormField>
          <FormField label="CardDAV URL">
            <input value={cardDavForm.collection_url} onChange={(event) => setCardDavForm((current) => ({ ...current, collection_url: event.target.value }))} autoFocus />
          </FormField>
          <div className="form-grid three">
            <FormField label="Authentication">
              <select value={cardDavForm.auth_type} onChange={(event) => setCardDavForm((current) => ({ ...current, auth_type: event.target.value as CardDavFormState["auth_type"] }))}>
                <option value="none">None</option>
                <option value="basic">Basic</option>
                <option value="bearer">Bearer token</option>
              </select>
            </FormField>
            <FormField label="Sync direction">
              <select value={cardDavForm.sync_direction} onChange={(event) => setCardDavForm((current) => ({ ...current, sync_direction: event.target.value as CardDavFormState["sync_direction"] }))}>
                <option value="read_only">Read-only</option>
                <option value="import">One-way import</option>
                <option value="two_way">Two-way marker</option>
              </select>
            </FormField>
            <FormField label="Display name">
              <input value={cardDavForm.display_name} onChange={(event) => setCardDavForm((current) => ({ ...current, display_name: event.target.value }))} placeholder={selectedBook?.name ?? "CardDAV"} />
            </FormField>
          </div>
          {cardDavForm.auth_type !== "none" &&
            <FormField label="Reusable credential">
              <select
                value={cardDavForm.credential_envelope_id}
                onChange={(event) => {
                  const credentialId = event.target.value;
                  const credential = cardDavCredentials.find((item) => item.id === credentialId);
                  const username = credential?.public_data?.username;
                  setCardDavForm((current) => ({
                    ...current,
                    credential_envelope_id: credentialId,
                    username: credentialId && username ? String(username) : "",
                    password: "",
                    bearer_token: ""
                  }));
                }}>
                <option value="">Enter source-specific credentials below</option>
                {cardDavCredentials.map((credential) => (
                  <option key={credential.id} value={credential.id}>
                    {credential.name}{credential.public_data.username ? ` (${String(credential.public_data.username)})` : ""}
                  </option>
                ))}
              </select>
            </FormField>
          }
          {cardDavCredentialsError && <DismissibleAlert tone="danger" resetKey={cardDavCredentialsError}>{cardDavCredentialsError}</DismissibleAlert>}
          {cardDavForm.auth_type === "basic" &&
            <div className="form-grid two">
              <FormField label="Username">
                <input value={cardDavForm.username} onChange={(event) => setCardDavForm((current) => ({ ...current, username: event.target.value }))} disabled={Boolean(cardDavForm.credential_envelope_id)} />
              </FormField>
              {!cardDavForm.credential_envelope_id &&
                <FormField label="Password">
                  <PasswordField value={cardDavForm.password} onValueChange={(value) => setCardDavForm((current) => ({ ...current, password: value }))} autoComplete="new-password" />
                </FormField>
              }
            </div>
          }
          {cardDavForm.auth_type === "bearer" && !cardDavForm.credential_envelope_id &&
            <FormField label="Bearer token">
              <PasswordField value={cardDavForm.bearer_token} onValueChange={(value) => setCardDavForm((current) => ({ ...current, bearer_token: value }))} autoComplete="new-password" />
            </FormField>
          }
          {cardDavDiscovery.length > 0 &&
            <SelectionList label="Discovered CardDAV address books" className="address-sync-result-list">
              {cardDavDiscovery.map((item) => (
                <SelectionListItem
                  selected={cardDavForm.collection_url === item.collection_url}
                  className="address-sync-result-row"
                  key={item.collection_url}
                  onClick={() => useDiscoveredCardDavBook(item)}>
                  <strong>{item.display_name || item.collection_url}</strong>
                  <small>{item.collection_url}</small>
                </SelectionListItem>
              ))}
            </SelectionList>
          }
        </form>
      </Dialog>

      <Dialog
        open={Boolean(syncInspector)}
        title={syncInspector ? `Sync: ${syncSourceLabel(syncInspector.source)}` : "Sync"}
        onClose={() => setSyncInspector(null)}
        closeDisabled={saving}
        className="address-sync-dialog"
        footerClassName="button-row compact-actions"
        footer={
          <>
            <Button type="button" onClick={() => setSyncInspector(null)} disabledReason={dialogCancelReason}>Close</Button>
            <Button type="button" onClick={() => syncInspector && void previewSelectedSync(syncInspector.source)} disabledReason={disabledReason([!syncInspector, "No sync source selected."], [saving, savingReason])}>Preview</Button>
            <Button type="button" variant="primary" onClick={() => syncInspector && void runSelectedSync(syncInspector.source)} disabledReason={disabledReason([!syncInspector, "No sync source selected."], [!canWriteSync, "You need permission to run address sync."], [saving, savingReason])}>Run sync</Button>
          </>
        }>
        {syncInspector &&
          <div className="address-sync-inspector">
            <div className="address-sync-source-card">
              <div>
                <strong>{syncSourceLabel(syncInspector.source)}</strong>
                <p>{syncInspector.source.external_address_book_ref || "No external reference."}</p>
              </div>
              <div className="status-row">
                <StatusBadge status={syncInspector.source.connector_type} />
                <StatusBadge status={syncInspector.source.status} />
                {syncInspector.source.read_only && <StatusBadge status="read-only" />}
              </div>
              <p className="muted">Last attempt: {formatDateTime(syncInspector.source.last_attempted_at, ADDRESS_DATE_TIME_OPTIONS)} · Last success: {formatDateTime(syncInspector.source.last_success_at, ADDRESS_DATE_TIME_OPTIONS)}</p>
              {syncInspector.source.last_error && <DismissibleAlert tone="danger" resetKey={syncInspector.source.last_error}>{syncInspector.source.last_error}</DismissibleAlert>}
              <div className="button-row compact-actions">
                <Button
                  type="button"
                  onClick={() => void toggleSyncSourceEnabled(syncInspector.source)}
                  disabledReason={disabledReason([!canWriteSync, "You need permission to manage address sync."], [saving, savingReason])}>
                  {syncInspector.source.enabled ? "Disable" : "Enable"}
                </Button>
                <Button
                  type="button"
                  variant="danger"
                  onClick={() => setConfirmState({ kind: "sync-source", source: syncInspector.source })}
                  disabledReason={disabledReason([!canWriteSync, "You need permission to manage address sync."], [saving, savingReason])}>
                  Disconnect
                </Button>
              </div>
            </div>

            <section className="address-detail-section">
              <h3>Preview</h3>
              <p className="muted">{planSummary(syncPlan)}</p>
              {syncPlan &&
                <div className="address-sync-plan-grid">
                  {syncPlan.items.slice(0, 100).map((item, index) => (
                    <div className="address-sync-plan-row" key={`${item.href ?? item.message ?? "item"}-${index}`}>
                      <StatusBadge status={item.action} label={syncActionLabel(item.action)} />
                      <span>{item.display_name || item.href || item.message || "Sync item"}</span>
                      {item.message && <small>{item.message}</small>}
                    </div>
                  ))}
                </div>
              }
            </section>

            <section className="address-detail-section">
              <h3>Conflicts</h3>
              {syncConflicts.length === 0 ? <p className="muted">No open conflicts.</p> :
                <div className="address-sync-record-list">
                  {syncConflicts.map((conflict) => (
                    <div className="address-sync-record-row" key={conflict.id}>
                      <span><strong>{conflict.field_path}</strong><small>{conflict.resource_href || conflict.remote_uid || conflict.contact_id}</small></span>
                      <Button type="button" onClick={() => openConflictReview(conflict)} disabledReason={disabledReason([saving, savingReason])}>Review</Button>
                    </div>
                  ))}
                </div>
              }
            </section>

            <section className="address-detail-section">
              <h3>Diagnostics</h3>
              {syncDiagnostics.length === 0 ? <p className="muted">No diagnostics recorded.</p> :
                <div className="address-sync-record-list">
                  {syncDiagnostics.slice(0, 20).map((diagnostic) => (
                    <div className="address-sync-record-row" key={diagnostic.id}>
                      <StatusBadge status={diagnostic.severity} />
                      <span><strong>{diagnostic.code}</strong><small>{diagnostic.message}</small></span>
                    </div>
                  ))}
                </div>
              }
            </section>

            <section className="address-detail-section">
              <h3>Tombstones</h3>
              {syncTombstones.length === 0 ? <p className="muted">No tombstones recorded.</p> :
                <div className="address-sync-record-list">
                  {syncTombstones.slice(0, 20).map((tombstone) => (
                    <div className="address-sync-record-row" key={tombstone.id}>
                      <span><strong>{tombstone.remote_uid || tombstone.resource_href || tombstone.contact_id}</strong><small>{formatDateTime(tombstone.synced_at || tombstone.remote_deleted_at || tombstone.local_deleted_at, ADDRESS_DATE_TIME_OPTIONS)}</small></span>
                    </div>
                  ))}
                </div>
              }
            </section>
          </div>
        }
      </Dialog>

      <Dialog
        open={Boolean(conflictDialog)}
        title="Review sync conflict"
        onClose={() => { setConflictDialog(null); setConflictMergeChoices({}); }}
        className="address-sync-dialog"
        footerClassName="button-row compact-actions"
        footer={conflictDialog && <>
          <Button type="button" onClick={() => { setConflictDialog(null); setConflictMergeChoices({}); }} disabledReason={dialogCancelReason}>Close</Button>
          <Button
            type="button"
            onClick={() => void resolveConflictWith(conflictDialog, "ignored", "ignored")}
            disabledReason={disabledReason([!canWriteSync, "You need permission to resolve sync conflicts."], [saving, savingReason])}>
            Ignore
          </Button>
          <Button
            type="button"
            onClick={() => void resolveConflictWith(conflictDialog, "keep_local")}
            disabledReason={disabledReason([!canWriteSync, "You need permission to resolve sync conflicts."], [saving, savingReason])}>
            Keep local
          </Button>
          <Button
            type="button"
            onClick={() => void applyMergedConflict(conflictDialog)}
            disabledReason={disabledReason(
              [!canWriteSync, "You need permission to resolve sync conflicts."],
              [!canMergeConflict(conflictDialog), "This conflict does not contain both local and remote field payloads."],
              [saving, savingReason]
            )}>
            Apply merged
          </Button>
          <Button
            type="button"
            variant="primary"
            onClick={() => void resolveConflictWith(conflictDialog, "use_remote")}
            disabledReason={disabledReason(
              [!canWriteSync, "You need permission to resolve sync conflicts."],
              [!canApplyRemoteConflict(conflictDialog), "This conflict does not contain a stored remote payload that can be applied automatically."],
              [saving, savingReason]
            )}>
            Use remote
          </Button>
        </>}>
        {conflictDialog &&
          <div className="address-conflict-review">
            <div className="address-sync-source-card">
              <div>
                <strong>{conflictDialog.field_path}</strong>
                <p>{conflictDialog.resource_href || conflictDialog.remote_uid || conflictDialog.contact_id || "No remote identifier."}</p>
              </div>
              <div className="status-row">
                <StatusBadge status={conflictDialog.status} />
                {conflictDialog.resolution && <StatusBadge status={conflictDialog.resolution} />}
              </div>
              {conflictDialog.metadata?.message && <p className="muted">{String(conflictDialog.metadata.message)}</p>}
              {!canApplyRemoteConflict(conflictDialog) && <DismissibleAlert tone="warning" dismissible={false}>This conflict predates stored field payloads or came from a stale write. It can be marked resolved or ignored, but the remote value cannot be applied automatically.</DismissibleAlert>}
            </div>
            <div className="address-conflict-grid">
              <div className="address-conflict-grid-header">Field</div>
              <div className="address-conflict-grid-header">Local</div>
              <div className="address-conflict-grid-header">Remote</div>
              <div className="address-conflict-grid-header">Use</div>
              {conflictFieldRows(conflictDialog).map((row) => (
                <div className={`address-conflict-row ${row.differs ? "has-difference" : ""}`} key={row.field}>
                  <strong>{row.field}</strong>
                  <span>{row.local}</span>
                  <span>{row.remote}</span>
                  <div>
                    {canMergeConflict(conflictDialog) ?
                      <SegmentedControl<ConflictMergeChoice>
                        className="address-conflict-choice"
                        role="group"
                        size="equal"
                        ariaLabel={`Source for ${row.field}`}
                        options={[{ id: "local", label: "Local" }, { id: "remote", label: "Remote" }]}
                        value={conflictMergeChoices[row.field] ?? "local"}
                        onChange={(choice) => setConflictMergeChoice(row.field, choice)}
                      /> :
                      "—"
                    }
                  </div>
                </div>
              ))}
            </div>
          </div>
        }
      </Dialog>

      <ConfirmDialog
        open={Boolean(confirmState)}
        title={confirmState?.kind === "book" ? "Delete address book" : confirmState?.kind === "list" ? "Delete address list" : confirmState?.kind === "sync-source" ? "Disconnect sync source" : "Delete contact"}
        message={
          confirmState?.kind === "book" ? `Delete "${confirmState.book.name}" and hide its contacts?` :
            confirmState?.kind === "list" ? `Delete "${confirmState.list.name}" and hide its entries?` :
              confirmState?.kind === "sync-source" ? `Disconnect "${confirmState.source.display_name}" from this address book? Local contacts are kept; source diagnostics, tombstones, and conflicts are removed.` :
                `Delete "${confirmState?.contact.display_name}"?`
        }
        confirmLabel={confirmState?.kind === "sync-source" ? "Disconnect" : "Delete"}
        tone="danger"
        busy={saving}
        onCancel={() => setConfirmState(null)}
        onConfirm={() => void confirmDelete()}
      />
    </div>
  );
}
