import { Fragment, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import {
  closestCenter,
  DndContext,
  PointerSensor,
  useSensor,
  useSensors,
  type DraggableAttributes,
  type DraggableSyntheticListeners,
  type DragEndEvent,
  type DragStartEvent,
} from "@dnd-kit/core";
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
  Building2,
  Check,
  ClipboardList,
  Copy,
  DatabaseZap,
  Edit3,
  Globe2,
  GripVertical,
  HardDrive,
  Image as ImageIcon,
  KeyRound,
  LayoutDashboard,
  Link2,
  ListChecks,
  MailPlus,
  Maximize2,
  Minimize2,
  Plus,
  RefreshCw,
  Save,
  ShieldCheck,
  SlidersHorizontal,
  Trash2,
  UsersRound,
  Video,
  X,
} from "lucide-react";
import type { AccessRequest, AccessRequestStatus } from "../../entities/access-request/types";
import type { ServiceAppRole, ServiceModuleId } from "../../entities/access/types";
import type { Client, ClientStatus, ClientTaskManagerWorkspaceBinding, ClientType } from "../../entities/client/types";
import type { EngineWorkflowAccessRequest } from "../../entities/engine-workflow-access-request/types";
import type { Invite, InviteStatus } from "../../entities/invite/types";
import { PUBLIC_POOL_CLIENT_ID, PUBLIC_POOL_CONTEXT_DESCRIPTION, PUBLIC_POOL_CONTEXT_LABEL, isPublicPoolClientId } from "../../entities/public-pool/constants";
import { createServiceLaunchLinkPatch, getServiceLaunchLink } from "../../entities/service/links";
import type { MediaKind, Service, ServiceMediaSource, ServiceStatus } from "../../entities/service/types";
import type { SyncState, SyncStatus } from "../../entities/sync/types";
import type { TaskerInviteRequest } from "../../entities/tasker-invite-request/types";
import type {
  ClientGroup,
  ClientMembership,
  ClientMembershipRole,
  ClientMembershipStatus,
  CoreAssistantAccessRole,
  LauncherUser,
  LauncherUserStatus,
} from "../../entities/user/types";
import {
  buildAccessMatrix,
  getClient,
  getClientUsers,
  getService,
  getUser,
  type AccessMatrixCell,
  type LauncherData,
  type LauncherSettings,
  type MeResponse,
  type TaskManagerWorkspaceCreationPolicy,
} from "../../shared/api/mockApi";
import {
  fetchAdminAuthentikSyncHealth,
  type AuthentikSyncHealth,
  type AuthentikSyncHealthState,
  type TaskManagerProjectSummary,
  type TaskManagerWorkspaceMemberRole,
  type TaskManagerWorkspaceSummary,
} from "../../shared/api/adminApi";
import { uploadStorageFile } from "../../shared/api/storageApi";
import { cn } from "../../shared/lib/cn";
import { formatDate, formatDateTime } from "../../shared/lib/format";
import { NodeDcDateField, NodeDcDeleteModal, NodeDcDropdown, NodeDcSelect, type NodeDcSelectOption } from "../../shared/nodedc-ui";
import { Button, IconButton } from "../../shared/ui/Button";
import { GlassSurface } from "../../shared/ui/Glass";

type AdminSection =
  | "overview"
  | "clients"
  | "users"
  | "groups"
  | "services"
  | "access"
  | "invites"
  | "sync"
  | "audit"
  | "misc"
  | "company";
type AdminOverlayMode = "admin" | "platform";
type AdminMutationOutcome = { ok: true } | { ok: false; message: string };
type PublicInviteTab = "incoming" | "outgoing";
type PublicInviteSourceFilter = "all" | "ndc" | "ops" | "engine";
type EngineWorkflowAccessRequestDecisionPatch =
  Partial<Pick<EngineWorkflowAccessRequest, "comment" | "requestedServiceRole">> & {
    serviceRole?: EngineWorkflowAccessRequest["requestedServiceRole"];
  };

type AccessAssignmentRole = Exclude<ServiceAppRole, "owner">;
export type AccessAssignmentValue = AccessAssignmentRole | "deny" | "unset";
type OperationalCoreRoleSelectValue = AccessAssignmentValue | "pending";

export interface SetUserServiceAccessCommand {
  userId: string;
  serviceId: string;
  value: AccessAssignmentValue;
}

export interface SetServiceModuleEntitlementCommand {
  clientId: string;
  userId: string;
  serviceId: string;
  moduleId: ServiceModuleId;
  enabled: boolean;
}

export interface CreateUserCommand {
  clientId: string;
  email: string;
  name?: string;
  role: ClientMembershipRole;
  groupIds: string[];
  provisionAuth: boolean;
  generatePassword: boolean;
}

export interface EnsureTaskManagerWorkspaceMemberCommand {
  clientId: string;
  userId: string;
  workspaceSlug?: string;
  role: TaskManagerWorkspaceMemberRole;
}

export interface EnsureTaskManagerProjectMemberCommand {
  clientId: string;
  userId: string;
  workspaceSlug: string;
  projectId: string;
  role: TaskManagerWorkspaceMemberRole;
}

const platformSections: Array<{ id: AdminSection; label: string; icon: React.ReactNode }> = [
  { id: "overview", label: "Обзор", icon: <LayoutDashboard size={16} /> },
  { id: "clients", label: "Компании", icon: <Building2 size={16} /> },
  { id: "users", label: "Пользователи", icon: <UsersRound size={16} /> },
  { id: "services", label: "Каталог сервисов", icon: <DatabaseZap size={16} /> },
  { id: "sync", label: "Синхронизация", icon: <RefreshCw size={16} /> },
  { id: "audit", label: "Аудит", icon: <ClipboardList size={16} /> },
  { id: "misc", label: "Разное", icon: <SlidersHorizontal size={16} /> },
];

const clientSections: Array<{ id: AdminSection; label: string; icon: React.ReactNode }> = [
  { id: "overview", label: "Обзор", icon: <LayoutDashboard size={16} /> },
  { id: "invites", label: "Инвайты", icon: <MailPlus size={16} /> },
  { id: "users", label: "Участники", icon: <UsersRound size={16} /> },
  { id: "access", label: "Доступы", icon: <KeyRound size={16} /> },
  { id: "groups", label: "Группы", icon: <ListChecks size={16} /> },
];

const publicPoolSections: Array<{ id: AdminSection; label: string; icon: React.ReactNode }> = [
  { id: "overview", label: "Обзор", icon: <LayoutDashboard size={16} /> },
  { id: "services", label: "Каталог сервисов", icon: <DatabaseZap size={16} /> },
  { id: "access", label: "Доступы", icon: <KeyRound size={16} /> },
  { id: "invites", label: "Инвайты", icon: <MailPlus size={16} /> },
  { id: "sync", label: "Синхронизация", icon: <RefreshCw size={16} /> },
  { id: "audit", label: "Аудит", icon: <ClipboardList size={16} /> },
  { id: "misc", label: "Разное", icon: <SlidersHorizontal size={16} /> },
];

const protectedLauncherUserIds = new Set(["user_root"]);

function isProtectedLauncherUser(userId: string): boolean {
  return protectedLauncherUserIds.has(userId);
}

function canSelfManageProtectedUser(me: MeResponse, userId: string): boolean {
  return isProtectedLauncherUser(userId) && me.user.id === userId;
}

function isProtectedFromCurrentActor(me: MeResponse, userId: string): boolean {
  return isProtectedLauncherUser(userId) && !canSelfManageProtectedUser(me, userId);
}

function isEngineServiceForAccessControl(service: Service): boolean {
  return (
    service.id === "service_nodedc" ||
    service.slug === "nodedc" ||
    service.slug === "engine" ||
    service.authentikApplicationSlug === "nodedc-engine"
  );
}

export function AdminOverlay({
  data,
  me,
  mode,
  activeClientId,
  onClose,
  onSetUserServiceAccess,
  onCreateInvite,
  onUpdateInvite,
  onDeleteInvite,
  onUpdateAccessRequest,
  onApproveAccessRequest,
  onRejectAccessRequest,
  onApproveTaskerInviteRequest,
  onRejectTaskerInviteRequest,
  onApproveEngineWorkflowAccessRequest,
  onRejectEngineWorkflowAccessRequest,
  onRetrySync,
  onCreateClient,
  onUpdateClient,
  onDeleteClient,
  onCreateUser,
  onUpdateUser,
  onDeleteUser,
  onUpdateMembership,
  onDeleteMembership,
  pendingAccessAssignments,
  pendingServiceModuleEntitlements,
  onCreateGroup,
  onUpdateGroup,
  onDeleteGroup,
  onUpdateService,
  onReorderServices,
  onCreateService,
  onDeleteService,
  onUpdateSettings,
  taskManagerWorkspaces,
  taskManagerWorkspacesLoading,
  taskManagerWorkspacesError,
  pendingTaskManagerMemberships,
  pendingTaskManagerProjectMemberships,
  onRefreshTaskManagerWorkspaces,
  onSetTaskManagerWorkspaceMemberRole,
  onSetTaskManagerProjectMemberRole,
  onSetServiceModuleEntitlement,
}: {
  data: LauncherData;
  me: MeResponse;
  mode: AdminOverlayMode;
  activeClientId: string;
  onClose: () => void;
  onSetUserServiceAccess: (command: SetUserServiceAccessCommand) => void;
  onCreateInvite: (invite: Pick<Invite, "clientId" | "email" | "role">) => void;
  onUpdateInvite: (inviteId: string, patch: Partial<Invite>) => void;
  onDeleteInvite: (inviteId: string) => void;
  onUpdateAccessRequest: (
    accessRequestId: string,
    patch: Partial<Pick<AccessRequest, "targetClientId" | "role" | "comment">>
  ) => void;
  onApproveAccessRequest: (
    accessRequestId: string,
    patch: Partial<Pick<AccessRequest, "targetClientId" | "role" | "comment">>
  ) => void;
  onRejectAccessRequest: (accessRequestId: string, patch: Partial<Pick<AccessRequest, "comment">>) => void;
  onApproveTaskerInviteRequest: (taskerInviteRequestId: string, patch: Partial<Pick<TaskerInviteRequest, "comment">>) => void;
  onRejectTaskerInviteRequest: (taskerInviteRequestId: string, patch: Partial<Pick<TaskerInviteRequest, "comment">>) => void;
  onApproveEngineWorkflowAccessRequest: (
    engineWorkflowAccessRequestId: string,
    patch: EngineWorkflowAccessRequestDecisionPatch
  ) => void;
  onRejectEngineWorkflowAccessRequest: (
    engineWorkflowAccessRequestId: string,
    patch: EngineWorkflowAccessRequestDecisionPatch
  ) => void;
  onRetrySync: (syncId: string) => void;
  onCreateClient: () => void;
  onUpdateClient: (clientId: string, patch: Partial<Client>) => void;
  onDeleteClient: (clientId: string) => void;
  onCreateUser: (command: CreateUserCommand) => void;
  onUpdateUser: (userId: string, patch: Partial<LauncherUser>) => void;
  onDeleteUser: (userId: string) => void;
  onUpdateMembership: (membershipId: string, patch: Partial<ClientMembership>) => void;
  onDeleteMembership: (membershipId: string) => void;
  pendingAccessAssignments: Record<string, AccessAssignmentValue>;
  pendingServiceModuleEntitlements: Record<string, boolean>;
  onCreateGroup: (clientId: string) => void;
  onUpdateGroup: (groupId: string, patch: Partial<ClientGroup>) => void;
  onDeleteGroup: (groupId: string) => void;
  onUpdateService: (serviceId: string, patch: Partial<Service>) => Promise<AdminMutationOutcome>;
  onReorderServices: (orderedServiceIds: string[]) => void;
  onCreateService: () => void;
  onDeleteService: (serviceId: string) => void;
  onUpdateSettings: (patch: Partial<LauncherSettings>) => Promise<AdminMutationOutcome>;
  taskManagerWorkspaces: TaskManagerWorkspaceSummary[];
  taskManagerWorkspacesLoading: boolean;
  taskManagerWorkspacesError: string | null;
  pendingTaskManagerMemberships: Record<string, boolean>;
  pendingTaskManagerProjectMemberships: Record<string, boolean>;
  onRefreshTaskManagerWorkspaces: () => void;
  onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
  onSetTaskManagerProjectMemberRole: (command: EnsureTaskManagerProjectMemberCommand) => void;
  onSetServiceModuleEntitlement: (command: SetServiceModuleEntitlementCommand) => void;
}) {
  const isRoot = me.launcherRole === "root_admin";
  const isPlatformMode = isRoot && mode === "platform";
  const [activeSection, setActiveSection] = useState<AdminSection | null>(null);
  const [isContentFullscreen, setIsContentFullscreen] = useState(false);
  const [selectedClientId, setSelectedClientId] = useState(activeClientId);
  const [selectedCompanyClientId, setSelectedCompanyClientId] = useState(
    data.clients.some((client) => client.id === activeClientId) ? activeClientId : data.clients[0]?.id ?? activeClientId
  );
  const [selectedCell, setSelectedCell] = useState<{ userId: string; serviceId: string } | null>(null);
  const [publicInviteTab, setPublicInviteTab] = useState<PublicInviteTab>("incoming");
  const [publicInviteSourceFilter, setPublicInviteSourceFilter] = useState<PublicInviteSourceFilter>("all");
  const [authentikSyncHealth, setAuthentikSyncHealth] = useState<AuthentikSyncHealth | null>(null);

  const fallbackClientId = data.clients[0]?.id ?? activeClientId;
  const selectedCompanyClientExists = data.clients.some((client) => client.id === selectedCompanyClientId);
  const scopedCompanyClientId = selectedCompanyClientExists ? selectedCompanyClientId : fallbackClientId;
  const selectedContextIsPublicPool = isRoot && isPublicPoolClientId(selectedClientId);
  const selectedClientExists = selectedContextIsPublicPool || data.clients.some((client) => client.id === selectedClientId);
  const scopedClientId = isRoot ? (selectedClientExists ? selectedClientId : fallbackClientId) : activeClientId;
  const isPublicPoolContext = isRoot && isPublicPoolClientId(scopedClientId);
  const currentClient = getClient(data, scopedClientId);
  const selectedCompanyClient = getClient(data, scopedCompanyClientId);
  const sections = isPlatformMode ? platformSections : isPublicPoolContext ? publicPoolSections : clientSections;
  const accessMatrix = useMemo(() => buildAccessMatrix(data, scopedClientId, isRoot), [data, scopedClientId, isRoot]);
  const selectedAccessCell =
    accessMatrix.cells.find((cell) => cell.userId === selectedCell?.userId && cell.serviceId === selectedCell?.serviceId) ??
    accessMatrix.cells[0] ??
    null;

  useEffect(() => {
    if (!isRoot || activeSection !== "sync") return;

    let cancelled = false;

    fetchAdminAuthentikSyncHealth()
      .then((health) => {
        if (!cancelled) setAuthentikSyncHealth(health);
      })
      .catch(() => {
        if (!cancelled) setAuthentikSyncHealth(null);
      });

    return () => {
      cancelled = true;
    };
  }, [activeSection, data.syncStatuses, isRoot]);

  useEffect(() => {
    if (isRoot && !selectedClientExists && data.clients.length) {
      setSelectedClientId(data.clients[0].id);
      setSelectedCell(null);
    }
  }, [data.clients, isRoot, selectedClientExists]);

  useEffect(() => {
    if (isRoot && !selectedCompanyClientExists && data.clients.length) {
      setSelectedCompanyClientId(data.clients[0].id);
    }
  }, [data.clients, isRoot, selectedCompanyClientExists]);

  useEffect(() => {
    if (activeSection && !sections.some((section) => section.id === activeSection)) {
      setActiveSection(sections[0]?.id ?? null);
    }
  }, [activeSection, sections]);

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") onClose();
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [onClose]);

  useEffect(() => {
    if (!activeSection) setIsContentFullscreen(false);
  }, [activeSection]);

  return (
    <div
      className={cn(
        "admin-panel-layer",
        activeSection && "admin-panel-layer--content-open",
        isContentFullscreen && "admin-panel-layer--fullscreen"
      )}
    >
      <aside className="admin-panel-nav">
        <div className="admin-panel-nav__head">
          <div>
            <p className="eyebrow">NODE.DC</p>
            <h2>{isPlatformMode ? "Платформа" : "Администрирование"}</h2>
          </div>
          <IconButton label={isPlatformMode ? "Закрыть платформу" : "Закрыть администрирование"} className="admin-panel-close" onClick={onClose}>
            <X size={15} strokeWidth={1.45} />
          </IconButton>
        </div>

        {isPlatformMode ? (
          <div className="admin-panel-client-select admin-panel-client-select--static">
            <span className="admin-panel-client-select__icon">
              <ShieldCheck size={16} />
            </span>
            <span className="admin-panel-client-select__name">Root Admin</span>
          </div>
        ) : isRoot ? (
          <div className="admin-panel-context-switcher">
            <button
              className={cn("admin-panel-client-select", selectedContextIsPublicPool && "admin-panel-client-select--active")}
              type="button"
              aria-pressed={selectedContextIsPublicPool}
              onClick={() => {
                setSelectedClientId(PUBLIC_POOL_CLIENT_ID);
                setSelectedCell(null);
              }}
            >
              <span className="admin-panel-client-select__icon">
                <Globe2 size={16} />
              </span>
              <span className="admin-panel-client-select__body">
                <span className="admin-panel-client-select__name">{PUBLIC_POOL_CONTEXT_LABEL}</span>
                <span className="admin-panel-client-select__description">{PUBLIC_POOL_CONTEXT_DESCRIPTION}</span>
              </span>
            </button>

            <div className="admin-panel-context-group">
              <span className="admin-panel-context-group__label">Компании</span>
              <NodeDcSelect
                value={scopedCompanyClientId}
                options={data.clients.map((client) => ({ value: client.id, label: client.name, description: client.legalName ?? undefined }))}
                label="Выбрать компанию"
                searchable
                minMenuWidth={292}
                onChange={(clientId) => {
                  setSelectedCompanyClientId(clientId);
                  setSelectedClientId(clientId);
                  setSelectedCell(null);
                }}
                trigger={({ open, selectedOption, toggle, setTriggerRef }) => (
                  <div
                    ref={setTriggerRef}
                    className={cn(
                      "admin-panel-client-select",
                      "admin-panel-client-select--company",
                      !selectedContextIsPublicPool && "admin-panel-client-select--active"
                    )}
                    aria-expanded={open}
                  >
                    <button
                      className="admin-panel-client-select__main"
                      type="button"
                      aria-pressed={!selectedContextIsPublicPool}
                      onClick={() => {
                        setSelectedClientId(scopedCompanyClientId);
                        setSelectedCell(null);
                      }}
                    >
                      <span className="admin-panel-client-select__icon">
                        <Building2 size={16} />
                      </span>
                      <span className="admin-panel-client-select__body">
                        <span className="admin-panel-client-select__name">{selectedOption?.label ?? selectedCompanyClient.name}</span>
                        {selectedOption?.description ? (
                          <span className="admin-panel-client-select__description">{selectedOption.description}</span>
                        ) : null}
                      </span>
                    </button>
                    <button
                      className="admin-panel-client-select__toggle"
                      type="button"
                      aria-label="Выбрать другую компанию"
                      aria-expanded={open}
                      onClick={(event) => {
                        event.stopPropagation();
                        toggle();
                      }}
                    >
                      <span className="admin-panel-client-select__chevron" aria-hidden="true" />
                    </button>
                  </div>
                )}
              />
            </div>
          </div>
        ) : (
          <div className="admin-panel-client-select">
            <span className="admin-panel-client-select__icon">
              <Building2 size={16} />
            </span>
            <span className="admin-panel-client-select__name">{currentClient.name}</span>
          </div>
        )}

        <nav className="admin-panel-nav-list">
          {sections.map((section) => (
            <button
              key={section.id}
              className={cn("admin-panel-nav-item", activeSection === section.id && "admin-panel-nav-item--active")}
              type="button"
              onClick={() => setActiveSection((current) => (current === section.id ? null : section.id))}
            >
              <span className="admin-panel-nav-item__icon">{section.icon}</span>
              <span>{section.label}</span>
            </button>
          ))}
        </nav>

        <div className="admin-panel-role">
          <span className="admin-panel-nav-item__icon">
            <ShieldCheck size={16} />
          </span>
          <span>{roleLabel(me.launcherRole)}</span>
        </div>
      </aside>

      {activeSection ? (
        <section className="admin-panel-content">
          <AdminHeader
            isFullscreen={isContentFullscreen}
            onToggleFullscreen={() => setIsContentFullscreen((current) => !current)}
            onCloseContent={() => setActiveSection(null)}
          >
            {isPublicPoolContext && activeSection === "invites" && publicInviteTab === "incoming" ? (
              <InviteSourceFilterTabs
                value={publicInviteSourceFilter}
                onChange={(sourceFilter) =>
                  setPublicInviteSourceFilter((current) => (current === sourceFilter ? "all" : sourceFilter))
                }
              />
            ) : null}
          </AdminHeader>
          <div className="admin-panel-content__body">
            {activeSection === "overview" ? (
              <OverviewSection
                data={data}
                clientId={scopedClientId}
                isPlatformMode={isPlatformMode}
                isPublicPoolContext={isPublicPoolContext}
                taskManagerWorkspaces={taskManagerWorkspaces}
                taskManagerWorkspacesLoading={taskManagerWorkspacesLoading}
                taskManagerWorkspacesError={taskManagerWorkspacesError}
                onRefreshTaskManagerWorkspaces={onRefreshTaskManagerWorkspaces}
                onUpdateClient={onUpdateClient}
              />
            ) : null}
            {activeSection === "clients" && isPlatformMode ? (
              <ClientsSection
                data={data}
                taskManagerWorkspaces={taskManagerWorkspaces}
                taskManagerWorkspacesLoading={taskManagerWorkspacesLoading}
                taskManagerWorkspacesError={taskManagerWorkspacesError}
                onRefreshTaskManagerWorkspaces={onRefreshTaskManagerWorkspaces}
                onCreateClient={onCreateClient}
                onUpdateClient={onUpdateClient}
                onDeleteClient={onDeleteClient}
              />
            ) : null}
            {activeSection === "users" ? (
              isPlatformMode ? (
                <PlatformUsersSection data={data} onUpdateUser={onUpdateUser} onDeleteUser={onDeleteUser} />
              ) : (
                <UsersSection
                  data={data}
                  clientId={scopedClientId}
                  onCreateUser={onCreateUser}
                  onUpdateUser={onUpdateUser}
                />
              )
            ) : null}
            {activeSection === "groups" ? (
              <GroupsSection
                data={data}
                clientId={scopedClientId}
                me={me}
                onCreateGroup={onCreateGroup}
                onUpdateGroup={onUpdateGroup}
                onDeleteGroup={onDeleteGroup}
              />
            ) : null}
            {activeSection === "services" && isRoot ? (
              <ServicesSection
                data={data}
                isPublicPoolContext={isPublicPoolContext}
                onUpdateService={onUpdateService}
                onReorderServices={onReorderServices}
                onCreateService={onCreateService}
                onDeleteService={onDeleteService}
              />
            ) : null}
            {activeSection === "access" ? (
              <AccessSection
                data={data}
                me={me}
                matrix={accessMatrix}
                selectedCell={selectedAccessCell}
                onSelectCell={(cell) => setSelectedCell({ userId: cell.userId, serviceId: cell.serviceId })}
                onSetUserServiceAccess={onSetUserServiceAccess}
                pendingAccessAssignments={pendingAccessAssignments}
                pendingServiceModuleEntitlements={pendingServiceModuleEntitlements}
                onUpdateUser={onUpdateUser}
                onUpdateMembership={onUpdateMembership}
                pendingTaskManagerMemberships={pendingTaskManagerMemberships}
                pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships}
                taskManagerWorkspaceCatalog={taskManagerWorkspaces}
                onSetTaskManagerWorkspaceMemberRole={onSetTaskManagerWorkspaceMemberRole}
                onSetTaskManagerProjectMemberRole={onSetTaskManagerProjectMemberRole}
                onSetServiceModuleEntitlement={onSetServiceModuleEntitlement}
              />
            ) : null}
            {activeSection === "invites" ? (
              <InvitesSection
                data={data}
                clientId={scopedClientId}
                actorUserId={me.user.id}
                isPublicPoolContext={isPublicPoolContext}
                publicInviteTab={publicInviteTab}
                publicInviteSourceFilter={publicInviteSourceFilter}
                onPublicInviteTabChange={setPublicInviteTab}
                onCreateInvite={onCreateInvite}
                onUpdateInvite={onUpdateInvite}
                onDeleteInvite={onDeleteInvite}
                onUpdateAccessRequest={onUpdateAccessRequest}
                onApproveAccessRequest={onApproveAccessRequest}
                onRejectAccessRequest={onRejectAccessRequest}
                onApproveTaskerInviteRequest={onApproveTaskerInviteRequest}
                onRejectTaskerInviteRequest={onRejectTaskerInviteRequest}
                onApproveEngineWorkflowAccessRequest={onApproveEngineWorkflowAccessRequest}
                onRejectEngineWorkflowAccessRequest={onRejectEngineWorkflowAccessRequest}
              />
            ) : null}
            {activeSection === "sync" ? (
              <SyncSection
                data={data}
                clientId={scopedClientId}
                isRoot={isRoot}
                authentikHealth={authentikSyncHealth}
                onRetrySync={onRetrySync}
              />
            ) : null}
            {activeSection === "audit" && isRoot ? <AuditSection data={data} /> : null}
            {activeSection === "misc" && isRoot ? <MiscSection data={data} onUpdateSettings={onUpdateSettings} /> : null}
          </div>
        </section>
      ) : null}
    </div>
  );
}

function AdminHeader({
  isFullscreen,
  onToggleFullscreen,
  onCloseContent,
  children,
}: {
  isFullscreen: boolean;
  onToggleFullscreen: () => void;
  onCloseContent: () => void;
  children?: ReactNode;
}) {
  return (
    <div className="admin-header">
      <div className="admin-header__filters">{children}</div>
      <div className="admin-header__actions">
        <IconButton
          label={isFullscreen ? "Свернуть панель" : "Открыть панель на весь экран"}
          className={cn("admin-circle-action", isFullscreen && "admin-circle-action--active")}
          type="button"
          onClick={onToggleFullscreen}
          aria-pressed={isFullscreen}
        >
          {isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
        </IconButton>
        <IconButton label="Закрыть панель раздела" className="admin-circle-action admin-content-close" type="button" onClick={onCloseContent}>
          <X size={15} strokeWidth={1.45} />
        </IconButton>
      </div>
    </div>
  );
}

function InviteSourceFilterTabs({
  value,
  onChange,
}: {
  value: PublicInviteSourceFilter;
  onChange: (sourceFilter: Exclude<PublicInviteSourceFilter, "all">) => void;
}) {
  const options: Array<{ id: Exclude<PublicInviteSourceFilter, "all">; label: string }> = [
    { id: "ndc", label: "NDC" },
    { id: "ops", label: "OPS" },
    { id: "engine", label: "ENGINE" },
  ];

  return (
    <div className="admin-header-source-tabs" aria-label="Тип входящих инвайтов">
      {options.map((option) => (
        <button
          key={option.id}
          type="button"
          className={cn("admin-header-source-tab", value === option.id && "admin-header-source-tab--active")}
          aria-pressed={value === option.id}
          onClick={() => onChange(option.id)}
        >
          {option.label}
        </button>
      ))}
    </div>
  );
}

function OverviewSection({
  data,
  clientId,
  isPlatformMode,
  isPublicPoolContext,
  taskManagerWorkspaces,
  taskManagerWorkspacesLoading,
  taskManagerWorkspacesError,
  onRefreshTaskManagerWorkspaces,
  onUpdateClient,
}: {
  data: LauncherData;
  clientId: string;
  isPlatformMode: boolean;
  isPublicPoolContext: boolean;
  taskManagerWorkspaces: TaskManagerWorkspaceSummary[];
  taskManagerWorkspacesLoading: boolean;
  taskManagerWorkspacesError: string | null;
  onRefreshTaskManagerWorkspaces: () => void;
  onUpdateClient: (clientId: string, patch: Partial<Client>) => void;
}) {
  const client = getClient(data, clientId);
  const clientUsers = getClientUsers(data, clientId);
  const clientServiceCount = data.grants.filter((grant) => grant.targetType === "client" && grant.targetId === clientId).length;
  const syncErrors = data.syncStatuses.filter((sync) => sync.state === "error" && (isPlatformMode || sync.objectId === clientId)).length;
  const newAccessRequests = data.accessRequests.filter((request) => request.status === "new").length;
  const approvedAccessRequests = data.accessRequests.filter((request) => request.status === "approved").length;
  const publicInvites = data.invites.filter((invite) => invite.clientId === PUBLIC_POOL_CLIENT_ID).length;

  if (isPublicPoolContext) {
    return (
      <section className="admin-section-grid">
        <MetricCard label="Входящих заявок" value={newAccessRequests} hint="Ожидают approve" danger={newAccessRequests > 0} />
        <MetricCard label="Public участников" value={clientUsers.length} hint="Открытый контур" />
        <MetricCard label="Public инвайтов" value={publicInvites} hint="Исходящие ссылки" />
        <MetricCard label="Подтверждено" value={approvedAccessRequests} hint="Заявки public pool" />

        <GlassSurface className="admin-wide-card">
          <h3>Public access pool</h3>
          <p className="admin-helper-note">
            Это системный контур для свободных запросов доступа. Заявки можно оставить в открытом контуре или вручную назначить
            в enterprise-клиента перед выпуском инвайта.
          </p>
          <div className="activity-list">
            {data.auditEvents.slice(0, 5).map((event) => (
              <div key={event.id} className="activity-row">
                <span>{formatDateTime(event.at)}</span>
                <strong>{event.action}</strong>
                <em>{event.objectName}</em>
              </div>
            ))}
          </div>
        </GlassSurface>
      </section>
    );
  }

  if (isPlatformMode) {
    return (
      <section className="admin-section-grid">
        <MetricCard label="Компаний" value={data.clients.length} hint="Контуры платформы" />
        <MetricCard label="Активных сервисов" value={data.services.filter((service) => service.status === "active").length} hint="В каталоге" />
        <MetricCard label="Клиентских грантов" value={data.grants.filter((grant) => grant.targetType === "client").length} hint="Назначения контуров" />
        <MetricCard label="Ошибок sync" value={syncErrors} hint="Требуют внимания" danger={syncErrors > 0} />

        <GlassSurface className="admin-wide-card">
          <h3>Последние действия платформы</h3>
          <div className="activity-list">
            {data.auditEvents.slice(0, 5).map((event) => (
              <div key={event.id} className="activity-row">
                <span>{formatDateTime(event.at)}</span>
                <strong>{event.action}</strong>
                <em>{event.objectName}</em>
              </div>
            ))}
          </div>
        </GlassSurface>
      </section>
    );
  }

  return (
    <section className="admin-section-grid">
      <MetricCard label="Участников" value={clientUsers.length} hint="Текущий контур" />
      <MetricCard label="Активных сервисов" value={data.services.filter((service) => service.status === "active").length} hint="В каталоге" />
      <MetricCard label="Подключений клиента" value={clientServiceCount} hint="Гранты клиента" />
      <MetricCard label="Ошибок sync" value={syncErrors} hint="Требуют внимания" danger={syncErrors > 0} />

      <GlassSurface className="admin-wide-card client-profile-card">
        <div className="table-toolbar client-profile-card__head">
          <div>
            <p className="eyebrow">Клиент</p>
            <h3>Профиль контура</h3>
          </div>
          <IconButton
            label={taskManagerWorkspacesLoading ? "Обновляем workspace Operational Core" : "Обновить workspace Operational Core"}
            className="admin-circle-action admin-circle-action--solid"
            type="button"
            disabled={taskManagerWorkspacesLoading}
            onClick={onRefreshTaskManagerWorkspaces}
          >
            <RefreshCw size={16} />
          </IconButton>
        </div>
        <ClientProfileEditorForm
          client={client}
          taskManagerWorkspaces={taskManagerWorkspaces}
          taskManagerWorkspacesError={taskManagerWorkspacesError}
          onSave={(patch) => onUpdateClient(client.id, patch)}
        />
      </GlassSurface>

      <GlassSurface className="admin-wide-card">
        <h3>Последние действия</h3>
        <div className="activity-list">
          {data.auditEvents.slice(0, 5).map((event) => (
            <div key={event.id} className="activity-row">
              <span>{formatDateTime(event.at)}</span>
              <strong>{event.action}</strong>
              <em>{event.objectName}</em>
            </div>
          ))}
        </div>
      </GlassSurface>
    </section>
  );
}

function ClientsSection({
  data,
  taskManagerWorkspaces,
  taskManagerWorkspacesLoading,
  taskManagerWorkspacesError,
  onRefreshTaskManagerWorkspaces,
  onCreateClient,
  onUpdateClient,
  onDeleteClient,
}: {
  data: LauncherData;
  taskManagerWorkspaces: TaskManagerWorkspaceSummary[];
  taskManagerWorkspacesLoading: boolean;
  taskManagerWorkspacesError: string | null;
  onRefreshTaskManagerWorkspaces: () => void;
  onCreateClient: () => void;
  onUpdateClient: (clientId: string, patch: Partial<Client>) => void;
  onDeleteClient: (clientId: string) => void;
}) {
  const [editingClientId, setEditingClientId] = useState<string | null>(null);
  const editingClient = data.clients.find((client) => client.id === editingClientId) ?? null;

  return (
    <>
      <GlassSurface className="table-shell">
        <div className="table-toolbar">
          <h3>Компании</h3>
          <IconButton label="Добавить компанию" className="admin-circle-action admin-circle-action--solid" type="button" onClick={onCreateClient}>
            <Plus size={17} />
          </IconButton>
        </div>
        <table className="admin-data-table">
          <thead>
            <tr>
              <th>Название</th>
              <th>Тип</th>
              <th>Статус</th>
              <th>Участников</th>
              <th>Demo</th>
              <th>Контакт</th>
              <th aria-label="Редактирование" />
            </tr>
          </thead>
          <tbody>
            {data.clients.map((client) => (
              <tr key={client.id}>
                <td className="services-admin-table__service">
                  <input
                    className="admin-table-input admin-table-input--strong"
                    value={client.name}
                    onChange={(event) => onUpdateClient(client.id, { name: event.target.value })}
                    aria-label={`Название клиента ${client.name}`}
                  />
                  <input
                    className="admin-table-input admin-table-input--muted"
                    value={client.legalName ?? ""}
                    onChange={(event) => onUpdateClient(client.id, { legalName: event.target.value || null })}
                    aria-label={`Юридическое название ${client.name}`}
                  />
                </td>
                <td>
                  <NodeDcSelect
                    className="admin-table-select-wrap"
                    triggerClassName="admin-table-select-trigger"
                    value={client.type}
                    options={clientTypeOptions}
                    label={`Тип клиента ${client.name}`}
                    minMenuWidth={156}
                    onChange={(type) => onUpdateClient(client.id, { type })}
                  />
                </td>
                <td>
                  <AdminStatusDropdown
                    value={client.status}
                    options={clientStatusOptions}
                    label={`Статус клиента ${client.name}`}
                    onChange={(status) => onUpdateClient(client.id, { status })}
                  />
                </td>
                <td>{data.memberships.filter((membership) => membership.clientId === client.id).length}</td>
                <td>
                  <NodeDcDateField
                    value={client.demoEndsAt ?? null}
                    label={`Demo до ${client.name}`}
                    onChange={(value) => onUpdateClient(client.id, { demoEndsAt: value })}
                  />
                </td>
                <td>
                  <input
                    className="admin-table-input"
                    value={client.contactEmail ?? ""}
                    onChange={(event) => onUpdateClient(client.id, { contactEmail: event.target.value || null })}
                    aria-label={`Контакт клиента ${client.name}`}
                  />
                </td>
                <td className="services-admin-table__actions">
                  <IconButton
                    label={`Редактировать клиента ${client.name}`}
                    className="admin-icon-action services-admin-table__edit"
                    type="button"
                    onClick={() => setEditingClientId(client.id)}
                  >
                    <Edit3 size={12} />
                  </IconButton>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </GlassSurface>

      {editingClient ? (
        <ClientEditorModal
          client={editingClient}
          taskManagerWorkspaces={taskManagerWorkspaces}
          taskManagerWorkspacesLoading={taskManagerWorkspacesLoading}
          taskManagerWorkspacesError={taskManagerWorkspacesError}
          onRefreshTaskManagerWorkspaces={onRefreshTaskManagerWorkspaces}
          onClose={() => setEditingClientId(null)}
          onSave={(patch) => {
            onUpdateClient(editingClient.id, patch);
            setEditingClientId(null);
          }}
          onDelete={() => {
            onDeleteClient(editingClient.id);
            setEditingClientId(null);
          }}
          canDelete={data.clients.length > 1}
        />
      ) : null}
    </>
  );
}

function UsersSection({
  data,
  clientId,
  onCreateUser,
  onUpdateUser,
}: {
  data: LauncherData;
  clientId: string;
  onCreateUser: (command: CreateUserCommand) => void;
  onUpdateUser: (userId: string, patch: Partial<LauncherUser>) => void;
}) {
  const [newUserEmail, setNewUserEmail] = useState("");
  const [newUserName, setNewUserName] = useState("");
  const [newUserRole, setNewUserRole] = useState<ClientMembershipRole>("member");
  const [newUserGroupId, setNewUserGroupId] = useState<string>("none");
  const rows = data.memberships
    .filter((membership) => membership.clientId === clientId)
    .map((membership) => ({ membership, user: getUser(data, membership.userId), client: getClient(data, membership.clientId) }));
  const clientGroups = data.groups.filter((group) => group.clientId === clientId);
  const groupOptions: Array<NodeDcSelectOption<string>> = [
    { value: "none", label: "Без группы" },
    ...clientGroups.map((group) => ({ value: group.id, label: group.name })),
  ];

  function handleCreateUser() {
    const email = newUserEmail.trim();

    if (!email) {
      return;
    }

    onCreateUser({
      clientId,
      email,
      name: newUserName.trim() || undefined,
      role: newUserRole,
      groupIds: newUserGroupId === "none" ? [] : [newUserGroupId],
      provisionAuth: true,
      generatePassword: true,
    });
    setNewUserEmail("");
    setNewUserName("");
    setNewUserRole("member");
    setNewUserGroupId("none");
  }

  return (
    <>
      <GlassSurface className="invite-form invite-form--compact">
        <div className="table-toolbar">
          <div>
            <p className="eyebrow">Launcher → Authentik</p>
            <h3>Создать участника</h3>
          </div>
          <IconButton
            label="Создать участника"
            className="admin-circle-action admin-circle-action--solid"
            type="button"
            disabled={!newUserEmail.trim()}
            onClick={handleCreateUser}
          >
            <Plus size={17} />
          </IconButton>
        </div>
        <div className="invite-form__fields">
          <input value={newUserEmail} onChange={(event) => setNewUserEmail(event.target.value)} placeholder="email@company.ru" />
          <input value={newUserName} onChange={(event) => setNewUserName(event.target.value)} placeholder="Имя пользователя" />
          <NodeDcSelect
            className="admin-table-select-wrap"
            triggerClassName="admin-modal-select-trigger"
            value={newUserRole}
            options={membershipRoleOptions}
            label="Роль"
            onChange={(role) => setNewUserRole(role)}
          />
          <NodeDcSelect
            className="admin-table-select-wrap"
            triggerClassName="admin-modal-select-trigger"
            value={newUserGroupId}
            options={groupOptions}
            label="Группа"
            onChange={(groupId) => setNewUserGroupId(groupId)}
          />
        </div>
      </GlassSurface>

      <GlassSurface className="table-shell table-shell--users">
        <div className="table-toolbar">
          <h3>Участники</h3>
        </div>
        <table className="admin-data-table admin-data-table--users">
          <thead>
            <tr>
              <th>Пользователь</th>
              <th>Клиент</th>
              <th>Кто пригласил</th>
              <th>Телефон</th>
              <th>Должность</th>
              <th>Заметки</th>
              <th>Статус аккаунта</th>
            </tr>
          </thead>
          <tbody>
            {rows.map(({ membership, user, client }) => {
              const protectedUser = user.id === "user_root";

              return (
                <tr key={membership.id}>
                  <td className="admin-user-cell">
                    <div className="admin-user-cell__fields">
                      <input
                        className="admin-table-input admin-table-input--strong"
                        value={user.name}
                        onChange={(event) => onUpdateUser(user.id, { name: event.target.value })}
                        aria-label={`Имя пользователя ${user.name}`}
                      />
                      <input
                        className="admin-table-input admin-table-input--muted"
                        value={user.email}
                        onChange={(event) => onUpdateUser(user.id, { email: event.target.value })}
                        aria-label={`Email пользователя ${user.name}`}
                      />
                    </div>
                  </td>
                  <td>{client.name}</td>
                  <td>
                    <MembershipInviterCell data={data} membership={membership} />
                  </td>
                  <td>
                    <input
                      className="admin-table-input"
                      value={user.phone ?? ""}
                      onChange={(event) => onUpdateUser(user.id, { phone: event.target.value || null })}
                      placeholder="—"
                      aria-label={`Телефон пользователя ${user.name}`}
                    />
                  </td>
                  <td>
                    <input
                      className="admin-table-input"
                      value={user.position ?? ""}
                      onChange={(event) => onUpdateUser(user.id, { position: event.target.value || null })}
                      placeholder="—"
                      aria-label={`Должность пользователя ${user.name}`}
                    />
                  </td>
                  <td>
                    <input
                      className="admin-table-input"
                      value={user.notes ?? ""}
                      onChange={(event) => onUpdateUser(user.id, { notes: event.target.value || null })}
                      placeholder="—"
                      aria-label={`Заметки пользователя ${user.name}`}
                    />
                  </td>
                  <td>
                    {protectedUser ? (
                      <AdminStaticPill>{statusOptionLabel(userStatusOptions, user.globalStatus)}</AdminStaticPill>
                    ) : (
                      <AdminStatusDropdown
                        value={user.globalStatus}
                        options={userStatusOptions}
                        label={`Статус аккаунта ${user.name}`}
                        onChange={(status) => onUpdateUser(user.id, { globalStatus: status })}
                      />
                    )}
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </GlassSurface>
    </>
  );
}

function PlatformUsersSection({
  data,
  onUpdateUser,
  onDeleteUser,
}: {
  data: LauncherData;
  onUpdateUser: (userId: string, patch: Partial<LauncherUser>) => void;
  onDeleteUser: (userId: string) => void;
}) {
  const [deleteUserId, setDeleteUserId] = useState<string | null>(null);
  const deletingUser = data.users.find((user) => user.id === deleteUserId) ?? null;
  const rows = data.users
    .map((user) => {
      const memberships = data.memberships.filter((membership) => membership.userId === user.id);
      return {
        user,
        memberships,
        origin: getPlatformUserOrigin(data, user, memberships),
      };
    })
    .sort((left, right) => {
      if (left.user.id === "user_root") return -1;
      if (right.user.id === "user_root") return 1;
      return left.origin.label.localeCompare(right.origin.label, "ru") || left.user.name.localeCompare(right.user.name, "ru");
    });

  return (
    <>
      <GlassSurface className="table-shell table-shell--sticky-user-column table-shell--platform-users">
        <div className="table-toolbar">
          <div>
            <h3>Пользователи платформы</h3>
            <p className="admin-helper-note">
              Глобальный реестр аккаунтов. Доступы к сервисам остаются в матрицах; здесь — происхождение, статус аккаунта и полное удаление.
            </p>
          </div>
        </div>
        <table className="admin-data-table admin-data-table--sticky-user-column admin-data-table--platform-users">
          <thead>
            <tr>
              <th>Пользователь</th>
              <th>Происхождение</th>
              <th>Контуры</th>
              <th>Статус аккаунта</th>
              <th>Создан</th>
              <th aria-label="Удаление" />
            </tr>
          </thead>
          <tbody>
            {rows.map(({ user, memberships, origin }) => {
              const protectedUser = user.id === "user_root";
              const contextLabel =
                memberships.length === 0
                  ? "—"
                  : memberships
                      .map((membership) => getClient(data, membership.clientId).name)
                      .filter((value, index, list) => list.indexOf(value) === index)
                      .join(", ");

              return (
                <tr key={user.id}>
                  <td className="admin-user-cell">
                    <div className="admin-user-cell__fields">
                      <input
                        className="admin-table-input admin-table-input--strong"
                        value={user.name}
                        disabled={protectedUser}
                        onChange={(event) => onUpdateUser(user.id, { name: event.target.value })}
                        aria-label={`Имя пользователя ${user.name}`}
                      />
                      <input
                        className="admin-table-input admin-table-input--muted"
                        value={user.email}
                        disabled={protectedUser}
                        onChange={(event) => onUpdateUser(user.id, { email: event.target.value })}
                        aria-label={`Email пользователя ${user.name}`}
                      />
                    </div>
                  </td>
                  <td>
                    <div className="platform-user-origin">
                      <strong>{origin.label}</strong>
                      {origin.detail ? <small>{origin.detail}</small> : null}
                    </div>
                  </td>
                  <td>
                    <span className="admin-table-text" title={contextLabel}>
                      {contextLabel}
                    </span>
                  </td>
                  <td>
                    {protectedUser ? (
                      <AdminStaticPill>{statusOptionLabel(userStatusOptions, user.globalStatus)}</AdminStaticPill>
                    ) : (
                      <AdminStatusDropdown
                        value={user.globalStatus}
                        options={userStatusOptions}
                        label={`Статус аккаунта ${user.name}`}
                        onChange={(status) => onUpdateUser(user.id, { globalStatus: status })}
                      />
                    )}
                  </td>
                  <td>{formatDate(user.createdAt)}</td>
                  <td className="services-admin-table__actions">
                    {protectedUser ? (
                      <span className="admin-table-action-placeholder" />
                    ) : (
                      <IconButton
                        label={`Удалить пользователя ${user.email} полностью`}
                        className="admin-icon-action invite-icon-action"
                        type="button"
                        onClick={() => setDeleteUserId(user.id)}
                      >
                        <Trash2 size={12} />
                      </IconButton>
                    )}
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </GlassSurface>

      <NodeDcDeleteModal
        isOpen={Boolean(deletingUser)}
        title="Удалить пользователя полностью"
        description={
          <>
            Будут удалены профиль <strong>{deletingUser?.email}</strong>, членства, сервисные доступы, заявки, инвайты и sync-записи.
            Действие необратимо.
          </>
        }
        confirmLabel="Удалить везде"
        onClose={() => setDeleteUserId(null)}
        onConfirm={() => {
          if (deletingUser) onDeleteUser(deletingUser.id);
          setDeleteUserId(null);
        }}
      />
    </>
  );
}

function GroupsSection({
  data,
  clientId,
  me,
  onCreateGroup,
  onUpdateGroup,
  onDeleteGroup,
}: {
  data: LauncherData;
  clientId: string;
  me: MeResponse;
  onCreateGroup: (clientId: string) => void;
  onUpdateGroup: (groupId: string, patch: Partial<ClientGroup>) => void;
  onDeleteGroup: (groupId: string) => void;
}) {
  const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
  const groups = data.groups.filter((group) => group.clientId === clientId);
  const editingGroup = groups.find((group) => group.id === editingGroupId) ?? null;

  return (
    <>
      <GlassSurface className="table-shell">
        <div className="table-toolbar">
          <h3>Группы</h3>
          <IconButton label="Создать группу" className="admin-circle-action admin-circle-action--solid" type="button" onClick={() => onCreateGroup(clientId)}>
            <Plus size={17} />
          </IconButton>
        </div>
        <table className="admin-data-table">
          <thead>
            <tr>
              <th>Название</th>
              <th>Описание</th>
              <th>Участников</th>
              <th>Подключённые сервисы</th>
              <th aria-label="Редактирование" />
            </tr>
          </thead>
          <tbody>
            {groups.map((group) => (
              <tr key={group.id}>
                <td>
                  <input
                    className="admin-table-input admin-table-input--strong"
                    value={group.name}
                    onChange={(event) => onUpdateGroup(group.id, { name: event.target.value })}
                    aria-label={`Название группы ${group.name}`}
                  />
                </td>
                <td>
                  <input
                    className="admin-table-input"
                    value={group.description ?? ""}
                    onChange={(event) => onUpdateGroup(group.id, { description: event.target.value || null })}
                    aria-label={`Описание группы ${group.name}`}
                  />
                </td>
                <td>{group.memberIds.length}</td>
                <td>{data.grants.filter((grant) => grant.targetType === "group" && grant.targetId === group.id).length}</td>
                <td className="services-admin-table__actions">
                  <IconButton
                    label={`Редактировать группу ${group.name}`}
                    className="admin-icon-action services-admin-table__edit"
                    type="button"
                    onClick={() => setEditingGroupId(group.id)}
                  >
                    <Edit3 size={12} />
                  </IconButton>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </GlassSurface>

      {editingGroup ? (
        <GroupEditorModal
          group={editingGroup}
          me={me}
          users={data.memberships
            .filter((membership) => membership.clientId === clientId)
            .map((membership) => getUser(data, membership.userId))}
          onClose={() => setEditingGroupId(null)}
          onSave={(patch) => {
            onUpdateGroup(editingGroup.id, patch);
            setEditingGroupId(null);
          }}
          onDelete={() => {
            onDeleteGroup(editingGroup.id);
            setEditingGroupId(null);
          }}
        />
      ) : null}
    </>
  );
}

type AdminStatusTone = "green" | "yellow" | "red" | "violet" | "muted";
type AdminStatusOption<T extends string> = { value: T; label: string; tone: AdminStatusTone };

const serviceStatusOptions: Array<AdminStatusOption<ServiceStatus>> = [
  { value: "active", label: "Активен", tone: "green" },
  { value: "maintenance", label: "Техработы", tone: "yellow" },
  { value: "hidden", label: "Скрыт", tone: "violet" },
  { value: "disabled", label: "Отключён", tone: "red" },
];

const clientTypeOptions: Array<NodeDcSelectOption<ClientType>> = [
  { value: "company", label: "Компания" },
  { value: "person", label: "Частное лицо" },
];

const membershipRoleOptions: Array<NodeDcSelectOption<ClientMembershipRole>> = [
  { value: "client_owner", label: "Owner", description: "Владелец клиента" },
  { value: "client_admin", label: "Admin", description: "Администратор клиента" },
  { value: "member", label: "Member", description: "Пользователь" },
];

const coreAssistantAccessRoleOptions: Array<NodeDcSelectOption<CoreAssistantAccessRole>> = [
  { value: "blocked", label: "Нет доступа", description: "Ассистент скрыт и недоступен", tone: "red" },
  { value: "member", label: "Есть доступ", description: "Личный ассистент в разрешённых контурах", tone: "green" },
  { value: "admin", label: "Админ", description: "Admin-команды в рамках Launcher scope", tone: "violet" },
];

const inviteRoleOptions: Array<NodeDcSelectOption<ClientMembershipRole>> = [
  { value: "member", label: "Member" },
  { value: "client_admin", label: "Client Admin" },
];

const clientStatusOptions: Array<AdminStatusOption<ClientStatus>> = [
  { value: "active", label: "Активен", tone: "green" },
  { value: "demo", label: "Demo", tone: "yellow" },
  { value: "suspended", label: "Приостановлен", tone: "red" },
  { value: "expired", label: "Истёк", tone: "red" },
];

const userStatusOptions: Array<AdminStatusOption<LauncherUserStatus>> = [
  { value: "active", label: "Активен", tone: "green" },
  { value: "invited", label: "Приглашён", tone: "yellow" },
  { value: "blocked", label: "Заблокирован", tone: "red" },
];

const mainStatusOptions: Array<NodeDcSelectOption<LauncherUserStatus>> = [
  { value: "active", label: "Активен", tone: "green" },
  { value: "blocked", label: "Заблокирован", tone: "red" },
  { value: "invited", label: "Приглашён", tone: "yellow", hidden: true },
];

const membershipStatusOptions: Array<AdminStatusOption<ClientMembershipStatus>> = [
  { value: "active", label: "Включён", tone: "green" },
  { value: "disabled", label: "Отключён", tone: "red" },
];

const inviteStatusOptions: Array<AdminStatusOption<InviteStatus>> = [
  { value: "created", label: "Создан", tone: "muted" },
  { value: "sent", label: "Отправлен", tone: "green" },
  { value: "accepted", label: "Принят", tone: "green" },
  { value: "expired", label: "Истёк", tone: "red" },
  { value: "revoked", label: "Отозван", tone: "red" },
];
const editableInviteStatusOptions: Array<AdminStatusOption<InviteStatus>> = inviteStatusOptions.filter(
  (option) => option.value !== "accepted"
);

const accessRequestStatusOptions: Array<AdminStatusOption<AccessRequestStatus>> = [
  { value: "new", label: "Входящая", tone: "yellow" },
  { value: "approved", label: "Подтверждено", tone: "green" },
  { value: "rejected", label: "Отклонено", tone: "red" },
];

const taskerInviteRequestStatusOptions: Array<AdminStatusOption<TaskerInviteRequest["status"]>> = [
  { value: "new", label: "Ожидает", tone: "yellow" },
  { value: "approved", label: "Подтверждено", tone: "green" },
  { value: "rejected", label: "Отклонено", tone: "red" },
  { value: "cancelled", label: "Отозвано", tone: "red" },
];

const engineWorkflowAccessRequestStatusOptions: Array<AdminStatusOption<EngineWorkflowAccessRequest["status"]>> = [
  { value: "new", label: "Ожидает", tone: "yellow" },
  { value: "approved", label: "Подтверждено", tone: "green" },
  { value: "rejected", label: "Отклонено", tone: "red" },
  { value: "cancelled", label: "Отозвано", tone: "red" },
];

const syncStatusOptions: Array<AdminStatusOption<SyncState>> = [
  { value: "synced", label: "Синхронизировано", tone: "green" },
  { value: "pending", label: "В очереди", tone: "yellow" },
  { value: "error", label: "Ошибка", tone: "red" },
  { value: "disabled", label: "Отключено", tone: "muted" },
];

const authentikSyncHealthOptions: Array<AdminStatusOption<AuthentikSyncHealthState>> = [
  { value: "ok", label: "OK", tone: "green" },
  { value: "pending", label: "Очередь", tone: "yellow" },
  { value: "error", label: "Ошибка", tone: "red" },
  { value: "not_configured", label: "Не настроен", tone: "red" },
];

const auditResultOptions: Array<AdminStatusOption<"success" | "warning" | "error">> = [
  { value: "success", label: "Успех", tone: "green" },
  { value: "warning", label: "Внимание", tone: "yellow" },
  { value: "error", label: "Ошибка", tone: "red" },
];

const accessAssignmentOptions: Array<NodeDcSelectOption<AccessAssignmentValue>> = [
  { value: "unset", label: "—", description: "Не назначен" },
  { value: "viewer", label: "Гость", description: "Просмотр", tone: "green" },
  { value: "member", label: "Участник", description: "Рабочий доступ", tone: "green" },
  { value: "admin", label: "Админ", description: "Администрирование", tone: "green" },
  { value: "deny", label: "Заблокирован", description: "Запрет доступа", tone: "red" },
];

const operationalCoreAccessAssignmentOptions: Array<NodeDcSelectOption<AccessAssignmentValue>> = [
  { value: "unset", label: "—", description: "Не назначен" },
  { value: "viewer", label: "Гость", description: "Просмотр", tone: "green" },
  { value: "member", label: "Участник", description: "Рабочий доступ", tone: "green" },
  { value: "admin", label: "Админ", description: "Системная роль", tone: "green", disabled: true, hidden: true },
  { value: "deny", label: "Заблокирован", description: "Запрет доступа", tone: "red" },
];

const engineAccessAssignmentOptions: Array<NodeDcSelectOption<AccessAssignmentValue>> = [
  { value: "unset", label: "—", description: "Не назначен" },
  { value: "viewer", label: "Гость", description: "Только явно открытые workflow", tone: "green" },
  { value: "member", label: "Участник", description: "Рабочий доступ", tone: "green" },
  { value: "admin", label: "Админ", description: "Существующее значение", tone: "green", hidden: true },
  { value: "deny", label: "Заблокирован", description: "Запрет доступа", tone: "red" },
];

const publicOperationalCoreAccessOptions: Array<NodeDcSelectOption<AccessAssignmentValue>> = [
  { value: "unset", label: "—", description: "Не назначен", hidden: true },
  { value: "viewer", label: "Workspace Guest", description: "Выдаётся только через Tasker", tone: "green", hidden: true },
  { value: "member", label: "Workspace Member", description: "Доступ к приглашённому workspace", tone: "green" },
  { value: "admin", label: "Service Admin", description: "Системная роль", tone: "green", disabled: true, hidden: true },
  { value: "deny", label: "Заблокирован", description: "Запрет доступа", tone: "red" },
];

const operationalCoreRoleOptions: Array<NodeDcSelectOption<OperationalCoreRoleSelectValue>> = [
  ...operationalCoreAccessAssignmentOptions,
  { value: "pending", label: "Сохраняем...", disabled: true, hidden: true },
];

const taskManagerProjectRoleOptions: Array<NodeDcSelectOption<OperationalCoreRoleSelectValue>> = [
  { value: "unset", label: "—", description: "Не назначен" },
  { value: "viewer", label: "Гость", description: "Просмотр", tone: "green" },
  { value: "member", label: "Участник", description: "Рабочий доступ", tone: "green" },
  { value: "admin", label: "Админ", description: "Системная роль", tone: "green", disabled: true, hidden: true },
  { value: "pending", label: "Сохраняем...", disabled: true, hidden: true },
];

const operationalCoreModules: Array<{
  id: ServiceModuleId;
  title: string;
  description: string;
  enabledLabel: string;
  disabledLabel: string;
}> = [
  {
    id: "codex_agents",
    title: "Codex Agent API",
    description: "Разрешает пользователю подключать локального Codex-агента к своим разрешённым workspace/project в Operational Core.",
    enabledLabel: "Модуль включён",
    disabledLabel: "Модуль выключен",
  },
];

function membershipRoleLabel(role: ClientMembershipRole): string {
  return membershipRoleOptions.find((option) => option.value === role)?.label ?? role;
}

function resolveCoreAssistantRole(membership: ClientMembership): CoreAssistantAccessRole {
  return membership.coreAssistantRole ?? "member";
}

function coreAssistantAccessRoleLabel(role: CoreAssistantAccessRole): string {
  return coreAssistantAccessRoleOptions.find((option) => option.value === role)?.label ?? role;
}

function inviteSourceLabel(invite: Invite): string {
  if (invite.source === "access_request") return "Заявка доступа";
  if (invite.source === "tasker_workspace_invite") return "Workspace-инвайт";
  return "Ручной инвайт";
}

function inviteCanBeCopied(invite: Invite): boolean {
  return invite.status === "created" || invite.status === "sent";
}

function inviteTerminalLabel(invite: Invite): string {
  if (invite.status === "accepted") return "Пользователь зарегистрирован";
  if (invite.status === "revoked") return "Ссылка отозвана";
  if (invite.status === "expired") return "Срок истёк";
  return "Ссылка недоступна";
}

function statusOptionLabel<T extends string>(options: Array<AdminStatusOption<T>>, value: T): string {
  return options.find((option) => option.value === value)?.label ?? value;
}

function mainStatusLabel(value: LauncherUserStatus): string {
  return mainStatusOptions.find((option) => option.value === value)?.label ?? value;
}

function getClientTaskManagerWorkspaces(client: Client): ClientTaskManagerWorkspaceBinding[] {
  const taskManager = client.integrations?.taskManager;
  const bySlug = new Map<string, ClientTaskManagerWorkspaceBinding>();

  for (const workspace of taskManager?.workspaces ?? []) {
    if (!workspace.slug) continue;
    bySlug.set(workspace.slug, {
      slug: workspace.slug,
      name: workspace.name ?? null,
      isPrimary: workspace.isPrimary === true,
      managedBy: workspace.managedBy ?? "launcher",
    });
  }

  if (taskManager?.workspaceSlug && !bySlug.has(taskManager.workspaceSlug)) {
    bySlug.set(taskManager.workspaceSlug, {
      slug: taskManager.workspaceSlug,
      name: taskManager.workspaceName ?? null,
      isPrimary: true,
      managedBy: "launcher",
    });
  }

  const workspaces = [...bySlug.values()];
  if (!workspaces.length) return [];

  if (!workspaces.some((workspace) => workspace.isPrimary)) {
    workspaces[0].isPrimary = true;
  }

  let primarySeen = false;
  return workspaces.map((workspace) => {
    const isPrimary = workspace.isPrimary === true && !primarySeen;
    if (isPrimary) primarySeen = true;
    return { ...workspace, isPrimary };
  });
}

function getPrimaryTaskManagerWorkspace(client: Client): ClientTaskManagerWorkspaceBinding | null {
  const workspaces = getClientTaskManagerWorkspaces(client);
  return workspaces.find((workspace) => workspace.isPrimary) ?? workspaces[0] ?? null;
}

function formatClientTaskManagerWorkspaces(client: Client): string {
  const workspaces = getClientTaskManagerWorkspaces(client);
  if (!workspaces.length) return "—";

  return workspaces
    .map((workspace) => `${workspace.name ?? workspace.slug}${workspace.isPrimary ? " · основной" : ""}`)
    .join(", ");
}

function getTaskManagerMembershipRole(data: LauncherData, clientId: string, userId: string, workspaceSlug: string): TaskManagerWorkspaceMemberRole {
  return data.taskManagerMemberships.find((membership) => membership.clientId === clientId && membership.userId === userId && membership.workspaceSlug === workspaceSlug)?.role ?? "unset";
}

function getTaskManagerProjectMembershipRole(
  data: LauncherData,
  clientId: string,
  userId: string,
  workspaceSlug: string,
  projectId: string
): TaskManagerWorkspaceMemberRole {
  return (
    data.taskManagerProjectMemberships.find(
      (membership) =>
        membership.clientId === clientId &&
        membership.userId === userId &&
        membership.workspaceSlug === workspaceSlug &&
        membership.projectId === projectId
    )?.role ?? "unset"
  );
}

function getWorkspaceCatalogProjects(workspace: ClientTaskManagerWorkspaceBinding, catalog: TaskManagerWorkspaceSummary[]): TaskManagerProjectSummary[] {
  return catalog.find((item) => item.slug === workspace.slug)?.projects ?? [];
}

function normalizeClientTaskManagerWorkspaceDraft(workspaces: ClientTaskManagerWorkspaceBinding[]): ClientTaskManagerWorkspaceBinding[] {
  const bySlug = new Map<string, ClientTaskManagerWorkspaceBinding>();

  for (const workspace of workspaces) {
    if (!workspace.slug) continue;
    bySlug.set(workspace.slug, {
      slug: workspace.slug,
      name: workspace.name ?? null,
      isPrimary: workspace.isPrimary === true,
      managedBy: workspace.managedBy ?? "launcher",
    });
  }

  const normalized = [...bySlug.values()];
  if (!normalized.length) return [];

  if (!normalized.some((workspace) => workspace.isPrimary)) {
    normalized[0].isPrimary = true;
  }

  let primarySeen = false;
  return normalized.map((workspace) => {
    const isPrimary = workspace.isPrimary === true && !primarySeen;
    if (isPrimary) primarySeen = true;
    return { ...workspace, isPrimary };
  });
}

function AdminStaticPill({ children }: { children: ReactNode }) {
  return <span className="admin-static-pill">{children}</span>;
}

const taskManagerWorkspacePolicyOptions: Array<NodeDcSelectOption<TaskManagerWorkspaceCreationPolicy>> = [
  {
    value: "any_authorized_user",
    label: "Все с доступом",
    description: "Пользователь с доступом к Operational Core может создать собственный workspace.",
    tone: "green",
  },
  {
    value: "task_admins_only",
    label: "Только админы",
    description: "Workspace создают только суперпользователь и админы Operational Core.",
    tone: "yellow",
  },
  {
    value: "disabled",
    label: "Отключено",
    description: "Создание workspace закрыто для всех через платформенную policy.",
    tone: "red",
  },
];

const mediaAccept = "image/*,video/*,.gif,.webm,.mov,.mp4,.m4v,.avi,.mkv";
const modalActionAccentRgb = [247, 248, 244] as const;

type EntityModalDeleteConfig = {
  label: string;
  title: string;
  description: ReactNode;
  onConfirm: () => void;
};

function ServicesSection({
  data,
  isPublicPoolContext,
  onUpdateService,
  onReorderServices,
  onCreateService,
  onDeleteService,
}: {
  data: LauncherData;
  isPublicPoolContext: boolean;
  onUpdateService: (serviceId: string, patch: Partial<Service>) => Promise<AdminMutationOutcome>;
  onReorderServices: (orderedServiceIds: string[]) => void;
  onCreateService: () => void;
  onDeleteService: (serviceId: string) => void;
}) {
  const [contentServiceId, setContentServiceId] = useState<string | null>(null);
  const [activeServiceId, setActiveServiceId] = useState<string | null>(null);
  const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } }));
  const sortedServices = useMemo(() => data.services.slice().sort((a, b) => a.order - b.order), [data.services]);
  const [orderedServiceIds, setOrderedServiceIds] = useState<string[]>(() => sortedServices.map((service) => service.id));
  const displayedServices = useMemo(() => {
    const servicesById = new Map(data.services.map((service) => [service.id, service]));
    const orderedServices = orderedServiceIds.map((serviceId) => servicesById.get(serviceId)).filter(Boolean) as Service[];
    const missingServices = sortedServices.filter((service) => !orderedServiceIds.includes(service.id));

    return [...orderedServices, ...missingServices];
  }, [data.services, orderedServiceIds, sortedServices]);
  const contentService = data.services.find((service) => service.id === contentServiceId) ?? null;

  useEffect(() => {
    if (!activeServiceId) {
      setOrderedServiceIds(sortedServices.map((service) => service.id));
    }
  }, [activeServiceId, sortedServices]);

  function handleDragStart(event: DragStartEvent) {
    setActiveServiceId(String(event.active.id));
  }

  function handleDragEnd(event: DragEndEvent) {
    const activeId = String(event.active.id);
    const overId = event.over ? String(event.over.id) : null;

    setActiveServiceId(null);

    if (!overId || activeId === overId) return;

    const oldIndex = orderedServiceIds.indexOf(activeId);
    const newIndex = orderedServiceIds.indexOf(overId);

    if (oldIndex === -1 || newIndex === -1) return;

    const nextIds = arrayMove(orderedServiceIds, oldIndex, newIndex);
    setOrderedServiceIds(nextIds);
    onReorderServices(nextIds);
  }

  function handleDragCancel() {
    setActiveServiceId(null);
  }

  return (
    <>
      <GlassSurface className="table-shell services-table-shell">
        <div className="table-toolbar">
          <div>
            <h3>{isPublicPoolContext ? "Каталог сервисов · открытый контур" : "Каталог сервисов"}</h3>
            {isPublicPoolContext ? (
              <p className="admin-helper-note">
                Пока это общий каталог модулей. Видимость и запуск для public users регулируются grants в матрице доступа открытого контура.
              </p>
            ) : null}
          </div>
          <IconButton label="Создать сервис" className="admin-circle-action admin-circle-action--solid" type="button" onClick={onCreateService}>
            <Plus size={17} />
          </IconButton>
        </div>
        <DndContext
          sensors={sensors}
          collisionDetection={closestCenter}
          onDragStart={handleDragStart}
          onDragEnd={handleDragEnd}
          onDragCancel={handleDragCancel}
        >
          <table className="services-admin-table">
            <ServiceTableColGroup />
            <thead>
              <tr>
                <th>Сервис</th>
                <th>Slug</th>
                <th>Статус</th>
                <th>Ссылка запуска</th>
                <th>Authentik</th>
                <th aria-label="Редактирование" />
                <th aria-label="Порядок" />
              </tr>
            </thead>
            <SortableContext items={displayedServices.map((service) => service.id)} strategy={verticalListSortingStrategy}>
              <tbody>
                {displayedServices.map((service) => (
                  <SortableServiceRow
                    key={service.id}
                    service={service}
                    onUpdateService={onUpdateService}
                    onOpenContent={() => setContentServiceId(service.id)}
                  />
                ))}
              </tbody>
            </SortableContext>
          </table>
        </DndContext>
      </GlassSurface>

      {contentService ? (
        <ServiceContentModal
          service={contentService}
          onClose={() => setContentServiceId(null)}
          onSave={async (patch) => {
            const result = await onUpdateService(contentService.id, patch);
            if (result.ok) {
              setContentServiceId(null);
            }
            return result;
          }}
          onDelete={() => {
            onDeleteService(contentService.id);
            setContentServiceId(null);
          }}
        />
      ) : null}
    </>
  );
}

function ServiceTableColGroup() {
  return (
    <colgroup>
      <col style={{ width: "23%" }} />
      <col style={{ width: "12%" }} />
      <col style={{ width: "12%" }} />
      <col style={{ width: "27%" }} />
      <col style={{ width: "15%" }} />
      <col style={{ width: "3.4rem" }} />
      <col style={{ width: "3.1rem" }} />
    </colgroup>
  );
}

function SortableServiceRow({
  service,
  onUpdateService,
  onOpenContent,
}: {
  service: Service;
  onUpdateService: (serviceId: string, patch: Partial<Service>) => Promise<AdminMutationOutcome>;
  onOpenContent: () => void;
}) {
  const { attributes, listeners, setActivatorNodeRef, setNodeRef, transform, transition, isDragging } = useSortable({ id: service.id });
  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
  };

  return (
    <tr ref={setNodeRef} style={style} className={cn("services-admin-table__row", isDragging && "services-admin-table__row--dragging")}>
      <ServiceTableCells
        service={service}
        onUpdateService={onUpdateService}
        onOpenContent={onOpenContent}
        dragAttributes={attributes}
        dragListeners={listeners}
        setDragHandleRef={setActivatorNodeRef}
      />
    </tr>
  );
}

function ServiceTableCells({
  service,
  onUpdateService,
  onOpenContent,
  dragAttributes,
  dragListeners,
  setDragHandleRef,
}: {
  service: Service;
  onUpdateService: (serviceId: string, patch: Partial<Service>) => Promise<AdminMutationOutcome>;
  onOpenContent: () => void;
  dragAttributes?: DraggableAttributes;
  dragListeners?: DraggableSyntheticListeners;
  setDragHandleRef?: (node: HTMLButtonElement | null) => void;
}) {
  return (
    <>
      <td className="services-admin-table__service">
        <input
          className="admin-table-input admin-table-input--strong"
          value={service.title}
          onChange={(event) => onUpdateService(service.id, { title: event.target.value })}
          aria-label={`Название сервиса ${service.title}`}
        />
        <input
          className="admin-table-input admin-table-input--muted"
          value={service.subtitle ?? ""}
          onChange={(event) => onUpdateService(service.id, { subtitle: event.target.value || null })}
          aria-label={`Подзаголовок сервиса ${service.title}`}
        />
      </td>
      <td>
        <input
          className="admin-table-input"
          value={service.slug}
          onChange={(event) => onUpdateService(service.id, { slug: event.target.value })}
          aria-label={`Slug сервиса ${service.title}`}
        />
      </td>
      <td>
        <ServiceStatusDropdown
          value={service.status}
          label={`Статус сервиса ${service.title}`}
          onChange={(status) => onUpdateService(service.id, { status })}
        />
      </td>
      <td className="services-admin-table__launch">
        <input
          className="admin-table-input"
          value={getServiceLaunchLink(service)}
          onChange={(event) => onUpdateService(service.id, createServiceLaunchLinkPatch(event.target.value))}
          aria-label={`Ссылка запуска сервиса ${service.title}`}
        />
      </td>
      <td>
        <input
          className="admin-table-input"
          value={service.authentikApplicationSlug ?? ""}
          onChange={(event) => onUpdateService(service.id, { authentikApplicationSlug: event.target.value || null })}
          aria-label={`Authentik slug сервиса ${service.title}`}
        />
      </td>
      <td className="services-admin-table__actions">
        <IconButton
          label={`Контент витрины ${service.title}`}
          className="admin-icon-action services-admin-table__edit"
          type="button"
          onClick={onOpenContent}
        >
          <Edit3 size={12} />
        </IconButton>
      </td>
      <td className="services-admin-table__drag-cell">
        <button
          ref={setDragHandleRef}
          className="services-admin-table__drag-handle"
          type="button"
          aria-label={`Перетащить сервис ${service.title}`}
          {...dragAttributes}
          {...dragListeners}
        >
          <GripVertical size={16} strokeWidth={1.9} />
        </button>
      </td>
    </>
  );
}

function ServiceStatusDropdown({
  value,
  label,
  onChange,
}: {
  value: ServiceStatus;
  label: string;
  onChange: (status: ServiceStatus) => void;
}) {
  const selectedOption = serviceStatusOptions.find((option) => option.value === value) ?? serviceStatusOptions[0];

  return (
    <NodeDcDropdown
      className="service-status-dropdown"
      minWidth={156}
      surfaceClassName="service-status-menu"
      trigger={({ open, toggle, setTriggerRef }) => (
        <button
          ref={setTriggerRef}
          className="service-status-trigger"
          data-status={value}
          data-tone={selectedOption.tone}
          type="button"
          aria-label={label}
          aria-expanded={open}
          onClick={toggle}
        >
          <span>{selectedOption.label}</span>
        </button>
      )}
    >
      {({ close }) => (
        <div className="admin-status-menu__list">
          {serviceStatusOptions.map((option) => (
            <button
              key={option.value}
              className="service-status-menu__option nodedc-ui-option"
              data-selected={option.value === value}
              data-status={option.value}
              data-tone={option.tone}
              type="button"
              onClick={() => {
                onChange(option.value);
                close();
              }}
            >
              <span className="service-status-menu__mark" aria-hidden="true" />
              <span>{option.label}</span>
            </button>
          ))}
        </div>
      )}
    </NodeDcDropdown>
  );
}

function AdminStatusDropdown<T extends string>({
  value,
  options,
  label,
  onChange,
}: {
  value: T;
  options: Array<AdminStatusOption<T>>;
  label: string;
  onChange: (value: T) => void;
}) {
  const selectedOption = options.find((option) => option.value === value) ?? options[0];

  return (
    <NodeDcDropdown
      className="admin-status-dropdown"
      minWidth={164}
      surfaceClassName="admin-status-menu"
      trigger={({ open, toggle, setTriggerRef }) => (
        <button
          ref={setTriggerRef}
          className="admin-status-trigger"
          data-tone={selectedOption.tone}
          type="button"
          aria-label={label}
          aria-expanded={open}
          onClick={toggle}
        >
          <span>{selectedOption.label}</span>
        </button>
      )}
    >
      {({ close }) => (
        <div className="admin-status-menu__list">
          {options.map((option) => (
            <button
              key={option.value}
              className="admin-status-menu__option nodedc-ui-option"
              data-selected={option.value === value}
              data-tone={option.tone}
              type="button"
              onClick={() => {
                onChange(option.value);
                close();
              }}
            >
              <span className="admin-status-menu__mark" aria-hidden="true" />
              <span>{option.label}</span>
            </button>
          ))}
        </div>
      )}
    </NodeDcDropdown>
  );
}

function AdminStatusPill<T extends string>({ value, options }: { value: T; options: Array<AdminStatusOption<T>> }) {
  const option = options.find((item) => item.value === value) ?? options[0];
  return (
    <span className="admin-status-trigger admin-status-trigger--static" data-tone={option.tone}>
      <span>{option.label}</span>
    </span>
  );
}

function ServiceContentModal({
  service,
  onClose,
  onSave,
  onDelete,
}: {
  service: Service;
  onClose: () => void;
  onSave: (patch: Partial<Service>) => Promise<AdminMutationOutcome>;
  onDelete: () => void;
}) {
  const [draft, setDraft] = useState<Service>(service);
  const draftRef = useRef<Service>(service);
  const serviceIdRef = useRef(service.id);
  const hasUnsavedDraftRef = useRef(false);
  const [mediaPreviewUrls, setMediaPreviewUrls] = useState<{ cover: string | null; ambient: string | null }>({
    cover: service.coverImageUrl ?? null,
    ambient: service.ambientVideoUrl ?? null,
  });
  const [uploadingSlot, setUploadingSlot] = useState<"cover" | "ambient" | null>(null);
  const [storageError, setStorageError] = useState<string | null>(null);
  const [saveError, setSaveError] = useState<string | null>(null);
  const [isSaving, setIsSaving] = useState(false);
  const [deleteOpen, setDeleteOpen] = useState(false);

  useEffect(() => {
    const isAnotherService = service.id !== serviceIdRef.current;

    if (!isAnotherService && hasUnsavedDraftRef.current) {
      return;
    }

    serviceIdRef.current = service.id;
    hasUnsavedDraftRef.current = false;
    draftRef.current = service;
    setDraft(service);
    setMediaPreviewUrls({
      cover: service.coverImageUrl ?? null,
      ambient: service.ambientVideoUrl ?? null,
    });
    setStorageError(null);
    setSaveError(null);
    setIsSaving(false);
    setUploadingSlot(null);
  }, [service]);

  useEffect(() => {
    return () => {
      Object.values(mediaPreviewUrls).forEach((previewUrl) => {
        if (previewUrl?.startsWith("blob:")) {
          URL.revokeObjectURL(previewUrl);
        }
      });
    };
  }, [mediaPreviewUrls]);

  function patchDraft(patch: Partial<Service>, options: { markUnsaved?: boolean } = {}) {
    const nextDraft = { ...draftRef.current, ...patch };
    hasUnsavedDraftRef.current = options.markUnsaved ?? true;
    draftRef.current = nextDraft;
    setDraft(nextDraft);
  }

  function update<K extends keyof Service>(key: K, value: Service[K]) {
    patchDraft({ [key]: value } as Partial<Service>);
  }

  function updateMediaPreview(slot: "cover" | "ambient", previewUrl: string | null) {
    setMediaPreviewUrls((current) => ({ ...current, [slot]: previewUrl }));
  }

  async function handleCoverUpload(file?: File) {
    if (!file) return;
    await uploadServiceMedia(file, "cover");
  }

  async function handleAmbientUpload(file?: File) {
    if (!file) return;
    await uploadServiceMedia(file, "ambient");
  }

  async function uploadServiceMedia(file: File, slot: "cover" | "ambient") {
    const localPreviewUrl = URL.createObjectURL(file);
    updateMediaPreview(slot, localPreviewUrl);
    setStorageError(null);
    setUploadingSlot(slot);

    try {
      const storedFile = await uploadStorageFile(file);
      const mediaKind = mediaKindFromFile(file);

      if (slot === "cover") {
        patchDraft({
          coverImageUrl: storedFile.url,
          coverMediaKind: mediaKind,
          coverMediaSource: "file",
          coverMediaFileName: storedFile.fileName,
        });
      } else {
        patchDraft({
          ambientVideoUrl: storedFile.url,
          ambientMediaKind: mediaKind,
          ambientMediaSource: "file",
          ambientMediaFileName: storedFile.fileName,
        });
      }
      updateMediaPreview(slot, storedFile.url);
    } catch (error) {
      setStorageError(error instanceof Error ? error.message : "Не удалось сохранить файл в storage");
      updateMediaPreview(slot, slot === "cover" ? (draftRef.current.coverImageUrl ?? null) : (draftRef.current.ambientVideoUrl ?? null));
    } finally {
      setUploadingSlot(null);
    }
  }

  async function handleSave() {
    setSaveError(null);
    setIsSaving(true);

    const currentDraft = draftRef.current;
    const result = await onSave({
      title: currentDraft.title,
      subtitle: currentDraft.subtitle,
      description: currentDraft.description,
      fullDescription: currentDraft.fullDescription,
      url: currentDraft.url,
      launchUrl: currentDraft.launchUrl,
      coverImageUrl: currentDraft.coverImageUrl,
      coverMediaKind: currentDraft.coverMediaKind,
      coverMediaSource: currentDraft.coverMediaSource,
      coverMediaFileName: currentDraft.coverMediaFileName,
      ambientVideoUrl: currentDraft.ambientVideoUrl,
      ambientMediaKind: currentDraft.ambientMediaKind,
      ambientMediaSource: currentDraft.ambientMediaSource,
      ambientMediaFileName: currentDraft.ambientMediaFileName,
    });

    if (!result.ok) {
      setSaveError(result.message);
      setIsSaving(false);
    } else {
      hasUnsavedDraftRef.current = false;
    }
  }

  return (
    <div className="service-content-modal-layer" role="dialog" aria-modal="true" aria-label={`Контент витрины ${service.title}`}>
      <article className="service-content-modal">
        <div className="service-content-modal__head">
          <div>
            <p className="eyebrow">Витрина сервиса</p>
            <h3>{service.title}</h3>
          </div>
          <IconButton label="Закрыть редактор" className="admin-circle-action" type="button" onClick={onClose}>
            <X size={15} strokeWidth={1.45} />
          </IconButton>
        </div>

        <div className="service-content-modal__grid">
          <label className="service-content-field">
            <span>Название в витрине</span>
            <input value={draft.title} onChange={(event) => update("title", event.target.value)} />
          </label>

          <label className="service-content-field">
            <span>Подзаголовок</span>
            <input value={draft.subtitle ?? ""} onChange={(event) => update("subtitle", event.target.value || null)} />
          </label>

          <label className="service-content-field service-content-field--wide">
            <span>Короткое описание</span>
            <textarea value={draft.description} onChange={(event) => update("description", event.target.value)} rows={3} />
          </label>

          <label className="service-content-field service-content-field--wide">
            <span>Description</span>
            <textarea value={draft.fullDescription ?? ""} onChange={(event) => update("fullDescription", event.target.value || null)} rows={5} />
          </label>

          <label className="service-content-field service-content-field--wide">
            <span>
              <Link2 size={14} /> Ссылка запуска
            </span>
            <input
              value={getServiceLaunchLink(draft)}
              onChange={(event) => patchDraft(createServiceLaunchLinkPatch(event.target.value))}
            />
          </label>

          <MediaSourceField
            label="Карточка"
            icon={<ImageIcon size={14} />}
            source={draft.coverMediaSource ?? "url"}
            value={draft.coverImageUrl ?? ""}
            fileName={draft.coverMediaFileName ?? null}
            isUploading={uploadingSlot === "cover"}
            previewSrc={mediaPreviewUrls.cover}
            previewKind={draft.coverMediaKind}
            onSourceChange={(source) => update("coverMediaSource", source)}
            onUrlChange={(value) => {
              update("coverImageUrl", value || null);
              update("coverMediaSource", "url");
              update("coverMediaKind", mediaKindFromUrl(value));
              update("coverMediaFileName", null);
              updateMediaPreview("cover", value || null);
            }}
            onFileChange={handleCoverUpload}
          />

          <MediaSourceField
            label="Фоновый контент"
            icon={<Video size={14} />}
            source={draft.ambientMediaSource ?? "url"}
            value={draft.ambientVideoUrl ?? ""}
            fileName={draft.ambientMediaFileName ?? null}
            isUploading={uploadingSlot === "ambient"}
            previewSrc={mediaPreviewUrls.ambient}
            previewKind={draft.ambientMediaKind}
            onSourceChange={(source) => update("ambientMediaSource", source)}
            onUrlChange={(value) => {
              update("ambientVideoUrl", value || null);
              update("ambientMediaSource", "url");
              update("ambientMediaKind", mediaKindFromUrl(value));
              update("ambientMediaFileName", null);
              updateMediaPreview("ambient", value || null);
            }}
            onFileChange={handleAmbientUpload}
          />

          {storageError ? <p className="service-content-storage-error">{storageError}</p> : null}
          {saveError ? <p className="service-content-storage-error">{saveError}</p> : null}
        </div>

        <div className="service-content-modal__foot">
          <Button variant="secondary" surface="modal" type="button" onClick={onClose}>
            Отмена
          </Button>
          <div className="service-content-modal__foot-actions">
            <Button variant="danger" surface="modal" type="button" icon={<Trash2 size={16} />} onClick={() => setDeleteOpen(true)}>
              Удалить
            </Button>
            <Button
              variant="accent"
              surface="modal"
              accentRgb={modalActionAccentRgb}
              type="button"
              disabled={uploadingSlot !== null || isSaving}
              icon={<Save size={16} />}
              onClick={handleSave}
            >
              {uploadingSlot ? "Сохраняем файл" : isSaving ? "Сохраняем" : "Сохранить"}
            </Button>
          </div>
        </div>
      </article>
      <NodeDcDeleteModal
        isOpen={deleteOpen}
        title="Удалить витрину"
        description={
          <>
            Витрина <strong>{service.title}</strong> будет удалена из каталога, нижней панели и матрицы доступов.
          </>
        }
        onClose={() => setDeleteOpen(false)}
        onConfirm={() => {
          setDeleteOpen(false);
          onDelete();
        }}
      />
    </div>
  );
}

function ClientEditorModal({
  client,
  taskManagerWorkspaces,
  taskManagerWorkspacesLoading,
  taskManagerWorkspacesError,
  onRefreshTaskManagerWorkspaces,
  onClose,
  onSave,
  onDelete,
  canDelete,
}: {
  client: Client;
  taskManagerWorkspaces: TaskManagerWorkspaceSummary[];
  taskManagerWorkspacesLoading: boolean;
  taskManagerWorkspacesError: string | null;
  onRefreshTaskManagerWorkspaces: () => void;
  onClose: () => void;
  onSave: (patch: Partial<Client>) => void;
  onDelete: () => void;
  canDelete: boolean;
}) {
  return (
    <div className="service-content-modal-layer" role="dialog" aria-modal="true" aria-label={`Редактор клиента ${client.name}`}>
      <article className="service-content-modal admin-entity-modal client-editor-modal">
        <EntityModalHead
          eyebrow="Клиент"
          title={client.name}
          onClose={onClose}
          actions={
            <IconButton
              label={taskManagerWorkspacesLoading ? "Обновляем workspace Operational Core" : "Обновить workspace Operational Core"}
              className="admin-circle-action admin-circle-action--solid"
              type="button"
              disabled={taskManagerWorkspacesLoading}
              onClick={onRefreshTaskManagerWorkspaces}
            >
              <RefreshCw size={16} />
            </IconButton>
          }
        />
        <ClientProfileEditorForm
          client={client}
          taskManagerWorkspaces={taskManagerWorkspaces}
          taskManagerWorkspacesError={taskManagerWorkspacesError}
          onCancel={onClose}
          onSave={onSave}
          deleteConfig={
            canDelete
              ? {
                  label: "Удалить",
                  title: "Удалить компанию",
                  description: (
                    <>
                      Компания <strong>{client.name}</strong>, ее участники, группы, инвайты и клиентские гранты будут удалены из демо-данных.
                    </>
                  ),
                  onConfirm: onDelete,
                }
              : undefined
          }
        />
      </article>
    </div>
  );
}

function ClientProfileEditorForm({
  client,
  taskManagerWorkspaces,
  taskManagerWorkspacesError,
  onCancel,
  onSave,
  deleteConfig,
}: {
  client: Client;
  taskManagerWorkspaces: TaskManagerWorkspaceSummary[];
  taskManagerWorkspacesError: string | null;
  onCancel?: () => void;
  onSave: (patch: Partial<Client>) => void;
  deleteConfig?: EntityModalDeleteConfig;
}) {
  const [draft, setDraft] = useState<Client>(client);
  const [avatarPreviewUrl, setAvatarPreviewUrl] = useState<string | null>(client.avatarUrl ?? null);
  const [uploadingAvatar, setUploadingAvatar] = useState(false);
  const [storageError, setStorageError] = useState<string | null>(null);
  const taskManagerWorkspaceBindings = getClientTaskManagerWorkspaces(draft);
  const selectedTaskManagerWorkspaceSlugs = new Set(taskManagerWorkspaceBindings.map((workspace) => workspace.slug));
  const primaryTaskManagerWorkspace = getPrimaryTaskManagerWorkspace(draft);

  useEffect(() => {
    setDraft(client);
    setAvatarPreviewUrl(client.avatarUrl ?? null);
    setUploadingAvatar(false);
    setStorageError(null);
  }, [client]);

  useEffect(() => {
    return () => {
      if (avatarPreviewUrl?.startsWith("blob:")) {
        URL.revokeObjectURL(avatarPreviewUrl);
      }
    };
  }, [avatarPreviewUrl]);

  function update<K extends keyof Client>(key: K, value: Client[K]) {
    setDraft((current) => ({ ...current, [key]: value }));
  }

  function resetDraft() {
    setDraft(client);
    setAvatarPreviewUrl(client.avatarUrl ?? null);
    setUploadingAvatar(false);
    setStorageError(null);
    onCancel?.();
  }

  function updateTaskManagerWorkspaces(workspaces: ClientTaskManagerWorkspaceBinding[]) {
    const primaryWorkspace = workspaces.find((workspace) => workspace.isPrimary) ?? workspaces[0] ?? null;
    setDraft((current) => ({
      ...current,
      integrations: {
        ...current.integrations,
        taskManager: {
          ...current.integrations?.taskManager,
          workspaceSlug: primaryWorkspace?.slug ?? null,
          workspaceName: primaryWorkspace?.name ?? null,
          workspaces,
        },
      },
    }));
  }

  function toggleTaskManagerWorkspace(workspace: TaskManagerWorkspaceSummary) {
    const currentWorkspaces = getClientTaskManagerWorkspaces(draft);
    const exists = currentWorkspaces.some((item) => item.slug === workspace.slug);
    const nextWorkspaces: ClientTaskManagerWorkspaceBinding[] = exists
      ? currentWorkspaces.filter((item) => item.slug !== workspace.slug)
      : [
          ...currentWorkspaces,
          {
            slug: workspace.slug,
            name: workspace.name,
            isPrimary: currentWorkspaces.length === 0,
            managedBy: "launcher",
          },
        ];

    updateTaskManagerWorkspaces(normalizeClientTaskManagerWorkspaceDraft(nextWorkspaces));
  }

  function setPrimaryTaskManagerWorkspace(workspaceSlug: string) {
    const nextWorkspaces = getClientTaskManagerWorkspaces(draft).map((workspace) => ({
      ...workspace,
      isPrimary: workspace.slug === workspaceSlug,
    }));
    updateTaskManagerWorkspaces(normalizeClientTaskManagerWorkspaceDraft(nextWorkspaces));
  }

  async function handleAvatarUpload(file?: File) {
    if (!file) return;

    const localPreviewUrl = URL.createObjectURL(file);
    setAvatarPreviewUrl(localPreviewUrl);
    setUploadingAvatar(true);
    setStorageError(null);

    try {
      const storedFile = await uploadStorageFile(file);
      update("avatarUrl", storedFile.url);
    } catch (error) {
      setStorageError(error instanceof Error ? error.message : "Не удалось сохранить аватар компании");
      setAvatarPreviewUrl(draft.avatarUrl ?? null);
    } finally {
      setUploadingAvatar(false);
    }
  }

  return (
    <>
      <div className="service-content-modal__grid client-profile-editor-grid">
        <label className="service-content-field">
          <span>Название</span>
          <input value={draft.name} onChange={(event) => update("name", event.target.value)} />
        </label>
        <label className="service-content-field">
          <span>Юридическое название</span>
          <input value={draft.legalName ?? ""} onChange={(event) => update("legalName", event.target.value || null)} />
        </label>
        <label className="service-content-field">
          <span>ИНН</span>
          <input value={draft.inn ?? ""} onChange={(event) => update("inn", event.target.value || null)} />
        </label>
        <label className="service-content-field">
          <span>Тип</span>
          <NodeDcSelect
            className="admin-modal-select-wrap"
            triggerClassName="admin-modal-select-trigger"
            value={draft.type}
            options={clientTypeOptions}
            label="Тип клиента"
            onChange={(type) => update("type", type)}
          />
        </label>
        <div className="service-content-field">
          <span>Статус</span>
          <AdminStatusDropdown value={draft.status} options={clientStatusOptions} label="Статус клиента" onChange={(status) => update("status", status)} />
        </div>
        <div className="service-content-field service-content-field--wide client-avatar-field">
          <span>Аватар компании</span>
          <div className="client-avatar-control">
            <div className="client-avatar-preview" aria-hidden="true">
              {avatarPreviewUrl ? <img className="client-avatar-preview__image" src={avatarPreviewUrl} alt="" /> : null}
            </div>
            <div className="client-avatar-control__copy">
              <strong>{avatarPreviewUrl ? "Аватар подключён" : "Аватар не задан"}</strong>
              <small>Показывается в верхнем переключателе компании.</small>
            </div>
            <label className="service-media-file-button client-avatar-upload-button">
              {uploadingAvatar ? "Загрузка..." : "Выберите файл"}
              <input
                type="file"
                accept="image/*"
                disabled={uploadingAvatar}
                onChange={(event) => {
                  void handleAvatarUpload(event.target.files?.[0]);
                  event.target.value = "";
                }}
              />
            </label>
            {avatarPreviewUrl ? (
              <button
                className="admin-icon-action client-avatar-clear-action"
                type="button"
                onClick={() => {
                  update("avatarUrl", null);
                  setAvatarPreviewUrl(null);
                }}
                aria-label="Убрать аватар"
              >
                <X size={11} />
              </button>
            ) : null}
          </div>
          {storageError ? <small className="client-avatar-error">{storageError}</small> : null}
        </div>
        <div className="service-content-field service-content-field--wide">
          <span>Operational Core workspaces</span>
          <div className="task-workspace-picker-card">
            <div className="task-workspace-picker" role="list" aria-label="Operational Core workspaces клиента">
              {taskManagerWorkspaces.length ? (
                taskManagerWorkspaces.map((workspace) => {
                  const selected = selectedTaskManagerWorkspaceSlugs.has(workspace.slug);
                  const primary = primaryTaskManagerWorkspace?.slug === workspace.slug;

                  return (
                    <button
                      key={workspace.slug}
                      className={cn("task-workspace-chip", selected && "task-workspace-chip--selected")}
                      type="button"
                      aria-pressed={selected}
                      onClick={() => toggleTaskManagerWorkspace(workspace)}
                    >
                      <span>
                        <strong>{workspace.name}</strong>
                        <small>
                          {workspace.slug} · {workspace.memberCount} участников · Launcher-managed
                        </small>
                      </span>
                      {selected ? (
                        <em
                          role="button"
                          tabIndex={0}
                          onClick={(event) => {
                            event.stopPropagation();
                            setPrimaryTaskManagerWorkspace(workspace.slug);
                          }}
                          onKeyDown={(event) => {
                            if (event.key !== "Enter" && event.key !== " ") return;
                            event.preventDefault();
                            event.stopPropagation();
                            setPrimaryTaskManagerWorkspace(workspace.slug);
                          }}
                        >
                          {primary ? "Основной" : "Сделать основным"}
                        </em>
                      ) : null}
                    </button>
                  );
                })
              ) : (
                <span className="task-workspace-picker__empty">Workspace Operational Core не загружены</span>
              )}
            </div>
          </div>
          <small>
            {taskManagerWorkspacesError
              ? taskManagerWorkspacesError
              : "Эти workspace доступны для детальных назначений пользователей в Operational Core."}
          </small>
        </div>
        <label className="service-content-field">
          <span>Контактное лицо</span>
          <input value={draft.contactName ?? ""} onChange={(event) => update("contactName", event.target.value || null)} />
        </label>
        <label className="service-content-field">
          <span>Email</span>
          <input value={draft.contactEmail ?? ""} onChange={(event) => update("contactEmail", event.target.value || null)} />
        </label>
        <div className="service-content-field">
          <span>Договор с</span>
          <NodeDcDateField
            value={draft.contractStartsAt ?? null}
            rangeStart={draft.contractStartsAt ?? null}
            rangeEnd={draft.contractEndsAt ?? null}
            label="Договор с"
            onChange={(value) => update("contractStartsAt", value)}
          />
        </div>
        <div className="service-content-field">
          <span>Договор до</span>
          <NodeDcDateField
            value={draft.contractEndsAt ?? null}
            rangeStart={draft.contractStartsAt ?? null}
            rangeEnd={draft.contractEndsAt ?? null}
            label="Договор до"
            onChange={(value) => update("contractEndsAt", value)}
          />
        </div>
        <div className="service-content-field">
          <span>Оплачено до</span>
          <NodeDcDateField value={draft.paidUntil ?? null} label="Оплачено до" onChange={(value) => update("paidUntil", value)} />
        </div>
        <div className="service-content-field">
          <span>Демо до</span>
          <NodeDcDateField value={draft.demoEndsAt ?? null} label="Демо до" onChange={(value) => update("demoEndsAt", value)} />
        </div>
        <label className="service-content-field service-content-field--wide">
          <span>Заметки</span>
          <textarea value={draft.notes ?? ""} onChange={(event) => update("notes", event.target.value || null)} rows={4} />
        </label>
      </div>
      <EntityModalFoot onClose={resetDraft} onSave={() => onSave(draft)} deleteConfig={deleteConfig} />
    </>
  );
}

function UserEditorModal({
  user,
  membership,
  client,
  onClose,
  onSave,
  onDelete,
}: {
  user: LauncherUser;
  membership: ClientMembership;
  client: Client;
  onClose: () => void;
  onSave: (userPatch: Partial<LauncherUser>, membershipPatch: Partial<ClientMembership>) => void;
  onDelete: () => void;
}) {
  const [userDraft, setUserDraft] = useState<LauncherUser>(user);
  const [membershipDraft, setMembershipDraft] = useState<ClientMembership>(membership);

  useEffect(() => {
    setUserDraft(user);
    setMembershipDraft(membership);
  }, [membership, user]);

  function updateUser<K extends keyof LauncherUser>(key: K, value: LauncherUser[K]) {
    setUserDraft((current) => ({ ...current, [key]: value }));
  }

  function updateMembership<K extends keyof ClientMembership>(key: K, value: ClientMembership[K]) {
    setMembershipDraft((current) => ({ ...current, [key]: value }));
  }

  return (
    <div className="service-content-modal-layer" role="dialog" aria-modal="true" aria-label={`Редактор пользователя ${user.name}`}>
      <article className="service-content-modal admin-entity-modal">
        <EntityModalHead eyebrow={client.name} title={user.name} onClose={onClose} />
        <div className="service-content-modal__grid">
          <label className="service-content-field">
            <span>Имя</span>
            <input value={userDraft.name} onChange={(event) => updateUser("name", event.target.value)} />
          </label>
          <label className="service-content-field">
            <span>Email</span>
            <input value={userDraft.email} onChange={(event) => updateUser("email", event.target.value)} />
          </label>
          <label className="service-content-field">
            <span>Телефон</span>
            <input value={userDraft.phone ?? ""} onChange={(event) => updateUser("phone", event.target.value || null)} />
          </label>
          <label className="service-content-field">
            <span>Должность</span>
            <input value={userDraft.position ?? ""} onChange={(event) => updateUser("position", event.target.value || null)} />
          </label>
          <div className="service-content-field">
            <span>Статус</span>
            <AdminStatusDropdown value={userDraft.globalStatus} options={userStatusOptions} label="Статус пользователя" onChange={(status) => updateUser("globalStatus", status)} />
          </div>
          <label className="service-content-field">
            <span>Роль в клиенте</span>
            <NodeDcSelect
              className="admin-modal-select-wrap"
              triggerClassName="admin-modal-select-trigger"
              value={membershipDraft.role}
              options={membershipRoleOptions}
              label="Роль в клиенте"
              onChange={(role) => updateMembership("role", role)}
            />
          </label>
          <div className="service-content-field">
            <span>Доступ</span>
            <AdminStatusDropdown
              value={membershipDraft.status}
              options={membershipStatusOptions}
              label="Доступ пользователя"
              onChange={(status) => updateMembership("status", status)}
            />
          </div>
          <label className="service-content-field service-content-field--wide">
            <span>Заметки</span>
            <textarea value={userDraft.notes ?? ""} onChange={(event) => updateUser("notes", event.target.value || null)} rows={4} />
          </label>
        </div>
        <EntityModalFoot
          onClose={onClose}
          onSave={() => onSave(userDraft, membershipDraft)}
          deleteConfig={{
            label: "Удалить",
            title: "Удалить участника",
            description: (
              <>
                Участник <strong>{user.name}</strong> будет удален из клиента <strong>{client.name}</strong>. Глобальный профиль пользователя останется в демо-справочнике.
              </>
            ),
            onConfirm: onDelete,
          }}
        />
      </article>
    </div>
  );
}

function GroupEditorModal({
  group,
  me,
  users,
  onClose,
  onSave,
  onDelete,
}: {
  group: ClientGroup;
  me: MeResponse;
  users: LauncherUser[];
  onClose: () => void;
  onSave: (patch: Partial<ClientGroup>) => void;
  onDelete: () => void;
}) {
  const [draft, setDraft] = useState<ClientGroup>(group);

  useEffect(() => setDraft(group), [group]);

  function toggleUser(userId: string) {
    setDraft((current) => ({
      ...current,
      memberIds: current.memberIds.includes(userId)
        ? current.memberIds.filter((item) => item !== userId)
        : [...current.memberIds, userId],
    }));
  }

  return (
    <div className="service-content-modal-layer" role="dialog" aria-modal="true" aria-label={`Редактор группы ${group.name}`}>
      <article className="service-content-modal admin-entity-modal">
        <EntityModalHead eyebrow="Группа" title={group.name} onClose={onClose} />
        <div className="service-content-modal__grid">
          <label className="service-content-field">
            <span>Название</span>
            <input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} />
          </label>
          <label className="service-content-field service-content-field--wide">
            <span>Описание</span>
            <textarea
              value={draft.description ?? ""}
              onChange={(event) => setDraft((current) => ({ ...current, description: event.target.value || null }))}
              rows={4}
            />
          </label>
          <div className="service-content-field service-content-field--wide">
            <span>Участники</span>
            <div className="admin-token-grid">
              {users.map((user) => {
                const lockedProtectedUser = isProtectedFromCurrentActor(me, user.id);

                return (
                  <button
                    key={user.id}
                    className="admin-token"
                    data-active={draft.memberIds.includes(user.id)}
                    type="button"
                    disabled={lockedProtectedUser}
                    title={lockedProtectedUser ? "Защищённого пользователя может менять в группах только он сам" : undefined}
                    onClick={() => toggleUser(user.id)}
                  >
                    {user.name}
                  </button>
                );
              })}
            </div>
          </div>
        </div>
        <EntityModalFoot
          onClose={onClose}
          onSave={() => onSave(draft)}
          deleteConfig={{
            label: "Удалить",
            title: "Удалить группу",
            description: (
              <>
                Группа <strong>{group.name}</strong> и привязанные к ней гранты сервисов будут удалены.
              </>
            ),
            onConfirm: onDelete,
          }}
        />
      </article>
    </div>
  );
}

function EntityModalHead({
  eyebrow,
  title,
  onClose,
  actions,
}: {
  eyebrow: string;
  title: string;
  onClose: () => void;
  actions?: ReactNode;
}) {
  return (
    <div className="service-content-modal__head">
      <div>
        <p className="eyebrow">{eyebrow}</p>
        <h3>{title}</h3>
      </div>
      <div className="service-content-modal__head-actions">
        {actions}
        <IconButton label="Закрыть редактор" className="admin-circle-action" type="button" onClick={onClose}>
          <X size={15} strokeWidth={1.45} />
        </IconButton>
      </div>
    </div>
  );
}

function EntityModalFoot({
  onClose,
  onSave,
  deleteConfig,
}: {
  onClose: () => void;
  onSave: () => void;
  deleteConfig?: EntityModalDeleteConfig;
}) {
  const [deleteOpen, setDeleteOpen] = useState(false);

  return (
    <>
      <div className="service-content-modal__foot">
        <Button variant="secondary" surface="modal" type="button" onClick={onClose}>
          Отмена
        </Button>
        <div className="service-content-modal__foot-actions">
          {deleteConfig ? (
            <Button variant="danger" surface="modal" type="button" icon={<Trash2 size={16} />} onClick={() => setDeleteOpen(true)}>
              {deleteConfig.label}
            </Button>
          ) : null}
          <Button variant="accent" surface="modal" accentRgb={modalActionAccentRgb} type="button" icon={<Save size={16} />} onClick={onSave}>
            Сохранить
          </Button>
        </div>
      </div>
      {deleteConfig ? (
        <NodeDcDeleteModal
          isOpen={deleteOpen}
          title={deleteConfig.title}
          description={deleteConfig.description}
          onClose={() => setDeleteOpen(false)}
          onConfirm={() => {
            setDeleteOpen(false);
            deleteConfig.onConfirm();
          }}
        />
      ) : null}
    </>
  );
}

function MediaSourceField({
  label,
  icon,
  source,
  value,
  fileName,
  isUploading = false,
  previewSrc,
  previewKind,
  onSourceChange,
  onUrlChange,
  onFileChange,
}: {
  label: string;
  icon: ReactNode;
  source: ServiceMediaSource;
  value: string;
  fileName?: string | null;
  isUploading?: boolean;
  previewSrc?: string | null;
  previewKind?: MediaKind | null;
  onSourceChange: (source: ServiceMediaSource) => void;
  onUrlChange: (value: string) => void;
  onFileChange: (file?: File) => void | Promise<void>;
}) {
  const inputId = `${label.replace(/\s+/g, "-").toLowerCase()}-${source}`;
  const displayFileName = isUploading ? "Сохраняем в storage..." : truncateText(fileName ?? "Файл не выбран", 15);
  const fileTitle = isUploading ? undefined : (fileName ?? "Файл не выбран");

  return (
    <div className="service-content-field service-content-field--wide service-media-field">
      <span>
        {icon} {label}
      </span>
      <div className="service-media-control">
        {source === "file" ? (
          <div className="service-media-file-control">
            <label className="service-media-file-button" htmlFor={inputId}>
              Выберите файл
            </label>
            <span className="service-media-file-name" title={fileTitle}>
              {displayFileName}
            </span>
            <input id={inputId} type="file" accept={mediaAccept} onChange={(event) => onFileChange(event.currentTarget.files?.[0])} />
          </div>
        ) : (
          <input className="service-media-url-input" value={value} onChange={(event) => onUrlChange(event.target.value)} placeholder="https://..." />
        )}

        <div className="service-media-source-switch" aria-label={`${label}: источник`}>
          <button
            type="button"
            className="service-media-source-button"
            data-active={source === "file"}
            aria-label="Файл с диска"
            onClick={() => onSourceChange("file")}
          >
            <HardDrive size={15} />
          </button>
          <button
            type="button"
            className="service-media-source-button"
            data-active={source === "url"}
            aria-label="Внешняя ссылка"
            onClick={() => onSourceChange("url")}
          >
            <Globe2 size={15} />
          </button>
        </div>
        <div className="service-media-preview" aria-hidden="true">
          {previewSrc ? <MediaPreview src={previewSrc} kind={previewKind} /> : icon}
        </div>
      </div>
    </div>
  );
}

function truncateText(value: string, maxLength: number) {
  return value.length > maxLength ? `${value.slice(0, maxLength)}…` : value;
}

function MediaPreview({ src, kind }: { src: string; kind?: MediaKind | null }) {
  if (kind === "video" || mediaKindFromUrl(src) === "video") {
    return <video src={src} autoPlay loop muted playsInline />;
  }

  return <img src={src} alt="" />;
}

function mediaKindFromFile(file: File): MediaKind {
  if (file.type.startsWith("video/")) return "video";
  if (file.type === "image/gif" || /\.gif$/i.test(file.name)) return "gif";
  return "image";
}

function mediaKindFromUrl(value: string): MediaKind | null {
  if (!value.trim()) return null;
  if (/\.(mp4|webm|mov|m4v|avi|mkv)(\?.*)?$/i.test(value)) return "video";
  if (/\.gif(\?.*)?$/i.test(value)) return "gif";
  return "image";
}

function AccessSection({
  data,
  me,
  matrix,
  selectedCell,
  onSelectCell,
  onSetUserServiceAccess,
  pendingAccessAssignments,
  pendingServiceModuleEntitlements,
  onUpdateUser,
  onUpdateMembership,
  pendingTaskManagerMemberships,
  pendingTaskManagerProjectMemberships,
  taskManagerWorkspaceCatalog,
  onSetTaskManagerWorkspaceMemberRole,
  onSetTaskManagerProjectMemberRole,
  onSetServiceModuleEntitlement,
}: {
  data: LauncherData;
  me: MeResponse;
  matrix: ReturnType<typeof buildAccessMatrix>;
  selectedCell: AccessMatrixCell | null;
  onSelectCell: (cell: AccessMatrixCell) => void;
  onSetUserServiceAccess: (command: SetUserServiceAccessCommand) => void;
  pendingAccessAssignments: Record<string, AccessAssignmentValue>;
  pendingServiceModuleEntitlements: Record<string, boolean>;
  onUpdateUser: (userId: string, patch: Partial<LauncherUser>) => void;
  onUpdateMembership: (membershipId: string, patch: Partial<ClientMembership>) => void;
  pendingTaskManagerMemberships: Record<string, boolean>;
  pendingTaskManagerProjectMemberships: Record<string, boolean>;
  taskManagerWorkspaceCatalog: TaskManagerWorkspaceSummary[];
  onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
  onSetTaskManagerProjectMemberRole: (command: EnsureTaskManagerProjectMemberCommand) => void;
  onSetServiceModuleEntitlement: (command: SetServiceModuleEntitlementCommand) => void;
}) {
  const hasUsers = matrix.users.length > 0;
  const isPublicPoolContext = isPublicPoolClientId(matrix.client.id);
  const clientTaskManagerWorkspaces = getClientTaskManagerWorkspaces(matrix.client);
  const primaryTaskManagerWorkspace = getPrimaryTaskManagerWorkspace(matrix.client);
  const [detailsCell, setDetailsCell] = useState<AccessMatrixCell | null>(null);
  const detailsService = detailsCell ? getService(data, detailsCell.serviceId) : null;

  if (!hasUsers) {
    return (
      <div className={cn("access-layout", isPublicPoolContext && "access-layout--single")}>
        <GlassSurface className="access-matrix">
          <div className="table-toolbar table-toolbar--access-matrix">
            <h3>Матрица доступа · {matrix.client.name}</h3>
            <span className="muted-text">Нет данных для матрицы</span>
          </div>
          <div className="access-empty-state">
            <strong>У клиента пока нет участников</strong>
            <span>Добавьте участника или инвайт, после этого здесь появятся ячейки доступа.</span>
          </div>
        </GlassSurface>
      </div>
    );
  }

  const accessGridTemplateColumns = `15rem repeat(2, 9.7rem) 12.6rem repeat(${matrix.services.length}, 11.25rem)`;

  return (
    <div className={cn("access-layout", isPublicPoolContext && "access-layout--single")}>
      <GlassSurface className="access-matrix">
        <div className="table-toolbar table-toolbar--access-matrix">
          <h3>Матрица доступа · {matrix.client.name}</h3>
          <span className="muted-text">Клик по ячейке открывает назначение</span>
        </div>
        <div className="matrix-scroll">
          <div className="access-matrix-grid" style={{ gridTemplateColumns: accessGridTemplateColumns }} role="table">
            <div className="access-grid-head access-grid-sticky" role="columnheader">
              Участник
            </div>
            <div className="access-grid-head" role="columnheader">
              MAIN
            </div>
            <div className="access-grid-head" role="columnheader">
              MAIN ROLE
            </div>
            <div className="access-grid-head" role="columnheader">
              NDC Core Assistant
            </div>
            {matrix.services.map((service) => (
              <div key={service.id} className="access-grid-head" role="columnheader">
                {service.title}
              </div>
            ))}

            {matrix.users.map((user) => {
              const membership = data.memberships.find((item) => item.clientId === matrix.client.id && item.userId === user.id);
              if (!membership) return null;

              const protectedUser = isProtectedLauncherUser(user.id);
              const protectedFromActor = isProtectedFromCurrentActor(me, user.id);
              const inviterMeta = getMembershipInviterMeta(data, membership);
              const pendingKey = `${matrix.client.id}:${user.id}:${primaryTaskManagerWorkspace?.slug ?? "primary"}`;
              const pendingTaskerAssignment = Boolean(pendingTaskManagerMemberships[pendingKey]);
              const forcedTaskManagerAdmin = membership.role === "client_owner";

              return (
                <Fragment key={user.id}>
                  <div className="access-grid-cell access-grid-sticky access-user-cell" role="rowheader">
                    <strong>{user.name}</strong>
                    <small>{user.email}</small>
                    {inviterMeta.showInAccessMatrix ? (
                      <small className="access-user-cell__inviter" title={`${inviterMeta.title} · ${inviterMeta.sourceLabel}`}>
                        через {inviterMeta.title}
                      </small>
                    ) : null}
                  </div>
                  <div className="access-grid-cell" role="cell">
                    <MainStatusControl
                      value={user.globalStatus}
                      protectedUser={protectedUser}
                      onChange={(status) => onUpdateUser(user.id, { globalStatus: status })}
                    />
                  </div>
                  <div className="access-grid-cell" role="cell">
                    <MainRoleControl
                      value={membership.role}
                      protectedUser={protectedUser || protectedFromActor}
                      onChange={(role) => onUpdateMembership(membership.id, { role })}
                    />
                  </div>
                  <div className="access-grid-cell" role="cell">
                    <CoreAssistantAccessControl
                      value={resolveCoreAssistantRole(membership)}
                      protectedUser={protectedUser || protectedFromActor}
                      onChange={(coreAssistantRole) => onUpdateMembership(membership.id, { coreAssistantRole })}
                    />
                  </div>
                  {matrix.services.map((service) => {
                    const cell = matrix.cells.find((item) => item.userId === user.id && item.serviceId === service.id)!;
                    const active = selectedCell?.userId === user.id && selectedCell.serviceId === service.id;
                    const isTaskManagerService = isOperationalCoreService(service);
                    const usePublicTaskerAccess = isPublicPoolContext && isTaskManagerService;

                    return (
                      <div key={service.id} className="access-grid-cell" role="cell">
                        <AccessCellControl
                          cell={cell}
                          service={service}
                          active={active}
                          pendingValue={pendingAccessAssignments[accessCellKey(user.id, service.id)]}
                          busy={!usePublicTaskerAccess && isTaskManagerService && pendingTaskerAssignment}
                          publicSelfService={usePublicTaskerAccess}
                          readOnly={protectedUser || protectedFromActor}
                          onSelectCell={onSelectCell}
                          onSetAccess={(value) => {
                            if (protectedUser || protectedFromActor) return;

                            const nextValue = value;
                            onSetUserServiceAccess({ userId: user.id, serviceId: service.id, value: nextValue });

                            if (usePublicTaskerAccess) return;
                            if (!isTaskManagerService || !primaryTaskManagerWorkspace || forcedTaskManagerAdmin) return;

                            onSetTaskManagerWorkspaceMemberRole({
                              clientId: matrix.client.id,
                              userId: user.id,
                              workspaceSlug: primaryTaskManagerWorkspace.slug,
                              role: accessAssignmentToTaskManagerRole(nextValue),
                            });
                          }}
                          onOpenDetails={isTaskManagerService ? () => setDetailsCell(cell) : undefined}
                        />
                      </div>
                    );
                  })}
                </Fragment>
              );
            })}
          </div>
        </div>
      </GlassSurface>

      {detailsCell && detailsService ? (
        <OperationalCoreAccessModal
          data={data}
          me={me}
          client={matrix.client}
          user={getUser(data, detailsCell.userId)}
          service={detailsService}
          cell={detailsCell}
          workspaces={isPublicPoolContext ? [] : clientTaskManagerWorkspaces}
          workspaceCatalog={taskManagerWorkspaceCatalog}
          publicSelfService={isPublicPoolContext}
          pendingAccessAssignments={pendingAccessAssignments}
          pendingServiceModuleEntitlements={pendingServiceModuleEntitlements}
          pendingTaskManagerMemberships={pendingTaskManagerMemberships}
          pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships}
          onClose={() => setDetailsCell(null)}
          onSetUserServiceAccess={onSetUserServiceAccess}
          onSetTaskManagerWorkspaceMemberRole={onSetTaskManagerWorkspaceMemberRole}
          onSetTaskManagerProjectMemberRole={onSetTaskManagerProjectMemberRole}
          onSetServiceModuleEntitlement={onSetServiceModuleEntitlement}
        />
      ) : null}
    </div>
  );
}

function PublicAccessUsersPanel({
  data,
  me,
  matrix,
  selectedCell,
  onSelectCell,
  onSetUserServiceAccess,
  pendingAccessAssignments,
  pendingServiceModuleEntitlements,
  onUpdateUser,
  onUpdateMembership,
  onSetServiceModuleEntitlement,
}: {
  data: LauncherData;
  me: MeResponse;
  matrix: ReturnType<typeof buildAccessMatrix>;
  selectedCell: AccessMatrixCell | null;
  onSelectCell: (cell: AccessMatrixCell) => void;
  onSetUserServiceAccess: (command: SetUserServiceAccessCommand) => void;
  pendingAccessAssignments: Record<string, AccessAssignmentValue>;
  pendingServiceModuleEntitlements: Record<string, boolean>;
  onUpdateUser: (userId: string, patch: Partial<LauncherUser>) => void;
  onUpdateMembership: (membershipId: string, patch: Partial<ClientMembership>) => void;
  onSetServiceModuleEntitlement: (command: SetServiceModuleEntitlementCommand) => void;
}) {
  const operationalCoreService = matrix.services.find(isOperationalCoreService) ?? null;
  const [detailsCell, setDetailsCell] = useState<AccessMatrixCell | null>(null);
  const detailsService = detailsCell ? getService(data, detailsCell.serviceId) : null;

  return (
    <GlassSurface className="table-shell table-shell--users table-shell--public-access-users">
      <div className="table-toolbar">
        <div>
          <h3>Пользователи открытого контура</h3>
          <p className="admin-helper-note">
            Блокировка аккаунта отключает вход через Authentik и не удаляет историю заявок, инвайтов и аудит.
          </p>
        </div>
      </div>
      <table className="admin-data-table admin-data-table--users admin-data-table--public-access-users">
        <thead>
          <tr>
            <th>Пользователь</th>
            <th>Кто пригласил</th>
            <th>Роль в контуре</th>
            <th>Статус в контуре</th>
            <th>Статус аккаунта</th>
            <th>Operational Core</th>
          </tr>
        </thead>
        <tbody>
          {matrix.users.map((user) => {
            const membership = data.memberships.find((item) => item.clientId === matrix.client.id && item.userId === user.id);
            if (!membership) return null;

            const protectedUser = isProtectedLauncherUser(user.id);
            const inviterMeta = getMembershipInviterMeta(data, membership);
            const operationalCoreCell = operationalCoreService
              ? matrix.cells.find((cell) => cell.userId === user.id && cell.serviceId === operationalCoreService.id) ?? null
              : null;

            return (
              <tr key={membership.id}>
                <td className="admin-user-cell">
                  <div className="admin-user-cell__fields">
                    <input
                      className="admin-table-input admin-table-input--strong"
                      value={user.name}
                      onChange={(event) => onUpdateUser(user.id, { name: event.target.value })}
                      aria-label={`Имя пользователя ${user.name}`}
                    />
                    <input
                      className="admin-table-input admin-table-input--muted"
                      value={user.email}
                      onChange={(event) => onUpdateUser(user.id, { email: event.target.value })}
                      aria-label={`Email пользователя ${user.name}`}
                    />
                  </div>
                </td>
                <td>
                  {inviterMeta.showInAccessMatrix ? (
                    <div className="membership-inviter-cell">
                      <span>{inviterMeta.title}</span>
                      <small>{inviterMeta.sourceLabel}</small>
                    </div>
                  ) : (
                    <span className="muted-text">—</span>
                  )}
                </td>
                <td>
                  {protectedUser ? (
                    <AdminStaticPill>{membershipRoleLabel(membership.role)}</AdminStaticPill>
                  ) : (
                    <NodeDcSelect
                      className="admin-table-select-wrap"
                      triggerClassName="admin-table-select-trigger"
                      value={membership.role}
                      options={membershipRoleOptions}
                      label={`Роль в открытом контуре ${user.name}`}
                      minMenuWidth={198}
                      onChange={(role) => onUpdateMembership(membership.id, { role })}
                    />
                  )}
                </td>
                <td>
                  {protectedUser ? (
                    <AdminStaticPill>{statusOptionLabel(membershipStatusOptions, membership.status)}</AdminStaticPill>
                  ) : (
                    <AdminStatusDropdown
                      value={membership.status}
                      options={membershipStatusOptions}
                      label={`Статус в открытом контуре ${user.name}`}
                      onChange={(status) => onUpdateMembership(membership.id, { status })}
                    />
                  )}
                </td>
                <td>
                  {protectedUser ? (
                    <AdminStaticPill>{statusOptionLabel(userStatusOptions, user.globalStatus)}</AdminStaticPill>
                  ) : (
                    <AdminStatusDropdown
                      value={user.globalStatus}
                      options={userStatusOptions}
                      label={`Статус аккаунта ${user.name}`}
                      onChange={(status) => onUpdateUser(user.id, { globalStatus: status })}
                    />
                  )}
                </td>
                <td>
                  {operationalCoreCell && operationalCoreService ? (
                    <AccessCellControl
                      cell={operationalCoreCell}
                      service={operationalCoreService}
                      active={selectedCell?.userId === user.id && selectedCell.serviceId === operationalCoreCell.serviceId}
                      pendingValue={pendingAccessAssignments[accessCellKey(user.id, operationalCoreCell.serviceId)]}
                      publicSelfService
                      onSelectCell={onSelectCell}
                      onSetAccess={() => undefined}
                      onOpenDetails={() => setDetailsCell(operationalCoreCell)}
                    />
                  ) : (
                    <span className="muted-text">—</span>
                  )}
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>
      {detailsCell && detailsService ? (
        <OperationalCoreAccessModal
          data={data}
          me={me}
          client={matrix.client}
          user={getUser(data, detailsCell.userId)}
          service={detailsService}
          cell={detailsCell}
          workspaces={[]}
          workspaceCatalog={[]}
          publicSelfService
          pendingAccessAssignments={pendingAccessAssignments}
          pendingServiceModuleEntitlements={pendingServiceModuleEntitlements}
          pendingTaskManagerMemberships={{}}
          pendingTaskManagerProjectMemberships={{}}
          onClose={() => setDetailsCell(null)}
          onSetUserServiceAccess={onSetUserServiceAccess}
          onSetTaskManagerWorkspaceMemberRole={() => undefined}
          onSetTaskManagerProjectMemberRole={() => undefined}
          onSetServiceModuleEntitlement={onSetServiceModuleEntitlement}
        />
      ) : null}
    </GlassSurface>
  );
}

function MainStatusControl({
  value,
  protectedUser,
  onChange,
}: {
  value: LauncherUserStatus;
  protectedUser: boolean;
  onChange: (value: LauncherUserStatus) => void;
}) {
  const label = mainStatusLabel(value);
  const allowed = value === "active";

  if (protectedUser) {
    return (
      <span className={cn("access-cell access-cell--main access-cell--readonly", allowed ? "access-cell--allowed" : "access-cell--exception")}>
        <strong>{label}</strong>
        <span>MAIN</span>
      </span>
    );
  }

  return (
    <NodeDcSelect
      value={value}
      options={mainStatusOptions}
      label="MAIN статус"
      minMenuWidth={320}
      menuClassName="access-cell-menu"
      onChange={onChange}
      trigger={({ open, toggle, setTriggerRef, selectedOption }) => (
        <button
          ref={setTriggerRef}
          className={cn("access-cell access-cell--main", value === "active" ? "access-cell--allowed" : "access-cell--exception")}
          type="button"
          aria-expanded={open}
          onClick={toggle}
        >
          <strong>{selectedOption.label}</strong>
          <span>MAIN</span>
        </button>
      )}
    />
  );
}

function MainRoleControl({
  value,
  protectedUser,
  onChange,
}: {
  value: ClientMembershipRole;
  protectedUser: boolean;
  onChange: (value: ClientMembershipRole) => void;
}) {
  const label = membershipRoleLabel(value);

  if (protectedUser) {
    return (
      <span className="access-cell access-cell--main access-cell--allowed access-cell--readonly">
        <strong>{label}</strong>
        <span>MAIN роль</span>
      </span>
    );
  }

  return (
    <NodeDcSelect
      value={value}
      options={membershipRoleOptions}
      label="MAIN роль"
      minMenuWidth={198}
      menuClassName="access-cell-menu"
      onChange={onChange}
      trigger={({ open, toggle, setTriggerRef, selectedOption }) => (
        <button ref={setTriggerRef} className="access-cell access-cell--main access-cell--allowed" type="button" aria-expanded={open} onClick={toggle}>
          <strong>{selectedOption.label}</strong>
          <span>MAIN роль</span>
        </button>
      )}
    />
  );
}

function CoreAssistantAccessControl({
  value,
  protectedUser,
  onChange,
}: {
  value: CoreAssistantAccessRole;
  protectedUser: boolean;
  onChange: (value: CoreAssistantAccessRole) => void;
}) {
  const label = coreAssistantAccessRoleLabel(value);
  const toneClass =
    value === "blocked"
      ? "access-cell--exception"
      : value === "admin"
        ? "access-cell--assistant-admin"
        : "access-cell--allowed";

  if (protectedUser) {
    return (
      <span className={cn("access-cell access-cell--main access-cell--readonly", toneClass)}>
        <strong>{label}</strong>
        <span>Assistant</span>
      </span>
    );
  }

  return (
    <NodeDcSelect
      value={value}
      options={coreAssistantAccessRoleOptions}
      label="NDC Core Assistant"
      minMenuWidth={244}
      menuClassName="access-cell-menu"
      onChange={onChange}
      trigger={({ open, toggle, setTriggerRef, selectedOption }) => (
        <button ref={setTriggerRef} className={cn("access-cell access-cell--main", toneClass)} type="button" aria-expanded={open} onClick={toggle}>
          <strong>{selectedOption.label}</strong>
          <span>Assistant</span>
        </button>
      )}
    />
  );
}

function OperationalCoreAccessModal({
  data,
  me,
  client,
  user,
  service,
  cell,
  workspaces,
  workspaceCatalog,
  publicSelfService = false,
  pendingAccessAssignments,
  pendingTaskManagerMemberships,
  pendingTaskManagerProjectMemberships,
  pendingServiceModuleEntitlements,
  onClose,
  onSetUserServiceAccess,
  onSetTaskManagerWorkspaceMemberRole,
  onSetTaskManagerProjectMemberRole,
  onSetServiceModuleEntitlement,
}: {
  data: LauncherData;
  me: MeResponse;
  client: Client;
  user: LauncherUser;
  service: Service;
  cell: AccessMatrixCell;
  workspaces: ClientTaskManagerWorkspaceBinding[];
  workspaceCatalog: TaskManagerWorkspaceSummary[];
  publicSelfService?: boolean;
  pendingAccessAssignments: Record<string, AccessAssignmentValue>;
  pendingTaskManagerMemberships: Record<string, boolean>;
  pendingTaskManagerProjectMemberships: Record<string, boolean>;
  pendingServiceModuleEntitlements: Record<string, boolean>;
  onClose: () => void;
  onSetUserServiceAccess: (command: SetUserServiceAccessCommand) => void;
  onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
  onSetTaskManagerProjectMemberRole: (command: EnsureTaskManagerProjectMemberCommand) => void;
  onSetServiceModuleEntitlement: (command: SetServiceModuleEntitlementCommand) => void;
}) {
  const membership = data.memberships.find((item) => item.clientId === client.id && item.userId === user.id);
  const protectedSelfManage = canSelfManageProtectedUser(me, user.id);
  const protectedUser = isProtectedLauncherUser(user.id) || isProtectedFromCurrentActor(me, user.id) || (membership?.role === "client_owner" && !protectedSelfManage);
  const basePendingValue = pendingAccessAssignments[accessCellKey(user.id, service.id)];
  const baseAssignmentValue = basePendingValue ?? accessAssignmentValue(cell);
  const baseSelectOptions = publicSelfService ? publicOperationalCoreAccessOptions : operationalCoreAccessAssignmentOptions;
  const baseSelectValue = publicSelfService ? publicOperationalCoreSelectValue(baseAssignmentValue) : baseAssignmentValue;
  const basePending = basePendingValue !== undefined;
  const baseAccessLocked = protectedUser || baseSelectValue === "admin";

  return (
    <div className="service-content-modal-layer" role="dialog" aria-modal="true" aria-label={`Operational Core доступы ${user.name}`}>
      <article className="service-content-modal admin-entity-modal task-access-modal">
        <EntityModalHead eyebrow="Доступы → Operational Core" title={user.name} onClose={onClose} />
        <div className="task-access-summary">
          <InfoLine label="Клиент" value={client.name} />
          <InfoLine label="Сервис" value={service.title} />
          <InfoLine label="Пользователь" value={user.email} />
        </div>

        <div className="task-workspace-access-list">
          <section className="task-workspace-access-card task-access-base-card">
            <div className="task-workspace-access-card__head">
              <div>
                <strong>Базовый доступ</strong>
                <small>
                  {publicSelfService
                    ? "Открытый контур: workspace member, service admin или блокировка instance."
                    : "Глобальная роль пользователя в Operational Core для выбранного клиента."}
                </small>
              </div>
              {baseAccessLocked ? (
                <AdminStaticPill>{operationalCoreStaticRoleLabel(baseSelectValue)}</AdminStaticPill>
              ) : (
                <NodeDcSelect
                  className="admin-table-select-wrap"
                  triggerClassName="admin-table-select-trigger"
                  value={baseSelectValue}
                  options={baseSelectOptions}
                  label={`Базовый доступ ${user.name} к Operational Core`}
                  minMenuWidth={220}
                  disabled={basePending}
                  onChange={(nextValue) => {
                    onSetUserServiceAccess({ userId: user.id, serviceId: service.id, value: nextValue });
                  }}
                />
              )}
            </div>
          </section>

          <section className="task-workspace-access-card task-module-access-card">
            <div className="task-module-access-card__head">
              <div>
                <strong>Дополнительные модули ops-слоя</strong>
                <small>Модули работают только внутри выданного доступа к Operational Core и не расширяют workspace/project права сами по себе.</small>
              </div>
            </div>
            <div className="task-module-access-list">
              {operationalCoreModules.map((module) => {
                const systemEnabled = hasSystemServiceModuleEntitlement(user, module.id);
                const enabled = systemEnabled || hasServiceModuleEntitlement(data, client.id, user.id, service.id, module.id);
                const pendingKey = serviceModuleEntitlementKey(client.id, user.id, service.id, module.id);
                const pending = Boolean(pendingServiceModuleEntitlements[pendingKey]);

                return (
                  <button
                    key={module.id}
                    className={cn("task-module-access-row", enabled && "task-module-access-row--enabled", pending && "task-module-access-row--pending")}
                    type="button"
                    aria-pressed={enabled}
                    disabled={pending || systemEnabled || protectedUser}
                    onClick={() =>
                      onSetServiceModuleEntitlement({
                        clientId: client.id,
                        userId: user.id,
                        serviceId: service.id,
                        moduleId: module.id,
                        enabled: !enabled,
                      })
                    }
                  >
                    <span className="task-module-access-row__meta">
                      <strong>{module.title}</strong>
                      <small>{module.description}</small>
                    </span>
                    <span className="task-module-access-row__state">
                      <span className="task-module-checker" aria-hidden="true">
                        {enabled ? <span /> : null}
                      </span>
                      <span>{pending ? "Сохраняем..." : systemEnabled ? "Системно включён" : enabled ? module.enabledLabel : module.disabledLabel}</span>
                    </span>
                  </button>
                );
              })}
            </div>
          </section>

          {!publicSelfService ? (
            workspaces.map((workspace) => {
              const role = getTaskManagerMembershipRole(data, client.id, user.id, workspace.slug);
              const projects = getWorkspaceCatalogProjects(workspace, workspaceCatalog);
              const pendingKey = `${client.id}:${user.id}:${workspace.slug}`;
              const pending = Boolean(pendingTaskManagerMemberships[pendingKey]) || basePendingValue !== undefined;
              const value: OperationalCoreRoleSelectValue = pending
                ? "pending"
                : baseAssignmentValue === "deny"
                  ? "deny"
                  : taskManagerRoleToAccessAssignment(role);

              return (
                <section key={workspace.slug} className="task-workspace-access-card">
                  <div className="task-workspace-access-card__head">
                    <div>
                      <strong>{workspace.name ?? workspace.slug}</strong>
                      <small>
                        {workspace.slug}
                        {workspace.isPrimary ? " · основной workspace" : ""}
                      </small>
                    </div>
                    {protectedUser || value === "admin" ? (
                      <AdminStaticPill>{operationalCoreStaticRoleLabel(value)}</AdminStaticPill>
                    ) : (
                      <NodeDcSelect
                        className="admin-table-select-wrap"
                        triggerClassName="admin-table-select-trigger"
                        value={value}
                        options={operationalCoreRoleOptions}
                        label={`Роль ${user.name} в ${workspace.name ?? workspace.slug}`}
                        minMenuWidth={180}
                        disabled={pending}
                        onChange={(nextValue) => {
                          if (nextValue === "pending") return;
                          if (nextValue === "admin") return;
                          onSetUserServiceAccess({ userId: user.id, serviceId: service.id, value: nextValue });
                          onSetTaskManagerWorkspaceMemberRole({
                            clientId: client.id,
                            userId: user.id,
                            workspaceSlug: workspace.slug,
                            role: accessAssignmentToTaskManagerRole(nextValue),
                          });
                        }}
                      />
                    )}
                  </div>
                  <div className="task-project-access-list">
                    <div className="task-project-access-list__head">
                      <strong>Проекты</strong>
                      <span>{projects.length ? "Точечные роли внутри выбранного workspace" : "В этом workspace пока нет проектов"}</span>
                    </div>
                    {projects.map((project) => {
                      const projectRole = getTaskManagerProjectMembershipRole(data, client.id, user.id, workspace.slug, project.id);
                      const projectPendingKey = `${client.id}:${user.id}:${workspace.slug}:${project.id}`;
                      const projectPending = Boolean(pendingTaskManagerProjectMemberships[projectPendingKey]);
                      const projectValue: OperationalCoreRoleSelectValue = projectPending ? "pending" : taskManagerRoleToAccessAssignment(projectRole);

                      return (
                        <div key={project.id} className="task-project-access-row">
                          <div className="task-project-access-row__meta">
                            <strong>{project.name}</strong>
                            <small>
                              {project.identifier}
                              {project.memberCount ? ` · ${project.memberCount} участников` : ""}
                            </small>
                          </div>
                          {protectedUser || projectValue === "admin" ? (
                            <AdminStaticPill>{operationalCoreStaticRoleLabel(projectValue)}</AdminStaticPill>
                          ) : (
                            <NodeDcSelect
                              className="admin-table-select-wrap"
                              triggerClassName="admin-table-select-trigger"
                              value={projectValue}
                              options={taskManagerProjectRoleOptions}
                              label={`Роль ${user.name} в проекте ${project.name}`}
                              minMenuWidth={180}
                              disabled={projectPending}
                              onChange={(nextValue) => {
                                if (nextValue === "pending") return;
                                if (nextValue === "admin") return;

                                const nextTaskManagerRole = accessAssignmentToTaskManagerRole(nextValue);
                                if (nextTaskManagerRole !== "unset") {
                                  if (baseAssignmentValue === "unset" || baseAssignmentValue === "deny") {
                                    onSetUserServiceAccess({ userId: user.id, serviceId: service.id, value: nextValue });
                                  }
                                  if (role === "unset") {
                                    onSetTaskManagerWorkspaceMemberRole({
                                      clientId: client.id,
                                      userId: user.id,
                                      workspaceSlug: workspace.slug,
                                      role: nextTaskManagerRole,
                                    });
                                  }
                                }

                                onSetTaskManagerProjectMemberRole({
                                  clientId: client.id,
                                  userId: user.id,
                                  workspaceSlug: workspace.slug,
                                  projectId: project.id,
                                  role: nextTaskManagerRole,
                                });
                              }}
                            />
                          )}
                        </div>
                      );
                    })}
                  </div>
                </section>
              );
            })
          ) : null}

          {!publicSelfService && !workspaces.length ? (
            <div className="access-empty-state">
              <strong>Workspace не привязаны к клиенту</strong>
              <span>Откройте карточку клиента и выберите Operational Core workspaces, после этого здесь появятся назначения.</span>
            </div>
          ) : null}
        </div>

        <div className="service-content-modal__foot">
          <Button variant="secondary" surface="modal" type="button" onClick={onClose}>
            Закрыть
          </Button>
        </div>
      </article>
    </div>
  );
}

function AccessCellControl({
  cell,
  service,
  active,
  pendingValue,
  busy = false,
  publicSelfService = false,
  readOnly = false,
  onSelectCell,
  onSetAccess,
  onOpenDetails,
}: {
  cell: AccessMatrixCell;
  service: Service;
  active: boolean;
  pendingValue?: AccessAssignmentValue;
  busy?: boolean;
  publicSelfService?: boolean;
  readOnly?: boolean;
  onSelectCell: (cell: AccessMatrixCell) => void;
  onSetAccess: (value: AccessAssignmentValue) => void;
  onOpenDetails?: () => void;
}) {
  const isPending = pendingValue !== undefined || busy;
  const assignmentValue = pendingValue ?? accessAssignmentValue(cell);
  const selectValue = publicSelfService ? publicOperationalCoreSelectValue(assignmentValue) : assignmentValue;
  const selectOptions = publicSelfService
    ? publicOperationalCoreAccessOptions
    : isEngineServiceForAccessControl(service)
      ? engineAccessAssignmentOptions
      : isOperationalCoreService(service)
        ? operationalCoreAccessAssignmentOptions
      : accessAssignmentOptions;
  const displayTitle = isPending
    ? publicSelfService
      ? publicAccessAssignmentLabel(assignmentValue)
      : accessAssignmentLabel(assignmentValue)
    : publicSelfService
      ? publicOperationalCoreCellTitle(cell)
      : accessCellTitle(cell);
  const displaySource = isPending
    ? "Сохраняем..."
    : publicSelfService
      ? publicOperationalCoreCellSubtitle(cell)
      : sourceLabel(cell.effectiveAccess.source);
  const cellClassName = cn(
    "access-cell",
    onOpenDetails && "access-cell--modal",
    cell.effectiveAccess.allowed && "access-cell--allowed",
    !cell.effectiveAccess.allowed && "access-cell--denied",
    cell.effectiveAccess.source === "exception" && !publicSelfService && "access-cell--exception",
    readOnly && "access-cell--readonly",
    isPending && "access-cell--pending",
    active && "access-cell--active"
  );

  if (onOpenDetails) {
    return (
      <button
        className={cellClassName}
        type="button"
        aria-busy={isPending}
        disabled={isPending}
        onClick={() => {
          onSelectCell(cell);
          onOpenDetails();
        }}
      >
        <strong>{displayTitle}</strong>
        <span>{displaySource}</span>
      </button>
    );
  }

  if (readOnly) {
    return (
      <span className={cellClassName}>
        <strong>{displayTitle}</strong>
        <span>{displaySource}</span>
      </span>
    );
  }

  return (
    <NodeDcSelect
      value={selectValue}
      options={selectOptions}
      label={`Назначить доступ ${cell.userId} / ${cell.serviceId}`}
      minMenuWidth={172}
      menuClassName="access-cell-menu"
      disabled={isPending}
      onChange={(value) => onSetAccess(value)}
      trigger={({ open, toggle, setTriggerRef }) => (
        <button
          ref={setTriggerRef}
          className={cellClassName}
          type="button"
          aria-expanded={open}
          aria-busy={isPending}
          onClick={() => {
            onSelectCell(cell);
            toggle();
          }}
        >
          <strong>{displayTitle}</strong>
          <span>{displaySource}</span>
        </button>
      )}
    />
  );
}

function InvitesSection({
  data,
  clientId,
  actorUserId,
  isPublicPoolContext,
  publicInviteTab,
  publicInviteSourceFilter,
  onPublicInviteTabChange,
  onCreateInvite,
  onUpdateInvite,
  onDeleteInvite,
  onUpdateAccessRequest,
  onApproveAccessRequest,
  onRejectAccessRequest,
  onApproveTaskerInviteRequest,
  onRejectTaskerInviteRequest,
  onApproveEngineWorkflowAccessRequest,
  onRejectEngineWorkflowAccessRequest,
}: {
  data: LauncherData;
  clientId: string;
  actorUserId: string;
  isPublicPoolContext: boolean;
  publicInviteTab: PublicInviteTab;
  publicInviteSourceFilter: PublicInviteSourceFilter;
  onPublicInviteTabChange: (tab: PublicInviteTab) => void;
  onCreateInvite: (invite: Pick<Invite, "clientId" | "email" | "role">) => void;
  onUpdateInvite: (inviteId: string, patch: Partial<Invite>) => void;
  onDeleteInvite: (inviteId: string) => void;
  onUpdateAccessRequest: (
    accessRequestId: string,
    patch: Partial<Pick<AccessRequest, "targetClientId" | "role" | "comment">>
  ) => void;
  onApproveAccessRequest: (
    accessRequestId: string,
    patch: Partial<Pick<AccessRequest, "targetClientId" | "role" | "comment">>
  ) => void;
  onRejectAccessRequest: (accessRequestId: string, patch: Partial<Pick<AccessRequest, "comment">>) => void;
  onApproveTaskerInviteRequest: (taskerInviteRequestId: string, patch: Partial<Pick<TaskerInviteRequest, "comment">>) => void;
  onRejectTaskerInviteRequest: (taskerInviteRequestId: string, patch: Partial<Pick<TaskerInviteRequest, "comment">>) => void;
  onApproveEngineWorkflowAccessRequest: (
    engineWorkflowAccessRequestId: string,
    patch: EngineWorkflowAccessRequestDecisionPatch
  ) => void;
  onRejectEngineWorkflowAccessRequest: (
    engineWorkflowAccessRequestId: string,
    patch: EngineWorkflowAccessRequestDecisionPatch
  ) => void;
}) {
  const [email, setEmail] = useState("");
  const [role, setRole] = useState<ClientMembershipRole>("member");
  const [deleteInviteId, setDeleteInviteId] = useState<string | null>(null);
  const [copiedInviteId, setCopiedInviteId] = useState<string | null>(null);
  const invites = data.invites.filter((invite) => invite.clientId === clientId);
  const engineWorkflowInviteRequests = data.engineWorkflowAccessRequests.filter((request) => request.requestType !== "service_role");
  const incomingRequestsTotal =
    data.accessRequests.length +
    data.taskerInviteRequests.filter((request) => request.status !== "cancelled").length +
    engineWorkflowInviteRequests.filter((request) => request.status !== "cancelled").length;
  const pendingIncomingRequests =
    data.accessRequests.filter((request) => request.status === "new").length +
    data.taskerInviteRequests.filter((request) => request.status === "new").length +
    engineWorkflowInviteRequests.filter((request) => request.status === "new").length;
  const deletingInvite = invites.find((invite) => invite.id === deleteInviteId) ?? null;
  const actor = getUser(data, actorUserId);
  const clientOptions: Array<NodeDcSelectOption<string>> = [
    { value: PUBLIC_POOL_CLIENT_ID, label: PUBLIC_POOL_CONTEXT_LABEL, description: PUBLIC_POOL_CONTEXT_DESCRIPTION },
    ...data.clients.map((client) => ({ value: client.id, label: client.name, description: client.legalName ?? undefined })),
  ];

  function handleCreateInvite() {
    if (!email.trim()) return;

    onCreateInvite({ clientId, email: email.trim(), role });
    setEmail("");
    setRole("member");
  }

  async function handleCopyInvite(invite: Invite) {
    const inviteUrl = buildInviteUrl(invite.token);

    try {
      await copyToClipboard(inviteUrl);
    } catch {
      return;
    }

    setCopiedInviteId(invite.id);

    if (invite.status === "created") {
      onUpdateInvite(invite.id, { status: "sent" });
    }

    window.setTimeout(() => {
      setCopiedInviteId((currentInviteId) => (currentInviteId === invite.id ? null : currentInviteId));
    }, 1800);
  }

  return (
    <div className="invites-layout invites-layout--catalog">
      {isPublicPoolContext ? (
        <GlassSurface className="admin-tabs-card">
          <div className="admin-tabs">
            <button
              type="button"
              className={cn("admin-tab-button", publicInviteTab === "incoming" && "admin-tab-button--active")}
              onClick={() => onPublicInviteTabChange("incoming")}
            >
              Входящие · {incomingRequestsTotal}
            </button>
            <button
              type="button"
              className={cn("admin-tab-button", publicInviteTab === "outgoing" && "admin-tab-button--active")}
              onClick={() => onPublicInviteTabChange("outgoing")}
            >
              Исходящие · {invites.length}
            </button>
          </div>
          <p className="admin-helper-note">
            {publicInviteTab === "incoming"
              ? `Входящие — общие заявки NODE.DC, Operational Core и Engine. Новых к обработке: ${pendingIncomingRequests}.`
              : "Исходящие — invite-ссылки, выпущенные вручную или созданные после approve. Принятые ссылки остаются историей."}
          </p>
        </GlassSurface>
      ) : null}

      {isPublicPoolContext && publicInviteTab === "incoming" ? (
        <AccessRequestsPanel
          data={data}
          clientOptions={clientOptions}
          sourceFilter={publicInviteSourceFilter}
          copiedInviteId={copiedInviteId}
          onCopyInvite={handleCopyInvite}
          onUpdateAccessRequest={onUpdateAccessRequest}
          onApproveAccessRequest={onApproveAccessRequest}
          onRejectAccessRequest={onRejectAccessRequest}
          onApproveTaskerInviteRequest={onApproveTaskerInviteRequest}
          onRejectTaskerInviteRequest={onRejectTaskerInviteRequest}
          onApproveEngineWorkflowAccessRequest={onApproveEngineWorkflowAccessRequest}
          onRejectEngineWorkflowAccessRequest={onRejectEngineWorkflowAccessRequest}
        />
      ) : (
        <>
      <GlassSurface className="invite-form invite-form--compact">
        <div className="table-toolbar">
          <div>
            <p className="eyebrow">Инвайт от {actor.name}</p>
            <h3>Создать приглашение</h3>
          </div>
          <IconButton
            label="Сгенерировать ссылку"
            className="admin-circle-action admin-circle-action--solid"
            type="button"
            disabled={!email.trim()}
            onClick={handleCreateInvite}
          >
            <MailPlus size={16} />
          </IconButton>
        </div>
        <div className="invite-form__fields">
          <input value={email} onChange={(event) => setEmail(event.target.value)} placeholder="email@company.ru" />
          <NodeDcSelect
            className="admin-table-select-wrap"
            triggerClassName="admin-modal-select-trigger"
            value={role}
            options={inviteRoleOptions}
            label="Роль инвайта"
            onChange={(nextRole) => setRole(nextRole)}
          />
        </div>
      </GlassSurface>

      <GlassSurface className="table-shell">
        <div className="table-toolbar">
          <div>
            <h3>{isPublicPoolContext ? "Исходящие инвайты" : "Инвайты"}</h3>
            {isPublicPoolContext ? (
              <p className="admin-helper-note">
                Отзыв блокирует только неиспользованную ссылку. Уже принятый инвайт управляется через Root Admin → Пользователи.
              </p>
            ) : null}
          </div>
        </div>
        <table className="admin-data-table">
          <thead>
            <tr>
              <th>Email</th>
              <th>Источник</th>
              <th>Роль</th>
              <th>Статус</th>
              <th>Ссылка</th>
              <th>Истекает</th>
              <th aria-label="Удаление" />
            </tr>
          </thead>
          <tbody>
            {invites.map((invite) => {
              const inviteUrl = buildInviteUrl(invite.token);
              const isCopied = copiedInviteId === invite.id;
              const isAccepted = invite.status === "accepted";
              const canCopyInvite = inviteCanBeCopied(invite);

              return (
                <tr key={invite.id}>
                  <td>
                    {isAccepted ? (
                      <span className="admin-table-text admin-table-text--strong">{invite.email}</span>
                    ) : (
                      <input
                        className="admin-table-input admin-table-input--strong"
                        value={invite.email}
                        onChange={(event) => onUpdateInvite(invite.id, { email: event.target.value })}
                        aria-label={`Email инвайта ${invite.email}`}
                      />
                    )}
                  </td>
                  <td>
                    <div className="membership-inviter-cell">
                      <span>{inviteSourceLabel(invite)}</span>
                      {invite.sourceWorkspaceName || invite.sourceWorkspaceSlug ? (
                        <small>{invite.sourceWorkspaceName ?? invite.sourceWorkspaceSlug}</small>
                      ) : null}
                    </div>
                  </td>
                  <td>
                    {isAccepted ? (
                      <AdminStaticPill>{membershipRoleLabel(invite.role)}</AdminStaticPill>
                    ) : (
                      <NodeDcSelect
                        className="admin-table-select-wrap"
                        triggerClassName="admin-table-select-trigger"
                        value={invite.role}
                        options={inviteRoleOptions}
                        label={`Роль инвайта ${invite.email}`}
                        minMenuWidth={172}
                        onChange={(nextRole) => onUpdateInvite(invite.id, { role: nextRole })}
                      />
                    )}
                  </td>
                  <td>
                    {isAccepted ? (
                      <AdminStatusPill value={invite.status} options={inviteStatusOptions} />
                    ) : (
                      <AdminStatusDropdown
                        value={invite.status}
                        options={editableInviteStatusOptions}
                        label={`Статус инвайта ${invite.email}`}
                        onChange={(status) => onUpdateInvite(invite.id, { status })}
                      />
                    )}
                  </td>
                  <td>
                    {canCopyInvite ? (
                      <div className="invite-link-cell">
                        <code title={inviteUrl}>{inviteUrl}</code>
                        {isCopied ? <span className="invite-link-cell__state">Скопировано</span> : null}
                        <IconButton
                          label={isCopied ? `Инвайт ${invite.email} скопирован` : `Скопировать инвайт ${invite.email}`}
                          className="admin-icon-action invite-icon-action"
                          type="button"
                          onClick={() => void handleCopyInvite(invite)}
                        >
                          <Copy size={11} />
                        </IconButton>
                      </div>
                    ) : (
                      <span className="muted-text">{inviteTerminalLabel(invite)}</span>
                    )}
                  </td>
                  <td>
                    {isAccepted ? (
                      <span className="muted-text">{formatDate(invite.expiresAt)}</span>
                    ) : (
                      <NodeDcDateField
                        value={invite.expiresAt}
                        label={`Инвайт истекает ${invite.email}`}
                        onChange={(value) => {
                          if (value) onUpdateInvite(invite.id, { expiresAt: value });
                        }}
                      />
                    )}
                  </td>
                  <td className="services-admin-table__actions">
                    <IconButton
                      label={`Удалить запись инвайта ${invite.email}`}
                      className="admin-icon-action invite-icon-action"
                      type="button"
                      onClick={() => setDeleteInviteId(invite.id)}
                    >
                      <Trash2 size={12} />
                    </IconButton>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </GlassSurface>
      <NodeDcDeleteModal
        isOpen={Boolean(deletingInvite)}
        title="Удалить запись инвайта"
        description={
          <>
            Будет удалена только запись invite-ссылки для <strong>{deletingInvite?.email}</strong>. Пользователь, доступы и
            история аккаунта не удаляются.
          </>
        }
        onClose={() => setDeleteInviteId(null)}
        onConfirm={() => {
          if (deletingInvite) onDeleteInvite(deletingInvite.id);
          setDeleteInviteId(null);
        }}
      />
        </>
      )}
    </div>
  );
}

function AccessRequestsPanel({
  data,
  clientOptions,
  sourceFilter,
  copiedInviteId,
  onCopyInvite,
  onUpdateAccessRequest,
  onApproveAccessRequest,
  onRejectAccessRequest,
  onApproveTaskerInviteRequest,
  onRejectTaskerInviteRequest,
  onApproveEngineWorkflowAccessRequest,
  onRejectEngineWorkflowAccessRequest,
}: {
  data: LauncherData;
  clientOptions: Array<NodeDcSelectOption<string>>;
  sourceFilter: PublicInviteSourceFilter;
  copiedInviteId: string | null;
  onCopyInvite: (invite: Invite) => Promise<void>;
  onUpdateAccessRequest: (
    accessRequestId: string,
    patch: Partial<Pick<AccessRequest, "targetClientId" | "role" | "comment">>
  ) => void;
  onApproveAccessRequest: (
    accessRequestId: string,
    patch: Partial<Pick<AccessRequest, "targetClientId" | "role" | "comment">>
  ) => void;
  onRejectAccessRequest: (accessRequestId: string, patch: Partial<Pick<AccessRequest, "comment">>) => void;
  onApproveTaskerInviteRequest: (taskerInviteRequestId: string, patch: Partial<Pick<TaskerInviteRequest, "comment">>) => void;
  onRejectTaskerInviteRequest: (taskerInviteRequestId: string, patch: Partial<Pick<TaskerInviteRequest, "comment">>) => void;
  onApproveEngineWorkflowAccessRequest: (
    engineWorkflowAccessRequestId: string,
    patch: EngineWorkflowAccessRequestDecisionPatch
  ) => void;
  onRejectEngineWorkflowAccessRequest: (
    engineWorkflowAccessRequestId: string,
    patch: EngineWorkflowAccessRequestDecisionPatch
  ) => void;
}) {
  const accessRequests = data.accessRequests.slice().sort((a, b) => b.createdAt.localeCompare(a.createdAt));
  const taskerInviteRequests = data.taskerInviteRequests
    .filter((request) => request.status !== "cancelled")
    .slice()
    .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
  const engineWorkflowAccessRequests = data.engineWorkflowAccessRequests
    .filter((request) => request.requestType !== "service_role" && request.status !== "cancelled")
    .slice()
    .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
  const showNodeDcRequests = sourceFilter === "all" || sourceFilter === "ndc";
  const showTaskerRequests = sourceFilter === "all" || sourceFilter === "ops";
  const showEngineRequests = sourceFilter === "all" || sourceFilter === "engine";

  return (
    <>
    {showNodeDcRequests ? (
    <GlassSurface className="table-shell">
      <div className="table-toolbar">
        <div>
          <h3>NODE.DC: входящие запросы доступа</h3>
          <p className="admin-helper-note">
            Перед approve выберите целевой контур. Аккаунт уже создан в Authentik и активируется после подтверждения.
          </p>
        </div>
      </div>

      {accessRequests.length === 0 ? (
        <div className="access-empty-state">
          <strong>Входящих заявок пока нет</strong>
          <span>Кнопка “Запросить доступ” на login будет отправлять пользователей в эту очередь.</span>
        </div>
      ) : (
        <div className="access-request-table-scroll">
          <table className="admin-data-table admin-data-table--access-requests">
            <thead>
              <tr>
                <th>Заявитель</th>
                <th>Контакты</th>
                <th>Компания</th>
                <th>Назначение</th>
                <th>Роль</th>
                <th>Статус</th>
                <th>Аккаунт</th>
                <th aria-label="Действия" />
              </tr>
            </thead>
            <tbody>
              {accessRequests.map((accessRequest) => {
                const approvedInvite = accessRequest.approvedInviteId
                  ? data.invites.find((invite) => invite.id === accessRequest.approvedInviteId) ?? null
                  : null;
                const isTerminal = accessRequest.status !== "new";
                const isCopied = Boolean(approvedInvite && copiedInviteId === approvedInvite.id);
                const approvedInviteCanBeCopied = Boolean(approvedInvite && inviteCanBeCopied(approvedInvite));

                return (
                  <tr key={accessRequest.id}>
                    <td>
                      <div className="access-request-applicant">
                        <strong>{formatAccessRequestName(accessRequest)}</strong>
                        <small>{formatDateTime(accessRequest.createdAt)}</small>
                      </div>
                    </td>
                    <td>
                      <div className="access-request-contact">
                        <span>{accessRequest.email}</span>
                        <small>{accessRequest.phone}</small>
                      </div>
                    </td>
                    <td>{accessRequest.company}</td>
                    <td>
                      <NodeDcSelect
                        className="admin-table-select-wrap"
                        triggerClassName="admin-table-select-trigger"
                        value={accessRequest.targetClientId}
                        options={clientOptions}
                        label={`Назначение заявки ${accessRequest.email}`}
                        minMenuWidth={220}
                        disabled={isTerminal}
                        onChange={(targetClientId) => onUpdateAccessRequest(accessRequest.id, { targetClientId })}
                      />
                    </td>
                    <td>
                      <NodeDcSelect
                        className="admin-table-select-wrap"
                        triggerClassName="admin-table-select-trigger"
                        value={accessRequest.role}
                        options={inviteRoleOptions}
                        label={`Роль заявки ${accessRequest.email}`}
                        minMenuWidth={172}
                        disabled={isTerminal}
                        onChange={(role) => onUpdateAccessRequest(accessRequest.id, { role })}
                      />
                    </td>
                    <td>
                      <AdminStatusPill value={accessRequest.status} options={accessRequestStatusOptions} />
                    </td>
                    <td>
                      {approvedInvite && approvedInviteCanBeCopied ? (
                        <div className="invite-link-cell">
                          <code title={buildInviteUrl(approvedInvite.token)}>{buildInviteUrl(approvedInvite.token)}</code>
                          {isCopied ? <span className="invite-link-cell__state">Скопировано</span> : null}
                          <IconButton
                            label={`Скопировать инвайт ${approvedInvite.email}`}
                            className="admin-icon-action invite-icon-action"
                            type="button"
                            onClick={() => void onCopyInvite(approvedInvite)}
                          >
                            <Copy size={11} />
                          </IconButton>
                        </div>
                      ) : approvedInvite ? (
                        <span className="muted-text">{inviteTerminalLabel(approvedInvite)}</span>
                      ) : accessRequest.status === "approved" ? (
                        <span className="muted-text">Активен</span>
                      ) : (
                        <span className="muted-text">—</span>
                      )}
                    </td>
                    <td>
                      {accessRequest.status === "new" ? (
                        <div className="access-request-decision-cluster">
                          <button
                            aria-label={`Подтвердить заявку ${accessRequest.email}`}
                            className="access-request-decision-button access-request-decision-button--accept"
                            type="button"
                            onClick={() =>
                              onApproveAccessRequest(accessRequest.id, {
                                targetClientId: accessRequest.targetClientId,
                                role: accessRequest.role,
                              })
                            }
                          >
                            <Check size={16} strokeWidth={2.6} />
                          </button>
                          <button
                            aria-label={`Отклонить заявку ${accessRequest.email}`}
                            className="access-request-decision-button access-request-decision-button--decline"
                            type="button"
                            onClick={() => onRejectAccessRequest(accessRequest.id, {})}
                          >
                            <X size={16} strokeWidth={2.5} />
                          </button>
                        </div>
                      ) : (
                        <span className="muted-text">—</span>
                      )}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </GlassSurface>
    ) : null}
    {showTaskerRequests ? (
    <TaskerInviteRequestsPanel
      requests={taskerInviteRequests}
      onApproveTaskerInviteRequest={onApproveTaskerInviteRequest}
      onRejectTaskerInviteRequest={onRejectTaskerInviteRequest}
    />
    ) : null}
    {showEngineRequests ? (
    <EngineWorkflowAccessRequestsPanel
      requests={engineWorkflowAccessRequests}
      onApproveEngineWorkflowAccessRequest={onApproveEngineWorkflowAccessRequest}
      onRejectEngineWorkflowAccessRequest={onRejectEngineWorkflowAccessRequest}
    />
    ) : null}
    </>
  );
}

function TaskerInviteRequestsPanel({
  requests,
  onApproveTaskerInviteRequest,
  onRejectTaskerInviteRequest,
}: {
  requests: TaskerInviteRequest[];
  onApproveTaskerInviteRequest: (taskerInviteRequestId: string, patch: Partial<Pick<TaskerInviteRequest, "comment">>) => void;
  onRejectTaskerInviteRequest: (taskerInviteRequestId: string, patch: Partial<Pick<TaskerInviteRequest, "comment">>) => void;
}) {
  const [copiedRequestId, setCopiedRequestId] = useState<string | null>(null);

  async function handleCopyTaskerInvite(request: TaskerInviteRequest) {
    if (!request.platformInviteToken) return;

    try {
      await copyToClipboard(buildInviteUrl(request.platformInviteToken));
    } catch {
      return;
    }

    setCopiedRequestId(request.id);
    window.setTimeout(() => {
      setCopiedRequestId((currentRequestId) => (currentRequestId === request.id ? null : currentRequestId));
    }, 1800);
  }

  return (
    <GlassSurface className="table-shell">
      <div className="table-toolbar">
        <div>
          <h3>Operational Core: входящие запросы доступа</h3>
          <p className="admin-helper-note">
            Self-service админы Operational Core создают эти заявки из настроек workspace. После approve ссылка становится доступна
            пригласившему пользователю.
          </p>
        </div>
      </div>

      {requests.length === 0 ? (
        <div className="access-empty-state">
          <strong>Workspace-инвайтов пока нет</strong>
          <span>Когда self-service admin добавит участника в Operational Core, заявка появится здесь.</span>
        </div>
      ) : (
        <div className="access-request-table-scroll">
          <table className="admin-data-table admin-data-table--access-requests">
            <thead>
              <tr>
                <th>Workspace</th>
                <th>Приглашённый</th>
                <th>Инициатор</th>
                <th>Роль</th>
                <th>Статус</th>
                <th>Ссылка</th>
                <th aria-label="Действия" />
              </tr>
            </thead>
            <tbody>
              {requests.map((request) => {
                const isCopied = copiedRequestId === request.id;

                return (
                  <tr key={request.id}>
                    <td>
                      <div className="access-request-applicant">
                        <strong>{request.workspaceName}</strong>
                        <small>{request.workspaceSlug}</small>
                      </div>
                    </td>
                    <td>
                      <div className="access-request-contact">
                        <span>{request.inviteeEmail}</span>
                        <small>{formatDateTime(request.createdAt)}</small>
                      </div>
                    </td>
                    <td>
                      <div className="access-request-contact">
                        <span>{request.inviterName}</span>
                        <small>{request.inviterEmail}</small>
                      </div>
                    </td>
                    <td>{taskerInviteRoleLabel(request.role)}</td>
                    <td>
                      <AdminStatusPill value={request.status} options={taskerInviteRequestStatusOptions} />
                    </td>
                    <td>
                      {request.platformInviteToken ? (
                        <div className="invite-link-cell">
                          <code title={buildInviteUrl(request.platformInviteToken)}>{buildInviteUrl(request.platformInviteToken)}</code>
                          {isCopied ? <span className="invite-link-cell__state">Скопировано</span> : null}
                          <IconButton
                            label={`Скопировать workspace-инвайт ${request.inviteeEmail}`}
                            className="admin-icon-action invite-icon-action"
                            type="button"
                            onClick={() => void handleCopyTaskerInvite(request)}
                          >
                            <Copy size={11} />
                          </IconButton>
                        </div>
                      ) : (
                        <span className="muted-text">—</span>
                      )}
                    </td>
                    <td>
                      {request.status === "new" ? (
                        <div className="access-request-decision-cluster">
                          <button
                            aria-label={`Подтвердить workspace-инвайт ${request.inviteeEmail}`}
                            className="access-request-decision-button access-request-decision-button--accept"
                            type="button"
                            onClick={() => onApproveTaskerInviteRequest(request.id, {})}
                          >
                            <Check size={16} strokeWidth={2.6} />
                          </button>
                          <button
                            aria-label={`Отклонить workspace-инвайт ${request.inviteeEmail}`}
                            className="access-request-decision-button access-request-decision-button--decline"
                            type="button"
                            onClick={() => onRejectTaskerInviteRequest(request.id, {})}
                          >
                            <X size={16} strokeWidth={2.5} />
                          </button>
                        </div>
                      ) : (
                        <span className="muted-text">—</span>
                      )}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </GlassSurface>
  );
}

function EngineWorkflowAccessRequestsPanel({
  requests,
  onApproveEngineWorkflowAccessRequest,
  onRejectEngineWorkflowAccessRequest,
}: {
  requests: EngineWorkflowAccessRequest[];
  onApproveEngineWorkflowAccessRequest: (
    engineWorkflowAccessRequestId: string,
    patch: EngineWorkflowAccessRequestDecisionPatch
  ) => void;
  onRejectEngineWorkflowAccessRequest: (
    engineWorkflowAccessRequestId: string,
    patch: EngineWorkflowAccessRequestDecisionPatch
  ) => void;
}) {
  return (
    <GlassSurface className="table-shell">
      <div className="table-toolbar">
        <div>
          <h3>NODE.DC Engine: запросы доступа</h3>
          <p className="admin-helper-note">
            Здесь только workflow-шаринг Engine. Повышения роли Engine обрабатываются через уведомления и аудит.
          </p>
        </div>
      </div>

      {requests.length === 0 ? (
        <div className="access-empty-state">
          <strong>Запросов Engine пока нет</strong>
          <span>Если пользователь Engine попросит выдать доступ к workflow, заявка появится здесь.</span>
        </div>
      ) : (
        <div className="access-request-table-scroll">
          <table className="admin-data-table admin-data-table--access-requests">
            <thead>
              <tr>
                <th>Workflow</th>
                <th>Пользователь</th>
                <th>Инициатор</th>
                <th>Роль</th>
                <th>Статус</th>
                <th aria-label="Действия" />
              </tr>
            </thead>
            <tbody>
              {requests.map((request) => (
                <tr key={request.id}>
                  <td>
                    <div className="access-request-applicant">
                      <strong>{request.workflowName}</strong>
                      <small>{request.workflowId}</small>
                    </div>
                  </td>
                  <td>
                    <div className="access-request-contact">
                      <span>{request.targetName || request.targetEmail}</span>
                      <small>{request.targetEmail}</small>
                    </div>
                  </td>
                  <td>
                    <div className="access-request-contact">
                      <span>{request.requesterName}</span>
                      <small>{request.requesterEmail}</small>
                    </div>
                  </td>
                  <td>{engineWorkflowRoleLabel(request.role)}</td>
                  <td>
                    <AdminStatusPill value={request.status} options={engineWorkflowAccessRequestStatusOptions} />
                  </td>
                  <td>
                    {request.status === "new" ? (
                      <div className="access-request-decision-cluster">
                          <button
                            aria-label={`Подтвердить доступ Engine ${request.targetEmail}`}
                            className="access-request-decision-button access-request-decision-button--accept"
                            type="button"
                            onClick={() => onApproveEngineWorkflowAccessRequest(request.id, {})}
                          >
                            <Check size={16} strokeWidth={2.6} />
                        </button>
                        <button
                          aria-label={`Отклонить доступ Engine ${request.targetEmail}`}
                          className="access-request-decision-button access-request-decision-button--decline"
                          type="button"
                          onClick={() => onRejectEngineWorkflowAccessRequest(request.id, {})}
                        >
                          <X size={16} strokeWidth={2.5} />
                        </button>
                      </div>
                    ) : (
                      <span className="muted-text">—</span>
                    )}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </GlassSurface>
  );
}

function formatAccessRequestName(accessRequest: AccessRequest) {
  return [accessRequest.lastName, accessRequest.firstName, accessRequest.middleName].filter(Boolean).join(" ");
}

function getMembershipInviterMeta(data: LauncherData, membership: ClientMembership) {
  const inviter = membership.invitedByUserId ? data.users.find((user) => user.id === membership.invitedByUserId) ?? null : null;
  const taskerRequest = membership.sourceTaskerInviteRequestId
    ? data.taskerInviteRequests.find((request) => request.id === membership.sourceTaskerInviteRequestId)
    : null;
  const engineRequest = membership.sourceEngineWorkflowAccessRequestId
    ? data.engineWorkflowAccessRequests.find((request) => request.id === membership.sourceEngineWorkflowAccessRequestId)
    : null;

  if (membership.source === "tasker_workspace_invite") {
    const title = inviter?.name ?? taskerRequest?.inviterName ?? "Operational Core";
    const subtitle = inviter?.email ?? taskerRequest?.inviterEmail ?? "Self-service invite";
    const workspaceLabel = taskerRequest?.workspaceName || taskerRequest?.workspaceSlug;

    return {
      title,
      subtitle,
      sourceLabel: workspaceLabel ? `Operational Core · ${workspaceLabel}` : "Operational Core",
      showInAccessMatrix: true,
    };
  }

  if (membership.source === "access_request") {
    return {
      title: "Публичная заявка",
      subtitle: inviter ? `approve: ${inviter.name}` : "NODE.DC",
      sourceLabel: "Заявка доступа",
      showInAccessMatrix: true,
    };
  }

  if (membership.source === "engine_workflow_access_request") {
    return {
      title: inviter?.name ?? engineRequest?.requesterName ?? "Engine",
      subtitle: inviter?.email ?? engineRequest?.requesterEmail ?? "Workflow share request",
      sourceLabel: engineRequest?.workflowName ? `Engine · ${engineRequest.workflowName}` : "Engine workflow",
      showInAccessMatrix: true,
    };
  }

  if (membership.inviteId) {
    return {
      title: inviter?.name ?? "NODE.DC",
      subtitle: inviter?.email ?? "Launcher invite",
      sourceLabel: "Launcher invite",
      showInAccessMatrix: true,
    };
  }

  return {
    title: inviter?.name ?? "NODE.DC",
    subtitle: inviter?.email ?? "Ручное добавление",
    sourceLabel: "Ручное добавление",
    showInAccessMatrix: false,
  };
}

function getPlatformUserOrigin(data: LauncherData, user: LauncherUser, memberships: ClientMembership[]) {
  const publicMembership = memberships.find((membership) => isPublicPoolClientId(membership.clientId));

  if (publicMembership) {
    const inviterMeta = getMembershipInviterMeta(data, publicMembership);

    if (publicMembership.source === "tasker_workspace_invite") {
      return {
        label: "Открытый контур · self-host invite",
        detail: `${inviterMeta.title} · ${inviterMeta.sourceLabel}`,
      };
    }

    if (publicMembership.source === "access_request") {
      const request = data.accessRequests.find((item) => item.email.toLowerCase() === user.email.toLowerCase());
      return {
        label: "Открытый контур · прямой запрос",
        detail: request?.company ? `Компания из заявки: ${request.company}` : inviterMeta.subtitle,
      };
    }

    if (publicMembership.source === "engine_workflow_access_request") {
      return {
        label: "Открытый контур · Engine workflow",
        detail: `${inviterMeta.title} · ${inviterMeta.sourceLabel}`,
      };
    }

    if (publicMembership.inviteId) {
      return {
        label: "Открытый контур · ручной инвайт",
        detail: inviterMeta.subtitle,
      };
    }

    return {
      label: "Открытый контур",
      detail: inviterMeta.subtitle,
    };
  }

  if (memberships.length > 0) {
    const primaryMembership = memberships[0];
    const client = getClient(data, primaryMembership.clientId);
    const extraContexts = memberships.length > 1 ? `, ещё ${memberships.length - 1}` : "";

    return {
      label: `Компания · ${client.name}`,
      detail: `${client.legalName ?? client.name}${extraContexts}`,
    };
  }

  const pendingInvite = data.invites.find((invite) => invite.email.toLowerCase() === user.email.toLowerCase());
  const pendingRequest = data.accessRequests.find((request) => request.email.toLowerCase() === user.email.toLowerCase());

  if (pendingInvite) {
    return {
      label: "Инвайт · без контура",
      detail: inviteSourceLabel(pendingInvite),
    };
  }

  if (pendingRequest) {
    return {
      label: "Заявка · без контура",
      detail: pendingRequest.company,
    };
  }

  return {
    label: "Без контура",
    detail: "Нет активных memberships",
  };
}

function MembershipInviterCell({ data, membership }: { data: LauncherData; membership: ClientMembership }) {
  const inviterMeta = getMembershipInviterMeta(data, membership);

  return (
    <div className="membership-inviter-cell">
      <span>{inviterMeta.title}</span>
      <small>{inviterMeta.subtitle}</small>
      <small>{inviterMeta.sourceLabel}</small>
    </div>
  );
}

function buildInviteUrl(token: string) {
  if (typeof window === "undefined") return `/invite/${token}`;

  return new URL(`/invite/${token}`, window.location.origin).toString();
}

async function copyToClipboard(value: string) {
  if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
    await navigator.clipboard.writeText(value);
    return;
  }

  if (typeof document === "undefined") {
    throw new Error("Clipboard API is not available");
  }

  const input = document.createElement("textarea");
  input.value = value;
  input.setAttribute("readonly", "");
  input.style.position = "fixed";
  input.style.opacity = "0";
  document.body.append(input);
  input.select();

  const copied = document.execCommand("copy");
  input.remove();

  if (!copied) {
    throw new Error("Fallback copy failed");
  }
}

function SyncSection({
  data,
  clientId,
  isRoot,
  authentikHealth,
  onRetrySync,
}: {
  data: LauncherData;
  clientId: string;
  isRoot: boolean;
  authentikHealth: AuthentikSyncHealth | null;
  onRetrySync: (syncId: string) => void;
}) {
  const syncRows = data.syncStatuses.filter((sync) => isRoot || sync.objectId === clientId || sync.objectName.includes(getClient(data, clientId).name));
  const authentikRows = syncRows.filter((sync) => sync.target === "authentik");
  const activeRows = authentikRows.filter((sync) => sync.state === "error" || isFreshPendingSync(sync));
  const historicalPendingRows = authentikRows.filter((sync) => sync.state === "pending" && !isFreshPendingSync(sync));
  const healthState = authentikHealth?.state ?? deriveAuthentikHealthState(authentikRows);
  const counts = authentikHealth?.counts ?? buildLocalAuthentikSyncCounts(authentikRows);

  return (
    <div className="sync-diagnostics">
      <GlassSurface className="sync-health-card">
        <div className="sync-health-card__main">
          <p className="eyebrow">Authentik</p>
          <h3>Диагностика синхронизации</h3>
          <p className="admin-helper-note">
            Экран показывает только реальные ошибки и свежие операции. Старые pending-строки свернуты отдельно как исторический след.
          </p>
        </div>
        <AdminStatusPill value={healthState} options={authentikSyncHealthOptions} />
        <div className="sync-health-card__metrics" aria-label="Сводка Authentik sync">
          <span>
            <strong>{counts.errors}</strong>
            Ошибки
          </span>
          <span>
            <strong>{counts.freshPending}</strong>
            Свежая очередь
          </span>
          <span>
            <strong>{counts.stalePending}</strong>
            Старый pending
          </span>
          <span>
            <strong>{counts.synced}</strong>
            Synced
          </span>
        </div>
      </GlassSurface>

      <GlassSurface className="table-shell">
        <div className="table-toolbar">
          <div>
            <h3>Требует внимания</h3>
            <p className="admin-helper-note">Ошибки и pending за последние 5 минут. Retry запускает реальный Authentik provision для затронутых пользователей.</p>
          </div>
        </div>
        {activeRows.length ? (
          <SyncRowsTable rows={activeRows} onRetrySync={onRetrySync} />
        ) : (
          <div className="access-empty-state">
            <strong>Активных проблем нет</strong>
            <span>Если доступы работают, старые pending-строки ниже можно считать историческим шумом до отдельной чистки ledger.</span>
          </div>
        )}
      </GlassSurface>

      {historicalPendingRows.length ? (
        <GlassSurface className="table-shell sync-history-card">
          <details>
            <summary>Исторические pending · {historicalPendingRows.length}</summary>
            <p className="admin-helper-note">
              Эти строки давно висят в очереди и не являются надежным признаком падения Authentik. Их можно перепроверить точечно через retry.
            </p>
            <SyncRowsTable rows={historicalPendingRows} onRetrySync={onRetrySync} />
          </details>
        </GlassSurface>
      ) : null}
    </div>
  );
}

function SyncRowsTable({ rows, onRetrySync }: { rows: SyncStatus[]; onRetrySync: (syncId: string) => void }) {
  return (
    <table className="admin-data-table admin-data-table--sync">
      <thead>
        <tr>
          <th>Объект</th>
          <th>Тип</th>
          <th>Цель</th>
          <th>Статус</th>
          <th>Последняя sync</th>
          <th>Ошибка</th>
          <th>Действие</th>
        </tr>
      </thead>
      <tbody>
        {rows.map((sync) => (
          <tr key={sync.id}>
            <td>{sync.objectName}</td>
            <td>{sync.objectType}</td>
            <td>{targetLabel(sync.target)}</td>
            <td>
              <AdminStatusPill value={sync.state} options={syncStatusOptions} />
            </td>
            <td>{formatDateTime(sync.lastSyncAt ?? sync.updatedAt)}</td>
            <td>{sync.error ?? "—"}</td>
            <td>
              <IconButton
                label={`Повторить синхронизацию ${sync.objectName}`}
                className="admin-icon-action services-admin-table__edit"
                type="button"
                onClick={() => onRetrySync(sync.id)}
              >
                <RefreshCw size={12} />
              </IconButton>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

function deriveAuthentikHealthState(rows: SyncStatus[]): AuthentikSyncHealthState {
  if (rows.some((sync) => sync.state === "error")) return "error";
  if (rows.some(isFreshPendingSync)) return "pending";
  return "ok";
}

function buildLocalAuthentikSyncCounts(rows: SyncStatus[]): AuthentikSyncHealth["counts"] {
  const pendingRows = rows.filter((sync) => sync.state === "pending");

  return {
    total: rows.length,
    synced: rows.filter((sync) => sync.state === "synced").length,
    pending: pendingRows.length,
    freshPending: pendingRows.filter(isFreshPendingSync).length,
    stalePending: pendingRows.filter((sync) => !isFreshPendingSync(sync)).length,
    errors: rows.filter((sync) => sync.state === "error").length,
    disabled: rows.filter((sync) => sync.state === "disabled").length,
  };
}

function isFreshPendingSync(sync: SyncStatus) {
  if (sync.state !== "pending") return false;
  const timestamp = Date.parse(sync.updatedAt ?? sync.lastSyncAt ?? "");
  if (!Number.isFinite(timestamp)) return false;
  return Date.now() - timestamp <= 5 * 60 * 1000;
}

function AuditSection({ data }: { data: LauncherData }) {
  return (
    <GlassSurface className="table-shell">
      <div className="table-toolbar">
        <div>
          <h3>Аудит</h3>
          <p className="admin-helper-note">Журнал действий: кто менял доступы, каталоги, инвайты и синхронизацию. Нужен для разбора спорных изменений.</p>
        </div>
      </div>
      <table className="admin-data-table">
        <thead>
          <tr>
            <th>Дата</th>
            <th>Пользователь</th>
            <th>Действие</th>
            <th>Объект</th>
            <th>Результат</th>
            <th>Детали</th>
          </tr>
        </thead>
        <tbody>
          {data.auditEvents.map((event) => (
            <tr key={event.id}>
              <td>{formatDateTime(event.at)}</td>
              <td>{event.actorName}</td>
              <td>{event.action}</td>
              <td>{event.objectName}</td>
              <td>
                <AdminStatusPill value={event.result} options={auditResultOptions} />
              </td>
              <td>{event.details ?? "—"}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </GlassSurface>
  );
}

function MiscSection({
  data,
  onUpdateSettings,
}: {
  data: LauncherData;
  onUpdateSettings: (patch: Partial<LauncherSettings>) => Promise<AdminMutationOutcome>;
}) {
  const [logoLinkUrl, setLogoLinkUrl] = useState(data.settings.brand.logoLinkUrl);
  const [workspaceCreationPolicy, setWorkspaceCreationPolicy] = useState(
    data.settings.taskManager.workspaceCreationPolicy
  );
  const [saveState, setSaveState] = useState<"idle" | "saving" | "saved" | "error">("idle");
  const [saveMessage, setSaveMessage] = useState<string | null>(null);

  useEffect(() => {
    setLogoLinkUrl(data.settings.brand.logoLinkUrl);
    setWorkspaceCreationPolicy(data.settings.taskManager.workspaceCreationPolicy);
  }, [data.settings.brand.logoLinkUrl, data.settings.taskManager.workspaceCreationPolicy]);

  const normalizedLogoLinkUrl = logoLinkUrl.trim() || "/";
  const hasChanges =
    normalizedLogoLinkUrl !== data.settings.brand.logoLinkUrl ||
    workspaceCreationPolicy !== data.settings.taskManager.workspaceCreationPolicy;

  async function handleSave() {
    setSaveState("saving");
    setSaveMessage(null);

    const result = await onUpdateSettings({
      brand: { logoLinkUrl: normalizedLogoLinkUrl },
      taskManager: { workspaceCreationPolicy },
    });

    if (result.ok) {
      setSaveState("saved");
      setSaveMessage("Сохранено");
      return;
    }

    setSaveState("error");
    setSaveMessage(result.message);
  }

  return (
    <GlassSurface className="table-shell admin-settings-panel">
      <div className="table-toolbar">
        <div>
          <h3>Разное</h3>
          <p className="admin-helper-note">
            Общие настройки платформы, которые должны применяться в лаунчере, Task Manager и системных auth-экранах.
          </p>
        </div>
        <Button
          variant="accent"
          type="button"
          icon={<Save size={16} />}
          disabled={!hasChanges || saveState === "saving"}
          onClick={handleSave}
        >
          {saveState === "saving" ? "Сохраняем" : "Сохранить"}
        </Button>
      </div>
      {saveMessage ? (
        <p className={cn("admin-settings-save-message", saveState === "error" && "admin-settings-save-message--error")}>
          {saveMessage}
        </p>
      ) : null}

      <div className="admin-settings-grid">
        <label className="admin-settings-field">
          <span>Ссылка логотипа</span>
          <input
            className="admin-table-input admin-settings-field__input"
            value={logoLinkUrl}
            placeholder="/"
            onChange={(event) => {
              setLogoLinkUrl(event.target.value);
              setSaveState("idle");
              setSaveMessage(null);
            }}
          />
          <small>Куда ведёт клик по логотипу NODE.DC. Можно указать относительный путь или полный URL.</small>
        </label>
        <label className="admin-settings-field">
          <span>Operational Core: создание workspace</span>
          <NodeDcSelect
            className="admin-modal-select-wrap"
            triggerClassName="admin-modal-select-trigger"
            value={workspaceCreationPolicy}
            options={taskManagerWorkspacePolicyOptions}
            label="Политика создания workspace в Operational Core"
            onChange={(value) => {
              setWorkspaceCreationPolicy(value);
              setSaveState("idle");
              setSaveMessage(null);
            }}
          />
          <small>Платформенная policy для новых пользователей без назначенных рабочих пространств в Task Manager.</small>
        </label>
      </div>
    </GlassSurface>
  );
}

function CompanySection({ data, clientId }: { data: LauncherData; clientId: string }) {
  const client = getClient(data, clientId);

  return (
    <GlassSurface className="company-panel">
      <p className="eyebrow">Профиль компании</p>
      <h3>{client.name}</h3>
      <div className="explanation-stack">
        <InfoLine label="Юридическое название" value={client.legalName ?? "—"} />
        <InfoLine label="Тип" value={client.type === "company" ? "Компания" : "Частное лицо"} />
        <InfoLine label="Контакт" value={`${client.contactName ?? "—"} · ${client.contactEmail ?? "—"}`} />
        <InfoLine label="Demo до" value={formatDate(client.demoEndsAt)} />
        <InfoLine label="Заметки" value={client.notes ?? "—"} />
      </div>
    </GlassSurface>
  );
}

function MetricCard({ label, value, hint, danger = false }: { label: string; value: number; hint: string; danger?: boolean }) {
  return (
    <GlassSurface className={cn("metric-card", danger && "metric-card--danger")}>
      <span>{label}</span>
      <strong>{value}</strong>
      <small>{hint}</small>
    </GlassSurface>
  );
}

function InfoLine({ label, value }: { label: string; value: string }) {
  return (
    <div className="info-line">
      <span>{label}</span>
      <strong>{value}</strong>
    </div>
  );
}

function roleLabel(role: string): string {
  const labels: Record<string, string> = {
    root_admin: "Root Admin",
    support_admin: "Support Admin",
    client_owner: "Client Owner",
    client_admin: "Client Admin",
    member: "Member",
  };
  return labels[role] ?? role;
}

function taskerInviteRoleLabel(role: TaskerInviteRequest["role"]): string {
  const labels: Record<TaskerInviteRequest["role"], string> = {
    guest: "Guest",
    member: "Member",
    admin: "Admin",
  };

  return labels[role] ?? role;
}

function engineWorkflowRoleLabel(role: EngineWorkflowAccessRequest["role"]): string {
  const labels: Record<EngineWorkflowAccessRequest["role"], string> = {
    viewer: "Просмотр",
    editor: "Редактор",
    admin: "Соавтор",
  };

  return labels[role] ?? role;
}

function engineServiceRoleLabel(role?: EngineWorkflowAccessRequest["requestedServiceRole"] | null): string {
  if (role === "member") return "Участник";
  if (role === "viewer") return "Гость";
  return "не назначена";
}

function sectionTitle(section: AdminSection): string {
  const labels: Record<AdminSection, string> = {
    overview: "Обзор",
    clients: "Клиенты",
    users: "Участники",
    groups: "Группы",
    services: "Каталог сервисов",
    access: "Доступы",
    invites: "Инвайты",
    sync: "Синхронизация",
    audit: "Аудит",
    misc: "Разное",
    company: "Профиль компании",
  };
  return labels[section];
}

function accessCellTitle(cell: AccessMatrixCell): string {
  if (!cell.effectiveAccess.allowed) return cell.effectiveAccess.source === "exception" ? accessAssignmentLabel("deny") : "—";
  return accessAssignmentRoleLabel(cell.effectiveAccess.appRole);
}

function accessAssignmentValue(cell: AccessMatrixCell): AccessAssignmentValue {
  if (cell.effectiveAccess.source === "exception" && !cell.effectiveAccess.allowed) return "deny";
  if (cell.effectiveAccess.source === "user" && cell.effectiveAccess.appRole) {
    return cell.effectiveAccess.appRole === "owner" ? "admin" : cell.effectiveAccess.appRole;
  }
  return "unset";
}

function accessAssignmentLabel(value: AccessAssignmentValue): string {
  return accessAssignmentOptions.find((option) => option.value === value)?.label ?? value;
}

function accessAssignmentRoleLabel(role?: ServiceAppRole | null): string {
  if (!role) return "—";
  return accessAssignmentLabel(role === "owner" ? "admin" : role);
}

function publicOperationalCoreCellTitle(cell: AccessMatrixCell): string {
  if (!cell.effectiveAccess.allowed) return "—";
  if (cell.effectiveAccess.appRole === "admin" || cell.effectiveAccess.appRole === "owner") return "Service Admin";
  if (cell.effectiveAccess.appRole === "viewer") return "Workspace Guest";
  return "Workspace Member";
}

function publicOperationalCoreCellSubtitle(cell: AccessMatrixCell): string {
  if (!cell.effectiveAccess.allowed) return "Не назначен";
  if (cell.effectiveAccess.appRole === "admin" || cell.effectiveAccess.appRole === "owner") return "Self-service";
  return "Workspace invite";
}

function publicAccessAssignmentLabel(value: AccessAssignmentValue): string {
  if (value === "admin") return "Service Admin";
  if (value === "viewer") return "Workspace Guest";
  if (value === "member") return "Workspace Member";
  if (value === "deny") return "—";
  return accessAssignmentLabel(value);
}

function publicOperationalCoreSelectValue(value: AccessAssignmentValue): AccessAssignmentValue {
  if (value === "viewer" || value === "member" || value === "admin" || value === "deny") return value;
  return "unset";
}

function accessCellKey(userId: string, serviceId: string): string {
  return `${userId}:${serviceId}`;
}

function serviceModuleEntitlementKey(_clientId: string, userId: string, serviceId: string, moduleId: ServiceModuleId): string {
  return `${userId}:${serviceId}:${moduleId}`;
}

function hasServiceModuleEntitlement(data: LauncherData, _clientId: string, userId: string, serviceId: string, moduleId: ServiceModuleId): boolean {
  return data.serviceModuleEntitlements.some(
    (entitlement) =>
      entitlement.userId === userId &&
      entitlement.serviceId === serviceId &&
      entitlement.moduleId === moduleId &&
      entitlement.enabled
  );
}

function hasSystemServiceModuleEntitlement(user: LauncherUser, moduleId: ServiceModuleId): boolean {
  return user.id === "user_root" && moduleId === "codex_agents";
}

function isOperationalCoreService(service: Service): boolean {
  return service.slug === "task-manager" || service.authentikApplicationSlug === "task-manager";
}

function accessAssignmentToTaskManagerRole(value: AccessAssignmentValue): TaskManagerWorkspaceMemberRole {
  if (value === "admin" || value === "member") return value;
  if (value === "viewer") return "guest";
  return "unset";
}

function taskManagerRoleToAccessAssignment(role: TaskManagerWorkspaceMemberRole): AccessAssignmentValue {
  if (role === "admin" || role === "member") return role;
  if (role === "guest") return "viewer";
  return "unset";
}

function operationalCoreStaticRoleLabel(value: OperationalCoreRoleSelectValue): string {
  if (value === "pending") return "Сохраняем...";
  return accessAssignmentLabel(value);
}

function sourceLabel(source?: AccessMatrixCell["effectiveAccess"]["source"]): string {
  if (!source) return "—";
  const labels = {
    client: "Клиент",
    group: "Группа",
    user: "Пользователь",
    exception: "Исключение",
  };
  return labels[source];
}

function targetLabel(target: SyncStatus["target"]): string {
  const labels: Record<SyncStatus["target"], string> = {
    authentik: "Authentik",
    task_manager: "Task Manager",
    nodedc: "NodeDC",
    service: "Сервис",
  };
  return labels[target];
}
